K3s + Helm + CircleCI 实战:从 GitHub/GitLab 导入项目到 Docker Hub 多环境部署

K3s + Helm + CircleCI 部署封面

适用场景:你已经有一台运行 K3s 的 ARM64 Linux 主机,希望把 GitHub 或 GitLab 项目导入 CircleCI,使用当前主机上的 CircleCI self-hosted runner 构建镜像,并通过 Helm 自动部署到 dev、main 或 master 环境。



本文基于实际项目:zszweb/emailAccountsAdmin,同时保留原有 GitLab/zszDeploy 流程的可恢复能力。文中的密码、Token、kubeconfig 和私有仓库凭据均使用变量名或占位符,不包含真实值。


目录


一、最终架构

整个发布链路如下:

GitHub/GitLab Push ➔ CircleCI Pipeline ➔ self-hosted Container Runner ➔ Node.js 构建 ➔ Kaniko 构建并推送 Docker Hub ➔ Helm upgrade --install ➔ K3s Deployment/Service/Ingress ➔ Traefik + cert-manager 提供 HTTPS

本次项目的关键边界:

部分当前方案
Git 仓库GitLab:https://gitlab.com/zszweb/emailAccountsAdmin
CircleCI 项目emailAccountsAdmin
runner 主机us-arm-12g,当前 K3s 主机
runner resource classzszweb/k3s-ci
runner 架构ARM64
镜像仓库Docker Hub
镜像地址docker.io/zszken/email-accounts-admin
Helm Chart仓库内 ./helm
dev 域名https://dev-email.zszweb.cn
production 域名https://email.zszweb.cn
旧流程GitLab Push Webhook ➔ zszDeploy ➔ .zsz-ci.yml
新流程CircleCI ➔ .circleci/config.yml

二、部署前提

1. 主机要求

建议准备一台可以稳定访问公网的 Linux ARM64 主机,并满足:

  • 已配置域名 DNS,域名指向主机公网 IP;
  • 放行 TCP 80 和 443;
  • 具备 root 或 sudo 权限;
  • 可以访问 GitHub、GitLab、CircleCI、Docker Hub、Let's Encrypt 和 npm registry;
  • 主机时间同步正常;
  • 不在仓库中保存 kubeconfig、Registry 密码或 CI Token。

2. 域名规划

本项目使用两个入口:

环境域名用途
devdev-email.zszweb.cndev 分支自动部署
productionemail.zszweb.cnmain 或 master 分支发布

如果更换域名,需要同时修改 CircleCI 配置、Helm 参数、证书配置和 DNS。

三、初始化 K3s 主机

以下步骤整理自旧笔记:K3s + Helm + NPM 命令式部署文档。版本号建议在正式执行前再次确认;生产环境应固定经过验证的版本,避免直接使用未经测试的 latest。

1. 设置主机名与内核参数

hostnamectl set-hostname k3s-master

cat <<'EOF' >> /etc/sysctl.conf
net.ipv4.ip_forward = 1
net.ipv4.conf.all.proxy_arp = 1
EOF

sysctl -p /etc/sysctl.conf

sysctl net.ipv4.ip_forward
sysctl net.ipv4.conf.all.proxy_arp

如果 /etc/sysctl.conf 中已经存在这些参数,不要重复追加;应先确认最终值。

2. 安装 K3s

curl -sfL https://get.k3s.io | sh -s - \
  --write-kubeconfig ~/.kube/config \
  --write-kubeconfig-mode 600

验证服务、节点和 Pod:

systemctl status k3s --no-pager
kubectl get nodes -o wide
kubectl get pods -A

节点必须处于 Ready。如果 kubectl 无法连接,先检查 k3s 服务和 kubeconfig 路径。

3. 安装 Helm

ARM64 主机示例:

HELM_VERSION="v4.1.4"
wget "https://get.helm.sh/helm-${HELM_VERSION}-linux-arm64.tar.gz"
tar -zxvf "helm-${HELM_VERSION}-linux-arm64.tar.gz"
install -m 0755 linux-arm64/helm /usr/local/bin/helm
helm version

也可以使用系统包管理器或官方安装脚本,但生产环境建议固定版本并在升级前验证 Chart 渲染结果。

四、安装 Traefik、cert-manager 与 HTTPS 基础设施

K3s 默认提供 Traefik。项目的 Helm Ingress 使用:

  • ingressClassName: traefik;
  • cert-manager 的 ClusterIssuer;
  • HTTP 到 HTTPS 的 Traefik Middleware;
  • Let's Encrypt HTTP-01 challenge。

1. 安装 cert-manager CRDs

CERT_MANAGER_VERSION="v1.20.2"
kubectl apply -f \
  "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.crds.yaml"

kubectl get crds | grep cert-manager

2. 安装 cert-manager 控制器

只安装 CRDs 不够,还需要安装 cert-manager 控制器:

helm repo add jetstack https://charts.jetstack.io
helm repo update

kubectl create namespace cert-manager \
  --dry-run=client -o yaml | kubectl apply -f -

helm upgrade --install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --version "${CERT_MANAGER_VERSION}"

kubectl get pods -n cert-manager

正常情况下应看到 cert-manager、cert-manager-cainjector 和 cert-manager-webhook。

3. 创建 ClusterIssuer

创建 production-issuer.yaml:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ${LETSENCRYPT_EMAIL}
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik

Shell 环境变量不会被 Kubernetes YAML 自动替换。实际执行前可以用模板工具渲染,或把邮箱作为非敏感配置写入文件。不要把 DNS API Token 或其他私密凭据写进公开仓库。

kubectl apply -f production-issuer.yaml
kubectl get clusterissuer letsencrypt-prod
kubectl describe clusterissuer letsencrypt-prod

4. 创建 HTTPS 重定向 Middleware

创建 https-redirect.yaml:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: redirect-https
  namespace: default
spec:
  redirectScheme:
    scheme: https
    permanent: true

应用并检查:

kubectl apply -f https-redirect.yaml
kubectl get middleware -n default
kubectl describe middleware redirect-https -n default

5. HTTPS 生效检查

kubectl get ingress -A
kubectl get certificate,certificaterequest,order,challenge -A
curl -I http://dev-email.zszweb.cn
curl -kI https://dev-email.zszweb.cn

如果 HTTP-01 challenge 失败,优先检查 DNS、80 端口、Traefik Pod、IngressClass 和 cert-manager 日志。

五、将 GitHub 或 GitLab 项目导入 CircleCI

CircleCI 的项目导入方式取决于代码托管平台,但仓库中都应放置 .circleci/config.yml。

1. GitHub 项目

  1. 登录 CircleCI。
  2. 选择 GitHub 组织或个人账户。
  3. 授权 CircleCI GitHub App,或使用当前组织已有连接。
  4. 在 Projects 中找到目标仓库。
  5. 选择 Set Up Project。
  6. 确认配置文件路径为 .circleci/config.yml。
  7. 选择目标分支并运行第一次 Pipeline。

如果仓库没有配置文件,可以先提交最小配置;正式部署前应替换为项目真实的 build、push 和 deploy 流程。

2. GitLab 项目

本项目实际使用 GitLab.com:

https://gitlab.com/zszweb/emailAccountsAdmin

导入步骤:

  1. 在 CircleCI 中选择 GitLab.com 连接。
  2. 授权 CircleCI 访问目标 GitLab 组织或项目。
  3. 选择 zszweb/emailAccountsAdmin。
  4. 确认 CircleCI 读取仓库根目录的 .circleci/config.yml。
  5. 在 CircleCI 项目设置中确认 GitLab push 能触发 Pipeline。
  6. 先用功能分支验证 validate,再合并到 dev。

CircleCI 通过 GitLab 集成读取代码,不需要把 GitLab 用户名、密码或个人 Token 写入 .circleci/config.yml。如果还要保留 GitLab Webhook 触发的旧系统,应单独管理 Webhook,不要把两套流程混在同一个脚本里。

3. 配置文件的职责边界

本项目同时存在两个文件:

文件作用当前处理
.circleci/config.ymlCircleCI 新流程由 CircleCI 读取
.zsz-ci.ymlGitLab/zszDeploy 旧流程保留,不删除、不修改

旧流程需要暂停时,只禁用 GitLab 中指向 zszDeploy 的 Push Webhook,不删除 .zsz-ci.yml,也不访问或处理 zszdeploy-server 仓库中的公开凭据和文件。

六、CircleCI Context 与变量

建议使用两个 Context(沿用现有 CI 兼容标识,暂不随仓库改名重命名):

  • emailaccoutsadmin-build:只绑定镜像构建任务;
  • emailaccoutsadmin-deploy:只绑定 Kubernetes 部署任务。

1. build Context

DOCKERHUB_USERNAME
DOCKERHUB_TOKEN
VITE_INSFORGE_BASE_URL
VITE_INSFORGE_ANON_KEY

其中:

  • DOCKERHUB_USERNAME 是 Docker Hub 用户名;
  • DOCKERHUB_TOKEN 是 Docker Hub Access Token,不建议使用账户密码;
  • VITE_INSFORGE_* 是前端构建时需要注入的公开运行配置,是否适合暴露给浏览器要遵循项目自身安全模型。

当前仓库配置中的非敏感默认值为:

REGISTRY=docker.io/zszken
REGISTRY_URL=docker.io
DEPOT_NAME_LOWER=email-accounts-admin

2. deploy Context

DOCKERHUB_USERNAME
DOCKERHUB_TOKEN

Kubernetes namespace 当前由配置固定为:

HELM_NAMESPACE=default

生产环境不需要把以下内容放入 Context:

KUBECONFIG_B64
HELM_OCI_USERNAME
HELM_OCI_PASSWORD

原因是:

  • 当前 CircleCI runner 就在 K3s 主机内,部署任务使用任务 Pod 注入的 ServiceAccount Token 访问集群;
  • 当前项目不发布 Helm Chart OCI,因此不需要 Helm OCI 凭据;
  • 不把宿主机 /root/.kube/config 复制到 CI,也不把完整 kubeconfig 编码成变量。

3. 密钥安全规则

  • 不把真实 Token 写入 YAML、Markdown、Git commit、Issue 或 Pipeline 日志;
  • 不在脚本中 echo 密钥;
  • 不把 Docker config、kubeconfig 或 Secret YAML 上传为 artifact;
  • 如果 Token 曾经粘贴到聊天、Issue 或日志,应立即轮换,并更新 CircleCI Context;
  • Registry、Webhook 和 Git 仓库访问凭据均通过 CircleCI Environment Variables 或 Context 管理。

七、self-hosted Container Runner

本项目使用当前 K3s 主机上的 CircleCI Container Runner,而不是 CircleCI 托管的 machine executor。

1. 检查 runner

在 K3s 主机上:

kubectl get pods -A -o wide | grep -Ei 'circleci|runner'

当前 runner 使用的 resource class:

zszweb/k3s-ci

CircleCI job 中应配置:

docker:
  - image: cimg/base:current
resource_class: zszweb/k3s-ci

2. ARM64 兼容

脚本根据 runner 架构下载对应的 kubectl:

case "$(uname -m)" in
  x86_64) KUBECTL_ARCH=amd64 ;;
  aarch64|arm64) KUBECTL_ARCH=arm64 ;;
  *) echo "Unsupported runner architecture: $(uname -m)" >&2; exit 1 ;;
esac

Kaniko 构建阶段同样根据架构选择镜像和执行器。不要默认把 ARM64 runner 当作 amd64 主机使用。

3. Kaniko 与 Docker daemon

当前构建任务使用 privileged runner 容器中的 Kaniko:

  • 不依赖 Docker daemon;
  • 不使用 setup_remote_docker;
  • 在 Kaniko Docker config 中写入 Docker Hub auth;
  • 使用 --destination 推送最终镜像。

如果构建失败,优先检查 runner 是否允许 privileged、/kaniko 是否可执行、Kaniko context 是否包含 Dockerfile 和 nginx.conf,以及 Docker Hub auth 是否写入正确位置。

八、CircleCI Pipeline 设计

当前 .circleci/config.yml 的工作流为:

validate
   │
   └── build_and_push
          ├── dev  ──> deploy_dev
          └── main/master ──> deploy_production

1. 分支与动作

分支validate构建并推送 Docker 镜像Helm 部署
dev是是email-accounts-admin-dev
main是是email-accounts-admin-main
master是是email-accounts-admin-master
其他分支是否否

2. 镜像 tag 规则

镜像 tag 为:

<小写分支名>-<Commit SHA 前 7 位>

例如:

docker.io/zszken/email-accounts-admin:dev-c9f1645

这种 tag 能同时表达发布分支和代码版本,便于回滚与排查。

3. validate 内容

本地可以先运行:

circleci config validate .circleci/config.yml
npm ci
npm run build
helm lint ./helm
helm template email-accounts-admin-dev ./helm \
  --set DEPOT_NAME_LOWER=email-accounts-admin \
  --set image.tag=dev-local \
  --set url=dev-email.zszweb.cn
helm template email-accounts-admin-main ./helm \
  --set DEPOT_NAME_LOWER=email-accounts-admin \
  --set image.tag=main-local \
  --set url=email.zszweb.cn

先通过渲染检查,再让 Pipeline 接触真实集群,可以减少 Helm YAML、域名和参数错误。

九、项目 Helm Chart 结构

本项目 Chart 位于:

helm/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── _helpers.tpl
    ├── emailAccoutsAdmin-deploy.yaml(历史模板文件名,暂不改动)
    ├── emailAccoutsAdmin-ingress.yaml(历史模板文件名,暂不改动)
    ├── emailAccoutsAdmin-svc.yaml(历史模板文件名,暂不改动)
    └── register.key.yaml

1. 动态 release 命名

资源名称由 .Release.Name 推导:

email-accounts-admin-dev-app
email-accounts-admin-main-app
email-accounts-admin-master-app

Deployment、Service、Ingress 同时带有:

app.kubernetes.io/instance: <Release.Name>
app.kubernetes.io/name: <Release.Name>-app
app.kubernetes.io/managed-by: Helm

这使 dev 与 main 能够在同一 namespace 并行运行。

2. 镜像拉取 Secret

CircleCI 部署前会幂等更新 Docker Hub pull secret:

kubectl -n "$HELM_NAMESPACE" create secret docker-registry \
  dockerhubkey-email-accouts-admin \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username="$DOCKERHUB_USERNAME" \
  --docker-password="$DOCKERHUB_TOKEN" \
  --dry-run=client -o yaml | kubectl apply -f -

Helm 使用已有 Secret:

--set imagePullSecret.existingName=dockerhubkey-email-accouts-admin

Chart 默认不会把镜像凭据写进仓库;imagePullSecret.create 只有在明确需要由 Chart 创建 Secret 时才启用。

3. 健康检查

Deployment 配置了 readiness probe 和 liveness probe。修改镜像或端口后,必须同时检查:

kubectl -n default rollout status deploy/email-accounts-admin-dev-app
kubectl -n default get pods -l app.kubernetes.io/instance=email-accounts-admin-dev
kubectl -n default get endpoints/email-accounts-admin-dev-app

十、暂停 GitLab/zszDeploy,切换到 CircleCI

这是一个可逆切换,不删除旧流程。

1. 先确认 CircleCI 新流程

建议按以下顺序:

  1. 提交 .circleci/config.yml 到功能分支;
  2. 确认 validate 成功;
  3. 合并到 dev;
  4. 确认 build_and_push 和 deploy_dev 成功;
  5. 打开 dev 域名确认页面可访问;
  6. 再暂停 GitLab 中指向 zszDeploy 的 Push Webhook。

2. 禁用而不是删除 Webhook

打开:

https://gitlab.com/zszweb/emailAccountsAdmin/-/hooks

找到 URL 指向 zszDeploy 的 Webhook:

  1. 只编辑这一条 Webhook;
  2. 将其设置为 inactive/disabled;
  3. 不删除 Webhook;
  4. 不修改其他 Webhook;
  5. 刷新页面,确认它仍然存在但处于禁用状态。

这样可以保证将来需要恢复 zszDeploy 时,只需要重新启用原 Webhook。

3. 回滚旧流程

如果 CircleCI 出现持续失败:

  1. 暂停 CircleCI 的生产部署;
  2. 确认没有正在执行的生产 Pipeline;
  3. 在 GitLab Webhooks 页面重新启用 zszDeploy Webhook;
  4. 保持 .zsz-ci.yml 不变;
  5. 观察下一次 GitLab push 是否由旧流程接管。

不要为了回滚去删除 CircleCI 配置或清理 zszdeploy-server。

十一、首次上线与 release 改名迁移

如果旧集群已经存在:

emailaccountsadmin-dev
emailaccountsadmin-main

而新配置希望使用:

email-accounts-admin-dev
email-accounts-admin-main
email-accounts-admin-master

不要直接修改或删除旧 release。推荐执行“新建、验证、清理”的迁移顺序。

1. 先部署新 release

CircleCI 自动部署 dev 时使用:

helm upgrade --install email-accounts-admin-dev ./helm \
  --namespace default \
  --set DEPOT_NAME_LOWER=email-accounts-admin \
  --set image.repository=docker.io/zszken \
  --set imagePullSecret.existingName=dockerhubkey-email-accouts-admin \
  --set image.tag=dev-<commit-sha> \
  --set url=dev-email.zszweb.cn

2. 验证新资源

helm status email-accounts-admin-dev -n default
kubectl -n default rollout status deploy/email-accounts-admin-dev-app
kubectl -n default get deploy,svc,ingress,pods \
  -l app.kubernetes.io/instance=email-accounts-admin-dev
curl -kfsS -o /dev/null -w '%{http_code}\n' \
  https://dev-email.zszweb.cn/

3. 再清理旧 release

确认新 Pod、Service、Ingress 和域名都正常后,才执行:

helm uninstall emailaccountsadmin-dev -n default

生产 main 同理,但应先部署并验证 email-accounts-admin-main,再清理 emailaccountsadmin-main。

4. 重复 Ingress 的风险

旧、新 release 同时存在时,如果两者使用同一个 host,例如 email.zszweb.cn,Traefik 可能出现:

  • 503 Service Unavailable;
  • 502 Bad Gateway;
  • 路由指向不确定;
  • 新旧服务之间的证书或 Middleware 冲突。

因此“新 release 部署完成”不等于“迁移完成”。必须在短时间内完成验证和旧 release 清理,然后重新访问域名确认返回 200。

如果新 release 使用全新的 TLS Secret,证书签发可能需要等待;也可以在确认安全的前提下复用已有、未过期且域名匹配的 TLS Secret。

十二、常用运维检查

1. CircleCI 配置

circleci config validate .circleci/config.yml

2. Helm

helm list -A
helm status email-accounts-admin-dev -n default
helm get values email-accounts-admin-dev -n default -a
helm history email-accounts-admin-dev -n default

3. Kubernetes

kubectl get nodes -o wide
kubectl get pods -A
kubectl -n default get deploy,svc,ingress,pods -o wide
kubectl -n default describe pod <pod-name>
kubectl -n default logs deploy/email-accounts-admin-dev-app --tail=100
kubectl -n default get secret

4. 域名与证书

kubectl get certificate,certificaterequest,order,challenge -A
kubectl describe certificate -n default
kubectl -n kube-system logs -l app.kubernetes.io/name=traefik --since=10m
curl -I http://dev-email.zszweb.cn
curl -kI https://dev-email.zszweb.cn

十三、常见故障排查

1. CircleCI 找不到配置

确认:

  • 文件路径是仓库根目录下的 .circleci/config.yml;
  • GitLab/GitHub 项目连接正确;
  • CircleCI 使用的分支包含该文件;
  • YAML 能通过 circleci config validate。

2. Pipeline 只运行 validate,不构建镜像

这是预期行为:当前配置只对 main、master、dev 执行 build_and_push。功能分支只做验证,合并到 dev 后才会构建和部署。

3. Docker Hub 推送失败

检查:

DOCKERHUB_USERNAME
DOCKERHUB_TOKEN

以及 Token 是否具备目标仓库的 push 权限。不要在日志中打印 Token。若 Token 已泄露,先在 Docker Hub 轮换,再更新 CircleCI Context。

4. Pod 为 ImagePullBackOff

kubectl -n default get secret dockerhubkey-email-accouts-admin
kubectl -n default describe pod <pod-name>
kubectl -n default get deploy/<release-name>-app \
  -o jsonpath='{.spec.template.spec.imagePullSecrets}'

检查镜像地址、tag、Secret namespace 和 Docker Hub Token 权限是否一致。

5. Helm 报 ownership metadata 错误

典型原因是旧 release 与新 release 争用同名资源。处理顺序:

  1. 不要强行覆盖生产资源;
  2. 检查 helm list -A;
  3. 使用 .Release.Name 动态命名的 Chart;
  4. 镜像 pull secret 优先使用已有 Secret;
  5. 确认新 release 后再清理旧 release。

6. Ingress 返回 502/503

按顺序检查:

kubectl -n default get ingress
kubectl -n default get svc,endpoints
kubectl -n default get pods -o wide
kubectl -n kube-system logs -l app.kubernetes.io/name=traefik --since=10m

如果旧、新 release 同时绑定同一域名,优先清理已经验证通过的旧 release。若 Service 没有 endpoint,则继续检查 Pod readiness、Service selector 和容器监听端口。

7. cert-manager 不签发证书

kubectl get clusterissuer letsencrypt-prod
kubectl get certificate,order,challenge -A
kubectl describe challenge -A
kubectl -n cert-manager logs deploy/cert-manager --tail=100

常见原因是 DNS 未生效、80 端口未放行、IngressClass 错误、HTTP 被其他代理拦截,或只安装了 CRDs 而没有安装 cert-manager 控制器。

十四、生产安全清单

发布前逐项确认:

  • [ ] CircleCI 项目连接的是正确的 GitHub/GitLab 仓库;
  • [ ] .circleci/config.yml 通过本地验证;
  • [ ] npm run build 成功;
  • [ ] helm lint 和 helm template 成功;
  • [ ] build/deploy Context 已创建且变量名正确;
  • [ ] Token 没有出现在仓库和日志;
  • [ ] self-hosted runner 为在线状态;
  • [ ] runner 架构与镜像构建目标匹配;
  • [ ] circleci-deployer 具备目标 namespace 的最小权限;
  • [ ] Docker Hub pull secret 位于目标 namespace;
  • [ ] DNS、80/443 和 Traefik 正常;
  • [ ] cert-manager ClusterIssuer 为 Ready;
  • [ ] 旧 zszDeploy Webhook 已按计划禁用但没有删除;
  • [ ] 已记录回滚步骤和旧 release 名称;
  • [ ] 生产发布只从 main 或 master 触发。

十五、最终状态

本次项目迁移后的状态为:

GitLab zszweb/emailAccountsAdmin
        │
        ├── .zsz-ci.yml(保留,旧流程可恢复)
        └── .circleci/config.yml
                  │
                  ├── dev   ➔ docker.io/zszken/email-accounts-admin:dev-<sha>
                  │             ➔ email-accounts-admin-dev
                  │             ➔ dev-email.zszweb.cn
                  │
                  └── main/master ➔ docker.io/zszken/email-accounts-admin:<branch>-<sha>
                                      ➔ email-accounts-admin-main/master
                                      ➔ email.zszweb.cn

核心原则只有三条:

  1. 代码仓库只保存流程,不保存凭据;
  2. 新 release 先验证,再删除旧 release;
  3. 旧 zszDeploy 只暂停,不删除,确保能够回滚。

当这三条边界保持清晰时,K3s、Helm、CircleCI 和 GitLab/GitHub 的职责就能相互解耦,后续新增项目也可以复用同一套部署模型。