Flux CD:从仓库制品到持续调谐、健康证据与安全退出
一次线上修复中,工程师直接执行 kubectl set image,Pod 很快恢复;十分钟后镜像又变回旧版本。大家先怀疑发布脚本重复执行,最后才在 Flux 的 Kustomization condition 里看到新一轮调谐:集群里的手工值偏离 Git,控制器把它收敛回仓库声明。手工命令没有“失效”,而是改了一个不再拥有最终决定权的运行对象。
另一次故障恰好相反:Git 提交已经合并,根 Kustomization 也显示 Ready=True,真实入口却持续返回错误。Source 已取得目标 revision、Kustomize 已 apply、Deployment 也满足 Kubernetes 健康条件,但错误的应用配置直到真实请求才暴露。GitOps 的价值不是把一次发布压成一个绿色状态,而是把来源、渲染、写入、健康和业务结果拆成可追踪的证据链。
先认清 Flux 不是一个 Git 拉取进程
Flux 的默认发行由 source-controller、kustomize-controller、helm-controller 和 notification-controller 组成。source-controller 读取 Git、OCI、Helm 仓库或 Bucket,生成带 revision 与 digest 的 Artifact;kustomize-controller 消费 Artifact,完成 build、可选 SOPS 解密、Server-Side Apply、inventory 与健康判断;helm-controller 管理 Helm release 的 install、upgrade、test、rollback 和 uninstall;notification-controller 处理出站事件与入站触发。镜像扫描和 Git 回写属于额外组件,不在默认集合中。
这套拆分决定了排障顺序。GitRepository Ready=True 只能说明远端内容可取得且 Artifact 已生成,不能证明 Kustomize build 成功;Kustomization Ready=True 只能说明配置的 apply 与健康判据通过,不能证明真实业务请求成功;HelmRelease Ready=True 也不能替代数据迁移、入口、身份或 SLO 验证。每次发布至少关联 Git revision、Artifact digest、消费方 observed revision、运行对象 generation 和一条真实请求结果。
动手前准备一个可销毁 Kubernetes 集群、一个测试 Git 仓库、能够创建 CRD/RBAC/NetworkPolicy 的集群身份,以及只对目标仓库有效的 Git 凭证。生产环境还要提前确定代理、私有 CA、DNS、出站白名单、镜像源和 Kubernetes 兼容版本。Flux 的版本与支持矩阵会变化,应在安装时从 Flux releases 与支持说明 选择同一发行 bundle 的 patch,并用目标版本 CLI 检查 Kubernetes;不要把旧教程中的集群版本当成长期合同。
CLI 是发起 bootstrap、观察和诊断的客户端,controllers 才是持续运行面。按照 Installation 为操作系统安装固定版本 CLI 后,先检查二进制身份与集群条件:
flux --version
kubectl version
kubectl config current-context
flux check --preflux check --pre 通过时,预期看到 Kubernetes API 和安装所需能力满足要求;失败时先按输出修复版本、API 或权限。不要用 cluster-admin 之外的随机高权限令牌反复试错,也不要在未确认 context 时继续 bootstrap。CLI 与 controllers 只保证有限 minor 跨度的兼容,长期应把 CLI、安装清单和 controller bundle 一起锁定。
Bootstrap 同时建立集群根和 Git 根
flux bootstrap 不是单纯的 kubectl install。它会在集群创建 CRD、RBAC、NetworkPolicy 与 controllers,同时把 gotk-components.yaml、gotk-sync.yaml 等文件写入 Git,再创建根 Source/Kustomization 让 Flux 管理自身。重复执行具有幂等意图,但仍可能更新组件、Git 文件和凭证对象,所以每次重跑都要使用一致的 namespace、branch、path、组件集合和安全 patch,并先审查 diff。
隔离仓库可以按 Generic Git bootstrap 使用 SSH deploy key。下面是读者可执行的骨架;尖括号必须替换为测试资源,不要把真实私钥提交进仓库:
flux bootstrap git \
--url=ssh://git@<git-host>/<org>/<repo> \
--branch=main \
--path=clusters/<cluster> \
--private-key-file=<deploy-key>
flux check
kubectl -n flux-system get deploy,pods
flux get sources git -A
flux get kustomizations -A执行后应看到 Git 出现组件与同步清单,flux-system 中四个默认 controller 逐步就绪,根 GitRepository 产生 revision,根 Kustomization 消费同一 revision。若仓库写入成功但 controller 拉取失败,优先检查集群侧 Secret、SSH known_hosts、CA 和代理,而不是再次创建仓库。若漏掉原有 --components-extra 后重跑,生成清单可能移除可选控制器;可选组件和 patch 应放在 bootstrap 参数及同目录 Kustomize 配置中版本化。
开发验证也可使用 flux install 或导出的安装清单,但它们不会自然建立“Flux 管理 Flux”的 Git 根。社区 Helm chart 与 Flux release 的同步节奏也可能不同。需要可恢复、自管理的团队平台时,bootstrap 更容易形成单一恢复入口;需要临时测试 controller 行为时,直接安装更轻,但测试结束必须自行删除 CRD、RBAC 与 namespace。
Source 把远端内容变成可寻址 Artifact
一个项目首先需要 Source,而不是直接让 Kustomization 读取任意 URL。GitRepository、OCIRepository 与 Bucket 会产生 Artifact;Artifact 是供下游消费的派生缓存,不是远端仓库备份。spec.interval 决定周期检查频率,webhook 或手工 reconcile 只会提前请求检查;真正证明“看到了什么”的字段是 status 中的 revision、digest 和 conditions。
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: demo-app
namespace: gitops-lab
spec:
interval: 1m
ref:
branch: main
url: ssh://git@<git-host>/<org>/<repo>
secretRef:
name: demo-app-readonly
ignore: |
/*
!/apps/
!/apps/demo/ref.branch 让 Source 跟踪主干;固定 tag 或 commit 能提高一次部署的可重复性,却不会自动前进。interval 越短,变更可见延迟越低,同时 Git 请求、Artifact 重建、队列和存储压力越高。ignore 会改变打包文件集,甚至覆盖默认忽略规则;仓库把测试数据、构建输出或密钥误放进允许目录时,Artifact 会被放大并扩大泄露面。应用前应在 CI 和隔离集群核对实际 Artifact 文件集。
创建后执行以下观察步骤:
kubectl apply -f source.yaml
flux reconcile source git demo-app -n gitops-lab
flux get source git demo-app -n gitops-lab
kubectl -n gitops-lab get gitrepository demo-app -o yaml预期 condition 为 Ready=True,Artifact 同时有 revision、digest、URL 与更新时间。错误 branch 常表现为 checkout/revision 失败;错误 deploy key、known_hosts 或 CA 会停在认证/TLS;代理不可达通常表现为连接超时。认证成功只说明“能拉取”,若供应链要求证明作者或构建身份,还要按 GitRepository 或 OCIRepository 配置签名验证,并观察 SourceVerified,不能用 pull success 代替来源可信。
Kustomization 把项目目录变成受管 inventory
项目目录应显式保存 kustomization.yaml,并把 CRD、controller、共享基础设施和应用拆成不同层。若目录中没有该文件,controller 可以自动生成目录级 Kustomization,这也可能把误放进去的合法 YAML 一并 apply。显式资源列表、固定渲染版本和 CI diff 更容易审查变更所有权。
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: demo-app
namespace: gitops-lab
spec:
interval: 5m
retryInterval: 1m
timeout: 3m
sourceRef:
kind: GitRepository
name: demo-app
path: ./apps/demo
targetNamespace: demo
prune: true
deletionPolicy: Orphan
wait: true
serviceAccountName: demo-reconcilerpath 选择 Artifact 内的渲染根;targetNamespace 给未声明 namespace 的对象设置目标,却不会创建 namespace;prune: true 让 inventory 中曾受管、但新 revision 已缺失的对象进入回收;deletionPolicy: Orphan 则在误删 Kustomization CR 时保留 inventory 中的 workload。两者并不矛盾:前者处理 source revision 减项,后者处理 Kustomization 自身删除。wait: true 会对本轮全部对象做健康检查,并忽略显式 healthChecks 列表;timeout 限制 build/apply/health 等操作等待;retryInterval 控制失败后的重试节奏;serviceAccountName 让 controller impersonate 项目身份,而不是用自身 cluster-admin 权限写入。
删除策略不能留给隐式默认。MirrorPrune 根据 prune 决定删除或孤儿保留,Delete 发出受管对象删除后允许 Kustomization 结束,WaitForTermination 还会等待对象从 etcd 消失,但只等到 spec.timeout,超时后仍允许 Kustomization 删除;Orphan 始终保留对象。对单个 PVC、共享 namespace 或 CRD 使用 kustomize.toolkit.fluxcd.io/prune: disabled 时,该对象会从自动回收中豁免,团队必须把后续清理责任登记给另一个 owner,不能把“没被删”误判成仍受 Flux 管理。
Server-Side Apply 会记录 field manager。人工修改 Flux 拥有的字段,下一轮通常被纠正;HPA、operator 或 mutating webhook 合法拥有的字段则要精确配置 SSA Merge/Ignore 策略。对整个对象 Ignore 会失去漂移保护,对 atomic list 使用 Merge 也不能保留所有并发写。先在测试资源观察 metadata.managedFields,再决定字段所有权,避免两个 controller 持续争写并制造 API Server 压力。
用正向实验证明提交真正到达业务入口
在 apps/demo 放一个 namespace、ConfigMap、Deployment 与 Service,镜像使用可访问的固定 digest,并让 HTTP 响应包含配置版本。提交并推送后,按同一条证据链观察:
flux reconcile source git demo-app -n gitops-lab
flux reconcile kustomization demo-app -n gitops-lab --with-source
flux get kustomization demo-app -n gitops-lab
kubectl -n gitops-lab get kustomization demo-app \
-o jsonpath='{.status.lastAppliedRevision}{"\n"}{.status.inventory.entries}{"\n"}'
kubectl -n demo rollout status deploy/demo --timeout=180s
kubectl -n demo port-forward svc/demo 18080:80
curl -fsS http://localhost:18080/version预期 Source revision 与目标 Git commit 对齐,Kustomization 的 lastAppliedRevision 跟上,inventory 包含本轮对象,Deployment rollout 完成,真实请求返回提交中声明的配置版本。端口转发只适合隔离验证;团队环境应从真实入口、身份和依赖链发请求。若前四项绿色而请求失败,问题已经越过 Source 与 apply 边界,应检查 Service selector、Ingress/Gateway、应用配置、依赖与数据,而不是继续强制 reconcile。
接着只改 Git 中 ConfigMap 版本并提交。再次执行 reconcile,预期响应随 revision 更新。再用 kubectl edit 手工修改受管 ConfigMap,等待一个 interval 或手工触发调谐,预期值恢复为 Git 声明。这个实验同时证明了调谐能力和写入所有权;若手工值长期保留,要检查对象是否进入 inventory、字段是否被 Ignore、Kustomization 是否 suspend,以及目标 revision 是否真的变化。
再复制一个无状态 ConfigMap 做删除反例。先保存 .status.inventory,从 Git 移除该对象并预览渲染差异;在 prune: true 下,下一轮应删除 ConfigMap,并从新 inventory 移除对应条目。随后给测试对象加 kustomize.toolkit.fluxcd.io/prune: disabled,重复减项,预期对象保留,但它不再随 Git 更新。最后在 deletionPolicy: Orphan 下删除测试 Kustomization,确认 workload 保留;重建后改为 WaitForTermination 再删除,确认控制器等待对象终止,并刻意用带 finalizer 的测试对象验证 timeout 只会结束等待、不会保证对象已经消失。每轮都使用可重建 namespace,不能拿生产 PVC 做删除语义试验。
反向实验把失败停在正确层
第一组反例只破坏 Source:把测试 Secret 的 deploy key 临时替换为无权读取仓库的 key,再触发 source reconcile。预期 GitRepository Ready=False,condition reason 指向认证或获取失败,Artifact revision 不前进,下游 Kustomization 不应凭空应用新提交。恢复原 Secret 后再次 reconcile,先看到 Source revision 前进,再看到 Kustomization 消费它。实验结束立即删除错误 key,并在 Git 服务端确认它从未获得额外仓库权限。
第二组反例只破坏健康:保持 Source 正常,把 Deployment 镜像改为不存在的 digest。预期 Source 仍 Ready=True,Kustomization 进入 Reconciling=True 与 Ready=False,Pod 出现 ImagePullBackOff,等待超过 timeout 后 condition 指向健康失败。修复 Git 并推送后,确认 observed generation、revision、rollout 和真实请求一起恢复。Reconciling=True 与 Ready=False 同时出现是状态机的合法中间态,不是 condition 自相矛盾。
第三组反例验证依赖。建立 platform -> demo-app 两个 Kustomization,让下游 dependsOn 上游,并故意使上游健康失败。预期下游出现 DependencyNotReady 且不 apply 新 generation。dependsOn 只门控下游开始,不组成跨对象事务;下游失败不会自动回滚上游,循环依赖会永久阻塞,删除顺序也要单独设计。
排障时按 Source、Artifact、build/decryption、dry-run/apply、inventory/prune、health、业务请求逐层缩小。常用读取命令如下:
flux get all -A
flux logs --all-namespaces --level=error
kubectl -n gitops-lab describe gitrepository demo-app
kubectl -n gitops-lab describe kustomization demo-app
kubectl -n flux-system logs deploy/source-controller --since=10m
kubectl -n flux-system logs deploy/kustomize-controller --since=10m日志中可能含仓库 URL、revision、资源名和错误响应,集中采集前应脱敏并限制读取。单看 controller 日志“reconciliation finished”不够,condition 与对象状态才是可查询合同;单看 Ready=True 也不够,真实请求仍是最后一段证据。
HelmRelease 维护的是另一套发布状态
HelmRelease 引用 source-controller 提供的 chart,helm-controller 观察 chart revision 与 values digest,执行 install、upgrade、test、rollback 与 uninstall。Kustomization inventory 和 Helm release storage 是两套所有权模型,不应把 Helm 渲染出的每个对象再交给另一个 Kustomization 重复管理。
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: demo-addon
namespace: gitops-lab
spec:
interval: 10m
timeout: 5m
targetNamespace: demo
storageNamespace: gitops-lab
serviceAccountName: demo-reconciler
chart:
spec:
chart: demo-addon
version: "<fixed-chart-version>"
sourceRef:
kind: HelmRepository
name: approved-charts
valuesFrom:
- kind: Secret
name: demo-addon-values
test:
enable: true
driftDetection:
mode: enabledmetadata.namespace 是 HelmRelease 对象位置,targetNamespace 是 chart 资源位置,storageNamespace 保存 release 元数据;改变后两者可能触发 uninstall/install,而不是无损移动。valuesFrom 按顺序合并,后项覆盖前项,inline spec.values 优先级最高。Secret/ConfigMap 添加 watch 标签可触发即时调谐,但也会增加事件频率。preserveValues 会复用历史 release values,削弱声明式可重建能力,通常应保持关闭。
安装失败时,RemediateOnFailure 倾向在重试间 uninstall 或 rollback,RetryOnFailure 倾向周期重试且不主动回退。无状态组件和带 PVC、CRD、长 Job 的组件不能共用一套无限重试策略。Helm test 只有显式启用才执行;若配置忽略测试失败,Ready=True 不再等于测试通过。CRD upgrade 的 Skip、Create、CreateReplace 还会改变 schema 与 storedVersions 风险,必须独立检查已有 CR,而不是只看 release condition。字段语义应随目标版本查阅 HelmRelease 文档。
凭证、SOPS 与租户身份决定爆炸半径
Source 拉取优先使用只读 deploy key;bootstrap 需要的仓库管理或写入身份不应长期复用;需要回写 Git 的自动化再使用只对目标仓库和分支有效的写身份。Secret 与 Source 位于同一 namespace,SSH host key、HTTPS CA、proxy credential 和 registry credential 都是敏感资产。轮换时先添加新凭证并验证新连接,再撤销旧凭证,同时检查历史日志、备份和 CI artifact 没有留下明文。
SOPS 让密文进入 Git,并由 kustomize-controller 在 build/apply 边界解密:
spec:
decryption:
provider: sops
secretRef:
name: sops-age
serviceAccountName: demo-kms-readerage 私钥、KMS 或 Vault/OpenBao 身份仍是高价值凭证。对象级 serviceAccountName 可把云端解密权限限制到租户,优于让共享 controller 持有全局 KMS 身份。先加入新 recipient、重加密并证明新旧密文都可解,再移除旧 key;先删旧 key 会让历史 revision 和灾备恢复无法解密。SOPS 解密后的 Secret 仍会进入 API Server 与 etcd,还需要静态加密、RBAC、审计和 Pod 读取限制。配置细节应就近核对 SOPS guide。
默认 kustomize-controller 和 helm-controller 可能以 cluster-admin 能力调谐,namespace 分仓并不等于租户隔离。团队平台应为每个 Kustomization/HelmRelease 指定最小 ServiceAccount,开启禁止跨 namespace 引用、禁止 remote bases 和默认 impersonation 身份等 multi-tenancy lockdown 配置,再用反例证明租户不能创建 ClusterRoleBinding、写其他 namespace 或引用其他租户 Source。controllers 仍共享进程、缓存、网络和故障域;强对抗或合规隔离需要独立集群或独立安装。
容量、成本与长期治理要看调谐乘法
Flux 的负载近似由对象数、调谐频率、Artifact 大小、渲染成本、目标集群 API 延迟和失败重试共同放大。把所有 Source interval 降到很短,会同时增加 Git/OCI 请求、Artifact 写入、队列、解压、build、server-side dry-run 和 API Server 流量;失败对象的重试又会叠加。容量评估应记录 controller workqueue depth/latency、reconcile duration、error/requeue、进程 CPU/内存、Artifact 存储、Git/registry 流量、API Server throttle 与对象总量的趋势,而不是套用一个固定“每集群可管理多少对象”的数字。
大 monorepo 可通过明确 path、ignore、按变更频率拆 Source 或使用 OCI Artifact 降低无关内容传输,但拆得过细会增加凭证、对象和队列管理成本。高可用副本能改善 controller 进程故障恢复,不会让外部 Git、registry、KMS 或 API Server 自动高可用;Artifact 也是派生缓存,灾备仍需恢复 source、凭证、bootstrap 清单和 SOPS 身份。
团队治理应为平台根、租户 Source、Kustomization、HelmRelease 和凭证分别指定 owner。每次模板升级审查 CRD、RBAC、controller flags、NetworkPolicy、资源限制、可选组件、镜像 digest 与 API 版本;告警至少区分 source fetch、build/decryption、apply/prune、health、Helm action 与业务验证。成本不只是 controller Pod,还包括 Git/registry 请求、Artifact 存储、日志索引、API Server 压力、云 KMS 调用和长期保留的测试 namespace、PVC、LoadBalancer。
架构选型上,少量可信团队、统一 Kubernetes 模型和希望组件化调谐的场景适合 Flux;需要把 Helm 与 Kustomize 作为独立控制器组合、希望以 Git 自管理控制面时优势明显。若组织强依赖集中式 UI、细粒度应用项目模型或大量人工发布编排,应比较相邻 GitOps 产品的操作模型;若发布需要跨系统长事务、数据库补偿和人工审批状态机,则应让流水线或工作流系统负责事务编排,Flux 只消费批准后的期望状态。
升级、回滚与卸载必须分清控制面和工作负载
升级先固定目标 release,检查 Kubernetes 支持、可选组件、feature gates、bootstrap patches、Git 中 API versions 和 CRD storedVersions。Flux 的 Upgrade 要求升级者同时考虑 Git 清单与集群存量对象;跨越已移除 beta API 的版本时,只替换 controller image 会留下无法读取的对象。先在 canary 集群迁移 Git 与 etcd,再用相同 bootstrap 参数更新组件,最后验证全部 Source/Kustomization/HelmRelease conditions、controller rollout、真实请求和告警链路。
回滚也不能只把镜像 tag 改回旧版。旧 controller 必须能理解新 CRD 字段和 storedVersions;若目标 minor 已移除旧 API,应先按 release migration instructions 恢复兼容对象,再 revert Git 中组件清单。应用回滚则 revert 业务 revision,让 Source 和消费方沿正常链路收敛;直接手改集群只会制造下一轮漂移。
退出有两种完全不同的结果。保留 workload 时,先 suspend Git 写入与镜像自动化,导出 Source、Kustomization、HelmRelease status、inventory、Helm storage、SOPS 身份和 field ownership,确认没有其他 controller 争写,再把业务 Kustomization 明确设为 Orphan 后预览并卸载 Flux。删除 workload 时,必须在 controllers 仍运行时审查 inventory,把目标 Kustomization 明确改为 Delete 或 WaitForTermination,再删除 Kustomization/HelmRelease,并按 Helm uninstall 核对普通对象、CRD/CR、PVC、LoadBalancer 和外部资源。只看到 Flux CR 消失不能证明 workload 已核销。
flux suspend kustomization demo-app -n gitops-lab
flux uninstall --dry-run
# 审查计划后再执行:flux uninstall
# 隔离实验若要先删除业务
kubectl -n gitops-lab patch kustomization demo-app --type=merge \
-p '{"spec":{"deletionPolicy":"WaitForTermination"}}'
kubectl -n gitops-lab delete kustomization demo-app
kubectl -n gitops-lab delete helmrelease demo-addon
kubectl get all,configmap,secret,pvc -n demoUninstall 明确说明 flux uninstall 移除 Flux controllers、CRD、RBAC、NetworkPolicy 与 Flux CR,却不会自动删除过去部署的 workload 或 Helm release。强删 Deployment、CRD 或整个 namespace 可能绕过 finalizer 并留下 release 与业务对象。实验清理后还要撤销 Git deploy key、webhook、KMS/Vault 权限,删除测试仓库/分支、namespace、PVC 与外部负载均衡资源,并再次检查集群里没有 Flux CRD、finalizer 和孤儿 field manager。退出证据与安装证据同等重要,因为它证明团队真正知道谁拥有运行状态。
