業務システムをコンテナ化してKubernetes(複数のコンテナを束ねて自動運用する基盤ソフトウェア)に載せ替えたあと、Podが理由もなく再起動を繰り返す事象に悩んだことはないでしょうか。エンタープライズの既存システムをリプレースする際によく起きるつまずきの一つです。この記事では、その原因の切り分け方と対策手順を整理します。
何が起きるか
Kubernetesでは、アプリケーションの実行単位を「Pod」と呼びます。Podは1つ以上のコンテナをまとめた最小のデプロイ単位です。
本番移行の直後によく見られるのが、Podが数分おきにCrashLoopBackOff(起動失敗を繰り返す状態)になる事象です。kubectl get pods で確認すると、RESTARTSの列が数十回、数百回と増え続けているケースがあります。
影響範囲は単純ではありません。Kubernetesには「自己修復(self-healing)」という仕組みがあり、失敗したPodを自動で作り直します。このため一見サービスは動いているように見えても、裏側では再起動が延々と続き、レスポンスタイムの劣化やDBコネクションの枯渇につながることがあります。
特に既存のオンプレ環境からリフト&シフトでコンテナ化した業務システムでは、この症状が出やすい傾向があります。理由は次の段階的な分解で説明します。
なぜ起きるか
原因は大きく3層に分けて考えると整理しやすくなります。
1層目: アプリケーション自体の起動失敗
最も多いのは、設定ファイルの読み込み失敗や、外部DB・APIへの接続タイムアウトです。オンプレ時代は起動時に数十秒待てば接続先のミドルウェアが立ち上がっていましたが、コンテナ環境では起動順序の保証がありません。Kubernetesはコンテナ同士の起動順序を制御する仕組みを標準では持たないため、アプリ側が「接続先がまだ準備できていない」ことを前提に設計されていないと失敗します。
2層目: リソース制限による強制終了
PodにはCPUとメモリの上限(limits)を設定できます。Javaアプリケーションのように起動時にヒープを大きく確保する処理は、メモリ上限に達した瞬間にOOMKilled(メモリ不足による強制終了)としてコンテナごと落とされます。オンプレのVMでは潤沢だったメモリが、コンテナのlimitsで想定より厳しく絞られていることが原因になっているケースがよくあります。
3層目: ヘルスチェックの設計ミス
KubernetesにはPodの死活監視の仕組みとして、livenessProbe(生存確認)とreadinessProbe(受信準備確認)があります。アプリの起動に時間がかかるにもかかわらず、livenessProbeの猶予時間(initialDelaySeconds)が短すぎると、正常に起動する前にKubernetesが「異常」と判断して強制的に再起動をかけてしまいます。これは実質的に自傷行為に近い設定ミスです。
この3層は互いに絡み合うため、ログだけを見て「アプリのバグだ」と早合点すると原因究明が長引きます。
自分のプロジェクトが該当するか確認する方法
実際の状態を切り分けるために、次の順で確認します。
# Podの状態とRESTARTS回数を確認
kubectl get pods -n <namespace>
# 直近の再起動理由を確認(Last StateとReasonに注目)
kubectl describe pod <pod-name> -n <namespace>
# クラッシュ直前のログを確認(前回のコンテナのログ)
kubectl logs <pod-name> -n <namespace> --previouskubectl describe pod の出力にある Last State が OOMKilled であれば2層目のメモリ問題です。Reason が Error でアプリログにスタックトレースが出ていれば1層目のアプリ起因です。
livenessProbeが原因かどうかは、同じ describe pod の Events 欄で Liveness probe failed が繰り返し出ているかを見ます。加えて、マニフェスト(Podの定義ファイル、YAML形式)の該当箇所を確認します。
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10initialDelaySeconds がアプリの実際の起動時間より短い場合、起動完了前にチェックが走ってしまいます。アプリ起動に30秒かかるなら、少なくとも30秒以上の値が必要です。
リソース設定は次のコマンドで確認できます。
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[*].resources}'limits.memory の値と、実際にアプリが使用するメモリ量(JVMならヒープサイズ設定と一致しているか)を突き合わせます。
対策の手順
原因が特定できたら、層ごとに対策します。
- アプリ起因の場合: 外部接続処理にリトライとバックオフ(一定間隔で再試行する仕組み)を実装する。接続先の準備を待つInitContainer(本体のコンテナより先に実行される準備用コンテナ)を追加する方法も有効です
- メモリ起因の場合:
limits.memoryを実測値の1.5倍程度に見直す。JVMアプリなら-Xmxの値をコンテナのメモリ上限より確実に小さく設定する - ヘルスチェック起因の場合:
initialDelaySecondsをアプリの実測起動時間に合わせて延長する。加えてstartupProbe(起動完了を待つ専用のプローブ)を導入し、起動中はlivenessProbeの判定を止める設計に変更する - 恒久対策: ステージング環境で本番相当の負荷テストを実施し、
kubectl top podでメモリ・CPUの実測値を取ってからlimitsを設定する
既存のオンプレ資産をそのままコンテナに詰め替えるだけでは、Kubernetesの自己修復や死活監視の前提に合わない設計が残ります。設定を一つずつ本番相当の条件で検証し直す作業は地味ですが、移行後の障害対応コストを大きく左右します。
まとめ
CrashLoopBackOffは単一の原因で起きるわけではなく、アプリ・リソース・ヘルスチェックの3層で切り分けることが出発点になります。
まず kubectl describe pod と --previous オプション付きのログ確認から始めてください。Last State の内容で大まかな層が絞り込めます。
既存システムのコンテナ化を進めている場合は、本番投入前に kubectl top pod で実測リソースを取り、limitsとProbeの設定値を実測値ベースで見直すことが再起動の再発防止につながります。