ALB で 502 Bad Gateway が出たときの原因と切り分け手順【AWS】

目次
  1. 🎯 結論:ALB の 502 で最も多い原因と最初にやること
  2. 🕵️ アクセスログの error_reason から原因を特定する
  3. 🛠️ 原因別に見る典型パターンと対処
  4. 🧰 切り分けを進めるためのコマンドと確認手順
  5. 🛡️ 再発防止:設定の見直しと監視の入れ方
本記事にはプロモーション(広告)が含まれています

ALB(Application Load Balancer)経由のアプリが突然 502 Bad Gateway を返す。このとき ALB 自体が壊れていることはほとんどありません。502 は「ALB はターゲットに接続できたが、ターゲットから有効な HTTP レスポンスを受け取れなかった」というシグナルで、調べる先はバックエンド側です。以下、アクセスログのどのフィールドを見れば原因が絞れるのか、代表的なパターンごとに何を直せばよいのかを、確認しやすい順に並べます。

🎯 結論:ALB の 502 で最も多い原因と最初にやること

現場で遭遇する ALB の 502 は、おおよそ次の 4 つに収束します。

  1. バックエンドの Keep-Alive タイムアウトが ALB のアイドルタイムアウトより短く、再利用しようとした接続が切られている
  2. バックエンドが HTTP として不正なレスポンスを返している(ヘッダが大きすぎる、Content-Length が実体と不一致、ヘッダに不正な文字が混入)
  3. アプリケーションプロセスがクラッシュ/再起動中で、接続直後に切断されている(ECS のデプロイ中、OOM Kill など)💥
  4. HTTPS ターゲットで TLS ハンドシェイクが成立していない

いずれも「ALB が接続した瞬間からレスポンス受信の途中まで」のどこかで失敗しています。504(タイムアウト)と違い、502 は「応答が返ってきたが解釈できない、または途中で切れた」状態だと覚えておくと切り分けが早くなります。

まず確認すべき3点:ターゲットのヘルスチェック・アクセスログ・アプリのログ

復旧を急ぐ場面では、次の 3 点を並行して見ます。

ひとつ目はターゲットのヘルスチェック状態です。

aws elbv2 describe-target-health \
  --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/my-tg/abcdef1234567890

State が healthy なのに 502 が出ている場合、ヘルスチェックのパス(例:/healthz)だけが軽量で成功し、実業務のパスで落ちている可能性が高くなります。逆に unhealthy が混ざっているなら、まずアプリが起動しきっていない、あるいは落ちているという線を疑います。

ふたつ目は ALB のアクセスログです。502 が「ALB が生成したもの」か「ターゲットが返した 502 をそのまま中継したもの」かは、target_status_code を見れば一発で分かります。ここが - なら ALB 生成、502 ならバックエンド(さらに背後の nginx や別サービス)が 502 を返しています。後者なら、調べる場所は ALB ではなくその先です🔎

3 つ目はアプリケーションと Web サーバのログです。502 の発生時刻に、アプリ側で例外・OOM・worker の再起動・broken pipe が記録されていないかを確認します。ALB のログとアプリのログの時刻を突き合わせるのが最短ルートです。

ALB の 502 と 503/504 の違い(どこで切れているか)

ステータス ALB が判断している内容 主な原因
502 Bad Gateway ターゲットに接続はできたが、レスポンスが不正 or 接続が途中で切れた Keep-Alive 不一致、不正ヘッダ、プロセスのクラッシュ、TLS 失敗
503 Service Unavailable ルーティングできる healthy なターゲットが 1 つもない 全台 unhealthy、ターゲット未登録、ルールの転送先不在
504 Gateway Timeout ターゲットがアイドルタイムアウト内に応答を返しきらなかった 遅いクエリ、外部 API 待ち、タイムアウト値不足

この 3 つを混同したまま調べると、504 の対処(タイムアウト延長)を 502 に適用して悪化させることがあります。502 でアイドルタイムアウトを「延ばす」のは基本的に逆効果で、多くの場合は後述のとおりバックエンド側の Keep-Alive を「ALB より長くする」のが正解です。

🕵️ アクセスログの error_reason から原因を特定する

アクセスログを有効化して 502 のレコードを抽出する

アクセスログが無効だと切り分けは一気に難しくなります。まだ有効化していない場合は、S3 バケットとバケットポリシーを用意した上で属性を設定します。

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
  --attributes \
    Key=access_logs.s3.enabled,Value=true \
    Key=access_logs.s3.bucket,Value=my-alb-logs \
    Key=access_logs.s3.prefix,Value=prod

⚠️ ログは Athena でクエリするのが実用的です。テーブル定義(CREATE TABLE 文)は ALB のログ形式のバージョンによってカラムが増えるため、必ず公式ドキュメントの最新の DDL をコピーしてください。テーブル作成後、502 だけを抽出します。

SELECT time,
       client_ip,
       target_ip,
       elb_status_code,
       target_status_code,
       request_processing_time,
       target_processing_time,
       response_processing_time,
       error_reason,
       classification,
       classification_reason,
       request
FROM   alb_logs
WHERE  elb_status_code = '502'
  AND  time >= '2024-01-01T00:00:00.000000Z'
ORDER  BY time DESC
LIMIT  100;

見るべきは次の組み合わせです。

  • target_status_code = '-' かつ target_processing_time = -1 → ALB はターゲットへ接続したがレスポンスを受け取れなかった。接続切断・不正レスポンス系の典型。
  • request_processing_time = -1 → ALB がリクエストをターゲットへ送りきれなかった。ターゲットが受信前に接続を閉じている。
  • target_status_code = '502' → 502 を作っているのはバックエンド自身。ALB の設定をいじっても直らない。
  • classification に Acceptable / Ambiguous / Severe が入っている → リクエストまたはレスポンスの HTTP としての妥当性に問題がある(Desync mitigation 関連)。classification_reason に BadHeader、BadContentLength、MultipleContentLength などが出ていれば、そこが直接の原因です🧨

代表的な error_reason と対応する原因の対照表

ひとつ注意点があります。error_reason フィールドに入るのは主に Lambda ターゲットのエラー理由です。EC2 や IP ターゲットでは - のままであることが多く、その場合は上記の target_status_code / 各 processing_time / classification_reason の組み合わせで判断します。

error_reason 意味 対処の方向
LambdaInvalidResponse Lambda の戻り値が ALB の期待する JSON 形式になっていない statusCode / headers / body / isBase64Encoded の形に整える
LambdaResponseTooLarge レスポンスが ALB の上限を超えた ボディを縮小、S3 への署名付き URL にオフロード
LambdaUnhandled Lambda 関数が未処理の例外で終了 関数内の例外ハンドリングを追加、CloudWatch Logs を確認
LambdaUserError / LambdaInternalError 権限不足・設定不備、または Lambda 側の問題 ALB からの Invoke 権限(resource-based policy)を確認
LambdaTimeout Lambda の実行時間超過(504 になる) 関数のタイムアウト値と処理内容を見直し
ClientDisconnected クライアントが先に切断 502 の直接原因ではないことが多い

各コードの正確な定義と、レスポンスサイズなどの上限値はアップデートされることがあります。Elastic Load Balancing のユーザーガイド(アクセスログのエラー理由コード/Lambda 関数をターゲットとして使用する)で最新の値を確認してください。

🛠️ 原因別に見る典型パターンと対処

Keep-Alive タイムアウトの不一致(ALB のアイドルタイムアウト > バックエンド)

断続的に、しかも低頻度(全リクエストの 0.01〜1% 程度)で 502 が出るなら、まずこれを疑います。

ALB はターゲットへのコネクションを Keep-Alive で再利用します。ここでバックエンドの Keep-Alive タイムアウトが ALB のアイドルタイムアウト(デフォルト 60 秒)より短いと、次のレースコンディションが起きます⚡

  1. バックエンドが「もう使われていない」と判断して接続を閉じる(FIN 送信)
  2. ほぼ同時に ALB がその接続でリクエストを送る
  3. ALB から見ると「リクエスト送信直後に接続が切れた」= 502

対処は単純で、バックエンドの Keep-Alive タイムアウトを ALB のアイドルタイムアウトより長くします。数秒の余裕を持たせます。

# nginx(ALB のアイドルタイムアウトが 60 秒の場合)
keepalive_timeout 75s;
# Apache
KeepAlive On
KeepAliveTimeout 75
// Node.js(既定の keepAliveTimeout は短いため明示的に延長する)
const server = app.listen(3000);
server.keepAliveTimeout = 65 * 1000;
server.headersTimeout  = 66 * 1000; // keepAliveTimeout より大きく

Go の http.Server は IdleTimeout、Java(Tomcat/Spring Boot)は server.tomcat.keep-alive-timeout や Connector の keepAliveTimeout が該当します。ALB 側のアイドルタイムアウトは次で確認・変更できます。

aws elbv2 describe-load-balancer-attributes \
  --load-balancer-arn <ALB_ARN> \
  --query "Attributes[?Key=='idle_timeout.timeout_seconds']"

バックエンドが HTTP として不正なレスポンスを返している(ヘッダサイズ・改行・Content-Length)

特定の URL・特定のユーザーだけ確実に 502 になるなら、レスポンスの中身を疑います。よくあるのは次の 3 つです🧐

  • レスポンスヘッダが大きすぎるケース。巨大な Set-Cookie(セッションに大量データを詰めている)、長大な JWT、大量のカスタムヘッダなど。ALB にはレスポンスヘッダの合計サイズ上限があり、超えると 502 になります。具体的な上限値は公式ドキュメントのクォータ(ELB のクォータ)を確認してください。
  • Content-Length と実際のボディ長が一致しない、またはヘッダが重複しているケース。Content-Length が 2 つある、Transfer-Encoding: chunked と Content-Length が同時に存在する、といったパターンです。アクセスログの classification_reason に MultipleContentLength や BadContentLength が出ます。
  • ヘッダ値に制御文字や生の改行、非 ASCII が入っているケース。ユーザー入力をそのままヘッダに埋め込んでいるアプリで発生しがちです(ファイル名を Content-Disposition に入れる処理など)。URL エンコードするか、RFC に従った形式に直します。

💡 切り分けには、ALB を経由せずターゲットへ直接リクエストしてヘッダを比較するのが確実です(手順は後述)。

HTTPS ターゲットの証明書・プロトコル不一致

ターゲットグループのプロトコルを HTTPS にしている構成では、ALB とターゲット間の TLS ハンドシェイクが失敗して 502 になることがあります。確認するのは次の点です。

  • ターゲットの証明書が期限切れになっていないか。ALB はターゲット証明書の CA 検証を行いませんが、ハンドシェイク自体が成立しなければ当然 502 になります
  • ターゲットが対応する TLS バージョン・暗号スイートが極端に限定されていないか。古い設定のみ、あるいは新しい設定のみに絞っていると噛み合わないことがあります
  • ターゲットグループのプロトコル/ポートが実際のリスニング設定と一致しているか。アプリは 8080 で平文待ち受けなのにターゲットグループが HTTPS:8080 になっている、といった取り違えは頻出です
  • ヘルスチェックのプロトコルもターゲットに合っているか

確認コマンド例:

# ターゲットの証明書と TLS バージョンを確認
openssl s_client -connect 10.0.1.23:8443 -servername example.internal </dev/null 2>/dev/null \
  | openssl x509 -noout -dates -subject

Lambda ターゲット/コンテナ(ECS・EKS)特有の 502 パターン

Lambda ターゲットの場合、502 の原因はほぼ「戻り値の形式」と「サイズ」に集約されます。ALB が要求する形は次のとおりです📐

{
  "statusCode": 200,
  "statusDescription": "200 OK",
  "isBase64Encoded": false,
  "headers": { "Content-Type": "application/json" },
  "body": "{\"message\":\"ok\"}"
}

body を文字列以外(オブジェクトのまま)で返す、statusCode を文字列で返す、そもそも return を忘れて undefined になる。いずれも LambdaInvalidResponse として 502 になります。バイナリを返す場合は isBase64Encoded: true と Base64 エンコードが必須です。

ECS / EKS では、デプロイやスケールインのタイミングに 502 が集中するのが典型です。よくあるのは次のケースです。

  • コンテナが SIGTERM を受けた直後に接続を即切断している。アプリで SIGTERM を受けたら新規受付を止めつつ、処理中のリクエストを完了させる graceful shutdown を実装します
  • ターゲットグループの Deregistration delay が短すぎて、処理中リクエストが終わる前に接続が切られている。既定 300 秒から短縮している場合は妥当性を再確認します⌛
aws elbv2 modify-target-group-attributes \
  --target-group-arn <TG_ARN> \
  --attributes Key=deregistration_delay.timeout_seconds,Value=60
  • EKS で Pod 削除と Endpoint 反映にラグがある。preStop に短い sleep を入れて、ターゲットからの登録解除が反映されるまで待たせます
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 15"]
  • OOM でコンテナが落ちている。ECS のタスク停止理由(OutOfMemoryError: Container killed due to memory usage)や Kubernetes の OOMKilled を確認し、メモリ上限を見直します

🧰 切り分けを進めるためのコマンドと確認手順

ターゲットに直接 curl してレスポンスを確認する

ALB を挟まずにターゲットへ直接リクエストすると、「不正なレスポンスを返しているのはアプリ自身か」が即座に判断できます。同一 VPC 内の踏み台や、同じサブネットの EC2 / タスクから実行します。

# ヘッダを含めて表示(ALB 経由時と比較する)
curl -sv http://10.0.1.23:8080/api/users -o /dev/null

# ヘッダの総サイズを概算する(巨大ヘッダの検出)
curl -sD - http://10.0.1.23:8080/api/users -o /dev/null | wc -c

# Keep-Alive で 2 回連続リクエストし、接続再利用時に切れないか確認
curl -sv --keepalive-time 90 \
  http://10.0.1.23:8080/health \
  http://10.0.1.23:8080/health -o /dev/null

判断の目安は次のとおりです。

  • 直接 curl は 200 で正常、ALB 経由だけ 502 → ヘッダサイズ、Keep-Alive、TLS など ALB とターゲットの「間」の問題🎯
  • 直接 curl でも異常(ヘッダが巨大、接続が切れる、TLS エラー) → アプリ/Web サーバ設定の問題
  • たまにしか再現しない → Keep-Alive 不一致かデプロイ由来を優先的に疑う

ヘッダサイズが怪しいときは、実際の Set-Cookie の長さを見ます。

curl -sD - http://10.0.1.23:8080/login -o /dev/null \
  | awk '/^[Ss]et-[Cc]ookie/ {print length($0), $0}'

CloudWatch メトリクスとターゲットグループの状態を突き合わせる

アクセスログは反映までに時間がかかるので、発生中の状況を把握するなら CloudWatch メトリクスのほうが向いています。名前空間 AWS/ApplicationELB で次のメトリクスを並べて見ます📊

メトリクス 読み取れること
HTTPCode_ELB_502_Count ALB が生成した 502 の数。ここだけ増えていればバックエンドの接続/レスポンス異常
HTTPCode_Target_5XX_Count ターゲット自身が返した 5xx。アプリのエラーを疑う
TargetConnectionErrorCount ALB がターゲットへの接続確立に失敗した数。SG/NACL、プロセス停止を疑う
UnHealthyHostCount 502 と同時に増えていればデプロイ・クラッシュ由来
TargetResponseTime 502 直前に急伸していれば過負荷・リソース枯渇の線
aws cloudwatch get-metric-statistics \
  --namespace AWS/ApplicationELB \
  --metric-name HTTPCode_ELB_502_Count \
  --dimensions Name=LoadBalancer,Value=app/my-alb/1234567890abcdef \
  --start-time 2024-01-01T00:00:00Z \
  --end-time 2024-01-01T01:00:00Z \
  --period 60 \
  --statistics Sum

HTTPCode_ELB_502_Count と UnHealthyHostCount が同じ時刻に立ち上がっているならデプロイ/プロセス異常、UnHealthyHostCount が 0 のまま 502 だけが薄く継続しているなら Keep-Alive 不一致や不正レスポンス、という読み方ができます。合わせて、ターゲットの OS 側で接続が切られていないかも確認します。

# ターゲット側で TIME_WAIT / 接続リセットの傾向を見る
ss -s
netstat -s | grep -iE "reset|overflow|listen"

listen queue overflow が増えていれば backlog 不足(somaxconn、アプリの listen backlog)も候補に入ります。

🛡️ 再発防止:設定の見直しと監視の入れ方

原因を潰したら、同じ形で再発しないように次の 4 点を定常設定として固定します。

💡 ひとつ目はタイムアウトの序列です。「ALB のアイドルタイムアウト < バックエンドの Keep-Alive タイムアウト」を原則とし、さらにその内側にアプリのリクエストタイムアウト、DB 接続タイムアウトを収めます。値をコード(Terraform / CloudFormation)で管理し、AMI やコンテナイメージの更新で巻き戻らないようにしてドキュメント化します。

ふたつ目、アクセスログは常時有効化しておきます。障害発生後に有効化しても、その時点より前のログは取得できません。S3 のライフサイクルルールで保持期間とコストをコントロールしつつ、常時出力にしておくのが実務上の前提です。Athena のテーブルも事前に作成しておくと、障害時にクエリを投げるだけで済みます。

3 つ目は 502 のアラームを分けることです。HTTPCode_ELB_502_Count と HTTPCode_Target_5XX_Count は別アラームにします。前者が鳴ったら ALB とターゲット間、後者が鳴ったらアプリのエラー、と対応が分岐するためです。

aws cloudwatch put-metric-alarm \
  --alarm-name alb-elb-502 \
  --namespace AWS/ApplicationELB \
  --metric-name HTTPCode_ELB_502_Count \
  --dimensions Name=LoadBalancer,Value=app/my-alb/1234567890abcdef \
  --statistic Sum --period 60 --evaluation-periods 2 \
  --threshold 5 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:ap-northeast-1:123456789012:alerts

4 つ目はデプロイ時の 502 を減らす設定です。graceful shutdown の実装、preStop の待機、Deregistration delay の適正化、ヘルスチェックの HealthyThresholdCount と Interval の調整をセットで見直します。特にヘルスチェックのパスを「DB 接続まで確認する」ものにしておくと、起動途中のインスタンスにトラフィックが流れて 502 になる事故を減らせます。

⚠️ ここで挙げた上限値(ヘッダサイズ、Lambda レスポンスサイズ、アイドルタイムアウトの設定可能範囲など)は、サービスのアップデートで変わり得ます。設計に組み込む前に、Elastic Load Balancing の公式ドキュメントとクォータのページで現在値を確認してください。

さらに学ぶには

ALB の挙動は、ターゲットグループ・ヘルスチェック・セキュリティグループ・VPC ルーティングの理解とセットで初めて素早く切り分けられるようになります。周辺知識の整理には以下も参考にしてください。