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 自体がクラッシュしていたわけではないという点がポイントで、アプリケーションの問題ではなく外部依存の問題だと早めに判断できました。

失敗の流れはこうです。

  1. repo-server が起動し、正常に動作を開始する
  2. argocd-redis のホスト名解決を試みる
  3. DNS が断続的にタイムアウトする
  4. ヘルスチェック処理が遅延する
  5. livenessProbetimeoutSeconds: 1 を超過する
  6. kubelet がコンテナを kill して再起動する
  7. これを繰り返して 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    # もう存在しない PodIP
    172.20.0.201  # もう存在しない PodIP

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-server1/1 Running に回復
  • ログに grpc.code=OK が流れ始めた

まとめ

Kubernetes のオブジェクト状態が正しくても、ノードローカルのデータパス実装が古い状態を持ち続けていることがあります。control-plane 障害などのあとは特に要注意だと感じました。