手工把调谐循环转一圈
目标
亲手构建一次 reconcile loop 中发生的事情:带所有权标记应用期望状态,去除易变字段,排除约定忽略的字段后比较,筛选删除候选,并拒绝空结果。
为什么重要
在 gitops-manifest 实验中,你手工制造漂移并手工恢复。只需再问一个问题,就能得到 GitOps 的最后一块拼图:**谁来执行应用。**如果由人执行,那只是整理得很好的部署脚本,而不是 GitOps。
要让控制器代为执行,判断标准必须写成代码:哪些是易变值,哪些字段留给其他控制器所有,如何标记本应用所有的对象,以及结果为空时如何处理。selfHeal 最坏会让手工变更消失,而 prune 最坏会让数据消失,两者风险等级不同。把判断写成脚本后,开关名称背后的实际行为就会清晰可见。
环境
此 Pod 没有 ArgoCD 控制器。可以创建 Application 对象,但它不会自行变为 Synced。因此本实验采用先写声明,再亲自构建按声明行动的工具。字段所有权、服务端应用和调度判断由 kwok 启动的真实 apiserver 处理,因此这些部分是真实的。工作目录为 /root/gitops-sh,使用其下的 k8s/、desired/、bin/、out/。
步骤
- 在
Application shop中声明开关和忽略字段。 - 带所有权标记以服务端方式应用期望状态。
- 用
bin/normalize.sh去除易变字段。 - 用
bin/drift.sh排除忽略字段后比较。 - 制造漂移,只恢复其中一类,并记录到
out/selfheal.txt。 - 用
bin/prune.sh筛选删除候选。 - 让
bin/sync.sh拒绝空结果。 - 制造已 Synced 但不健康的状态,并整理到
out/status.txt。
参考
- 第 2 步不要声明
spec.replicas。声明约定忽略的字段会把所有权转移到本方,造成每次冲突。 - 第 5 步若省略
--force-conflicts,镜像不会恢复。这不是失败,而是字段所有权不同,请阅读消息。 - 第 7 步拒绝时必须不删除任何内容并停止。评分器会检查期间对象是否消失。
- 三个脚本都要赋予执行权限。
声明循环如何运行
在 /root/gitops-sh/k8s/application.yaml 中写入并应用 argocd 命名空间的 Application shop。在 syncPolicy.automated 中设置 prune: true、selfHeal: true、allowEmpty: false;在 syncOptions 中加入 PruneLast=true 和 ServerSideApply=true;通过 ignoreDifferences 忽略 apps/Deployment 的 /spec/replicas。目标命名空间为 sh-lab。
此 Pod 没有 ArgoCD 控制器,该对象不会自行变为 Synced。这里用它声明循环应如何运行,后续脚本按声明行动。allowEmpty 默认虽为 false,本实验仍显式写出意图。ignoreDifferences 防止回滚 HPA 等其他控制器正当所有的字段。
带所有权标记应用期望状态
在 /root/gitops-sh/desired/ 创建 deployment.yaml(名称 orders)和 configmap.yaml(名称 orders-config)。二者命名空间均为 sh-lab,且 argocd.argoproj.io/tracking-id 注解以 shop: 开头。Deployment 不要写 spec.replicas。然后用 --server-side --field-manager=argocd-controller 一次应用整个目录。
规范做法是完全不声明约定忽略的字段,否则会取得该字段所有权,之后每次与 HPA 或人工修改冲突。tracking-id 是 ArgoCD 标记自身所有权的注解,形式为 <앱>:<그룹>/<종류>:<네임스페이스>/<이름>。没有所有权标记,第 6 步就看不到该对象。应用后用 kubectl get deploy orders -o yaml --show-managed-fields 查看字段所有者。
创建去除易变字段的预处理
创建 /root/gitops-sh/bin/normalize.sh <매니페스트>。从输出 YAML 中删除 metadata 的 resourceVersion、uid、generation、creationTimestamp、managedFields 和顶层 status。人工声明的内容(kind、名称、命名空间、注解、spec)必须保留。
若直接比较 Kubernetes 自动填充且持续变化的值,即使没有变更也会每次报告差异,reconcile loop 将永不停止。反之,删除过多又会看不到真实差异,因此保留什么与删除什么同样重要。可用 python3 的 yaml 模块读取,再用 yaml.safe_dump_all 输出。评分器会用 fixture 实际运行脚本。
排除忽略字段后比较
创建 /root/gitops-sh/bin/drift.sh <선언파일> <실제파일>。先规范化,再去掉 apps/Deployment 的 spec.replicas 进行比较;相同则输出 SYNCED 并以 0 结束,不同则输出 OUTOFSYNC 并以 1 结束。
比较方向是检查声明内容是否原样存在于实际状态。实际对象含有大量 Kubernetes 自动字段,若都算作差异就无法通过。可逐层遍历声明侧的键,检查实际侧是否有相同值;列表的长度和顺序也要比较。评分器会用三组配对实际运行脚本。
制造漂移并只恢复其中一类
用 kubectl scale 把 orders 的 replicas 提高到 5,再用 kubectl set image 把镜像标签改成其他值。随后用 --server-side --field-manager=argocd-controller --force-conflicts 重新应用声明目录,并在 /root/gitops-sh/out/selfheal.txt 中写五行:DRIFT_FIELD、IGNORED、LIVE_REPLICAS、IMAGE_RESTORED、CONFLICT_RESOLUTION。
这里同时展示两件事:未声明的 spec.replicas 属于其他所有者,因此保持不变;已声明的镜像会恢复。若去掉 --force-conflicts,镜像也不会恢复,因为 kubectl set image 已取得该字段所有权。真实 ArgoCD 使用 ServerSideApply=true 同步时也以这种方式收回所有权。LIVE_REPLICAS 必须从集群读取。
按所有权标记筛选删除候选
不创建声明文件,直接在 sh-lab 中创建一个 ConfigMap,并添加 argocd.argoproj.io/tracking-id。然后创建 /root/gitops-sh/bin/prune.sh,对标记为本应用所有、但声明目录中不存在的每个对象输出一行 PRUNE=<종류>/<이름>。不要真的删除。
判断有三步:读取声明目录构建应存在列表;在集群中查找本应用所有对象;筛选后者有而前者没有的对象。无所有权标记的对象无论何时创建都不属于候选,不要误抓 kube-root-ca.crt 等集群自动创建对象。评分器会独立计算并核对输出。
渲染结果为空时停止
创建 /root/gitops-sh/bin/sync.sh。读取环境变量 DESIRED_DIR(默认值为声明目录);若没有任何清单,输出包含 REFUSED 的消息并以非 0 结束。若存在,则用 --server-side --field-manager=argocd-controller --force-conflicts 应用,输出 APPLIED=<개수> 并以 0 结束。
只需一个把 source.path 误改为空目录的提交,应存在列表就会变为 0,该应用管理的所有对象都会成为删除候选。审查者漏看一个字符就足够造成事故,因此直接拒绝空结果是最直接的防御。拒绝时必须不删除任何内容。评分器会分别用空目录和正常目录运行,并确认对象未消失。
制造已 Synced 但不健康的状态
在 /root/gitops-sh/desired/broken.yaml 中声明带所有权标记、具有无法调度 nodeSelector 的 Deployment broken,并用 sync.sh 应用。然后在 /root/gitops-sh/out/status.txt 中写五行:SYNC、HEALTH、REASON、PRUNE、ALLOW_EMPTY。
Sync 表示是否与仓库相同,Health 表示是否正常运行,它们是不同轴。因此完全按仓库部署、Pod 却无法启动的组合是合理状态。实际工作中镜像标签错误正会产生这种状态;混淆两轴会在错误方向排查。给 nodeSelector 写集群中不存在的标签即可复现。PRUNE 和 ALLOW_EMPTY 原样写入第 1 步声明的值。