Kubernetes 基础体系 · 第 56/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes GitOps:期望状态、调谐、Secret、漂移、晋级和回滚

GitOps 不是“把 Kubernetes YAML 放进 Git”这么简单。它是一种交付控制模型:

Git 中的版本化声明描述期望状态,集群中的控制器持续观察实际状态,并通过调谐把实际状态拉回期望状态。

因此,GitOps 的核心不是某个命令,而是几个相互关联的问题:

  • 期望状态和实际状态分别是什么;
  • 谁负责比较和调谐;
  • Secret 如何进入声明式交付链路;
  • 集群被人工修改后如何识别和修复漂移;
  • 一个版本如何从开发环境晋级到生产环境;
  • 回滚究竟是恢复 Git、恢复 Argo CD 历史,还是回退 Kubernetes 工作负载。

本文示例以 Kubernetes 当前稳定 API 为基础,使用 Argo CD 说明 GitOps 控制器的常见实现。Argo CD 自身的 CRD、命令和行为可能随版本变化,使用前应以所安装版本的文档和 CRD 定义为准。


一、先区分三个“状态”

1. 期望状态

期望状态是系统应该达到的配置集合,例如:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: ghcr.io/example/web:1.4.2
          ports:
            - containerPort: 8080

它表达了几个约束:

  • demo 命名空间中存在名为 web 的 Deployment;
  • Deployment 期望有 3 个副本;
  • Pod 模板使用 ghcr.io/example/web:1.4.2
  • 标签和选择器必须匹配。

这份 YAML 不是“执行三条命令”的脚本,而是一个目标描述。重复应用同一份声明,理论上应得到相同的目标结果,这种性质称为幂等性

2. 实际状态

实际状态是 Kubernetes API Server 当前记录的对象,以及控制器观察到的运行结果。例如:

kubectl -n demo get deployment web \
  -o jsonpath='{.spec.replicas}{" "}{.status.readyReplicas}{" "}{.spec.template.spec.containers[0].image}{"\n"}'

可能输出:

2 2 ghcr.io/example/web:1.4.1

这里至少存在两层实际状态:

  1. API 对象状态:Deployment 的 spec.replicas 是 2,镜像是 1.4.1
  2. 运行时状态:当前有 2 个 Pod Ready。

API 对象的 spec 更接近“当前被声明的目标”,而 status 是 Kubernetes 控制器根据观察结果写入的状态。status 通常不能由用户直接当作期望状态提交。

3. 观察状态

控制器还会看到 API 对象之外的事实,例如:

  • Pod 是否已经调度;
  • 容器是否启动;
  • 镜像是否能拉取;
  • 节点是否健康;
  • LoadBalancer 是否分配地址;
  • Job 是否成功结束。

所以,一个 Deployment 可能在 Git 和 API Server 中都显示 replicas: 3,但实际只有 2 个 Pod Ready。GitOps 控制器通常可以发现对象定义是否同步,却不能仅凭对象定义保证应用健康。

可以用下面的抽象表示:

D=Git 渲染出的期望资源集合D = \text{Git 渲染出的期望资源集合}

L=API Server 中的实际资源集合L = \text{API Server 中的实际资源集合}

O=控制器观察到的运行时事实O = \text{控制器观察到的运行时事实}

GitOps 的基础比较主要是 DDLL,健康检查则进一步使用 OO


二、什么是调谐:从差异到收敛

1. 调谐循环

Kubernetes 控制器普遍采用调谐循环:

  1. 读取期望或当前对象;
  2. 读取依赖对象和运行时状态;
  3. 计算当前状态与目标状态的差异;
  4. 执行创建、更新、删除或等待;
  5. 再次观察结果。

Argo CD 在此基础上把 Git 仓库作为期望状态来源。简化的数据流如下:

flowchart LR
    G[Git Repository] --> R[Manifest Renderer]
    R --> D[Desired Resources]
    A[Argo CD Controller] -->|读取| D
    A -->|读取| K[Kubernetes API Server]
    K --> L[Live Resources]
    A --> C{比较与健康评估}
    D --> C
    L --> C
    C -->|差异存在且允许同步| S[Apply / Delete]
    S --> K
    K --> W[Deployment/Service 等 Kubernetes Controllers]
    W --> O[Pods、ReplicaSets、运行时状态]
    O --> K

关键点是:Argo CD 通常不是直接管理 Pod,而是把 Deployment、Service、ConfigMap 等声明提交给 API Server;之后由 Kubernetes 自己的控制器创建 ReplicaSet 和 Pod。

2. 一个完整的调谐算例

假设 Git 中的 Deployment 是:

spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: web
          image: ghcr.io/example/web:1.4.2

第一次观察到:

Git desired: replicas=3, image=1.4.2
Cluster live: replicas=2, image=1.4.1

控制器计算出差异:

replicas: 2 -> 3
image:    1.4.1 -> 1.4.2

它向 API Server 提交更新。Deployment 控制器随后不会直接“把旧 Pod 改成新镜像”,而是:

  1. 发现 Pod 模板哈希改变;
  2. 创建新 ReplicaSet;
  3. 按 RollingUpdate 策略逐步创建新 Pod、删除旧 Pod;
  4. 等待新 Pod Ready;
  5. 更新 Deployment 的 status

因此,Argo CD 的同步成功不必然等于应用已经可用。同步成功通常表示资源更新请求已被接受或完成,而健康状态还要看 Deployment、Service、探针和应用自身。

3. 收敛不是无条件保证

调谐要达到稳定状态,至少需要满足以下条件:

  • 期望状态可被 Kubernetes API 接受;
  • 资源之间没有互相冲突的控制器;
  • 控制器拥有所需 RBAC 权限;
  • 依赖对象最终可以创建;
  • 期望状态本身是可实现的;
  • 外部系统不会持续把状态改回去。

例如 Git 要求:

spec:
  replicas: 3

但另一个 HPA 根据 CPU 将副本数改为 5。此时两个控制器都在写同一个字段:

GitOps: replicas -> 3
HPA:    replicas -> 5
GitOps: replicas -> 3
HPA:    replicas -> 5

这不是“调谐速度不够”,而是两个控制回路的目标冲突。通常应让 HPA 管理副本数,并避免 GitOps 强制覆盖该字段;否则系统会振荡。


三、Argo CD Application:把 Git 映射到集群

Argo CD 中最重要的对象之一是 Application。它描述:

  • Git 仓库地址;
  • 路径或 Helm Chart;
  • 目标集群;
  • 目标命名空间;
  • 是否自动同步;
  • 是否允许剪枝和自愈。

示例:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-demo
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/platform-config.git
    targetRevision: main
    path: apps/web/overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: demo
  syncPolicy:
    automated:
      prune: false
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

前置条件包括:

  • 集群中已经安装 Argo CD;
  • Argo CD 能访问该 Git 仓库;
  • apps/web/overlays/dev 能被渲染为合法 Kubernetes 资源;
  • Argo CD 使用的 ServiceAccount 有权限访问目标命名空间;
  • demo 命名空间不存在时,安装版本支持并允许 CreateNamespace=true

应用创建后可以观察:

kubectl apply -f application.yaml

argocd app get web-demo

常见结果类似:

Name:               argocd/web-demo
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          demo
Sync Status:        Synced
Health Status:      Healthy

Synced 表示 Argo CD 认为期望资源与实际资源一致;Healthy 是健康评估结果。两者不是同一个维度:

  • SyncedOutOfSync:配置是否一致;
  • HealthyProgressingDegraded:运行状态是否健康。

自动同步并不等于自动修复一切

automated.selfHeal: true 通常表示 Argo CD 发现受管理对象被直接修改后,会再次同步,使其回到 Git 版本。

prune: true 则允许删除 Git 中已经不存在的受管理资源。这个选项风险更高:

  • 从 Git 删除文件可能触发集群删除;
  • 误删路径或错误分支可能造成大范围破坏;
  • 某些资源拥有持久化数据,删除资源与删除数据的后果可能不同;
  • 自定义资源可能有级联删除行为。

生产环境通常需要明确资源所有权,并通过项目范围、命名空间范围、同步窗口、审批或策略检查限制删除影响。


四、GitOps 中的 Secret:编码不是加密

1. Kubernetes Secret 的实际含义

Kubernetes Secret 使用 data 字段时,值必须是 Base64 编码:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: demo
type: Opaque
data:
  username: YXBw
  password: c3VwZXItc2VjcmV0

解码后分别是:

username: app
password: super-secret

Base64 只是一种编码,不提供保密性。任何能读取这个 Secret 的主体,或者能读取包含它的 Git 文件的人,都可以解码。

Kubernetes 对 Secret 的保护依赖多个层次:

  • API Server 的认证和 RBAC;
  • etcd 的访问控制;
  • 可选的 etcd 静态加密配置;
  • 节点、日志、备份系统和控制器的访问边界。

即使启用了 etcd 加密,也不意味着 Secret 在 API 响应、Pod 环境变量、进程内存或日志中自动安全。

2. 不应把明文 Secret 直接提交 Git

下面这种文件即使 data 使用 Base64,也不应直接提交:

data:
  password: c3VwZXItc2VjcmV0

因为 Git 历史会保留旧提交。删除当前文件并不能消除已经泄露的密码。发生泄露时,正确动作是:

  1. 立即吊销或轮换凭据;
  2. 检查 Git 历史、CI 日志和构建产物;
  3. 删除或限制暴露源;
  4. 重新发布使用新凭据的应用;
  5. 检查访问审计记录。

3. 三种常见 Secret 方案

方案 A:Sealed Secrets

Sealed Secrets 的思路是:

  1. 使用集群控制器的公钥在离线环境加密 Secret;
  2. 把加密后的自定义资源提交 Git;
  3. 控制器在集群内解密并生成普通 Secret。

Git 中看到的是类似:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-credentials
  namespace: demo
spec:
  encryptedData:
    password: Ag...

优点是适合 GitOps,开发者不需要把明文提交到 Git。边界是:

  • 加密私钥必须被妥善备份和保护;
  • 丢失控制器密钥可能导致历史密文无法解密;
  • 能在集群中读取普通 Secret 的主体仍然可以读取凭据;
  • 密文虽然不能直接解密,但删除、替换和回滚仍会影响真实 Secret。

方案 B:SOPS 加密文件

SOPS 可以使用云 KMS、GPG 或 age 加密 YAML、JSON 等文件。Git 中提交密文,部署前由受信任的渲染或同步组件解密。

这种模式的安全边界取决于“谁在什么位置解密”:

Git 密文
  -> CI 或 Argo CD 插件解密
  -> 生成 Kubernetes Secret
  -> API Server

如果 CI 日志打印了解密内容,或者渲染服务的权限过大,密码仍可能泄露。SOPS 只解决文件静态存储的加密问题,不自动解决运行时权限问题。

方案 C:External Secrets

External Secrets Operator 等方案让 Git 中只保存外部密钥的引用:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: demo
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: cloud-secrets
    kind: ClusterSecretStore
  target:
    name: db-credentials
  data:
    - secretKey: password
      remoteRef:
        key: production/database
        property: password

该示例依赖具体 Operator 的 CRD;apiVersion 和字段不能脱离安装版本直接假定。它的运行链路是:

  1. GitOps 应用 ExternalSecret
  2. Operator 使用云厂商或外部系统身份读取密钥;
  3. Operator 创建或更新普通 Kubernetes Secret;
  4. Pod 从普通 Secret 读取配置。

优点是凭据可以在外部系统轮换,不必修改 Git。风险是增加了一个控制器和外部依赖,必须验证:

  • Operator 是否在线;
  • 工作负载身份是否有最小读取权限;
  • 外部密钥删除或改名后的行为;
  • Secret 更新能否触发应用重新加载。

4. Secret 更新不一定会让应用立即使用新值

Secret 作为卷挂载时,kubelet 通常会最终更新挂载文件;但应用必须重新读取文件才能使用新值。

Secret 作为环境变量注入时,已经启动的进程不会自动获得新环境变量。常见做法是用 Secret 的哈希生成 Pod 模板注解:

spec:
  template:
    metadata:
      annotations:
        config-hash: "由渲染工具根据 Secret 内容计算的哈希"

哈希变化会改变 Pod 模板,Deployment 创建新 ReplicaSet,Pod 重新启动。

这里不能把“Secret 内容变化”与“Deployment 自动滚动更新”混为一谈。是否发生滚动更新取决于 Pod 模板是否改变,以及具体控制器是否负责重新加载。


五、漂移:集群为什么会偏离 Git

漂移是期望状态与实际状态不一致:

Δ=diff(D,L)\Delta = \operatorname{diff}(D, L)

Δ\Delta \neq \varnothing 时,系统处于 OutOfSync 或等价状态。

1. 常见漂移来源

人工修改

kubectl -n demo scale deployment web --replicas=5

如果 Git 仍然要求 3 个副本,Argo CD 会看到:

desired replicas: 3
live replicas:    5

启用自愈后,Argo CD 可能把它改回 3;未启用自愈时,差异会持续存在。

HPA 修改

HPA 通过 scaleTargetRef 修改 Deployment 的副本数。这是合法的控制行为,不应简单视为错误漂移。若副本数由 HPA 管理,就不应同时让 GitOps 强制管理该字段。

Mutating Admission Webhook 修改

服务网格、Pod 安全组件或平台 Webhook 可能注入:

  • sidecar 容器;
  • 环境变量;
  • 卷;
  • 标签;
  • 注解。

如果 Git 期望对象没有这些字段,比较结果可能长期 OutOfSync。

Kubernetes 控制器填充字段

API Server 或控制器可能添加默认值、状态字段和 managedFields。严谨的 GitOps 实现不会把所有服务端生成字段都当作用户声明差异,但不同工具的比较规则存在差异。

2. 漂移的诊断路径

先看 Argo CD:

argocd app diff web-demo
argocd app get web-demo
argocd app history web-demo

再看实际对象:

kubectl -n demo get deployment web -o yaml

查看谁修改了字段:

kubectl -n demo get deployment web -o json \
  | jq '.metadata.managedFields[] |
        {manager,operation,fieldsType,time}'

使用 managedFields 时要注意,它是服务端字段管理信息,不等价于完整审计记录;要追踪“谁在什么时候执行了什么请求”,仍应查看 API Server 审计日志或平台审计系统。

然后比较渲染结果,而不是只比较仓库中的模板:

helm template web ./charts/web \
  -f ./env/dev-values.yaml > /tmp/desired.yaml

kubectl -n demo get deployment web -o yaml > /tmp/live.yaml
diff -u /tmp/desired.yaml /tmp/live.yaml

如果使用 Kustomize,则应使用对应的 kustomize build。只看 Helm 模板而不带实际 Values,是常见误判来源。

3. 不要随意忽略差异

Argo CD 可以配置 ignoreDifferences,例如忽略 HPA 管理的副本数字段。但忽略差异意味着:

控制器不再把这部分差异当作需要修复的错误。

这不是“隐藏界面噪声”那么简单。若错误地忽略镜像、权限、容器命令等字段,Git 可能看起来 Synced,实际却运行着未经审查的配置。

正确原则是:

  • 先确定字段的真实所有者;
  • 只忽略由明确控制器生成的字段;
  • 记录忽略原因;
  • 定期验证忽略规则没有扩大范围。

六、Helm 在 GitOps 中负责什么

Helm 是 Kubernetes 的打包和模板工具;Argo CD 是持续比较和同步的控制器。两者职责不同。

1. Helm 渲染链路

一个 Chart 可能包含:

Chart.yaml
values.yaml
templates/
  deployment.yaml
  service.yaml

渲染命令:

helm template web ./charts/web \
  --namespace demo \
  -f ./env/dev-values.yaml \
  --set image.tag=1.4.2

Helm 将 Chart、Values 和命令行覆盖合并,渲染成 Kubernetes 清单。Argo CD 可以在集群内调用 Helm 渲染,然后比较渲染结果与集群对象。

在这种模式中,Git 通常保存:

  • Chart;
  • Values;
  • 环境覆盖;
  • 版本引用。

GitOps 控制器保存并执行同步,但不一定创建 Helm 的传统 Release 秘密。也就是说,Argo CD 使用 Helm 渲染,不等于 helm install 创建了一个由 Helm CLI 管理的 Release。具体行为取决于 Argo CD 的 Helm 集成方式。

2. Helm Release 回滚与 GitOps 回滚不是同一件事

传统 Helm 流程:

helm upgrade web ./charts/web -n demo
helm history web -n demo
helm rollback web 3 -n demo

这里 Helm Release 记录了历史修订。

GitOps 流程通常是:

Git 提交
  -> Argo CD 渲染
  -> API Server

回滚一般应通过 Git 恢复到旧提交或旧版本引用:

git revert <bad-commit>
git push

然后 Argo CD 重新渲染并同步。这样 Git 仍然记录了生产状态,其他环境和审计流程也能理解这次回退。

直接执行:

argocd app rollback web-demo <revision>

是否可用、具体语义和限制取决于 Argo CD 版本及应用历史。即使命令成功,Git 仍可能保留错误版本;后续自动同步还可能再次把 Git 版本应用回去。因此临时回滚后通常必须把 Git 修正到目标版本。


七、晋级:从环境差异转为版本流动

晋级是把已经在低风险环境验证过的版本,推进到更高环境,例如:

开发 -> 集成 -> 预发布 -> 生产

GitOps 的关键是晋级提交应改变“版本引用”,而不是在生产集群中手工修改对象。

1. 一个清晰的仓库结构

platform-config/
├── apps/
│   └── web/
│       ├── base/
│       │   ├── deployment.yaml
│       │   ├── service.yaml
│       │   └── kustomization.yaml
│       └── overlays/
│           ├── dev/
│           │   ├── kustomization.yaml
│           │   └── patch.yaml
│           ├── staging/
│           └── prod/
└── argocd/
    ├── web-dev.yaml
    ├── web-staging.yaml
    └── web-prod.yaml

base 描述共用结构,环境 Overlay 描述副本数、域名、资源限制和镜像版本等差异。

例如生产 Overlay 可以通过镜像替换:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
images:
  - name: ghcr.io/example/web
    newTag: 1.4.2

开发环境可能先使用 1.4.2-rc.1,测试通过后,生产 Overlay 通过一个独立提交改为不可变的 1.4.2

2. 为什么推荐不可变版本

如果 Deployment 使用:

image: ghcr.io/example/web:latest

即使镜像仓库中的 latest 被重新推送,Git 中的 YAML 也没有变化,Argo CD 可能不会触发配置同步;节点上的重新拉取行为还受 imagePullPolicy 和缓存影响。

更可追踪的做法是:

image: ghcr.io/example/web:1.4.2

或者使用镜像 digest:

image: ghcr.io/example/web@sha256:abcdef...

Tag 便于阅读,digest 更接近不可变身份。实际晋级流程应同时记录:

  • 应用版本;
  • 镜像 digest;
  • Chart 版本;
  • 配置变更;
  • 数据库迁移版本;
  • 审批和验证结果。

3. 晋级的因果链

一个受控晋级可以是:

  1. CI 构建镜像;
  2. 运行单元测试、镜像扫描和部署验证;
  3. 将镜像推送到仓库;
  4. 生成版本 Tag 或 digest;
  5. 更新开发 Overlay;
  6. Argo CD 同步开发环境;
  7. 运行集成测试和验收;
  8. 通过 Pull Request 将同一版本引用复制到预发布;
  9. 预发布验证通过后,再更新生产 Overlay;
  10. 生产 Argo CD 在审批或同步窗口允许时部署。

“同一版本”非常重要。如果重新构建一次再推送,虽然源码 Tag 相同,也可能得到不同镜像。晋级应尽量推广已经构建并验证过的镜像身份,而不是重新构建。


八、回滚:回到哪个状态、回滚什么对象

回滚不是单一动作。至少要区分三类。

1. 配置回滚

如果错误来自 Deployment、Service 或 ConfigMap 的 Git 提交,使用 Git revert:

git revert <bad-commit>
git push origin main

Argo CD 发现新提交后,重新渲染旧配置并同步。

优点是:

  • 目标状态重新回到 Git;
  • 审计记录连续;
  • 其他集群可以复用同一恢复提交。

2. 工作负载版本回滚

Deployment 自身记录 ReplicaSet 修订,可以查看:

kubectl -n demo rollout history deployment/web
kubectl -n demo rollout undo deployment/web
kubectl -n demo rollout status deployment/web

但在启用 GitOps 自动同步和自愈时,直接 kubectl rollout undo 可能只产生临时状态:

Deployment 回到旧 ReplicaSet
Git 仍然要求新镜像
Argo CD 发现漂移
Argo CD 再次应用新镜像

因此该方法适合紧急止血,但之后仍应提交 Git 回滚或暂停自动同步并完成正式修复。

3. 数据库回滚

应用代码可以从 1.4.2 回到 1.4.1,数据库却未必能安全回退。尤其是破坏性迁移:

版本 1.4.2:
  添加新列,并删除旧列

回滚到 1.4.1:
  旧代码仍依赖已删除的旧列

这时 Kubernetes Deployment 回滚成功,应用仍可能启动失败或数据访问失败。

更安全的迁移通常遵循扩展/收缩模式:

  1. 先添加新结构,不删除旧结构;
  2. 发布同时兼容新旧结构的应用;
  3. 回填数据;
  4. 切换读写逻辑;
  5. 确认旧版本不再需要后再删除旧结构。

所以一次生产回滚必须回答:

  • 只回滚应用镜像,还是同时回滚配置;
  • 数据库迁移是否可逆;
  • 消息格式是否兼容;
  • 缓存是否需要清理;
  • 外部 API 是否已经产生不可逆副作用;
  • 回滚后探针和业务指标是否恢复。

九、Kubernetes RollingUpdate 与 GitOps 回滚的关系

Deployment 默认使用 RollingUpdate。可以明确限制滚动过程:

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1

含义是:

  • maxUnavailable: 0:更新时不主动减少可用副本;
  • maxSurge: 1:最多额外创建一个 Pod。

假设原来有 3 个副本:

开始: old=3, new=0
步骤1: old=3, new=1,等待 new Ready
步骤2: old=2, new=2,等待 new Ready
步骤3: old=1, new=3,删除最后一个旧 Pod
结束: old=0, new=3

这并不保证零停机,因为还存在:

  • 应用启动时间;
  • readinessProbe 配置错误;
  • 节点资源不足;
  • 新版本启动后立即崩溃;
  • Service 端点更新延迟;
  • 应用自身无法处理并发版本。

若新 Pod 永远不 Ready,Deployment 会停留在 Progressing 或最终表现为失败。诊断命令:

kubectl -n demo rollout status deployment/web
kubectl -n demo describe deployment/web
kubectl -n demo get pods -l app=web
kubectl -n demo describe pod <pod-name>
kubectl -n demo logs <pod-name> -c web

命令结果需要区分:

  • ImagePullBackOff:镜像地址、凭据或网络问题;
  • CrashLoopBackOff:进程启动后退出,需看日志和配置;
  • Pending:调度、资源、亲和性或污点问题;
  • Readiness probe failed:容器可能运行,但未达到流量接收条件。

GitOps 只能把声明提交给 Kubernetes。它不能替应用修复错误的端口、数据库连接或启动逻辑。


十、失败路径与恢复操作

1. Git 提交无法渲染

表现可能是:

ComparisonError
Manifest generation error

常见原因:

  • Helm Values 类型错误;
  • Kustomize 路径错误;
  • 模板引用不存在;
  • 使用了集群未安装的 CRD;
  • 清单使用了目标集群不支持的 API 版本。

先在本地复现:

helm template web ./charts/web -f env/prod-values.yaml
kustomize build apps/web/overlays/prod
kubectl apply --dry-run=server -f rendered.yaml

kubectl apply --dry-run=server 需要访问目标 API Server,可以检查资源结构和字段,但不等价于完整运行时验证。

2. 同步失败

表现可能是:

PermissionDenied
the server doesn't have a resource type
admission webhook denied the request

诊断顺序应包括:

kubectl auth can-i create deployment \
  --as=system:serviceaccount:argocd:argocd-application-controller \
  -n demo

kubectl api-resources
kubectl get crd
kubectl get events -n demo --sort-by=.lastTimestamp

需要分别判断:

  • Argo CD 是否有 RBAC 权限;
  • API 版本是否存在;
  • CRD 是否先安装;
  • Admission Webhook 是否拒绝;
  • 目标命名空间是否正确;
  • 资源是否被另一个控制器占用。

3. 同步成功但应用不可用

这通常是“配置一致、运行不健康”:

Sync Status: Synced
Health Status: Degraded

可能原因包括:

  • Deployment 已更新,但 Pod 没有 Ready;
  • Service selector 与 Pod 标签不匹配;
  • Ingress 后端端口错误;
  • Secret 键名不一致;
  • 应用依赖数据库或外部 API 失败;
  • 新版本有逻辑错误。

不能通过反复点击 Sync 解决。应从工作负载、事件、探针、应用日志和业务指标建立故障链。


十一、并发、所有权和“谁可以改生产”

GitOps 的一致性依赖明确的资源所有权。以下对象不应同时被多个独立系统无约束地管理:

  • Deployment 的 spec.replicas
  • HPA 关联的 scale 子资源;
  • Service 的 spec.ports
  • Helm、Operator 和 Argo CD 同时管理的同一 CR;
  • Secret 的全部字段;
  • Admission Webhook 自动注入的字段。

可以把字段所有权表示为:

镜像版本       -> GitOps
副本数         -> HPA
Pod 注入标签   -> Webhook
Secret 内容    -> 外部密钥系统
状态字段       -> Kubernetes Controller

这比“整个 YAML 归一个工具”更准确,因为现代 Kubernetes 使用 Server-Side Apply 的字段管理能力,可以在字段级别表达管理者。但字段管理仍不能消除语义冲突:两个控制器即使没有立刻触发冲突,也可能不断写入不同目标。

生产环境还应限制直接修改:

开发人员 -> 提交 Git PR
CI       -> 验证和更新版本
Argo CD  -> 同步集群
紧急运维 -> 受审计的临时操作

紧急 kubectl edit 不是绝对禁止,而是必须理解其后果:如果它没有进入 Git,下一次调谐可能覆盖它;如果启用了自愈,覆盖可能很快发生。


十二、GitOps 不等于完整的发布策略

GitOps 负责把版本和配置可靠地送到集群,但流量切换策略仍由 Kubernetes 资源和流量组件实现。

Rolling

Deployment 逐步替换 ReplicaSet,适合大多数无状态服务。它控制 Pod 替换速度,但不天然提供按用户、地域或百分比的业务流量分配。

Canary

Canary 需要额外的流量控制能力,例如 Ingress、Service Mesh 或网关。典型过程是:

版本 A 接收 100%
版本 B 接收 0%
版本 B 接收 5%
观察错误率和延迟
版本 B 接收 25%
继续观察
版本 B 接收 100%

GitOps 可以管理 Canary 对象、权重和分析规则,但指标系统、流量代理和分析控制器必须正确协作。

Blue-Green

Blue 和 Green 是两套并行工作负载,Service 或网关决定流量指向哪一套。切换通常很快,但需要承担双份资源、数据库兼容和切换后验证成本。

回滚与流量策略

流量切回旧版本不一定等于数据库和消息回滚。若新版本已经写入新格式,旧版本是否仍能读取,必须在发布设计中验证。发布策略解决“如何暴露版本”,GitOps 解决“哪个声明版本应被系统持续维持”,两者不是替代关系。


十三、一个可验证的最小流程

下面流程适用于已经安装 Argo CD、拥有可访问镜像仓库的测试集群。

首先提交一个 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: http

创建命名空间并应用:

kubectl create namespace demo
kubectl apply -f application.yaml

检查同步和运行状态:

argocd app get web-demo
kubectl -n demo get deployment,replicaset,pod
kubectl -n demo rollout status deployment/web

然后模拟漂移:

kubectl -n demo scale deployment web --replicas=4
kubectl -n demo get deployment web -o jsonpath='{.spec.replicas}{"\n"}'

selfHeal 生效,经过一次调谐后副本数应回到 Git 中的 2;验证:

kubectl -n demo get deployment web \
  -o jsonpath='{.spec.replicas}{"\n"}'

再修改 Git 中的镜像版本,提交并推送:

git add .
git commit -m "promote web to 1.27"
git push

检查:

argocd app sync web-demo
kubectl -n demo rollout status deployment/web
kubectl -n demo get pods -l app=web \
  -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.containers[0].image}{"\n"}'

最后执行正式回滚:

git revert <promote-commit>
git push
argocd app sync web-demo
kubectl -n demo rollout status deployment/web

这个流程验证了四件不同的事:

  1. Git 清单能否渲染和同步;
  2. Kubernetes Deployment 能否完成滚动更新;
  3. 人工修改是否形成可观察漂移;
  4. Git 回退是否能驱动工作负载回到旧版本。

十四、需要明确的生产边界

GitOps 提供的是声明、审计和持续调谐,不会自动解决以下问题:

  • Secret 的运行时泄露;
  • 数据库不可逆迁移;
  • 错误版本的业务逻辑;
  • 外部依赖不可用;
  • 节点资源不足;
  • 流量网关配置错误;
  • 破坏性删除;
  • 多控制器之间的写入冲突。

一个可靠的回滚条件可以形式化为:

RollbackSafe=ArtifactAvailableConfigCompatibleDataCompatibleTrafficSwitchableDependencyRecoverable\text{RollbackSafe} = \text{ArtifactAvailable} \land \text{ConfigCompatible} \land \text{DataCompatible} \land \text{TrafficSwitchable} \land \text{DependencyRecoverable}

其中:

  • ArtifactAvailable:旧镜像和 Chart 仍可获取;
  • ConfigCompatible:旧版本能使用当前配置;
  • DataCompatible:旧版本能读写当前数据;
  • TrafficSwitchable:流量可以切回旧版本;
  • DependencyRecoverable:外部依赖状态允许恢复。

只满足“旧镜像还在”,并不能推出系统可安全回滚。

GitOps 最终建立的是一个持续运行的控制回路:

Git 版本
  -> 清单渲染
  -> 期望状态
  -> API Server
  -> Kubernetes 控制器
  -> 运行时状态
  -> 差异与健康观测
  -> 再次调谐

理解这条回路后,Secret、漂移、晋级和回滚就不再是孤立功能,而是同一个问题的不同阶段:如何让可审计的期望状态,在真实、有并发、有故障的集群中逐步收敛,并在收敛失败时保留可验证的恢复路径。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。