跳过正文

ArgoCD + Kustomize GitOps 体系实践

·2128 字·10 分钟·
目录

为什么要用 GitOps
#

在真正落地 GitOps 之前,我们的发版流程大概是这样的:CI 构建镜像、推送到镜像仓库,然后 Jenkins Pipeline 执行 kubectl set image 更新 Deployment。表面上看没什么问题,但随着环境数量增加(测试、预发、多套生产),问题开始暴露出来。

配置漂移
#

最典型的问题:某人在排查问题时直接 kubectl edit deployment 改了副本数或环境变量,没有同步回仓库。几周后另一个同事做发布,把这个"临时修改"覆盖掉了,问题重新出现,排查了半天才发现原因。

kubectl set image 这类命令改的是集群里的实际状态,但 Git 里的 YAML 文件并不知道这件事。时间久了,集群实际运行的配置和 Git 里的声明之间就产生了不可见的漂移。

环境一致性难以保证
#

多套环境,每套都有自己微妙的差别。测试环境的 replica 是 1,生产是 3;不同生产环境使用不同云厂商(AWS EKS / 阿里云 ACK),ingress class 不一样,storage class 不一样。以前这些差异散落在各种 Jenkins 脚本和 sed 命令里,没有一个地方能一眼看清楚"这个环境和那个环境到底有什么不同"。

回滚难题
#

传统 kubectl rollout undo 只能回滚镜像,如果这次发布同时改了 ConfigMap,回滚不会帮你还原 ConfigMap。想完整回滚必须找到上一个版本的 YAML 文件手动 apply,但你得先找到它在哪里。

审计与变更追踪
#

“这个配置是谁改的、什么时候改的、为什么改”——这些问题在传统模式下基本无解,除非你的团队非常自律地维护 changelog。而 GitOps 把所有变更都记录在 Git 提交历史里,git loggit blame 就是天然的审计日志。


技术选型
#

ArgoCD vs Flux
#

维度ArgoCDFlux v2
UI有完整 Web UI,直观无官方 UI(有第三方)
多集群管理原生支持,一个 ArgoCD 管多个集群需要额外配置
同步模式Pull + ReconcilePull + Reconcile
Kustomize 支持原生内置原生内置
Helm 支持支持(HelmRelease 方式)支持(HelmRelease CRD)
学习曲线相对平缓,UI 降低门槛纯 GitOps 哲学,更"原教旨"
社区活跃度非常活跃,CNCF 毕业项目活跃,CNCF 毕业项目
通知能力原生 notifications controller需要额外配置

我们选了 ArgoCD,核心原因是 Web UI。团队里不是所有人都熟悉 CLI,UI 让非 DevOps 成员也能看到各服务的同步状态、健康状态,降低了沟通成本。多集群场景下 ArgoCD 的体验也更顺畅——一个 ArgoCD 实例部署在阿里云 ACK,同时管理 AWS EKS 的多个集群。

Kustomize vs Helm
#

这两个不是完全对立的选项,但针对我们的场景做了权衡:

Helm 的问题

  • Chart 模板语法复杂,{{ if .Values.xxx }}{{ end }} 嵌套深了可读性很差
  • 自定义资源需要用 _helpers.tpl,调试困难
  • values.yaml 覆盖层次多了容易搞不清楚最终渲染结果是什么

Kustomize 的优势

  • 纯 YAML,没有模板语法,看到什么就是什么
  • kustomize build 可以随时预览最终输出
  • patches 机制让环境差异表达得很清晰——base 是通用的,overlay 只写差异
  • kubectl 内置支持(kubectl apply -k),不需要额外安装

选择 Kustomize 还有一个现实原因:我们的服务大多是内部开发的,没有"发布 Chart 给别人用"的需求,Helm 的打包分发能力对我们是多余的。


仓库目录结构设计
#

这是整个 GitOps 体系里最重要的决策,结构设计得不好后面改起来很痛。

Monorepo 方案
#

我们把所有服务的 K8s 配置放在一个仓库里(gitops-repo),而不是每个服务一个仓库。原因:

  1. ArgoCD 轮询仓库有频率限制,多仓库意味着多个 webhook 和轮询连接
  2. 跨服务的关联变更可以在一个 PR 里完成(比如同时更新 A 服务和它依赖的 ConfigMap)
  3. 权限管理集中,只需要管好这一个仓库的分支保护规则

目录结构
#

gitops-repo/
├── base/
│   ├── service-a/
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   ├── hpa.yaml
│   │   └── kustomization.yaml
│   ├── service-b/
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   └── kustomization.yaml
│   └── infra/
│       ├── cert-manager/
│       └── ingress-nginx/
├── overlays/
│   ├── qa/
│   │   ├── service-a/
│   │   │   ├── kustomization.yaml
│   │   │   └── patches/
│   │   │       ├── deployment-replicas.yaml
│   │   │       └── hpa-minmax.yaml
│   │   └── service-b/
│   │       └── kustomization.yaml
│   ├── pre/
│   │   ├── service-a/
│   │   │   └── kustomization.yaml
│   │   └── service-b/
│   │       └── kustomization.yaml
│   ├── prod-aws/
│   │   ├── service-a/
│   │   │   ├── kustomization.yaml
│   │   │   └── patches/
│   │   │       ├── deployment-resources.yaml
│   │   │       └── ingress-class.yaml
│   │   └── service-b/
│   │       └── kustomization.yaml
│   └── prod-aliyun/
│       ├── service-a/
│       │   ├── kustomization.yaml
│       │   └── patches/
│       │       ├── deployment-resources.yaml
│       │       └── ingress-alb.yaml
│       └── service-b/
│           └── kustomization.yaml
└── argocd/
    ├── projects/
    │   ├── qa-project.yaml
    │   ├── pre-project.yaml
    │   └── prod-project.yaml
    └── applicationsets/
        ├── qa-appset.yaml
        ├── pre-appset.yaml
        ├── prod-aws-appset.yaml
        └── prod-aliyun-appset.yaml

关键原则

  • base/ 只放通用配置,不能有任何环境特定的值(不能有 namespace: production
  • overlays/ 只写差异,能在 base 里写的不要在 overlay 重复
  • argocd/ 目录存放 ArgoCD 自身的配置,这些资源也由 ArgoCD 管理(App of Apps 模式)

base/ 的 kustomization.yaml
#

# base/service-a/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yaml
  - service.yaml
  - hpa.yaml

base 里的 Deployment 不写 namespace,不写具体的副本数(或者写一个安全的默认值),镜像 tag 用 latest 占位,后续由 CI 更新:

# base/service-a/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-a
spec:
  replicas: 1
  selector:
    matchLabels:
      app: service-a
  template:
    metadata:
      labels:
        app: service-a
    spec:
      containers:
        - name: service-a
          image: 123456789.dkr.ecr.us-west-2.amazonaws.com/service-a:latest
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
          env:
            - name: APP_ENV
              value: "default"

overlays/ 各环境的 kustomization.yaml
#

QA 环境(最简化,副本数少,资源限制低):

# overlays/qa/service-a/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: qa

resources:
  - ../../../base/service-a

images:
  - name: 123456789.dkr.ecr.us-west-2.amazonaws.com/service-a
    newTag: "a1b2c3d"  # 由 CI 更新

patches:
  - path: patches/deployment-replicas.yaml
  - path: patches/hpa-minmax.yaml
# overlays/qa/service-a/patches/deployment-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-a
spec:
  replicas: 1

生产环境(AWS)(高可用,AWS 特定 ingress):

# overlays/prod-aws/service-a/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: production

resources:
  - ../../../base/service-a

images:
  - name: 123456789.dkr.ecr.us-west-2.amazonaws.com/service-a
    newTag: "a1b2c3d"

patches:
  - path: patches/deployment-resources.yaml
  - path: patches/ingress-class.yaml
# overlays/prod-aws/service-a/patches/deployment-resources.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-a
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: service-a
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: 2000m
              memory: 2Gi

生产环境(阿里云)(阿里云 ACK,使用阿里云 ALB ingress):

# overlays/prod-aliyun/service-a/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: production

resources:
  - ../../../base/service-a
  - ingress.yaml  # 阿里云环境独有的 ALB ingress,base 里没有

images:
  - name: registry.cn-hangzhou.aliyuncs.com/myorg/service-a
    newTag: "a1b2c3d"

patches:
  - path: patches/deployment-resources.yaml
  - path: patches/deployment-registry.yaml  # 替换镜像仓库地址

Kustomize 关键用法
#

Strategic Merge Patch vs JSON Patch
#

Kustomize 支持两种 patch 方式,选哪个取决于要改什么:

Strategic Merge Patch(推荐,大多数情况够用):

# 只写你要改的字段,其余字段会被保留
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-a
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: service-a
          env:
            - name: LOG_LEVEL
              value: "warn"

注意:对于 List 类型字段(比如 containersenv),Strategic Merge Patch 会按 key 字段合并,不是简单替换。containersname 作为 merge key,envname 作为 merge key。

JSON Patch(适合精确操作,比如删除某个字段):

# overlays/prod/patches/remove-debug.yaml
- op: remove
  path: /spec/template/spec/containers/0/env/2  # 删除第三个环境变量
# kustomization.yaml 里引用 JSON Patch
patches:
  - path: patches/remove-debug.yaml
    target:
      kind: Deployment
      name: service-a

configMapGenerator
#

直接在 kustomization.yaml 里生成 ConfigMap,还会自动添加内容 hash 后缀,让 Deployment 感知到 ConfigMap 变化:

configMapGenerator:
  - name: service-a-config
    literals:
      - APP_ENV=production
      - LOG_LEVEL=info
    files:
      - config/app.properties

generatorOptions:
  disableNameSuffixHash: false  # 默认 false,会追加 hash,推荐保留

生成的 ConfigMap 名称会变成 service-a-config-k8bcm9mh5b 这样,Deployment 引用 ConfigMap 时,Kustomize 会自动替换成带 hash 的名称。好处是:ConfigMap 内容变化 → hash 变化 → Deployment 的 volumes.configMap.name 变化 → Deployment 触发滚动更新。

images 字段:CI/CD 集成的关键
#

这是 CI 更新镜像 tag 的标准方式:

# kustomization.yaml
images:
  - name: 123456789.dkr.ecr.us-west-2.amazonaws.com/service-a
    newTag: "abc1234"

CI 里用 kustomize edit set image 更新,不用手动 sed 替换 YAML:

cd overlays/qa/service-a
kustomize edit set image \
  123456789.dkr.ecr.us-west-2.amazonaws.com/service-a=123456789.dkr.ecr.us-west-2.amazonaws.com/service-a:${GIT_SHA}

也可以用 newName 同时替换仓库地址:

images:
  - name: service-a  # base 里用短名
    newName: 123456789.dkr.ecr.us-west-2.amazonaws.com/service-a
    newTag: "abc1234"

namePrefix / nameSuffix
#

如果想让同一套配置部署到同一个 namespace 的不同实例(比如蓝绿部署),可以用 namePrefix:

namePrefix: blue-
# 所有资源名称都会变成 blue-service-a, blue-service-a-config 等

生产环境我们用得不多,主要是 QA 环境有时候需要同时跑多个版本做对比测试。

验证构建结果
#

在提交前养成习惯,先 kustomize build 看看最终输出:

# 预览 QA 环境的 service-a 最终 YAML
kustomize build overlays/qa/service-a

# 和上一个版本做 diff
kustomize build overlays/qa/service-a | kubectl diff -f - --context=qa-cluster

ArgoCD 配置
#

Application 资源
#

最基本的 Application 定义:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: service-a-qa
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io  # 删除 App 时级联删除 K8s 资源
spec:
  project: qa-project

  source:
    repoURL: https://github.com/myorg/gitops-repo
    targetRevision: main
    path: overlays/qa/service-a

  destination:
    server: https://kubernetes.default.svc  # 本集群
    namespace: qa

  syncPolicy:
    automated:
      prune: true      # 删除 Git 里已移除的资源
      selfHeal: true   # 发现漂移自动修复
    syncOptions:
      - CreateNamespace=true  # namespace 不存在时自动创建
      - PrunePropagationPolicy=foreground
      - RespectIgnoreDifferences=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

prune: true 要谨慎:开启后,如果你从 kustomization.yaml 里移除了某个资源(比如一个 Service),下次同步时 ArgoCD 会把集群里对应的 Service 删掉。这是期望行为,但如果手滑把资源从 Git 里删了,可能造成意外中断。建议生产环境把 automated 去掉,改为手动触发同步,或者至少把 prune 设为 false,删除资源单独操作。

selfHeal: true:有人直接 kubectl edit 改了集群资源,ArgoCD 会在下次 reconcile 时(默认 3 分钟)把改动回滚回 Git 里的状态。这是 GitOps 的核心保障,但刚开始用的时候团队需要适应"所有改动必须走 Git"的习惯。

ignoreDifferences
#

有些字段是 K8s 控制器自动填充的,或者你故意不想被 ArgoCD 管理,可以忽略:

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas  # 如果你用了 HPA,replicas 由 HPA 管理,不要让 ArgoCD 覆盖
    - group: ""
      kind: ConfigMap
      name: service-a-generated
      jsonPointers:
        - /data  # 某些 CM 内容由运行时生成

ApplicationSet:自动化管理多环境
#

手动为每个服务每个环境创建 Application 资源太繁琐,ApplicationSet 可以按规则自动生成。

List Generator(适合环境数量固定、配置差异明显的场景):

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: service-a-appset
  namespace: argocd
spec:
  generators:
    - list:
        elements:
          - env: qa
            cluster: https://qa-eks.example.com
            namespace: qa
            revision: main
          - env: pre
            cluster: https://pre-eks.example.com
            namespace: pre
            revision: main
          - env: prod-aws
            cluster: https://prod-aws-eks.example.com
            namespace: production
            revision: main
          - env: prod-aliyun
            cluster: https://prod-aliyun-ack.example.com
            namespace: production
            revision: main

  template:
    metadata:
      name: "service-a-{{env}}"
      namespace: argocd
    spec:
      project: "{{env}}-project"
      source:
        repoURL: https://github.com/myorg/gitops-repo
        targetRevision: "{{revision}}"
        path: "overlays/{{env}}/service-a"
      destination:
        server: "{{cluster}}"
        namespace: "{{namespace}}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true

Git Generator(适合服务数量多、目录结构规律的场景):

spec:
  generators:
    - git:
        repoURL: https://github.com/myorg/gitops-repo
        revision: main
        directories:
          - path: overlays/qa/*  # 自动发现 overlays/qa/ 下的所有子目录

Git Generator 会把每个发现的目录路径作为一个元素,生成对应的 Application。新增服务只需要在 Git 里创建目录,ApplicationSet 会自动发现并创建 Application,不需要手动操作 ArgoCD。

ArgoCD Project
#

Project 用来做隔离和权限控制:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: qa-project
  namespace: argocd
spec:
  description: QA 环境项目

  sourceRepos:
    - "https://github.com/myorg/gitops-repo"

  destinations:
    - namespace: qa
      server: https://qa-eks.example.com

  clusterResourceWhitelist:
    - group: ""
      kind: Namespace

  namespaceResourceBlacklist:
    - group: ""
      kind: ResourceQuota  # QA 不允许修改 ResourceQuota

  roles:
    - name: developer
      description: 开发人员可以同步但不能删除
      policies:
        - p, proj:qa-project:developer, applications, sync, qa-project/*, allow
        - p, proj:qa-project:developer, applications, get, qa-project/*, allow
      groups:
        - myorg:developers

CI/CD 集成
#

整个流程分两个阶段:CI 负责构建和推送镜像,然后更新 GitOps 仓库的镜像 tag;ArgoCD 检测到 Git 变化后自动同步到集群。

CI 阶段(以 GitHub Actions 为例)
#

# .github/workflows/build-and-deploy.yaml
name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.tag.outputs.tag }}
    steps:
      - uses: actions/checkout@v4

      - name: Generate image tag
        id: tag
        run: echo "tag=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-west-2

      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push image
        env:
          ECR_REGISTRY: 123456789.dkr.ecr.us-west-2.amazonaws.com
          IMAGE_NAME: service-a
          IMAGE_TAG: ${{ steps.tag.outputs.tag }}
        run: |
          docker build -t $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG

  update-gitops:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout GitOps repo
        uses: actions/checkout@v4
        with:
          repository: myorg/gitops-repo
          token: ${{ secrets.GITOPS_TOKEN }}

      - name: Install kustomize
        run: |
          curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
          sudo mv kustomize /usr/local/bin/

      - name: Update image tag in QA overlay
        env:
          IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
          ECR_REGISTRY: 123456789.dkr.ecr.us-west-2.amazonaws.com
        run: |
          cd overlays/qa/service-a
          kustomize edit set image \
            $ECR_REGISTRY/service-a=$ECR_REGISTRY/service-a:$IMAGE_TAG

      - name: Commit and push
        run: |
          git config user.name "ci-bot"
          git config user.email "ci-bot@myorg.com"
          git add .
          git commit -m "chore: update service-a to ${{ needs.build.outputs.image-tag }}"
          git push

生产环境的镜像 tag 更新通常不直接从 CI 推,而是通过 PR 的方式——CI 创建一个 PR 更新生产环境的镜像 tag,人工 review 后合并,ArgoCD 才会自动同步。这多了一层人工确认的保障。

CD 阶段
#

ArgoCD 配置了 webhook,GitHub 推送后几秒内 ArgoCD 就能检测到变化并开始同步。如果没有配置 webhook,默认是 3 分钟轮询一次。

可以用 argocd CLI 手动触发同步(适合紧急发布):

argocd app sync service-a-qa --prune

踩坑记录
#

这部分是真实踩过的坑,文档里一般不会告诉你这些。

1. Kustomize patches 路径写错导致静默失败
#

现象:修改了 overlay 里的 patch 文件,推送后 ArgoCD 显示同步成功,但集群里的资源没有变化。

原因kustomization.yaml 里的 patches 路径写错了,指向了一个不存在的文件,但 Kustomize 某些版本不会报错,直接忽略了这个 patch。

patches:
  - path: patches/deployment-replicas.yaml  # 实际文件是 patch/deployment-replicas.yaml

解法:养成在提交前 kustomize build 验证的习惯。如果 patch 文件不存在,新版本的 Kustomize 会报错,但不要依赖这个行为,显式验证更安全。

还有一个更隐蔽的变体:patch 文件存在,但 patch 的目标资源 metadata.name 写错了,导致 patch 没有匹配到任何资源。比如 base 里资源名是 service-a,但 patch 文件里写成了 service_a(下划线),strategic merge patch 找不到目标,静默跳过。

2. argocd sync 卡住不动
#

现象argocd app sync 命令执行后,应用状态变成 Syncing,但一直没有完成,等了很久才超时报错。

常见原因

PreSync / Sync Hook 挂了:如果你用了 argocd.argoproj.io/hook: PreSync 的 Job,Job 失败了会导致整个同步卡住(取决于 argocd.argoproj.io/hook-delete-policy)。检查:

# 查看 hook job 状态
kubectl get jobs -n qa -l app.kubernetes.io/managed-by=Helm
kubectl describe job <job-name> -n qa

资源 Finalizer 死锁:某个资源有 Finalizer,但控制器已经不存在了,资源删不掉,同步卡住。解法:

# 手动清除 Finalizer(谨慎操作)
kubectl patch <resource> <name> -n <ns> \
  --type=json \
  -p='[{"op": "remove", "path": "/metadata/finalizers"}]'

Webhook 证书问题:如果有 validating/mutating webhook,证书过期或 webhook service 不可用,kubectl apply 会被拒绝,ArgoCD 也会卡住。

# 检查 webhook
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

3. 多集群 ArgoCD:主集群在阿里云,管理 AWS 集群
#

我们的 ArgoCD 部署在阿里云 ACK 上,需要管理 AWS EKS 集群。注册外部集群的步骤:

# 在本机(或 CI),kubeconfig 里需要同时有两个集群的 context
# 确保 argocd CLI 登录的是阿里云上的 ArgoCD
argocd login argocd.internal.myorg.com

# 注册 AWS EKS 集群
argocd cluster add aws-eks-us-west-2 \
  --kubeconfig ~/.kube/config \
  --name prod-aws-eks

# 验证
argocd cluster list

踩到的坑:EKS 的 kubeconfig 使用 aws eks get-token 命令生成临时 token,这个 token 有效期只有 15 分钟。argocd 注册集群时会把这个 ServiceAccount token 存在 argocd namespace 下的 Secret 里,但如果注册时使用的是你的个人 IAM 身份,argocd 的 controller 后续无法续期 token。

正确做法:在 EKS 集群里创建专用 ServiceAccount,绑定足够权限,用 SA token 注册,而不是用 aws eks get-token

# 在 EKS 集群里创建 SA
kubectl create serviceaccount argocd-manager -n kube-system
kubectl create clusterrolebinding argocd-manager \
  --clusterrole=cluster-admin \
  --serviceaccount=kube-system:argocd-manager

# 获取 SA token(K8s 1.24+ 需要手动创建)
kubectl create token argocd-manager -n kube-system --duration=87600h

# 用 bearer token 注册
argocd cluster add <cluster-context> \
  --name prod-aws-eks \
  --bearer-token <token> \
  --server https://<eks-endpoint>

4. ApplicationSet 更新不触发同步
#

现象:修改了 ApplicationSet 里的某个字段(比如 syncPolicy),但已存在的 Application 没有更新。

原因:ApplicationSet controller 负责创建和删除 Application,但不会修改已经存在的 Application 的所有字段(具体哪些字段受控取决于版本和配置)。

解法

  • 在 ApplicationSet 的 syncPolicy 加上 preservedFields,明确哪些字段由用户管理
  • 或者删除对应的 Application 让 ApplicationSet 重新创建
  • 检查 applicationset-controller 的日志确认是否有相关警告
kubectl logs -n argocd \
  -l app.kubernetes.io/component=applicationset-controller \
  --tail=100

5. Secret 管理:不能明文存 GitOps 仓库
#

这是很多团队最开始犯的错误:把 Secret 的明文 YAML 放进 GitOps 仓库,然后发现 GitHub 告警说仓库里有敏感信息。

我们用的方案:Sealed Secrets

Sealed Secrets 由 Bitnami 开源,分两个组件:

  • sealed-secrets-controller:部署在集群里,持有解密私钥
  • kubeseal:CLI 工具,用集群的公钥加密 Secret,生成 SealedSecret 资源

加密后的 SealedSecret 可以安全地提交到 Git,只有对应集群的 controller 能解密:

# 安装 kubeseal
brew install kubeseal

# 获取集群公钥
kubeseal --fetch-cert \
  --controller-namespace=sealed-secrets \
  --controller-name=sealed-secrets-controller \
  > cluster-cert.pem

# 加密 Secret
kubectl create secret generic db-password \
  --from-literal=password=supersecret123 \
  --dry-run=client \
  -o yaml | \
  kubeseal \
    --cert cluster-cert.pem \
    --format yaml \
    > overlays/qa/service-a/sealed-db-password.yaml

生成的 sealed-db-password.yaml 长这样:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-password
  namespace: qa
spec:
  encryptedData:
    password: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...(加密后的内容)
  template:
    metadata:
      name: db-password
      namespace: qa
    type: Opaque

提交到 Git,ArgoCD 同步后,controller 自动解密并创建对应的 Secret。

注意:Sealed Secrets 是按集群(或按 namespace)加密的,一个环境的 SealedSecret 在另一个环境的集群里无法解密。每个环境需要单独加密。

另一个方案是 External Secrets Operator,从 AWS Secrets Manager / Vault / 阿里云 KMS 等外部存储读取 Secret,更适合已经有集中 Secret 管理系统的团队。

6. HPA 与 ArgoCD 的副本数冲突
#

如果服务开启了 HPA,HPA 会动态调整 replica 数。ArgoCD 同步时会把 Deployment 的 spec.replicas 改回 Git 里的值,然后 HPA 再改回去,产生频繁的 reconcile 循环。

解法是在 Application 的 ignoreDifferences 里忽略 spec.replicas

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas

或者在 kustomization.yaml 里直接不设置 replicas,让 HPA 完全掌控。但注意:如果 HPA 被删了,Deployment 会保持上次 HPA 设置的副本数,可能不是你期望的默认值。


常用 argocd CLI 命令速查
#

# 登录
argocd login argocd.internal.myorg.com --sso

# 查看所有应用状态
argocd app list

# 查看单个应用详情(包括同步状态、健康状态、资源列表)
argocd app get service-a-qa

# 手动触发同步
argocd app sync service-a-qa

# 同步并强制删除不在 Git 里的资源
argocd app sync service-a-qa --prune

# 预览变更(不实际同步,很有用)
argocd app diff service-a-qa

# 回滚到上一个版本
argocd app rollback service-a-qa

# 回滚到指定历史版本(先查 history)
argocd app history service-a-qa
argocd app rollback service-a-qa <history-id>

# 暂停自动同步(紧急情况下临时关闭自动同步)
argocd app set service-a-qa --sync-policy none

# 恢复自动同步
argocd app set service-a-qa --sync-policy automated

# 手动刷新(强制 ArgoCD 重新从 Git 拉取,不等轮询)
argocd app get service-a-qa --refresh

# 强制刷新(清除缓存,适合 Helm chart 有变化但没检测到的情况)
argocd app get service-a-qa --hard-refresh

# 删除应用(加 --cascade 会同时删除 K8s 资源)
argocd app delete service-a-qa --cascade

# 查看所有集群
argocd cluster list

# 查看同步失败的原因
argocd app get service-a-qa -o json | jq '.status.conditions'

# 管理 Project
argocd proj list
argocd proj get qa-project
argocd proj role list qa-project

一些运维习惯
#

落地 GitOps 之后,有几个习惯能让日常运维更顺:

所有变更走 PR:即使是紧急修复,也要 PR + Squash Merge,保持 Git 历史干净。紧急程度不是绕过 PR 的理由,而是减少 Review 等待时间的理由(比如只要一个人 approve 就合)。

保持 base 精简:base 只放所有环境共用的内容,遇到"这个字段大多数环境都一样,只有一个环境不同"的情况,还是把这个字段放 overlay 里,base 里删掉。不然 base 里的值会变成一个隐藏的"默认值",新来的同学很容易误解。

定期 kustomize build 验证:在 CI 里加一步 kustomize build 检查,确保所有 overlay 都能正常构建,防止有人改了 base 资源名称但没有更新 patch 里的 target。

ArgoCD 的 notification 配置起来:同步失败、健康状态变化及时推送到 IM(我们用钉钉),不然靠人工看 UI 发现问题太慢。


参考链接
#

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。

相关文章

GitOps 落地实战:ArgoCD + Kustomize 多环境管理

·730 字·4 分钟
GitOps 不只是「把配置放 Git 里」,真正落地需要解决 overlay 结构设计、ApplicationSet 管理多集群、image updater 自动化,以及 sync wave、resource hook 这些细节。这篇文章记录我们团队从传统 CI/CD 迁移到 GitOps 的实际过程。