背景#

这次故障一开始看起来像两个互不相干的问题:

  1. ArgoCD 里 gateway Application 一直是 Degraded
  2. 其他几个 Application 又出现了 OutOfSync 或者后续的 Unknown

如果只看表面,很容易把它归类成“Cilium Gateway API 不稳定”或者“ArgoCD 自己抽风”。

但真正麻烦的地方在于,这次故障不是单点失效,而是一条跨层级的链路问题:

ArgoCD 健康状态异常
-> 先看到 Gateway Degraded
-> 再发现 repo-server 拉 Git 也失败
-> 往下追到 CoreDNS 外部解析异常
-> 再继续追到 ZITADEL 根本没有把 backend Service 建出来
-> 最后发现 Vault 里的 ZITADEL master key 长度也不对

也就是说,最上层暴露出来的是 Gateway 和 ArgoCD,真正的根因却横跨了:

  • Cilium Gateway API
  • ArgoCD repo-server
  • CoreDNS
  • systemd-resolved
  • Vault + External Secrets
  • ZITADEL Helm 安装链路

这篇文章就按我实际排查的顺序,把整个问题怎么拆开、怎么验证、最后怎么收敛讲清楚。

初始症状#

最早看到的是 ArgoCD 状态不对:

  • gatewaySynced/Degraded
  • personal-serviceszitadel 一度有漂移
  • 后面甚至出现 Unknown

与此同时,Uptime Kuma 也有两个监控项报错,所以一开始很难判断哪些是同一条故障链,哪些只是噪音。

这类时候我比较在意一件事:不要一上来就把所有红的东西都当成同一个根因。

所以我先分了三条线:

  1. GitOps 漂移线:为什么 OutOfSync
  2. Gateway 健康线:为什么 Degraded
  3. 运行时连通性线:为什么后面又出现 Unknown

第一层:先把“假问题”剥掉#

ExternalSecret 漂移其实不是故障,只是默认字段不一致#

personal-serviceszitadel 最早的 OutOfSync,根因其实不复杂。

live 对象里的 ExternalSecret 被控制器补了一些默认字段,比如:

  • deletionPolicy
  • conversionStrategy
  • decodingStrategy
  • metadataPolicy
  • template.engineVersion
  • template.mergePolicy

Git 里没写,ArgoCD 就一直觉得 live 和 desired 不一致。

这个问题的处理很直接:

  1. 把这些默认字段显式补回 Git
  2. 重新同步

做完以后,这一批 OutOfSync 基本就消掉了。

这一步的价值不是“修了一个大问题”,而是把后面真正的红灯从噪音里抠出来。

第二层:为什么 gateway 一直是 Degraded#

一开始我怀疑的是 Cilium GatewayClass#

最先看到的 live 状态是:

  • Gateway 已经 Accepted=True
  • 但是 Programmed=False
  • reason 是 AddressNotAssigned

继续看下去,发现 Cilium 给 Gateway 生成的是 LoadBalancer Service,但集群里并没有可用的 CiliumLoadBalancerIPPool,于是它的 external IP 一直是 <pending>

这说明:

  • Gateway 控制器本身不是完全挂掉
  • 但默认的 cilium GatewayClass 和当前 homelab 网络模型并不匹配

所以我先做了第一轮修复:

  1. 新增 CiliumGatewayClassConfig
  2. 把 Service 类型改成 NodePort
  3. 新增 GatewayClass cilium-nodeport
  4. Gateway 改用这个新的 class

这个修改是必要的,因为它解决了“默认 Cilium GatewayClass 需要 LB IPAM,而我当前集群没有”的错配问题。

但问题并没有彻底结束。

真正拖住 gateway 健康的是一条坏掉的 HTTPRoute#

后面在继续查 condition 的时候,我发现 gateway 这个 Application 之所以还在 Degraded,并不完全是 Gateway 自身有问题。

罪魁祸首其实是 zitadel 对应的一条 HTTPRoute

ResolvedRefs=False
BackendNotFound
Service "zitadel" not found

这一下定位就完全变了。

也就是说:

  • Gateway 主资源已经没那么糟了
  • 但是它下面挂着的 route 有一个后端引用无效
  • ArgoCD 最终把这整个应用判成了 Degraded

这一步很重要,因为它把问题从“Cilium Gateway API 失效”改写成了“Gateway 下面有一个 backend 根本不存在”。

第三层:为什么 ZITADEL backend Service 根本不存在#

既然 HTTPRoute 指向的 Service/zitadel 不存在,那就不能只修 route,得继续往回看 ZITADEL 为什么没起来。

这时候看到两个连续问题。

问题 A:ZITADEL master key 长度错误#

ZITADEL 的 setup job 一直失败,报错本质上是:

masterkey must be 32 bytes

根因不在 Helm,也不在 Gateway,而在 Vault 里的密钥值本身不合法。

这类问题很容易误导人,因为表面上你看到的是:

  • Helm release 失败
  • Service 没创建
  • HTTPRoute backend 不存在
  • Gateway degraded

但最底层其实只是一个 secret 值的格式错了。

修复动作是:

  1. 旋转 Vault secret/homelab/zitadel 里的 master-key
  2. 确保它是合法的 32-byte 值
  3. 重新触发 ZITADEL 安装

做到这里,理论上 ZITADEL 应该能起来了。

但实际上还差一步。

问题 B:Helm 还在因为外部 DNS 失败而拉不到 chart#

即使 master key 修好了,ZITADEL 还是一度装不起来,因为集群内部对 charts.zitadel.com 的解析不稳定。

这时候问题正式从应用层掉进了基础设施层。

第四层:ArgoCD 为什么后来变成 Unknown#

在我修 manifest 漂移和 Gateway 的过程中,ArgoCD 状态又发生了变化:

原本是 Degraded / OutOfSync,后来一些应用直接变成了 Unknown

这通常不是资源本身坏了,而是 ArgoCD 连“判断你现在是什么状态”都做不到了。

argocd-repo-server 的报错,真正的问题是:

lookup github.com on 10.43.0.10:53: server misbehaving

这说明 repo-server 访问 GitHub 失败,原因不是 GitHub 挂了,而是集群内部 DNS 解析失败。

于是问题链继续往下游走:

ArgoCD Unknown
-> repo-server 无法解析 github.com
-> CoreDNS 解析外部域名失败

第五层:CoreDNS 其实在转发给一个容器里根本不可达的 stub resolver#

最后定位到的 CoreDNS 根因,是这次我觉得最值得记住的地方。

现场状态#

CoreDNS 当时的配置逻辑大致是:

forward . /etc/resolv.conf

而节点上的 /etc/resolv.conf,实际又是 systemd-resolved 的 stub:

127.0.0.53

在宿主机上这可能没问题,因为本机能访问自己的本地 stub。

但 CoreDNS Pod 是一个容器。

当它把外部查询继续转发到 127.0.0.53 时,那个地址指向的是容器自己的 loopback,不是宿主机的 systemd-resolved。

结果就是:

  • CoreDNS 对外域名解析 SERVFAIL
  • repo-server 不能解析 github.com
  • Helm 不能解析 charts.zitadel.com
  • cloudflared 也可能解析失败

这一层才是整条链里最隐蔽、但影响范围最大的故障点。

修复动作#

1. 让 kubelet 给 Pod 传递真实 resolver 列表,而不是 stub#

在 K3s 配置里加入:

kubelet-arg:
  - resolv-conf=/run/systemd/resolve/resolv.conf

这样 dnsPolicy: Default 的 Pod,比如 CoreDNS,就不再继承 /etc/resolv.conf 里的 127.0.0.53,而是继承真实上游列表。

2. 给节点的 systemd-resolved 增加 fallback DNS#

即使 Tailscale MagicDNS 在网络抖动后出现负缓存或者不可用,节点也还有公共解析器兜底:

[Resolve]
DNS=8.8.8.8 1.1.1.1
FallbackDNS=8.8.4.4

3. live 先救火,CoreDNS 直接改成公共 forwarders#

为了尽快恢复集群,我先在 live 上把 CoreDNS 改成:

forward . 1.1.1.1 8.8.8.8

这一步让 repo-server 和 Helm 安装链路马上恢复工作能力。

4. 再把这件事固化进仓库#

只做 live patch 不够,因为下次重装、重启、或者 addon 重建时它可能又漂走。

所以我后来把 CoreDNS 的目标状态写回了 k8s/ansible

  • setup-k3s.yaml
  • fix-dns-fallback.yaml

现在这两个入口都会在 K3s ready 之后重新把 CoreDNS 的 external forwarders 调整到公共解析器,而不是继续依赖某次手工 patch。

这一步很关键,因为它把“临时救火”变成了“可重复执行的恢复能力”。

最后一跳:把坏掉的 HTTPRoute zitadel 重建掉#

当 ZITADEL backend 已经真实存在以后,HTTPRoute zitadel 其实还出现过一次“状态没刷新”的情况。

它继续报:

ResolvedRefs=False
BackendNotFound Service "zitadel" not found

这时候最有效的动作不是继续等,而是:

  1. 删除这条 stale 的 HTTPRoute
  2. 触发一次 ArgoCD sync
  3. 让它按 Git 中的定义重新创建

重建之后,condition 就恢复成:

ResolvedRefs=True
Service reference is valid

随后 gateway Application 也终于从 Degraded 变回了 Healthy

这次顺手修掉的另外两类问题#

虽然主线是 Gateway/ArgoCD/CoreDNS/ZITADEL,但这次我也顺手把另外两块技术债一起收了。

Oracle 业务 secret 归位#

之前 Oracle 集群的一部分业务 secret 还留在 secret/homelab/* 下,和当前约定不一致。

这次我做了两步:

  1. Git manifest 统一改为 secret/oracle-k3s/<service>
  2. live Vault 中把已经迁移完的旧重复路径清掉

保留例外只有 homelab/kopia,因为 Oracle 的备份任务本来就是复用 homelab 的 Kopia 服务。

Uptime Kuma 误报清理#

最后剩下的两个报错监控,并不是服务真的挂了,而是预期 HTTP 状态码过时了。

我直接把监控配置里的 accepted status codes 修正后,heartbeat 立刻恢复正常。

我从这次故障里记住的 5 个点#

1. Gateway Degraded 不等于 Gateway 本体坏了#

在 Gateway API 体系里,父资源的健康状态可能被某条子 route 的 backend 引用拖垮。

先看 Gateway 自己的 condition,也要继续看下面每条 HTTPRouteResolvedRefs

2. ArgoCD Unknown 优先查 repo-server 能不能做 DNS#

只要 repo-server 连 github.com 都解析不了,后面很多健康判断都会失真。

这时候修 YAML 通常没意义,先让 Git 拉取得回来。

3. forward . /etc/resolv.conf 在 systemd-resolved stub 场景下很危险#

宿主机的 127.0.0.53 对宿主机本身有效,不代表对容器也有效。

这类问题平时不容易暴露,但一旦碰到外部 chart 拉取、Git 仓库访问、Tunnel 出站解析,就会一起炸。

4. Secret 格式错误会在上层表现成完全不同的症状#

这次 ZITADEL master-key 长度错误,最终在界面上看起来像:

  • Helm chart 失败
  • Service 缺失
  • HTTPRoute backend 不存在
  • Gateway degraded

所以排查链路里一定要愿意往 Secret 和 Job 日志那一层下钻。

5. live 修复必须回写 repo,不然只是“延迟复发”#

这次如果只停在:

  • patch CoreDNS
  • 改一下 Vault
  • 手动重跑 ZITADEL

那下次节点重建或者服务重建还是会踩回去。

真正有价值的是把这些结论变成:

  • Git manifest
  • Ansible playbook
  • Runbook
  • 事故复盘文档

最终收尾状态#

这次处理完以后,最终状态是:

  • ArgoCD 主要应用恢复 Synced/Healthy
  • gateway 恢复健康
  • ZITADEL backend 和 route 都恢复正常
  • CoreDNS 外部解析链路恢复
  • CoreDNS 修复已固化到 Ansible
  • Oracle 旧重复 Vault 路径已清理
  • Uptime Kuma 误报已消除

换句话说,这次不是简单地“把红灯点灭了”,而是把一条跨越 GitOps、DNS、身份系统和网关的故障链,拆成了几个能验证、能回放、能写进仓库的修复点。

结语#

我现在越来越觉得,homelab 最难的不是“把组件装起来”,而是当多个控制面同时出问题时,能不能保持一条稳定的排查顺序。

这次故障给我的一个提醒是:

当你看到的是 Gateway 和 ArgoCD 报错,真正该怀疑的可能是 DNS、Secret,甚至是某个根本没成功创建出来的 backend Service。

如果以后再碰到类似现象,我大概率会按这个顺序重新走一遍:

  1. 先剥离 GitOps 漂移噪音
  2. 再看 Gateway / Route conditions
  3. 再看 backend Service 是否真的存在
  4. 再看 ArgoCD repo-server 的 DNS
  5. 最后再往 CoreDNS 和节点 resolver 下钻

这条链,这次已经替我验证过一次了。