TL;DR
- ALB が返す 502 は、ALB → ターゲット間の 接続失敗・不正レスポンス・接続切断 が主原因です。「インフラ起因」と決めつけがちですが、ターゲット側 (Pod) の不安定(プロセスクラッシュ・半開状態) が ALB 502 として表面化しているケースも多いです。
- 切り分けは「ALB アクセスログ → Target Group ヘルスチェック → Pod ログ・イベント」の3層が定石。それぞれの層で見るべきフィールド・メトリクス・コマンドが違います。
- 実例では、断続的な 502 の原因は アプリ例外+Pod 規模不足の複合 でした。インフラ設定変更ゼロ、アプリチーム側の修正のみで収束しています。
環境
| 項目 | 内容 |
|---|---|
| クラウド | AWS |
| ロードバランサ | Application Load Balancer (ALB) |
| アプリ実行基盤 | EKS on EC2(マネージドノードグループ上でマイクロサービスを稼働) |
| ルーティング | ALB → Target Group(ターゲットタイプ=IP) → Pod(AWS Load Balancer Controller / TargetGroupBinding 想定) |
| 監視 | CloudWatch メトリクス + ALB アクセスログ (S3) |
EKS on EC2 を前提に書きますが、3層切り分けの考え方は ALB → ECS / EC2 / Lambda 構成でも同じです。
本記事で扱う「3層切り分け」のスコープを先にお示しします。各層で見るフィールド・メトリクス・コマンドが異なります。
図1: ALB 502 の3層切り分けスコープ(第1層=ALBアクセスログ/第2層=Target Group ヘルスチェック/第3層=Pod ログ・イベント)
起きたこと
ある日、アプリ担当者から「特定の画面で 502 が 断続的に 発生する」と連絡が入りました。監視アラートより先に人検知です。全リクエストが落ちているわけではなく、一部のリクエストだけ 502 を返している、という状況でした。
502 = Bad Gateway は「ALB がターゲットから有効な応答を得られなかった」ことを示すステータスです。AWS 公式仕様上、ALB が 502 を返すのは主に以下のケースです (アプリが正常に 500 / 503 を返した場合は、ALB はそれをそのまま中継するため 502 にはなりません)。
- ターゲットへの TCP 接続失敗 (TLS ハンドシェイク失敗、SG/NACL 拒否、ターゲット停止中)
- ターゲットからの 不正な HTTP レスポンス (不正ヘッダ・プロトコル違反)
- ターゲットとの 接続が応答完了前に切断 (Pod クラッシュ、keep-alive timeout ミスマッチ等)
「ALB から先のターゲット = EKS Pod の通信不全」を疑うのは正しい方向ですが、通信不全の根本原因がアプリ層 (Pod プロセスの異常) にある ことも多いです。先入観を持たずに層を分けて確認していきます。
第1層: ALB アクセスログ
最初に見るのは ALB アクセスログ (S3 出力) です。フィールドの組み合わせで「ALB 側で完結している 502 か、ターゲットからの応答異常か」が分かります。
見るべきフィールド
| フィールド | 意味 |
|---|---|
elb_status_code |
ALB がクライアントに返したステータスコード |
target_status_code |
ターゲット (Pod) が ALB に返したステータスコード。- ならターゲットからの応答なし |
request_processing_time |
クライアント受信〜ターゲット送信までの時間 |
target_processing_time |
ターゲット送信〜ターゲット応答受信までの時間。-1 は ターゲットから有効な応答を取得できなかった ことを示す |
response_processing_time |
ターゲット応答受信〜クライアント返信までの時間 |
パターン別の読み方
ALB が 502 を返すとき、target_status_code は通常 - (有効なHTTPレスポンスを受け取れていない) になります。target_processing_time の値で「接続段階の失敗か、応答の途中切断か」を判別できます。
elb_status_code |
target_status_code |
target_processing_time |
想定される原因層 |
|---|---|---|---|
502 |
- |
-1 |
ターゲット未到達 or 接続失敗 (TCP接続失敗、TLSハンドシェイク失敗、SG/NACL拒否、ターゲット停止) → 第2層へ |
502 |
- |
数値あり | 接続後の応答異常 (不正レスポンス、接続途中切断、Podクラッシュ、keep-aliveミスマッチ) → 第2層・第3層へ |
504 |
- |
数値あり (大) | ALB アイドルタイムアウト超過 (アプリ処理遅延)。502ではないが参考 |
500 / 503
|
同じ 500 / 503
|
数値あり | ターゲット自身の 5xx を ALB がそのまま中継。ALB は 502 を生成しない (本記事スコープ外、アプリログを直接調査) |
ALB は ターゲットが正常に返した 5xx (500 / 503 等) はそのまま中継します。
elb_status_code = 502が出ているなら、ターゲットから「正常な HTTP レスポンス」を受け取れていない (= 接続/応答段階で何か起きている) と判断できます。
抽出手段
ALB アクセスログ (S3) は Athena でクエリするか、件数が少なければ S3 Select で十分です。Athena の標準的なテーブル定義では elb_status_code / target_status_code は STRING 型で定義されるため、比較値は文字列リテラル で書きます。
-- Athena 例: 直近1時間の 502 を target_status_code 別に集計
-- AWS 標準 DDL: elb_status_code / target_status_code / time は string 型 (time は ISO8601 文字列)
-- パーティション (year/month/day) を併用してスキャン量を抑えるのが実務的
SELECT target_status_code, COUNT(*) AS cnt
FROM alb_access_logs
WHERE year = '2026' AND month = '06' AND day = '14' -- 環境の DDL に合わせて調整
AND elb_status_code = '502'
AND from_iso8601_timestamp(time) >= current_timestamp - interval '1' hour
GROUP BY target_status_code
ORDER BY cnt DESC;
target_status_code がほぼ - で埋まっていれば、第2層・第3層で「ターゲット側で何が起きているか」を裏取りします。
第2層: Target Group ヘルスチェック履歴
第1層で「ターゲットからの応答なし」が多いと出たら、Target Group のヘルスチェックを確認します。Pod 自体が断続的に Unhealthy 判定されていないかを見る層です。
見るべき CloudWatch メトリクス
| メトリクス | 確認ポイント |
|---|---|
UnHealthyHostCount |
一時的にスパイクしているか。断続発生のタイミングと一致するか |
HealthyHostCount |
想定 Pod 数を下回っていないか |
TargetConnectionErrorCount |
ALB → ターゲット間の TCP 接続失敗数 |
HTTPCode_ELB_5XX_Count |
ALB 自身が生成した 5xx の総数 (500 / 502 / 503 / 504 を含む)。発生タイミングの相関確認に有効。502 単独の詳細判定はアクセスログ側で行う |
HTTPCode_Target_5XX_Count |
ターゲットが正常に返した 5xx の総数 (500 / 503 等)。502 の調査では原則使わない |
UnHealthyHostCount が断続的にスパイクし、HTTPCode_ELB_5XX_Count と TargetConnectionErrorCount が同期して増加していれば、特定の Pod が一時的に落ちて再起動を繰り返している 可能性が高いです。次の層で kubectl ベースで裏取りします。
第3層: Pod ログ・イベント
最後は EKS 側です。Pod 単位で「落ちている事実」と「落ちた理由」を取りに行きます。
確認コマンド
# Pod の現状と再起動回数 / Last State
kubectl describe pod <pod-name> -n <namespace>
# 直前のコンテナのログ (再起動した場合に有効)
# 単一コンテナの Pod ならこれで十分
kubectl logs <pod-name> -n <namespace> --previous
# サイドカーなど複数コンテナを含む Pod では -c <container-name> でコンテナを指定
kubectl logs <pod-name> -n <namespace> --previous -c <container-name>
# Namespace のイベントを時系列で
kubectl get events -n <namespace> --sort-by='.lastTimestamp'
# Deployment / ReplicaSet の Pod 状態
kubectl get pods -n <namespace> -o wide
確認項目
-
アプリ例外:
kubectl logs --previousでクラッシュ直前のスタックトレース。HTTP リクエスト処理中のNullPointerException/OutOfMemoryError/ DB接続失敗など -
OOMKilled:
describe podのLast State: Terminated, Reason: OOMKilled。コンテナのメモリ requests/limits の見直し - CrashLoopBackOff: 起動 → クラッシュ → 再起動を繰り返している状態
-
スケジューリング失敗:
eventsにFailedScheduling(ノードリソース不足) やFailedCreatePodSandBox(CNI / ENI 上限) - ReadinessProbe 失敗: Pod が Ready にならず Target Group から外れる
ALB は UnHealthyHostCount が一定数を超えると当該 Pod へのルーティングを止めますが、その判定に至るまでの間、一部のリクエストで 502 が観測されることがあります。これが「断続的 502」の典型的な発生メカニズムの一つです。
実例:断続502 の原因がアプリ例外+規模不足だった
ある EKS on EC2 のマイクロサービス構成で、ALB 502 が断続的に発生したケースです。完全汎用化のため、固有の構成情報は省きます。
切り分けの経路
-
第1層 (ALB アクセスログ): 502 のうち相当数で
target_status_code = -。target_processing_timeは 数値あり (接続後の切断) と-1(接続自体の失敗) が混在。Pod プロセスがクラッシュするタイミングによって、接続維持できているケースと既にポートが閉じているケースの両方が発生していると推定 -
第2層 (Target Group ヘルス):
UnHealthyHostCountが時間帯によりスパイク。HTTPCode_ELB_5XX_CountとTargetConnectionErrorCountも同期して増加。「一部の Pod が落ちて接続を維持できなくなっている」第1層の推定と整合 -
第3層 (Pod ログ・イベント):
kubectl logs --previous -c <container>でアプリ側の例外スタックトレースを確認。並行してkubectl get eventsに Pod の再起動・OOMKilled系メッセージ。describe podでRestarts回数が増加
当たり
- アプリケーション層の例外: 特定の入力データで未ハンドリング例外が発生し、ワーカーが死ぬ
- Pod / ノード規模不足: 健全な Pod に負荷が寄って遅延・タイムアウト、結果として更に Unhealthy 判定が拡大
対処
- アプリチームへ修正依頼: 例外ハンドリングの追加・該当ロジックの修正
- インフラ側の設定変更(ALB アイドルタイムアウト / Target Group ヘルスチェック閾値 / HPA 設定など)は実施せず
「インフラを触らずに収束させられた」のは、3層切り分けで原因が アプリ層に閉じている と早い段階で確信できたためです。
まとめ
| 教訓 |
|---|
| ALB 502 は「ターゲットからの正常な応答を得られない」状態。ターゲットが返した 5xx (500/503) は ALB が中継するだけで 502 にはならない |
まず elb_status_code = '502' の target_status_code と target_processing_time を確認し、接続段階の失敗か応答段階の異常かを判別する |
| 切り分けは ALB アクセスログ → Target Group ヘルス → Pod ログ・イベント の3層 |
| 断続的 502 は一部の Pod / 一部のリクエストだけ落ちているシグナル。レイヤ別ログで当たりをつける |
| インフラ担当が「アプリ起因」を切り分け切ってアプリチームに渡せると、責任分界が明確になり修正が早い |
EKS on EC2 を前提に書きましたが、ALB → ECS / EC2 / Lambda 構成でも、第1層・第2層の考え方は同じです。第3層が ECS タスクログや EC2 アプリログに置き換わるだけです。
同じ AWS マルチリージョン構成で踏んだ別の罠は、姉妹記事 「cannot execute INSERT in a read-only transaction」— Aurora Global Database で踏んだセカンダリ書込みのワナ にまとめています。あわせてどうぞ。
