Argo CD が CrashLoopBackOff になったけど、原因は Redis でも CoreDNS でもなかった
はじめに
先日、Argo CD の argocd-repo-server が突然 CrashLoopBackOff に陥りました。
ログを見ると Redis への接続エラーが出ていていました。調べてみると Redis も CoreDNS も生きていて、原因はもっと深いところにありました。
環境は Cilium を CNI として使っていて、kube-proxy replacement は無効という構成です。
最初に見えていたエラー
argocd-repo-server のログにはこんなエラーが出ていました。
dial tcp: lookup argocd-redis: i/o timeout
Liveness probe failed: context deadline exceeded
Container repo-server failed liveness probe, will be restarted
Pod のイベントを見ると liveness probe が繰り返し失敗していて、kubelet がコンテナを kill → 再起動を繰り返していました。repo-server 自体がクラッシュしていたわけではないという点がポイントで、アプリケーションの問題ではなく外部依存の問題だと早めに判断できました。
失敗の流れはこうです。
- repo-server が起動し、正常に動作を開始する
argocd-redisのホスト名解決を試みる- DNS が断続的にタイムアウトする
- ヘルスチェック処理が遅延する
livenessProbeのtimeoutSeconds: 1を超過する- kubelet がコンテナを kill して再起動する
- これを繰り返して CrashLoopBackOff になる
調査の流れ
まず「本当に Redis が壊れているか」を確認しました
argocd-redis の Service と Endpoints を確認しました。どちらも正常に存在していました。Redis プロセスへの TCP 接続も通りました。
→ Redis は問題なし。
次に「CoreDNS がダウンしていないか」を確認しました
CoreDNS の Pod はすべて Running で、ログにも異常はありませんでした。
→ CoreDNS プロセス自体は問題なし。
ここで「Redis も CoreDNS も生きているのに DNS が失敗する」という状況になりました。
DNS の失敗を再現・絞り込む
影響を受けている Pod が動いているワーカーノード上にデバッグ Pod を立てて DNS を叩いてみました。
- CoreDNS の Pod IP に直接クエリを投げる → 成功
kube-dnsの Service IP(ClusterIP)経由でクエリを投げる → 断続的に失敗
ここで「CoreDNS 自体ではなく、Service IP 経由のロードバランシングが壊れている」という仮説が立ちました。
さらに複数のワーカーノードで同じテストをしてみると、特定のノードでだけ失敗し、他のノードでは成功することがわかりました。問題はクラスター全体ではなく、ノード固有のものでした。
Kubernetes のオブジェクト状態は正しかった
kube-dns の Service・Endpoints を確認すると、有効な CoreDNS バックエンドの IP しか登録されていませんでした。kube-proxy が書き込む iptables のルールも、全ノードで正しいバックエンドを参照していました。
→ Kubernetes のオブジェクトレイヤーは正しい。
ということは問題はその下にあります。
Cilium の BPF マップを確認したら犯人が見つかった
cilium-dbg bpf lb list
失敗していたノードの出力を見ると、kube-dns Service のバックエンドとしてすでに存在しない Pod の IP が残っていました。
# 正しい状態(正常なノード)
kube-dns (UDP/TCP :53)
backends:
172.20.0.58 # 現在の CoreDNS Pod
172.20.0.80 # 現在の CoreDNS Pod
# 壊れた状態(問題のあったノード)
kube-dns (UDP/TCP :53)
backends:
172.20.0.58 # 現在の CoreDNS Pod
172.20.0.80 # 現在の CoreDNS Pod
172.20.0.6 # もう存在しない Pod の IP
172.20.0.201 # もう存在しない Pod の IP
DNS クエリが kube-dns Service IP に来るたびに、これらの stale なバックエンドにもトラフィックが分散され、当然タイムアウトしていました。
なぜこうなったか
調査中に、数日前に control-plane / API サーバーで短時間の障害があったことがわかりました。ちょうどそのタイミングで CoreDNS Pod が再起動し、kube-dns の Endpoints が更新されていました。
CoreDNS が再起動するとバックエンドの IP が変わります。通常であれば Cilium はこの変更を検知して BPF マップを更新するのですが、control-plane 障害の影響で一部のノードが再同期に失敗し、古い IP がマップに残り続けたというのが最も自然な説明です。
問題が特定のノードだけで起きていたのも、この再同期の成否がノードごとに違ったからだと考えられます。
直し方
影響を受けたノードで Cilium Pod を再起動します。
# ノードを指定して Cilium Pod を削除(DaemonSet が再作成してくれる)
kubectl -n kube-system delete pod -l k8s-app=cilium \
--field-selector spec.nodeName=<影響ノード名>
Cilium Pod が再起動すると BPF マップが再構築され、stale なエントリが消えます。
なぜ他の対処では直らなかったか
| 対処 | なぜダメか |
|---|---|
| repo-server を再起動 | ノードの BPF マップは変わらないので再発する |
| Redis を再起動 | 同上 |
| CoreDNS を再起動 | 同上。CoreDNS が再起動しても、各ノードの BPF マップは自動では更新されない |
| Cilium を再起動 | BPF マップが再構築されて、正常に戻る |
stale なエントリは Kubernetes オブジェクトの下のレイヤーにあるので、Kubernetes オブジェクトを操作しても取れません。
修正後の確認
Cilium を再起動したあと:
cilium-dbg bpf lb listから stale なバックエンド IP が消えていることを確認- 問題ノードからの DNS クエリが連続 20 回すべて成功
argocd-repo-serverが1/1 Runningに回復- ログに
grpc.code=OKが流れ始めた
まとめ
Kubernetes のオブジェクト状態が正しくても、ノードローカルのデータパス実装が古い状態を持ち続けていることがあります。control-plane 障害などのあとは特に要注意だと感じました。