使用k8s k3s kuboard 部署 php hyperf 框架

1.Hyperf 生产环境部署实践:动态更新策略探讨

在基于 Hyperf 框架进行生产环境开发时,如何高效、安全地更新应用代码是一个普遍面临的关键问题。Hyperf 作为构建在 Swoole 之上的高性能框架,其常驻内存的特性意味着:当代码文件发生变更后,必须重启 Worker 进程才能使修改生效,从而加载最新代码到内存中。针对这一核心挑战,业界普遍存在以下几种解决方案:

  1. 服务进程重启 (Brute-force Restart)​

    • 操作方式:​ 直接停止(Kill)现有 Hyperf 服务进程,然后重新启动。
    • 优点:​ 实现极其简单直观,无需额外工具或配置。
    • 缺点:​ 属于"暴力"重启,会导致当前正在处理的请求丢失,服务出现短暂中断,不适用于对可用性要求高的生产环境。
  2. 开发环境热更新机制 (Hyperf Watcher - Development Only)​

    • 操作方式:​ 利用 Hyperf 提供的热更新组件(如 hyperf/watcher),在检测到代码文件变动时自动触发服务重启。
    • 优点:​ 开发体验便捷,显著提升开发效率。
    • 缺点:​ 仅适用于本地开发或测试环境。​ 其本质仍是重启整个 Worker 进程组,在生产环境中同样会造成请求中断。强烈不建议用于生产环境。​
  3. 基于 Docker 与 Nginx Upstream 的蓝绿/滚动发布

    • 核心思路:​ ​ 为更新启动一个包含新代码的新容器实例(新服务)。待新实例完全就绪并通过健康检查后,通过动态修改 Nginx upstream配置,用新容器的实际 IP 地址和端口替换旧容器实例的条目,从而将流量平滑切换到新版本服务。

    • 主要技术挑战:​

      • 新容器实例需动态分配端口(避免与旧实例端口冲突)。
      • 如何自动发现新启动的服务实例(服务发现)。
      • 如何实时、可靠地更新 Nginx upstream配置(配置动态更新)。
    • 优点:​​ 相较于前两种方案,能实现更平滑的流量切换,避免瞬间中断,用户体验更佳。

    • 缺点:​​ 需自行解决端口管理、服务发现和配置动态更新等复杂性问题。

  4. Kubernetes (K8s) 滚动更新 (Rolling Update)​

    • 核心思路(类似方案3,但自动化):​ 在 K8s 集群中,通过 Deployment 等资源定义更新时,会启动包含新代码的 Pod(新服务实例)。当新 Pod 就绪并通过探针检查后,Kubernetes 控制平面会自动将其添加至 Service 的 Endpoints(接收外部流量)。同时,系统将旧 Pod 从 Endpoints 中移除(停止接收新流量)。可配置优雅终止(preStophook)确保旧 Pod 处理完现有请求后被安全删除。
    • 技术挑战:​ 需具备 Kubernetes 基础知识和运维能力。
    • 优点:​ 平台自动化处理滚动更新、健康检查(存活/就绪探针)、自动扩缩容、服务发现、负载均衡及优雅终止,是实现生产环境无缝更新的最佳实践之一。

本文重点聚焦于 Hyperf 应用在生产环境中的部署与更新策略。​​ 文中内容的探讨基于读者已熟悉 Hyperf 框架原理,并对 Kubernetes 核心概念(如 Deployment, Pod, Service, Ingress 等)有基本了解的前提展开。 关于k8s的一些入门教程:

  1. Kubernetes简介
  2. kuboard.cn/learning/
  3. kubernetes.io/zh-cn/docs/...

部署流程图:

1、开发者提交代码到master分支后,执行构建脚本.sh

  1. 拉取master分支最新代码
  2. 生成版本号
  3. 创建临时容器,用于代码的编译
  4. 进入容器执行composer install、生成proxy文件
  5. 将代码打包,减小镜像的大小
  6. 根据版本号创建镜像,将打包好的代码COPY到镜像内
  7. 将镜像推送至 镜像仓库
  8. k8s更新镜像:kubectl set image deployment/<Deployment的名称> <容器名称>=<新镜像地址:新版本标签>

2、k8s接收到命令后,进行镜像的更新

这构建流程不仅适用于php hyperf, golang项目在部署时也同样适用,构建脚本.sh 后期也可替换成Jenkins,流程大同小异,关键点在于 构建脚本.sh 的编写,与k8s的部署与配置

所需的软件和环境

框架mineAdmin3.0,一个基于hyperf框架的企业级后台管理系统。本文中使用该框架做例子。当然也可以是其他项目或框架,比如:gin项目。CICD流程都差不多。

系统 :使用虚拟机进行安装CentOS-7.9,,虚拟机和系统搭建,请看这篇文章

k8sKubernetes 是一个可移植、可扩展的开源平台,用于管理容器化的工作负载和服务,方便进行声明式配置和自动化。Kubernetes 拥有一个庞大且快速增长的生态系统,其服务、支持和工具的使用范围广泛。

k3sK3s 是一个轻量级的 Kubernetes 发行版,它针对边缘计算、物联网等场景进行了高度优化。K3s 适用于以下场景:边缘计算-Edge、物联网-IoT、CI、Development、ARM、嵌入 K8s

kuboardkuboard 一个Kubernetes 多集群管理界面,可视化的管理k8s/k3s的集群

初始化

准备好2台CentOS-7.9的服务器

名称 ip 配置
k3s-server 192.168.119.131 2核4G
k3s-agent 192.168.119.132 2核4G
csharp 复制代码
#在server节点上执行,需要重启服务器
sudo hostnamectl set-hostname k3s-server

#在agent节点上执行,需要重启服务器
sudo hostnamectl set-hostname k3s-agent

sudo systemctl stop firewalld      # 立即停止防火墙
sudo systemctl disable firewalld  # 禁止开机启动

安装docker

arduino 复制代码
sudo yum remove docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-engine
arduino 复制代码
sudo curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo
sudo yum clean all
sudo yum makecache

sudo yum install -y yum-utils device-mapper-persistent-data lvm2

sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

sudo yum makecache fast
bash 复制代码
# 查看可用版本列表
sudo yum list docker-ce --showduplicates | sort -r

# 安装指定版本(26.1.4)
sudo yum install -y docker-ce-26.1.4 docker-ce-cli-26.1.4 containerd.io
bash 复制代码
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": [
    "https://registry.cn-hangzhou.aliyuncs.com",
    "https://docker.mirrors.ustc.edu.cn",
    "https://docker.xuanyuan.run"
  ]
}
EOF
sudo systemctl enable docker
sudo systemctl start docker

#验证docker
[root@localhost ~]# docker -v
Docker version 26.1.4, build 5650f9b

遇到镜像拉取不下来时,建议使用轩辕镜像,方便后续操作。

server 和 agent 服务都需要安装docker

安装k3s

使用离线安装方式进行安装,下载离线包server、agent上都分别下载

bash 复制代码
#下载安装脚本
wget https://rancher-mirror.rancher.cn/k3s/k3s-install.sh
chmod +x k3s-install.sh

# 下载 K3s 二进制和镜像(替换版本号)下载太慢的话,可以在宿主机上下载,再上传到虚拟机
wget -4 https://github.com/k3s-io/k3s/releases/download/v1.33.4-rc1%2Bk3s1/k3s
sudo chmod +x k3s && sudo mv k3s /usr/local/bin/

wget -4 https://github.com/k3s-io/k3s/releases/download/v1.33.4-rc1%2Bk3s1/k3s-airgap-images-amd64.tar
sudo mkdir -p /var/lib/rancher/k3s/agent/images/
sudo cp k3s-airgap-images-amd64.tar /var/lib/rancher/k3s/agent/images/

添加镜像加速: server、agent节点分别添加

bash 复制代码
sudo tee /etc/rancher/k3s/registries.yaml > /dev/null <<'EOF'
mirrors:
  "docker.io":
    endpoint:
      - "https://这里填自己的docker加速地址"
  "k8s.gcr.io":
    endpoint:
      - "https://registry.aliyuncs.com/google_containers"
      - "https://gcr.mirrors.ustc.edu.cn"
EOF

server节点安装

perl 复制代码
INSTALL_K3S_SKIP_DOWNLOAD=true ./k3s-install.sh 

#安装结果:
[root@k3s_server ~]# INSTALL_K3S_SKIP_DOWNLOAD=true ./k3s-install.sh 
[INFO]  Skipping k3s download and verify
[INFO]  Skipping installation of SELinux RPM
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s.service
[INFO]  systemd: Enabling k3s unit
Created symlink from /etc/systemd/system/multi-user.target.wants/k3s.service to /etc/systemd/system/k3s.service.
[INFO]  systemd: Starting k3s

#查看一下状态,是否正确启动了
[root@k3s-server ~]# k3s kubectl get nodes -o wide
NAME         STATUS   ROLES                  AGE   VERSION            INTERNAL-IP       EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION           CONTAINER-RUNTIME
k3s-server   Ready    control-plane,master   11m   v1.33.4-rc1+k3s1   192.168.119.131   <none>        CentOS Linux 7 (Core)   3.10.0-1160.el7.x86_64   containerd://2.0.5-k3s2

接着再server节点查看url和token:

csharp 复制代码
[root@k3s_server ~]# sudo cat /etc/rancher/k3s/k3s.yaml | grep server
    server: https://127.0.0.1:6443
[root@k3s_server ~]# sudo cat /var/lib/rancher/k3s/server/node-token
K104c4194aac85b338608773eda1fea40b817ced6ef6f4b6879b11ab9c9423ee8b3::server:0e1f5e91b653c171e42aef9d8c853bfa

如果安装失败了,进行卸载再重新操作一遍:

bash 复制代码
/usr/local/bin/k3s-uninstall.sh

agent节点安装

K3S_URL:替换成自己的server节点ip地址

K3S_TOEKN:由server获取

ini 复制代码
INSTALL_K3S_SKIP_DOWNLOAD=true K3S_URL=https://192.168.119.131:6443 K3S_TOKEN=K104c4194aac85b338608773eda1fea40b817ced6ef6f4b6879b11ab9c9423ee8b3::server:0e1f5e91b653c171e42aef9d8c853bfa ./k3s-install.sh
perl 复制代码
[root@k3s_agent ~]# INSTALL_K3S_SKIP_DOWNLOAD=true K3S_URL=https://192.168.119.131:6443 K3S_TOKEN=K104c4194aac85b338608773eda1fea40b817ced6ef6f4b6879b11ab9c9423ee8b3::server:0e1f5e91b653c171e42aef9d8c853bfa ./k3s-install.sh
[INFO]  Skipping k3s download and verify
[INFO]  Skipping installation of SELinux RPM
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-agent-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s-agent.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s-agent.service
[INFO]  systemd: Enabling k3s-agent unit
Created symlink from /etc/systemd/system/multi-user.target.wants/k3s-agent.service to /etc/systemd/system/k3s-agent.service.
[INFO]  systemd: Starting k3s-agent
Job for k3s-agent.service failed because the control process exited with error code. See "systemctl status k3s-agent.service" and "journalctl -xe" for details.
perl 复制代码
#查看一下状态,是否正确启动了,再server执行
[root@k3s-server ~]# k3s kubectl get nodes -o wide
NAME         STATUS   ROLES                  AGE   VERSION            INTERNAL-IP       EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION           CONTAINER-RUNTIME
k3s-agent    Ready    <none>                 7s    v1.33.4-rc1+k3s1   192.168.119.132   <none>        CentOS Linux 7 (Core)   3.10.0-1160.el7.x86_64   containerd://2.0.5-k3s2
k3s-server   Ready    control-plane,master   15m   v1.33.4-rc1+k3s1   192.168.119.131   <none>        CentOS Linux 7 (Core)   3.10.0-1160.el7.x86_64   containerd://2.0.5-k3s2

如果安装失败了,进行卸载再重新操作一遍:

bash 复制代码
/usr/local/bin/k3s-agent-uninstall.sh 

安装kuboard

在server节点上执行

ini 复制代码
sudo docker run -d \
  --restart=unless-stopped \
  --name=kuboard \
  -p 8081:80/tcp \
  -p 10081:10081/tcp \
  -e KUBOARD_ENDPOINT="http://{server的ip地址}:80" \
  -e KUBOARD_AGENT_SERVER_TCP_PORT="10081" \
  -v /root/kuboard-data:/data \
  eipwork/kuboard:v3

进入管理后台: http://192.168.119.131:8081/ admin Kuboard123

集群导入成功,到此 kuboard 的安装就完成了

安装mineadmin

关于mineAdmin的部署与启动这里就不再进行说明。要确保服务能正常启动.

csharp 复制代码
docker run -it \
  --name mineadmin \
  -p 9501:9501 \
  -v $(pwd):/opt/www \
  mineadmin:latest

#先在.env文件配置好数据库和redis新增
#进入容器
dokcer exec -it mineadmin /bin/bash

# 安装依赖包(开发环境)
composer install -vvv

# 创建数据库(可选,也可以手动创建)
mysql -u root -p -e "CREATE DATABASE mineadmin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

# 执行数据库迁移
php bin/hyperf.php migrate

# 填充初始数据
php bin/hyperf.php db:seed
#启动程序
/opt/www # php bin/hyperf.php start
[INFO] Worker#1 started.
[INFO] Worker#0 started.
[INFO] Worker#2 started.
[INFO] Worker#3 started.
[INFO] HTTP Server listening at 0.0.0.0:9503
[INFO] HTTP Server listening at 0.0.0.0:9501
[INFO] Worker#6 started.
[INFO] Worker#4 started.
[INFO] Worker#7 started.
[INFO] Worker#5 started.

创建镜像仓库

镜像仓库可以自己搭建、者使用阿里云的镜像仓库,这里以阿里云镜像仓库为例。

在server、agent节点上进行登入

ini 复制代码
docker login --username=xxxx registry.cn-shenzhen.aliyuncs.com

到此,阿里云镜像仓库创建完毕,基础镜像和项目镜像都会上传到这个仓库里。

构建镜像

为了加快镜像的构建速度,将镜像分为:基础镜像项目镜像,基础镜像是php、swoole等一些配置。项目镜像包含了项目的代码。

基础镜像

./deploy/base.dockerfile

基础镜像只需要构建一次。后面就不需要了,除非修改了php配置或者增加了php扩展等

bash 复制代码
FROM hyperf/hyperf:8.1-alpine-v3.19-swoole-slim
LABEL maintainer="MineManage Developers <group@stye.cn>" version="1.0" license="MIT" app.name="MineManage"

##
# ---------- env settings ----------·
##
# --build-arg timezone=Asia/Shanghai
ARG timezone

ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
    APP_ENV=prod \
    SCAN_CACHEABLE=(true)

# update
RUN set -ex \
    # show php version and extensions
    && php -v \
    && php -m \
    && php --ri swoole \
    #  ---------- some config ----------
    && cd /etc/php* \
    # - config PHP
    && { \
        echo "upload_max_filesize=128M"; \
        echo "post_max_size=128M"; \
        echo "memory_limit=1G"; \
        echo "date.timezone=${TIMEZONE}"; \
    } | tee conf.d/99_overrides.ini \
    # - config timezone
    && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
    && echo "${TIMEZONE}" > /etc/timezone \
    # ---------- clear works ----------
    && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
    && echo -e "\033[42;37m Build Completed :).\033[0m\n"

# update
RUN set -ex \
    #  ---------- some config ----------
    && cd /etc/php81 \
    # - config timezone
    && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
    && echo "${TIMEZONE}" > /etc/timezone \
    && echo -e "\033[42;37m Build Completed :).\033[0m\n"

RUN set -ex && \
    sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
    && apk update \
    && apk add --no-cache libstdc++ openssl git bash autoconf pcre2-dev zlib-dev re2c gcc g++ make \
    php81-pear php81-dev php81-tokenizer php81-fileinfo php81-simplexml php81-xmlwriter \
    && apk add --no-cache --virtual .build-deps $PHPIZE_DEPS zlib-dev libaio-dev openssl-dev curl-dev  c-ares-dev \
    && pecl81 channel-update pecl.php.net \
    && pecl81 install --configureoptions 'enable-reader="yes"' xlswriter \
    && echo "extension=xlswriter.so" >> /etc/php81/conf.d/60-xlswriter.ini \
    && php -m \
    && php -v \
    && php --ri swoole \
    && mkdir -p /app-src \
    # ---------- clear works ----------
    && apk del .build-deps \
    && rm -rf /var/cache/apk/* /tmp/* /usr/share/man /usr/local/bin/php* \
    && echo -e "\033[42;37m Build Completed :).\033[0m\n"

ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so

WORKDIR /opt/www
csharp 复制代码
# 项目根目录执行
docker build -f ./deploy/base.dockerfile -t mineadmin:base  .

[root@k3s-server deploy]# docker images | grep mineadmin
mineadmin                                   base                           905322f09690   About a minute ago   383MB
mineadmin                                   latest                         331df88ac067   19 hours ago         118MB
#打标签
docker tag 905322f09690 registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:base
#推送镜像
docker push registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:base

如果遇到镜像拉取不下来,请更换其他源。或者提前把 hyperf/hyperf:8.1-alpine-v3.19-swoole-slim pull 下来

项目镜像

./deploy.dockerfile.tlp

bash 复制代码
# 这里要替换成自己的基础镜像地址

FROM registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:base

COPY {package_name} /opt/www/{project_tar_gz} 
RUN rm -rf /opt/www/{package_name}

EXPOSE 9501 9502 9503

修改.dockerignore 文件

diff 复制代码
**
!app/
!bin/
!config/
!databases/
!plugin/
!storage/
!composer.*
!.env.example
!*.tar.gz

编写build.sh

./deploy/build.sh

bash 复制代码
#!/bin/bash

# --- Color Definitions ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

# --- Script Configuration ---

#镜像仓库地址
REGISTRY_URL="registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin"
#构建容器名称
CONTAINER_NAME="mineadmin"
#打包名称(项目名称)
PROJECT_NAME="mineadmin"
#k8s命名空间
K8S_NAMESPACE="mineadmin"
PROJECT_NAME_TAR_GZ="${PROJECT_NAME}.tar.gz"
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
WEB_PATH=$(dirname "$SCRIPT_DIR")

# --- Main Script ---
cd "$WEB_PATH" || exit

# 1. Parse branch argument
BRANCH=${1:-master}
if [ "$BRANCH" != "master" ] && [ "$BRANCH" != "dev" ]; then
  echo -e "${RED}❌ 错误:无效的分支名称。请使用 'master' 或 'dev'。${NC}"
  exit 1
fi

echo -e "${CYAN}===================================================${NC}"
echo -e "${CYAN}🚀 开始为分支 '${YELLOW}$BRANCH${CYAN}' 执行构建...${NC}"
echo -e "${CYAN}===================================================${NC}"

# 2. Pull latest code from Git
echo -e "\n${BLUE}Step 1: 从 Git 拉取最新代码...${NC}"
git checkout $BRANCH > /dev/null 2>&1
git pull origin $BRANCH > /dev/null 2>&1
if [ $? -ne 0 ]; then
  echo -e "${RED}❌ 错误:从 Git 拉取代码失败。${NC}"
  exit 1
fi
echo -e "${GREEN}✔️ Git 代码拉取成功。${NC}"

# Step 2: Get version from timestamp1
echo -e "\n${BLUE}Step 2: 正在生成时间戳版本号...${NC}"
VERSION=$(date +%y%m%d%H%M%S)
if [ -z "$VERSION" ]; then
  echo -e "${RED}❌ 错误:生成时间戳版本号失败。${NC}"
  exit 1
fi
echo -e "${GREEN}✔️ 使用时间戳作为版本号:${YELLOW}$VERSION${NC}"


# Step 3: Prepare configuration file
# 如果使用了k8s的configMap功能,就不需要copy配置文件了
echo -e "\n${BLUE}Step 3: 准备配置文件...${NC}"
ENV_FILE=".env.$BRANCH"
if [ ! -f "$ENV_FILE" ]; then
  echo -e "${RED}❌ 错误:配置文件 '$ENV_FILE' 不存在。${NC}"
  exit 1
fi
cp "$ENV_FILE" .env
echo -e "${GREEN}✔️ 已将 '$ENV_FILE' 复制到 '.env'。${NC}"

# Check container status
echo -e "\n${BLUE}Step 4: 检查容器状态...${NC}"
CONTAINER_ID=$(docker ps -qf "name=^${CONTAINER_NAME}$")
if [ -z "$CONTAINER_ID" ]; then
  echo -e "${RED}❌ 错误:找不到名为 '$CONTAINER_NAME' 的正在运行的容器。${NC}"
  echo -e "${YELLOW}请确保容器已根据环境要求提前启动。${NC}"
  exit 1
fi
echo -e "${GREEN}✔️ 容器 '$CONTAINER_NAME' 正在运行。${NC}"

# Execute commands inside the container11
echo -e "\n${BLUE}Step 5: 在容器内执行构建命令...${NC}"

echo -e "  - 正在执行 'composer install'..."
docker exec $CONTAINER_NAME composer install --no-dev --optimize-autoloader > /dev/null 2>&1
if [ $? -ne 0 ]; then
  echo -e "${RED}  - ❌ 错误:'composer install' 执行失败${NC}"
  exit 1
fi

echo -e "  - 正在执行 'composer dump-autoload -o'..."
docker exec $CONTAINER_NAME composer dump-autoload -o > /dev/null 2>&1
if [ $? -ne 0 ]; then
  echo -e "${RED}  - ❌ 错误:'composer dump-autoload -o' 执行失败。${NC}"
  exit 1
fi

echo -e "  - 正在生成 proxy 代理文件..."
docker exec $CONTAINER_NAME php /opt/www/bin/hyperf.php > /dev/null 2>&1
if [ $? -ne 0 ]; then
  echo -e "${RED}  - ❌ 错误:生成代理文件命令执行失败。${NC}"
  exit 1
fi

docker exec $CONTAINER_NAME test -d /opt/www/runtime/container
if [ $? -ne 0 ]; then
  echo -e "${RED}  - ❌ 错误:Hyperf 代理文件目录 /opt/www/runtime/container 未找到,生成失败。${NC}"
  exit 1
fi
echo -e "${GREEN}✔️ 容器内构建命令执行成功。${NC}"

# 5. Package the code1
PACKAGE_NAME="${PROJECT_NAME}-${VERSION}.tar.gz"
echo -e "\n${BLUE}Step 6: 📦 将代码打包到 '${YELLOW}$PACKAGE_NAME${BLUE}'...${NC}"
tar --exclude=.dockerignore \
    --exclude='*.log' \
    --exclude='.idea' \
    --exclude='.devcontainer' \
    --exclude='.git' \
    --exclude='runtime' \
    --exclude='*.tar.gz' \
    -zcvf $PACKAGE_NAME ./* > /dev/null 2>&1
if [ $? -ne 0 ]; then
  echo -e "${RED}❌ 错误:打包代码失败。${NC}"
  exit 1
fi
echo -e "${GREEN}✔️ 代码打包成功。${NC}"

# 6. Build Docker image
IMAGE_TAG="$VERSION"
FULL_IMAGE_NAME="${REGISTRY_URL}:${IMAGE_TAG}"
TEMPLATE_FILE="deploy/deploy.dockerfile.tlp"
DEPLOY_DOCKERFILE="deploy/deploy.dockerfile"

echo -e "\n${BLUE}Step 7: 🐳 构建 Docker 镜像...${NC}"
echo -e "  - 镜像名称: ${YELLOW}$FULL_IMAGE_NAME${NC}"

if [ ! -f "$TEMPLATE_FILE" ]; then
  echo -e "${RED}❌ 错误:模板文件 '$TEMPLATE_FILE' 不存在。${NC}"
  exit 1
fi

sed -e "s|{package_name}|${PACKAGE_NAME}|g" -e "s|{project_tar_gz}|${PROJECT_NAME_TAR_GZ}|g" "$TEMPLATE_FILE" > "$DEPLOY_DOCKERFILE"
if [ $? -ne 0 ]; then
  echo -e "${RED}❌ 错误:创建 '$DEPLOY_DOCKERFILE' 失败。${NC}"
  exit 1
fi

docker build -f "$DEPLOY_DOCKERFILE" -t "$FULL_IMAGE_NAME" . > /dev/null 2>&1
if [ $? -ne 0 ]; then
  echo -e "${RED}❌ 错误:构建 Docker 镜像失败。${NC}"
  rm "$DEPLOY_DOCKERFILE"
  exit 1
fi
echo -e "${GREEN}✔️ Docker 镜像构建成功。${NC}"

# Clean up temporary Dockerfile
rm "$DEPLOY_DOCKERFILE"

# 7. Push image to registry
echo -e "\n${BLUE}Step 8: 📤 上传镜像到仓库...${NC}"
docker push $FULL_IMAGE_NAME > /dev/null 2>&1
if [ $? -ne 0 ]; then
  echo -e "${RED}❌ 错误:上传镜像失败。${NC}"
  exit 1
fi
echo -e "${GREEN}✔️ 镜像上传成功。${NC}"

echo -e "\n${GREEN}===================================================${NC}"
echo -e "${GREEN}✨ 构建和上传成功完成! ✨${NC}"
echo -e "${GREEN}===================================================${NC}"
echo -e "镜像名称:${YELLOW}$FULL_IMAGE_NAME${NC}\n"

# 8. 更新k8s
echo -e "\n${BLUE}Step 9: 🚀 更新 Kubernetes Deployment...${NC}"
# 设置mineadmin 进行镜像更新
kubectl set image deployment/${PROJECT_NAME} ${PROJECT_NAME}=${FULL_IMAGE_NAME}
if [ $? -ne 0 ]; then
  echo -e "${RED}❌ 错误:触发 Kubernetes Deployment 更新失败。${NC}"
  echo -e "${RED}请检查:${NC}"
  echo -e "${RED}  1. 是否有权限访问 Kubernetes 集群 (kubectl config)。${NC}"
  echo -e "${RED}  2. Deployment 名称 '${PROJECT_NAME}' 和容器名称 '${PROJECT_NAME}' 是否正确。${NC}"
  exit 1
fi
echo -e "${GREEN}✔️ Deployment 更新已成功触发。${NC}"

# 9. (可选但强烈推荐) 检查更新状态
echo -e "\n${BLUE}Step 10: 🔍 监控滚动更新状态...${NC}"
kubectl rollout status deployment/${PROJECT_NAME}
if [ $? -ne 0 ]; then
  echo -e "${RED}❌ 错误:Deployment 滚动更新失败。请使用 'kubectl describe deployment/${PROJECT_NAME}' 查看详情。${NC}"
  exit 1
fi

echo -e "\n${GREEN}===================================================${NC}"
echo -e "${GREEN}✅ CI/CD 流程全部成功完成!应用已更新。 ✨${NC}"
echo -e "${GREEN}===================================================${NC}"

连接k3s集群

在执行build.sh脚本之前,需要对k8s集群有访问权限。如果是在k3s-server 节点上执行构建脚本的话,就不需要做此操作。在其他服务器上构建时,需要做以下操作:

bash 复制代码
# 下载最新版本的 kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"

# 赋予执行权限
chmod +x ./kubectl

# 将其移动到您的PATH路径下,例如 /usr/local/bin
sudo mv ./kubectl /usr/local/bin/kubectl

# 创建 .kube 目录 : kubectl 默认会在用户的主目录下的 .kube 文件夹中寻找名为 config 的文件。
mkdir -p $HOME/.kube

#将 k8s-service节点上的配置文件复制过来:sudo cat /etc/rancher/k3s/k3s.yaml,修改server:https://192.168.119.131:6443

vim $HOME/.kube/config
csharp 复制代码
# 在构建节点上执行,看是否能获取到集群信息
[root@localhost ~]# kubectl get nodes
NAME         STATUS   ROLES                  AGE     VERSION
k3s-agent    Ready    <none>                 5d19h   v1.33.4-rc1+k3s1
k3s-server   Ready    control-plane,master   5d19h   v1.33.4-rc1+k3s1

执行build.sh

在项目根目录执行 sh deploy/build.sh 看是否能够正常:

  1. 打包出来的tag.gz文件,是否正常
  2. 是否能push到镜像仓库
  3. 对刚创建出来的 镜像,进行测试。根据镜像 创建一个临时容器,进入容器,启动项目,看是否能启动成功

到此完成了项目的拉取、编译、构建镜像功能。下一步就是在k8s(k3s)上使用镜像部署项目

k3s部署项目

创建命名空间

配置ConfigMap

ConfigMap 是 Kubernetes 中用于存储非敏感配置数据的资源对象。它可以将配置信息(如环境变量、配置文件、命令行参数等)与应用程序代码分离,从而实现配置的集中管理和动态更新。

也可通过yaml文件创建:deploy/k8s/ConfigMap.yaml

configMap会以环境变量的方式,写入pod的容器内,在代码里还是使用env('APP_NAME', 'MineAdmin'), 方式获取配置文件信息

创建Deployment

创建镜像仓库账号密码

配置启动命令

bash 复制代码
/bin/sh -c 'tar -zxf mineadmin.tar.gz . && php bin/hyperf.php start'

配置服务端口

项目启动了哪些服务就填写对应的端口

配置探针

启动探针,用于判断容器是否启动成功。

存活探针 就绪探针 每个探针都有自己所属的功能和应用场景,根据实际情况来选择。一般配置存活探针、就绪探针就可以保证服务的正常运行

配置文件存储

将容器内的logs目录挂载到宿主机的目录下,方便后续的日志采集或者查看日志

创建service

启动deployment 启动成功:

配置Ingress

由于我们是在本地环境下部署的,所有得在本机hosts上添加域名。 C:\Windows\System32\drivers\etc\hosts

访问admin-mineadmin.com

在生成环境中,前面还有一层高可用的 LBS做负载均衡。流量路径:用户请求->DNS->lbs->ingress->service->pod->服务

遇到的问题

在创建deployment过程中,会遇到各种各样的问题,以下是一些解决思路:

1、首先保证项目镜像是正常,能正常启动的。可以在agent节点,把镜像拉取下来,创建一个临时容器,看项目是否能够正确启动。

bash 复制代码
docker -it  --namne test1 registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:250820150754
#创建临时容器
docker run -it  --namne test1 registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:250820150754
#解压启动
tar -zxf mineadmin.tar.gz . && php bin/hyperf.php start

项目的镜像正常了,到了k8s不正常,怎么排查?

初始化容器时,先不启动服务,先执行sleep 3600。 这样子容器就启动成功了,然后可以手动的进入容器,检查是什么原因导致服务启动失败的

bash 复制代码
#k8s使用的是containerd,命令跟docker一致,只不过docker 换成了 crictl
#有可能 mineadmin-f6dcd85c9 部署在server 或 agent 两边都查一下
crictl ps -a | grep mineadmin-f6dcd85c9
#进入容器
crictl exec -it {CONTAINER} /bin/bash

进入容器之后,首先查看

  1. 代码是否打包成功、解压出来的文件是否有缺失
  2. configMap 是否写入容器的环境变量里。执行 env 查看容器环境变量
  3. 然后尝试启动服务看服务是否正常启动。php bin/hyperf.php start

启动成功之后,退出容器,回到管理后台,更新回正常的启动命令,然后更新Deployment

拉取镜像失败

分别在 server agent 节点 创建 /etc/rancher/k3s/registries.yaml 文件

bash 复制代码
sudo tee /etc/rancher/k3s/registries.yaml > /dev/null <<'EOF'
mirrors:
  "docker.io":
    endpoint:
      - "https://这里填自己的docker加速地址"
  "k8s.gcr.io":
    endpoint:
      - "https://registry.aliyuncs.com/google_containers"
      - "https://gcr.mirrors.ustc.edu.cn"
EOF
arduino 复制代码
#agent节点 重启k3s-agent
sudo systemctl restart k3s-agent

#server节点 重启k3s
sudo systemctl restart k3s

总结

到此完成了代码镜像的构建、k8s的安装与部署,以及代码的发布。该方案还有许多完善的地方,比如:

  1. 构建脚本.sh 可以更换成 Jenkins ,实现CICD自动发布
  2. 多个节点下服务日志的收集。需要使用ELK进行收集与查询

生成环境下,建议使用各个云产商的k8s服务,毕竟开发人员不是专业的运维人员,k8s的运维还是比较复杂的。比如阿里云的ACK功能,后面有时间的话,会把Jenkins加上。

相关推荐
JaguarJack3 小时前
FrankenPHP 原生支持 Windows 了
后端·php·服务端
BingoGo3 小时前
FrankenPHP 原生支持 Windows 了
后端·php
JaguarJack1 天前
PHP 的异步编程 该怎么选择
后端·php·服务端
BingoGo1 天前
PHP 的异步编程 该怎么选择
后端·php
JaguarJack2 天前
为什么 PHP 闭包要加 static?
后端·php·服务端
ServBay3 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
用户962377954483 天前
CTF 伪协议
php
BingoGo5 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack5 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo6 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php