GitOps 实施文档(ArgoCD + Kustomize 自动发布)
本文档记录当前生产在用的 ArgoCD + Kustomize GitOps 自动发布方案:一个配置仓库统一纳管多个应用(本文以
web-api为例),通用 CI 流程集中在模板库 并按功能模块拆分 维护,源码仓库 push 主干后经 静态检查门禁 → 构建镜像 → 回写配置仓库 → ArgoCD 自动同步 → 飞书卡片通知 完成发布。质检模块按语言拆分(Python / Go / C++ ...),应用按自身语言选择组合入口。关键敏感信息(Token、密码、内网 IP、Webhook 等)一律脱敏,实际值以运维密码库/集群为准,切勿写入 git。
0. 占位符对照
| 占位符 | 含义 | 示例形态 |
|---|---|---|
<GITLAB_HOST> |
GitLab 域名 | gitlab.example.local |
<REGISTRY> |
镜像仓库地址 | <GITLAB_HOST>:5050 |
<CONFIG_REPO> |
GitOps 配置仓库路径 | group/deploy |
<CI_TEMPLATES_REPO> |
CI 模板库仓库路径(存放可复用流水线) | group/ci-templates |
<SOURCE_REPO> |
应用源码仓库路径 | group/<app> |
<APP> |
应用名 | web-api |
<NS_PROD> |
生产命名空间 | devops |
<NODE_IP> |
K8s 节点 IP(NodePort 访问入口) | 10.x.x.x |
<DEPLOY_TOKEN> |
对配置仓库有写权限的 Access Token(CI 回写用) | glpat-******** |
<REPO_READ_TOKEN> |
对配置仓库有读权限的 Token(ArgoCD 用,可与上共用) | glpat-******** |
<RUNNER_TAG> |
指定 CI Runner 的 tag | build_runner_01 |
<FEISHU_WEBHOOK> |
飞书群机器人 Webhook URL | https://open.feishu.cn/open-apis/bot/v2/hook/******** |
<ARGOCD_PWD> |
ArgoCD 初始 admin 密码 | 见密码库 |
<PULL_SECRET> |
命名空间内的镜像拉取密钥 | registry-pull-secret |
1. 方案概述
- 目标 :代码合并到主干后,先跑静态检查门禁,通过才构建镜像,随后自动回写配置仓库触发 ArgoCD 同步到生产,最后把结果推送到飞书。
- 单一事实源 :所有 K8s 清单集中在配置仓库
<CONFIG_REPO>,ArgoCD 只监听它。 - 多应用 :配置仓库按应用分目录(
<APP>/base+<APP>/overlays/prod),每个应用一个 ArgoCD Application。本文以web-api(对外提供 HTTP,含ServiceNodePort)为唯一示例;无对外端口的应用(如消费端)去掉service.yaml即可,结构一致。 - 核心组件 :
- Kustomize :
base + overlays/prod组织清单,overlay 用images.newTag管理镜像 tag(CI 回写此处)。 - ArgoCD :每应用一个 Application,监听配置仓库对应 overlay 路径,
automated(prune + selfHeal)自动同步。 - CI 模板库
<CI_TEMPLATES_REPO>:把 质量门禁 / 构建 / 回写 / 回退 / 通知 拆成按功能划分的可复用模块 (gitops/modules/*.yml),再由分语言组合入口 (pipeline-python.yml/pipeline-go.yml/pipeline-cpp.yml)把「通用模块 + 对应语言质检模块」组装成完整流水线;各应用只include匹配自身语言的入口 + 少量variables覆盖差异(见 §8)。
- Kustomize :
核心认知:ArgoCD 只监听 git(配置仓库),不监听镜像仓库。新镜像产生后,必须由 CI 把新 tag 写进配置仓库产生 git 变更,ArgoCD 才会同步。
DRY 原则:流程只在模板库里维护一份 。改扫描规则/加通知字段/换构建方式,只改模板库;各应用不动(或仅调variables)。用ref锁定模板版本,避免模板变更"隔空"影响所有应用。
说明:当前生产为单一prod环境 。如需 dev/test,只需在各应用下新增overlays/dev、overlays/test并各自加一个 Application,CI 回写到对应 overlay 即可(结构完全一致)。
2. 架构与数据流
#mermaid-svg-gjCTFxj9NZKpDxlS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gjCTFxj9NZKpDxlS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gjCTFxj9NZKpDxlS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gjCTFxj9NZKpDxlS .error-icon{fill:#552222;}#mermaid-svg-gjCTFxj9NZKpDxlS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gjCTFxj9NZKpDxlS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gjCTFxj9NZKpDxlS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gjCTFxj9NZKpDxlS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gjCTFxj9NZKpDxlS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gjCTFxj9NZKpDxlS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gjCTFxj9NZKpDxlS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gjCTFxj9NZKpDxlS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gjCTFxj9NZKpDxlS .marker.cross{stroke:#333333;}#mermaid-svg-gjCTFxj9NZKpDxlS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gjCTFxj9NZKpDxlS p{margin:0;}#mermaid-svg-gjCTFxj9NZKpDxlS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gjCTFxj9NZKpDxlS .cluster-label text{fill:#333;}#mermaid-svg-gjCTFxj9NZKpDxlS .cluster-label span{color:#333;}#mermaid-svg-gjCTFxj9NZKpDxlS .cluster-label span p{background-color:transparent;}#mermaid-svg-gjCTFxj9NZKpDxlS .label text,#mermaid-svg-gjCTFxj9NZKpDxlS span{fill:#333;color:#333;}#mermaid-svg-gjCTFxj9NZKpDxlS .node rect,#mermaid-svg-gjCTFxj9NZKpDxlS .node circle,#mermaid-svg-gjCTFxj9NZKpDxlS .node ellipse,#mermaid-svg-gjCTFxj9NZKpDxlS .node polygon,#mermaid-svg-gjCTFxj9NZKpDxlS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gjCTFxj9NZKpDxlS .rough-node .label text,#mermaid-svg-gjCTFxj9NZKpDxlS .node .label text,#mermaid-svg-gjCTFxj9NZKpDxlS .image-shape .label,#mermaid-svg-gjCTFxj9NZKpDxlS .icon-shape .label{text-anchor:middle;}#mermaid-svg-gjCTFxj9NZKpDxlS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gjCTFxj9NZKpDxlS .rough-node .label,#mermaid-svg-gjCTFxj9NZKpDxlS .node .label,#mermaid-svg-gjCTFxj9NZKpDxlS .image-shape .label,#mermaid-svg-gjCTFxj9NZKpDxlS .icon-shape .label{text-align:center;}#mermaid-svg-gjCTFxj9NZKpDxlS .node.clickable{cursor:pointer;}#mermaid-svg-gjCTFxj9NZKpDxlS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gjCTFxj9NZKpDxlS .arrowheadPath{fill:#333333;}#mermaid-svg-gjCTFxj9NZKpDxlS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gjCTFxj9NZKpDxlS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gjCTFxj9NZKpDxlS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gjCTFxj9NZKpDxlS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gjCTFxj9NZKpDxlS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gjCTFxj9NZKpDxlS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gjCTFxj9NZKpDxlS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gjCTFxj9NZKpDxlS .cluster text{fill:#333;}#mermaid-svg-gjCTFxj9NZKpDxlS .cluster span{color:#333;}#mermaid-svg-gjCTFxj9NZKpDxlS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gjCTFxj9NZKpDxlS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gjCTFxj9NZKpDxlS rect.text{fill:none;stroke-width:0;}#mermaid-svg-gjCTFxj9NZKpDxlS .icon-shape,#mermaid-svg-gjCTFxj9NZKpDxlS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gjCTFxj9NZKpDxlS .icon-shape p,#mermaid-svg-gjCTFxj9NZKpDxlS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gjCTFxj9NZKpDxlS .icon-shape .label rect,#mermaid-svg-gjCTFxj9NZKpDxlS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gjCTFxj9NZKpDxlS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gjCTFxj9NZKpDxlS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gjCTFxj9NZKpDxlS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} include(分语言入口)
通过才继续
commit & push
手动 rollback(whole_commit)
CI 模板库
modules/*.yml + pipeline-.yml
CI quality:
按语言静态检查(硬门禁)
Python:ruff+bandit / Go:golangci+gosec / C++:cppcheck+flawfinder
开发者 push 主干
(SOURCE_REPO)
.gitlab-ci.yml = include + variables
CI build:
docker build & push :sha/:latest
镜像仓库 REGISTRY
CI deploy:
回写 overlays/prod 的 newTag
CONFIG_REPO//overlays/prod
ArgoCD:
NS_PROD
CI notify:
飞书卡片(含扫描结果)
发布流:push 主干 → include 分语言组合入口 → 质量门禁(语言对应扫描) → 构建镜像 → 回写配置仓库 → ArgoCD 自动同步 → 飞书通知;此外每条流水线附带一个手动回退(rollback) job,可一键把该应用回退到上一版本(详见 §8 / §10)。
3. 前置条件
- 可访问的 K8s 集群与管理员 kubeconfig;生产命名空间
<NS_PROD>。 - GitLab(本方案 HTTP 协议访问配置仓库);镜像仓库
<REGISTRY>及命名空间内的拉取密钥<PULL_SECRET>。 - 配置仓库
<CONFIG_REPO>;对其有write_repository、角色 Maintainer 的<DEPLOY_TOKEN>(CI 回写用)。 - CI 模板库
<CI_TEMPLATES_REPO>(存放按功能拆分的模块gitops/modules/*.yml与分语言组合入口gitops/pipeline-<lang>.yml);各源码仓库需能include它(同 GitLab 实例内、对该仓库有读权限即可;私有库可在 include 时用 token 或将其设为 internal)。 - 各源码仓库配置 CI/CD 变量
DEPLOY_TOKEN、FEISHU_BOT_WEBHOOK(见 §8 前置)。 - 可用的 CI Runner(tag
<RUNNER_TAG>),能拉取docker、alpine/git、curlimages/curl及各语言质检镜像(Pythonpython:3.11-slim、Gogolangci/golangci-lint+securego/gosec、C++alpine/silkeh/clang等)。
4. 配置仓库目录结构
<APP>/ # 每个应用一个目录(本文示例 web-api)
base/
kustomization.yaml # resources:configmap.yaml + deployment.yaml(有对外端口再加 service.yaml)
configmap.yaml # 该应用全部环境变量(deployment 用 envFrom 引入,env 不再内联)
deployment.yaml # Deployment;image 不带 tag(占位)由 overlay 注入;env 走 envFrom
service.yaml # 仅对外提供端口的应用需要(无端口的消费端删掉即可)
overlays/
prod/
kustomization.yaml # namespace: <NS_PROD>;images.newTag(CI 回写此处)
argocd/
<APP>-app.yaml # 每个应用一个 ArgoCD Application 清单(留档,可 kubectl apply)
README.md
base/deployment.yaml的image只写仓库名不带 tag:<REGISTRY>/<SOURCE_REPO>。- overlay 用
images.newTag指定实际镜像 tag,初始值设为当前线上镜像 sha,保证首次接管为 no-op。 - 环境差异(副本数、资源、env 等)用 overlay 的
patches覆盖 base,不改 base。
<APP>/overlays/prod/kustomization.yaml 示例:
yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: <NS_PROD>
resources:
- ../../base
images:
- name: <REGISTRY>/<SOURCE_REPO>
newTag: <当前线上镜像 sha>
含密钥的配置(数据库/密钥等)视为敏感,理想上不入 git ;当前为便捷用明文 ConfigMap,后续建议迁移到 Sealed Secrets / 外部密钥方案(敏感项拆到 Secret,用
envFrom.secretRef引入)。
4.1 环境变量独立到 ConfigMap
把应用的环境变量从 deployment.yaml 里内联的 env 列表 抽到独立的 configmap.yaml,Deployment 改用 envFrom.configMapRef 一次性引入。好处:
- 配置与工作负载解耦 :改环境变量只动
configmap.yaml,deployment.yaml保持稳定;变量再多也不会把 Deployment 撑得又长又难读。 - 一处维护:多个容器/副本共享同一份配置;ConfigMap 变更经 ArgoCD 同步(Pod 需重建才加载新值,可配 reloader 或发布时滚动)。
- 敏感项可平滑升级 :将来把密钥类拆到
Secret,再加一条envFrom.secretRef即可,非敏感项仍留 ConfigMap。
4.2 完整示例文件(可直接照抄填充)
以下给出
web-api(HTTP 应用,含 Service)全套 deploy 文件 :configmap.yaml+deployment.yaml(envFrom)+service.yaml+ overlay + ArgoCD Application。所有敏感值(密码、连接串、密钥)均以<脱敏-...>占位,实际以集群/密码库为准,切勿明文入 git 。无对外端口的消费端:删掉service.yaml、去掉 Deployment 的ports/httpGet探针即可,其余一致。
4.2.1 web-api/base/configmap.yaml(所有环境变量集中于此)
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: web-api-config
labels:
app: web-api
data:
APP_ENV: "prod"
API_KEY: "<脱敏-建议迁移到 Secret>"
MYSQL_HOST: "<脱敏-数据库地址>"
MYSQL_PASSWORD: "<脱敏-数据库密码>"
REDIS_URL: "<脱敏-Redis 连接串>"
# ...其余环境变量继续按 KEY: "value" 追加(ConfigMap data 值必须是字符串)
4.2.2 web-api/base/deployment.yaml(env 走 envFrom,不再内联)
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-api
labels:
app: web-api
spec:
replicas: 1
selector:
matchLabels:
app: web-api
template:
metadata:
labels:
app: web-api
annotations:
instrumentation.opentelemetry.io/inject-python: "true" # 如启用 OTel 自动注入,否则删除
spec:
imagePullSecrets:
- name: <PULL_SECRET>
restartPolicy: Always
containers:
- name: web-api
image: <REGISTRY>/<SOURCE_REPO> # 只写仓库名,不带 tag;tag 由 overlay 注入
imagePullPolicy: Always
ports:
- containerPort: 8001
envFrom:
- configMapRef:
name: web-api-config # 环境变量全部来自 ConfigMap(见 4.2.1)
# 将来把密钥拆到 Secret,再加:
# - secretRef:
# name: web-api-secret
livenessProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 1Gi
web-api/base/service.yaml(对外暴露端口才需要)
yaml
apiVersion: v1
kind: Service
metadata:
name: web-api
labels:
app: web-api
spec:
type: NodePort
selector:
app: web-api
ports:
- name: http
port: 8001
targetPort: 8001
nodePort: 30001 # 30000-32767,集群内唯一
web-api/base/kustomization.yaml
yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- configmap.yaml
- deployment.yaml
- service.yaml
web-api/overlays/prod/kustomization.yaml (CI 只回写这里的 newTag)
yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: <NS_PROD>
resources:
- ../../base
images:
- name: <REGISTRY>/<SOURCE_REPO>
newTag: <当前线上镜像 sha> # 初值填线上现有 sha,保证首次接管零漂移
argocd/web-api-app.yaml
yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web-api
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: http://<GITLAB_HOST>/<CONFIG_REPO>.git
targetRevision: main
path: web-api/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: <NS_PROD>
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ApplyOutOfSyncOnly=true
4.2.3 落库后的完整目录树
<CONFIG_REPO>/
├── web-api/
│ ├── base/
│ │ ├── configmap.yaml # 环境变量集中于此
│ │ ├── deployment.yaml # env 走 envFrom
│ │ ├── service.yaml # 无对外端口的应用删掉
│ │ └── kustomization.yaml # resources 含 configmap.yaml
│ └── overlays/prod/
│ └── kustomization.yaml # CI 回写 newTag
├── argocd/
│ └── web-api-app.yaml
└── README.md
新增应用:复制
web-api/目录改名、改configmap.yaml与deployment.yaml里的name/镜像,加一份argocd/<APP>-app.yaml(仅name/path不同)即可。
5. 安装 ArgoCD
bash
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/<版本>/manifests/install.yaml
kubectl -n argocd get pods # 等待全部 Running
5.1 暴露 ArgoCD(NodePort + 关闭内置 TLS,可选)
bash
kubectl -n argocd patch configmap argocd-cmd-params-cm --type merge \
-p '{"data":{"server.insecure":"true"}}'
kubectl -n argocd patch svc argocd-server --type merge -p '{"spec":{"type":"NodePort","ports":[
{"name":"http","port":80,"targetPort":8080,"nodePort":30080,"protocol":"TCP"},
{"name":"https","port":443,"targetPort":8080,"nodePort":30443,"protocol":"TCP"}]}}'
kubectl -n argocd rollout restart deploy argocd-server
-
访问:
http://<NODE_IP>:30080,账号admin,初始密码:bashkubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d -
登录后立即改密码 ,并可删除
argocd-initial-admin-secret。
6. 注册配置仓库凭据(ArgoCD 只需读权限)
yaml
apiVersion: v1
kind: Secret
metadata:
name: repo-config
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
type: Opaque
stringData:
type: git
url: http://<GITLAB_HOST>/<CONFIG_REPO>.git
username: oauth2
password: <REPO_READ_TOKEN>
forceHttpBasicAuth: "true"
该 Secret 含 Token,不要提交进 git ,仅
kubectl apply到集群。
7. 创建 Application(每应用一个)
结构完全一致,仅 name / source.path 不同。<APP>/argocd/<APP>-app.yaml 示例:
yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: <APP>
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: http://<GITLAB_HOST>/<CONFIG_REPO>.git
targetRevision: main
path: <APP>/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: <NS_PROD>
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ApplyOutOfSyncOnly=true
8. CI:模块化模板库 + 分语言组合入口
把流水线按功能拆成独立模块 放进模板库 <CI_TEMPLATES_REPO>:通用模块 (common / build / deploy / rollback / notify,与语言无关)+ 分语言质检模块 (quality-python / quality-go / quality-cpp,各自封装该语言的 lint + 安全扫描)。再由分语言组合入口 (pipeline-python.yml / pipeline-go.yml / pipeline-cpp.yml)把「通用模块 + 对应语言质检」拼成完整流水线。各应用源码仓库的 .gitlab-ci.yml 只做两件事:include 匹配自身语言的入口 + 用 variables 覆盖差异。
为什么按功能模块拆:
- 关注点分离 :改扫描规则只动
quality-*;改构建方式只动build;加通知字段只动notify------互不影响。 - 多语言可插拔 :新增 Go/C++/前端项目 = 加一个
quality-<lang>.yml+ 一个pipeline-<lang>.yml入口,通用模块(build/deploy/rollback/notify)完全复用。 - 通用阶段与语言解耦 :
build(打镜像)、deploy(回写 overlay)、rollback(回退)、notify(飞书卡片)对任何语言都一样,只认「Dockerfile + 配置仓库」,不关心源码语言。
前置:
- 源码仓库 → Settings → CI/CD → Variables 添加
DEPLOY_TOKEN(值<DEPLOY_TOKEN>,Masked;主干受保护则勾 Protected)与FEISHU_BOT_WEBHOOK(值<FEISHU_WEBHOOK>,Masked)。 - 确保源码仓库能读取
<CI_TEMPLATES_REPO>(同实例内、internal 或授权即可)。
8.1 模板库结构
<CI_TEMPLATES_REPO>/
gitops/
modules/
common.yml # stages + 公共 variables + .on-master-push(触发条件)
quality-python.yml # Python 质检:ruff + bandit(硬门禁)
quality-go.yml # Go 质检:golangci-lint + gosec(硬门禁)
quality-cpp.yml # C++ 质检:cppcheck + flawfinder(硬门禁)
build.yml # docker build & push(与语言无关)
deploy.yml # 回写 overlay 的 newTag(记录 PREV 供回退)
rollback.yml # 手动回退(whole_commit)
notify.yml # 飞书成功/失败卡片
pipeline-python.yml # 组合入口:common + quality-python + build + deploy + rollback + notify
pipeline-go.yml # 组合入口:common + quality-go + build + deploy + rollback + notify
pipeline-cpp.yml # 组合入口:common + quality-cpp + build + deploy + rollback + notify
README.md
约定:通用模块与语言无关 ,质检模块每语言一份、只需产出统一的门禁结果 + 一行
SCAN_SUMMARY(写入scan.env,dotenv 传给 notify 卡片)。所有质检 job 的stage均为quality;build不设needs,靠 stage 顺序等待整个quality阶段全过------因此换语言只换质检模块,门禁逻辑天然成立。
8.2 通用模块
gitops/modules/common.yml
yaml
stages: [ quality, build, deploy, rollback, notify ]
variables:
# ------ 可被各应用覆盖的参数(下方为默认值)------
APP_NAME: "$CI_PROJECT_NAME" # 卡片标题/回写提交信息用,默认取仓库名
SRC_DIR: "src" # 代码扫描目录(web-api 用 backend;Go 常为 .)
RUNNER_TAG: "<RUNNER_TAG>" # CI Runner tag
REGISTRY_URL: "<REGISTRY>" # 镜像仓库地址
DEPLOY_REPO_HOST: "<GITLAB_HOST>" # 配置仓库主机
DEPLOY_REPO_PATH: "<CONFIG_REPO>.git" # 配置仓库路径
OVERLAY_FILE: "$CI_PROJECT_NAME/overlays/prod/kustomization.yaml" # 回写目标;名≠目录时覆盖
APP_DIR: "$CI_PROJECT_NAME" # 配置仓库中该应用目录(回退时整目录还原);名≠目录时覆盖
IMAGE_TAG: "$REGISTRY_URL/$CI_PROJECT_PATH"
# ------ 需在各应用 CI 变量里配置(勿硬编码)------
# DEPLOY_TOKEN 对 <CONFIG_REPO> 有写权限的 token
# FEISHU_BOT_WEBHOOK 飞书机器人 webhook
# 统一触发条件:仅主干 push(供各 job extends)
.on-master-push:
rules:
- if: '$CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "push"'
gitops/modules/build.yml
yaml
# 质量通过才构建:不设 needs,靠 stage 顺序等待整个 quality 阶段全过(与语言无关)
build-and-push:
extends: .on-master-push
stage: build
image: docker:25.0
tags: [ "$RUNNER_TAG" ]
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" -u "$CI_REGISTRY_USER" --password-stdin
script:
- docker build -t "$IMAGE_TAG:$CI_COMMIT_SHA" .
- docker tag "$IMAGE_TAG:$CI_COMMIT_SHA" "$IMAGE_TAG:latest"
- docker push "$IMAGE_TAG:$CI_COMMIT_SHA"
- docker push "$IMAGE_TAG:latest"
after_script:
- docker rmi "$IMAGE_TAG:$CI_COMMIT_SHA" "$IMAGE_TAG:latest" || true
gitops/modules/deploy.yml
yaml
# 回写配置仓库触发 ArgoCD;同时记录本次发布前 HEAD(回退目标)
update-manifest:
extends: .on-master-push
stage: deploy
image:
name: alpine/git:latest
entrypoint: [""] # 该镜像默认 entrypoint 是 git,需清空
tags: [ "$RUNNER_TAG" ]
script:
- git config --global user.email "gitlab-ci@local"
- git config --global user.name "devops-bot"
- git clone "http://oauth2:${DEPLOY_TOKEN}@${DEPLOY_REPO_HOST}/${DEPLOY_REPO_PATH}" deploy
- cd deploy
- PREV_DEPLOY_COMMIT=$(git rev-parse HEAD) # = 上一版本回退目标
- echo "PREV_DEPLOY_COMMIT=${PREV_DEPLOY_COMMIT}" >> "$CI_PROJECT_DIR/deploy.env"
- sed -i "s#^\(\s*newTag:\).*#\1 ${CI_COMMIT_SHA}#" "${OVERLAY_FILE}"
- git add -A
- |
if git diff --cached --quiet; then
echo "镜像 tag 未变化,跳过提交"
else
git commit -m "deploy ${APP_NAME} ${CI_COMMIT_SHA}"
git push origin HEAD:main
fi
artifacts:
when: always
reports:
dotenv: deploy.env # 把 PREV_DEPLOY_COMMIT 传给 rollback
gitops/modules/rollback.yml
yaml
# 手动回退到上一版本(whole_commit:镜像 tag + 该应用其余清单一起回退)
# when:manual + 非阻塞:本次发布正常则无需理会(不点不跑,流水线照常成功);
# 发现问题时进本条流水线点该 job 的 ▶ 运行即可回退(跑完后仍可后补点击)。
# 原理:ArgoCD selfHeal=true、git 是唯一真相源,回退必须改配置仓库;
# 把该应用目录整体还原到 PREV_DEPLOY_COMMIT,以正向新 commit 覆盖推 main,ArgoCD 自动同步。
rollback:
extends: .on-master-push # 仅提供 if 条件,when 在下方 rules 覆盖为 manual
stage: rollback
image:
name: alpine/git:latest
entrypoint: [""]
tags: [ "$RUNNER_TAG" ]
needs:
- job: update-manifest
artifacts: true # 继承 dotenv 里的 PREV_DEPLOY_COMMIT
resource_group: ${APP_NAME}-deploy # 与发布/多次回退串行,防来回横跳
script:
- test -n "${PREV_DEPLOY_COMMIT}" || (echo "缺少 PREV_DEPLOY_COMMIT,无法回退" && exit 1)
- git config --global user.email "gitlab-ci@local"
- git config --global user.name "devops-bot"
- git clone "http://oauth2:${DEPLOY_TOKEN}@${DEPLOY_REPO_HOST}/${DEPLOY_REPO_PATH}" deploy
- cd deploy
- git checkout "${PREV_DEPLOY_COMMIT}" -- "${APP_DIR}/" # 整目录还原(newTag + 其余清单成套回退)
- git add -A
- |
if git diff --cached --quiet; then
echo "当前已是该版本,无需回退"
else
git commit -m "rollback ${APP_NAME} to ${PREV_DEPLOY_COMMIT} (undo pipeline ${CI_PIPELINE_ID})"
git push origin HEAD:main
fi
# 回退结果卡片:用镜像自带 busybox wget,免 apk
- |
cat > rb.json <<EOF
{"msg_type":"interactive","card":{"config":{"wide_screen_mode":true},
"header":{"template":"orange","title":{"tag":"plain_text","content":"⏮ ${APP_NAME} 已回退"}},
"elements":[{"tag":"div","text":{"tag":"lark_md","content":"**已撤销发布**\n流水线 ${CI_PIPELINE_ID}(${CI_COMMIT_SHORT_SHA})\n**回退到** \`${PREV_DEPLOY_COMMIT}\`\n操作人:${GITLAB_USER_NAME}"}}]}}
EOF
wget -q -O - --header="Content-Type: application/json" --post-data="$(cat rb.json)" "${FEISHU_BOT_WEBHOOK}" || true
rules:
- if: '$CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "push"'
when: manual
allow_failure: true
gitops/modules/notify.yml
卡片里的
${SCAN_SUMMARY}由各语言质检模块写入scan.env(一行摘要),故 notify 与语言无关 。notify-success不设needs------dotenv 变量会自动从更早阶段(quality/deploy)传入,无需按语言写死 job 名。
yaml
.notify-base:
stage: notify
image:
name: curlimages/curl:latest # 镜像自带 curl,免 apk 安装(内网拉 apk 源很慢)
entrypoint: [""] # 该镜像默认 entrypoint 是 curl,需清空
tags: [ "$RUNNER_TAG" ]
notify-success:
extends: .notify-base # 不设 needs:自动继承 quality/deploy 的 dotenv(SCAN_SUMMARY 等)
script:
- |
SAFE_TITLE=$(printf '%s' "$CI_COMMIT_TITLE" | sed 's/\\/\\\\/g; s/"/\\"/g')
cat > card.json <<EOF
{
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": true},
"header": {"template": "green", "title": {"tag": "plain_text", "content": "✅ CI流程通过 · ${APP_NAME}"}},
"elements": [
{"tag": "div", "fields": [
{"is_short": true, "text": {"tag": "lark_md", "content": "**🔖 提交**\n${CI_COMMIT_SHORT_SHA} (${CI_COMMIT_REF_NAME})"}},
{"is_short": true, "text": {"tag": "lark_md", "content": "**👤 提交人**\n${GITLAB_USER_NAME}"}}
]},
{"tag": "div", "text": {"tag": "lark_md", "content": "**📝 说明**\n${SAFE_TITLE}"}},
{"tag": "hr"},
{"tag": "div", "text": {"tag": "lark_md", "content": "**🔍 代码扫描**\n${SCAN_SUMMARY}"}},
{"tag": "div", "text": {"tag": "lark_md", "content": "**⏮ 如需回退本次发布**:点下方按钮进入流水线,运行 **rollback**(手动)任务即可回退到上一版本。"}},
{"tag": "action", "actions": [{"tag": "button", "text": {"tag": "plain_text", "content": "查看流水线 / 回退"}, "url": "${CI_PIPELINE_URL}", "type": "primary"}]}
]
}
}
EOF
curl -sS -X POST "${FEISHU_BOT_WEBHOOK}" -H 'Content-Type: application/json' -d @card.json
rules:
- if: '$CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "push"'
when: on_success
notify-failure: # catch-all,不设 needs,任一阶段失败即触发
extends: .notify-base
script:
- |
SAFE_TITLE=$(printf '%s' "$CI_COMMIT_TITLE" | sed 's/\\/\\\\/g; s/"/\\"/g')
cat > card.json <<EOF
{
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": true},
"header": {"template": "red", "title": {"tag": "plain_text", "content": "❌ CI流程失败 · ${APP_NAME}"}},
"elements": [
{"tag": "div", "fields": [
{"is_short": true, "text": {"tag": "lark_md", "content": "**🔖 提交**\n${CI_COMMIT_SHORT_SHA} (${CI_COMMIT_REF_NAME})"}},
{"is_short": true, "text": {"tag": "lark_md", "content": "**👤 提交人**\n${GITLAB_USER_NAME}"}}
]},
{"tag": "div", "text": {"tag": "lark_md", "content": "**📝 说明**\n${SAFE_TITLE}"}},
{"tag": "hr"},
{"tag": "div", "text": {"tag": "lark_md", "content": "⚠️ 可能原因:代码扫描未通过 / 构建失败,请点击下方按钮查看日志。"}},
{"tag": "action", "actions": [{"tag": "button", "text": {"tag": "plain_text", "content": "查看失败原因"}, "url": "${CI_PIPELINE_URL}", "type": "danger"}]}
]
}
}
EOF
curl -sS -X POST "${FEISHU_BOT_WEBHOOK}" -H 'Content-Type: application/json' -d @card.json
rules:
- if: '$CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "push"'
when: on_failure
8.3 分语言质检模块
统一约定:每个质检模块 = 一个
stage: quality的 job ,内部先跑「全量扫描(不阻断,统计并写一行SCAN_SUMMARY到scan.env)」,再跑「硬门禁命令(真错误 / 中高危漏洞 → 非 0 退出即失败)」。下方级别/规则集为参考值,按项目微调。
gitops/modules/quality-python.yml(ruff + bandit)
yaml
quality-python:
extends: .on-master-push
stage: quality
image: python:3.11-slim
tags: [ "$RUNNER_TAG" ]
variables:
RUFF_SELECT: "E9,F63,F7,F82" # 硬门禁:只卡真错误(语法/未定义名/逻辑错误)
script:
- pip install --no-cache-dir ruff bandit
# ------ 全量统计(不阻断)------
- ruff check "$SRC_DIR" -q --output-format=concise > ruff.txt || true
- RUFF_TOTAL=$(wc -l < ruff.txt | tr -d ' ')
- bandit -r "$SRC_DIR" -f json -o bandit.json || true
- |
B=$(python - <<'PY'
import json
d=json.load(open("bandit.json")); s={"HIGH":0,"MEDIUM":0,"LOW":0}
for r in d.get("results",[]): s[r["issue_severity"]]=s.get(r["issue_severity"],0)+1
print(f"{s['HIGH']}/{s['MEDIUM']}/{s['LOW']}")
PY
)
echo "SCAN_SUMMARY=Ruff 通过(${RUFF_TOTAL} 处可优化)|Bandit 高/中/低 ${B}" >> "$CI_PROJECT_DIR/scan.env"
# ------ 硬门禁 ------
- ruff check "$SRC_DIR" --select "$RUFF_SELECT" # 真错误即失败
- bandit -r "$SRC_DIR" -ll # 中/高危即失败
artifacts:
when: always
reports:
dotenv: scan.env
gitops/modules/quality-go.yml(golangci-lint + gosec)
yaml
quality-go:
extends: .on-master-push
stage: quality
# 建议自建"质检镜像"预装 golangci-lint + gosec(内网 go install 慢);此处用官方 lint 镜像 + go run 兜底
image: golangci/golangci-lint:latest
tags: [ "$RUNNER_TAG" ]
variables:
GO_TARGET: "./..."
script:
- cd "$SRC_DIR" # go.mod 所在目录(应用可将 SRC_DIR 设为 .)
# ------ 全量统计(不阻断)------
- golangci-lint run "$GO_TARGET" --out-format=line-number > golint.txt 2>&1 || true
- GO_TOTAL=$(grep -c ':' golint.txt || echo 0)
- go run github.com/securego/gosec/v2/cmd/gosec@latest -fmt=json -out=gosec.json "$GO_TARGET" || true
- GO_HIGH=$(grep -o '"severity": *"HIGH"' gosec.json | wc -l | tr -d ' ')
- GO_MED=$(grep -o '"severity": *"MEDIUM"' gosec.json | wc -l | tr -d ' ')
- echo "SCAN_SUMMARY=golangci-lint 通过(${GO_TOTAL} 处可优化)|gosec 高/中 ${GO_HIGH}/${GO_MED}" >> "$CI_PROJECT_DIR/scan.env"
# ------ 硬门禁 ------
- golangci-lint run "$GO_TARGET" # lint 有问题即失败(含 govet/staticcheck)
- go run github.com/securego/gosec/v2/cmd/gosec@latest -severity medium "$GO_TARGET" # 中/高危即失败
artifacts:
when: always
reports:
dotenv: scan.env
gitops/modules/quality-cpp.yml(cppcheck + flawfinder)
yaml
quality-cpp:
extends: .on-master-push
stage: quality
image: ubuntu:22.04 # 建议自建预装 cppcheck/flawfinder(+可选 clang-tidy)的质检镜像
tags: [ "$RUNNER_TAG" ]
variables:
CPP_PATHS: "src include" # 参与扫描的目录
before_script:
- apt-get update -y && apt-get install -y --no-install-recommends cppcheck flawfinder >/dev/null
script:
# ------ 全量统计(不阻断)------
- cppcheck --enable=warning,portability --xml --xml-version=2 $CPP_PATHS 2> cppcheck.xml || true
- CPP_TOTAL=$(grep -c '<error ' cppcheck.xml || echo 0)
- flawfinder --quiet --dataonly --minlevel=1 $CPP_PATHS > flaw.txt || true
- FLAW_TOTAL=$(grep -c 'hits' flaw.txt || echo 0)
- echo "SCAN_SUMMARY=cppcheck 通过(${CPP_TOTAL} 处告警)|flawfinder 命中 ${FLAW_TOTAL}" >> "$CI_PROJECT_DIR/scan.env"
# ------ 硬门禁 ------
- cppcheck --enable=warning --error-exitcode=1 $CPP_PATHS # 出现 error 即失败
- flawfinder --error-level=4 $CPP_PATHS # 高危(≥4)命中即失败(新版 flawfinder 支持)
artifacts:
when: always
reports:
dotenv: scan.env
8.4 分语言组合入口
各入口用 include: local 把「通用模块 + 对应语言质检模块」拼起来(local 在模板库自身内、按 include 时锁定的 ref 解析)。
gitops/pipeline-python.yml
yaml
include:
- local: '/gitops/modules/common.yml'
- local: '/gitops/modules/quality-python.yml'
- local: '/gitops/modules/build.yml'
- local: '/gitops/modules/deploy.yml'
- local: '/gitops/modules/rollback.yml'
- local: '/gitops/modules/notify.yml'
gitops/pipeline-go.yml / gitops/pipeline-cpp.yml
与上面只差质检模块那一行 (quality-go.yml / quality-cpp.yml),其余完全相同:
yaml
# pipeline-go.yml
include:
- local: '/gitops/modules/common.yml'
- local: '/gitops/modules/quality-go.yml' # ← 换成对应语言
- local: '/gitops/modules/build.yml'
- local: '/gitops/modules/deploy.yml'
- local: '/gitops/modules/rollback.yml'
- local: '/gitops/modules/notify.yml'
8.5 各应用最小 .gitlab-ci.yml
web-api(Python,扫描目录 backend):
yaml
include:
- project: '<CI_TEMPLATES_REPO>'
ref: v1 # 建议锁 tag/commit,避免模板变更"隔空"影响
file: '/gitops/pipeline-python.yml'
variables:
SRC_DIR: backend
某 Go 应用(go.mod 在仓库根):
yaml
include:
- project: '<CI_TEMPLATES_REPO>'
ref: v1
file: '/gitops/pipeline-go.yml'
variables:
SRC_DIR: .
某 C++ 应用(扫描 src include):
yaml
include:
- project: '<CI_TEMPLATES_REPO>'
ref: v1
file: '/gitops/pipeline-cpp.yml'
variables:
CPP_PATHS: "src include"
8.6 关键设计说明
- 按功能模块拆分 :
common/build/deploy/rollback/notify与语言无关、全应用复用;quality-<lang>每语言一份、只封装该语言的 lint+安全扫描。改哪块只动哪块文件。 - 分语言入口 :
pipeline-<lang>.yml= 通用模块 + 对应质检模块;应用按语言include其一。新增语言 = 加quality-<lang>.yml+pipeline-<lang>.yml两文件,通用模块零改动。 - 门禁天然成立 :
build不写needs,靠 stage 顺序等整个quality阶段全过才构建;故换语言换质检 job 名也不影响门禁。 - 质检产出统一 :每个
quality-<lang>写一行SCAN_SUMMARY到scan.env(dotenv);notify-success不设needs、自动继承该变量渲染卡片------通知层与语言彻底解耦。 - DRY / 参数化 :
APP_NAME/OVERLAY_FILE/APP_DIR默认按仓库名推导,应用名=配置仓库目录名时零配置 ;差异用应用variables覆盖(同名以应用为准)。敏感值(DEPLOY_TOKEN/FEISHU_BOT_WEBHOOK)走项目 CI 变量,不入任何 yaml。 - 版本锁定 :应用
include用ref: <tag/commit>锁模板版本;改模块 → 打新 tag → 单应用验证 → 全量 bump ref,避免"改模板即影响所有应用"。 - 手动回退(whole_commit) :
rollbackwhen:manual+allow_failure:true,默认不跑不影响成功;update-manifest把发布前 HEAD 写入deploy.env传给它,git checkout $PREV -- $APP_DIR/整目录还原后正向 commit 覆盖推main,resource_group串行化。 - 免 apk 提速 :notify 用
curlimages/curl、rollback 卡片用alpine/git自带busybox wget,均entrypoint: [""],避免内网apk add curl卡顿。 - 通知措辞:本 CI 只做检查/构建/回写,标题用「CI流程通过/失败」,不用「发布」(真正上线由 ArgoCD 同步完成)。
- 失败通知不设 needs :
notify-failure为 catch-all,覆盖含扫描未过在内的任意阶段失败。 - 质检镜像建议 :Go/C++ 若内网拉工具慢,建议自建预装工具的质检镜像 (如
golangci-lint+gosec、cppcheck+flawfinder),把image换成它,删掉before_script/go run @latest的临时安装步骤。 - 应用私有 job :应用仍可在自身
.gitlab-ci.yml追加/覆盖 job(同名覆盖、异名新增),模板只负责通用部分。
8.7 迁移 / 扩展
从「内联 CI」迁移:
- 在
<CI_TEMPLATES_REPO>落地gitops/modules/*.yml与pipeline-<lang>.yml(§8.1--8.4),打 tag(如v1)。 - 各应用把大段
.gitlab-ci.yml换成 §8.5 的 include 版;确认项目 CI 变量DEPLOY_TOKEN/FEISHU_BOT_WEBHOOK已配置。 - push 一次验证 quality→build→deploy→notify 正常、飞书收到卡片;确认后删掉历史内联 job。
新增一门语言(如 Rust):
- 加
gitops/modules/quality-rust.yml(镜像用rust:slim,跑cargo clippy -- -D warnings作门禁 +cargo audit,写一行SCAN_SUMMARY)。 - 加
gitops/pipeline-rust.yml(复制任一入口,仅把质检那行换成quality-rust.yml)。 - 该语言的应用
include: /gitops/pipeline-rust.yml即可,通用模块无需改动。
9. 首次纳管
bash
# 1) 用 overlay 渲染结果与线上对比,确认零漂移再纳管
kubectl kustomize <APP>/overlays/prod
kubectl diff -k <APP>/overlays/prod # 期望无实质差异
# 2) 应用 Application(含 Token 的 repo Secret 见 §6,另行 apply)
kubectl apply -f argocd/<APP>-app.yaml
# 3) 确认同步与健康
kubectl -n argocd get application <APP> # Synced / Healthy
kubectl -n <NS_PROD> get pods -l app=<APP>
⚠️ 注意:即便
kubectl diff -k显示零漂移,首次被 ArgoCD 纳管时仍可能触发一次滚动 (例如线上资源曾被 k8slens/kubectl 直接编辑,导致last-applied-configuration与实际 spec 不一致,ArgoCD 接管后按声明重新对齐,pod-template 归一化引发一次 RollingUpdate)。对无状态服务通常无感;对消费端类应用(若有幂等/去重机制,如 RedisSETNX),短暂双实例并存也不会重复处理。首次纳管建议在低峰期执行并观察一次滚动结果。
10. 日常使用与回滚
- 日常发布:改代码 → push 主干 → 门禁通过 → 自动构建 + 回写 → ArgoCD 同步 → 飞书通知。
- 回滚 (按推荐优先级):
- 流水线手动 rollback(首选) :进对应流水线(成功卡片「查看流水线 / 回退」按钮直达),点
rollbackjob 的 ▶ 运行。它按whole_commit把该应用回退到本次发布前的上一版本(镜像 tag + 该应用其余清单成套回退),推回配置仓库后 ArgoCD 自动同步,并发飞书回退卡片。无需登录集群或 ArgoCD。 - ArgoCD UI:选历史同步版本 Rollback(临时手段,注意 selfHeal 下仍以 git 为准)。
- Git 层面:
git revert配置仓库对应回写提交(保持 git 为唯一事实源)。
- 流水线手动 rollback(首选) :进对应流水线(成功卡片「查看流水线 / 回退」按钮直达),点
- 状态查看 :ArgoCD UI(
http://<NODE_IP>:30080)查看资源树、同步历史、健康状态;飞书群收发布结果卡片。
11. 踩坑记录汇总
| 问题 | 现象 | 解决 |
|---|---|---|
alpine/git / gitleaks 等镜像 entrypoint |
git: 'sh' is not a git command 之类 |
镜像声明加 entrypoint: [""] |
| 受保护分支推送被拒 | not allowed to push to protected branches |
<DEPLOY_TOKEN> 提为 Maintainer / 放开分支推送 |
| Token 轮换 | 旧 Token 失效 | 同步更新 CI 变量 DEPLOY_TOKEN 与 ArgoCD repo Secret 两处 |
| ruff 在 pipefail 下整行失败 | lint job 在统计行退出 1、scan.env: no matching files |
runner 默认 set -o pipefail,ruff 有告警返回非0;改为先写文件(`... > f |
| curl 行被 YAML 误解析 | 'Content-Type: application/json' 里的冒号使该 script 项被解析为 dict |
把 curl 与生成 payload 的 heredoc 放进同一个 `- |
| 首次纳管触发滚动 | diff -k 零漂移但 apply 后仍新建 ReplicaSet |
线上曾被直接编辑致 last-applied-configuration 不一致;低峰期纳管、接受一次归一化滚动即可 |
飞书返回 params error, msg_type need |
直接 GET / 缺字段 | POST 且 body 含 msg_type(文本 text 或卡片 interactive) |
| include 模板未锁 ref | 模板一改,全部应用下次 pipeline 隐式变更 | include 固定 ref: <tag/commit>,模板升级走"改 tag → 单应用验证 → 全量 bump" |
| 应用 variables 未生效 | 覆盖参数没起作用 | 确认写在应用 variables:(覆盖模板默认);敏感值用项目 CI 变量;注意 include 的 file 路径以 / 开头 |
apk add curl 卡很久 |
notify 卡在 apk add --no-cache curl 长时间无输出 |
内网拉 Alpine 源慢;改用自带工具的镜像:notify 用 curlimages/curl、rollback 卡片用 alpine/git 自带的 busybox wget,均加 entrypoint: [""] |
rollback 提示缺 PREV_DEPLOY_COMMIT |
手动跑 rollback 报"无法回退" | 该值由 update-manifest 写入 deploy.env;确认 rollback needs: [{job: update-manifest, artifacts: true}] 且 update-manifest 有 artifacts.reports.dotenv |
12. 安全与维护提醒(脱敏要求)
- 含密钥的文件(repo Secret、业务 Deployment 的明文 env)不入 git;如需入库引入 Sealed Secrets / 外部密钥方案。
- 文档、README、issue 中一律使用占位符,禁止 出现真实 Token(
glpat-*)、密码、内网 IP、数据库/Redis 连接串、飞书 App Secret/Webhook 等。 - 模板库与各应用 CI 里不硬编码
DEPLOY_TOKEN/FEISHU_BOT_WEBHOOK,统一走项目 CI 变量(Masked)。 - ArgoCD admin 初始密码登录后立即修改;
argocd-initial-admin-secret可删除。 - Token 到期前轮换,并同步更新「CI 变量
DEPLOY_TOKEN」与「ArgoCD repo Secret」两处。 - 飞书 Webhook 泄露会被人任意发消息,建议放 CI 变量(Masked)而非硬编码;已泄露则在飞书机器人设置中重置。