GitOps最佳实践 (gitlab ci + ArgoCD)

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,含 Service NodePort)为唯一示例;无对外端口的应用(如消费端)去掉 service.yaml 即可,结构一致。
  • 核心组件
    • Kustomizebase + 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)。

核心认知:ArgoCD 只监听 git(配置仓库),不监听镜像仓库。新镜像产生后,必须由 CI 把新 tag 写进配置仓库产生 git 变更,ArgoCD 才会同步。
DRY 原则:流程只在模板库里维护一份 。改扫描规则/加通知字段/换构建方式,只改模板库;各应用不动(或仅调 variables)。用 ref 锁定模板版本,避免模板变更"隔空"影响所有应用。
说明:当前生产为单一 prod 环境 。如需 dev/test,只需在各应用下新增 overlays/devoverlays/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_TOKENFEISHU_BOT_WEBHOOK(见 §8 前置)。
  • 可用的 CI Runner(tag <RUNNER_TAG>),能拉取 dockeralpine/gitcurlimages/curl 及各语言质检镜像(Python python:3.11-slim、Go golangci/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.yamlimage 只写仓库名不带 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.yamldeployment.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.yamldeployment.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,初始密码:

    bash 复制代码
    kubectl -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 均为 qualitybuild 不设 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_SUMMARYscan.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_SUMMARYscan.env(dotenv);notify-success 不设 needs、自动继承该变量渲染卡片------通知层与语言彻底解耦。
  • DRY / 参数化APP_NAME/OVERLAY_FILE/APP_DIR 默认按仓库名推导,应用名=配置仓库目录名时零配置 ;差异用应用 variables 覆盖(同名以应用为准)。敏感值(DEPLOY_TOKEN/FEISHU_BOT_WEBHOOK)走项目 CI 变量,不入任何 yaml。
  • 版本锁定 :应用 includeref: <tag/commit> 锁模板版本;改模块 → 打新 tag → 单应用验证 → 全量 bump ref,避免"改模板即影响所有应用"。
  • 手动回退(whole_commit)rollback when:manual+allow_failure:true,默认不跑不影响成功;update-manifest 把发布前 HEAD 写入 deploy.env 传给它,git checkout $PREV -- $APP_DIR/ 整目录还原后正向 commit 覆盖推 mainresource_group 串行化。
  • 免 apk 提速 :notify 用 curlimages/curl、rollback 卡片用 alpine/git 自带 busybox wget,均 entrypoint: [""],避免内网 apk add curl 卡顿。
  • 通知措辞:本 CI 只做检查/构建/回写,标题用「CI流程通过/失败」,不用「发布」(真正上线由 ArgoCD 同步完成)。
  • 失败通知不设 needsnotify-failure 为 catch-all,覆盖含扫描未过在内的任意阶段失败。
  • 质检镜像建议 :Go/C++ 若内网拉工具慢,建议自建预装工具的质检镜像 (如 golangci-lint+goseccppcheck+flawfinder),把 image 换成它,删掉 before_script/go run @latest 的临时安装步骤。
  • 应用私有 job :应用仍可在自身 .gitlab-ci.yml 追加/覆盖 job(同名覆盖、异名新增),模板只负责通用部分。

8.7 迁移 / 扩展

从「内联 CI」迁移

  1. <CI_TEMPLATES_REPO> 落地 gitops/modules/*.ymlpipeline-<lang>.yml(§8.1--8.4),打 tag(如 v1)。
  2. 各应用把大段 .gitlab-ci.yml 换成 §8.5 的 include 版;确认项目 CI 变量 DEPLOY_TOKEN/FEISHU_BOT_WEBHOOK 已配置。
  3. push 一次验证 quality→build→deploy→notify 正常、飞书收到卡片;确认后删掉历史内联 job。

新增一门语言(如 Rust)

  1. gitops/modules/quality-rust.yml(镜像用 rust:slim,跑 cargo clippy -- -D warnings 作门禁 + cargo audit,写一行 SCAN_SUMMARY)。
  2. gitops/pipeline-rust.yml(复制任一入口,仅把质检那行换成 quality-rust.yml)。
  3. 该语言的应用 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)。对无状态服务通常无感;对消费端类应用(若有幂等/去重机制,如 Redis SETNX),短暂双实例并存也不会重复处理。首次纳管建议在低峰期执行并观察一次滚动结果。

10. 日常使用与回滚

  • 日常发布:改代码 → push 主干 → 门禁通过 → 自动构建 + 回写 → ArgoCD 同步 → 飞书通知。
  • 回滚 (按推荐优先级):
    • 流水线手动 rollback(首选) :进对应流水线(成功卡片「查看流水线 / 回退」按钮直达),点 rollback job 的 ▶ 运行。它按 whole_commit 把该应用回退到本次发布前的上一版本(镜像 tag + 该应用其余清单成套回退),推回配置仓库后 ArgoCD 自动同步,并发飞书回退卡片。无需登录集群或 ArgoCD。
    • ArgoCD UI:选历史同步版本 Rollback(临时手段,注意 selfHeal 下仍以 git 为准)。
    • Git 层面:git revert 配置仓库对应回写提交(保持 git 为唯一事实源)。
  • 状态查看 :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)而非硬编码;已泄露则在飞书机器人设置中重置。
相关推荐
可乐ea2 小时前
Anthropic 的 CI/CD 值班智能体:Claude Tag 当一线响应者的架构拆解与踩坑复盘
ci/cd·架构·claude·devops·ai智能体·mcp
数据库技术讲堂4 小时前
从 GitOps 到数据库变更:NineData 如何打通 CI/CD 的数据库治理链路
java·数据库·ci/cd
汪海游龙21 小时前
本地源码还是 Maven 版本?Gradle composite build 双轨依赖的正确接法
android·ci/cd·kotlin
Zadig1 天前
Zadig 全面支持 CRD,至此所有 K8s 资源类型均可一键发布!
后端·devops
Ashley的成长之路1 天前
前端性能优化实战手册·第5篇(最终篇):性能监控与 CI/CD 集成
前端·ci/cd·性能优化
小小测试开发3 天前
AI Agent 回归测试:Replay 录一次、CI 跑千遍,像 Jest 一样给非确定性系统写断言
人工智能·ci/cd
JavaDog程序狗4 天前
【指南】uni-app微信小程序多环境CI-CD完全指南
ci/cd·uni-app·jenkins
Patrick_Wilson5 天前
sccache 用在 Rust 上为什么常「不省编译」:原理、限制与 Windows 接入
ci/cd·rust·编译器
北风toto5 天前
研发效能与后端核心技术全景指南:从CI/CD到共性组件实战
java·开发语言·ci/cd