ALBの502をerror_reasonで特定し原因別に直す
目次
ALB経由のアプリが突然502 Bad Gatewayを返す。このときALB自体の障害はほとんどありません。502は「ALBはターゲットに接続できたが、有効なHTTPレスポンスを受け取れなかった」という意味で、調査対象はバックエンドです。アクセスログのどのフィールドで原因を絞り、パターンごとに何を直すのかを順に見ていきます。
結論:ALB の 502 で最も多い原因と最初にやること
現場で遭遇するALBの502は、おおよそ次の4つに収束します。
- バックエンドのKeep-AliveタイムアウトがALBのアイドルタイムアウトより短く、再利用しようとした接続が切られている
- バックエンドがHTTPとして不正なレスポンスを返している(ヘッダが大きすぎる、Content-Lengthが実体と不一致、ヘッダに不正な文字が混入)
- アプリケーションプロセスがクラッシュ/再起動中で、接続直後に切断されている(ECSのデプロイ中、OOM Killなど)
- 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→ リクエストを送りきれなかった。ターゲットが受信前に接続を閉じている。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のユーザーガイドで最新の値を確認してください。
原因別に見る典型パターンと対処
Keep-Alive タイムアウトの不一致(ALB のアイドルタイムアウト > バックエンド)
断続的に、しかも低頻度(全リクエストの0.01〜1%程度)で502が出るなら、まずこれを疑います。
ALBはターゲットへのコネクションをKeep-Aliveで再利用します。バックエンドのKeep-AliveタイムアウトがALBのアイドルタイムアウト(デフォルト60秒)より短いと、次のレースコンディションが起きます。
- バックエンドが「もう使われていない」と判断して接続を閉じる(FIN送信)
- ほぼ同時にALBがその接続でリクエストを送る
- 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になるなら、レスポンスの中身を疑います。
レスポンスヘッダが大きすぎるケース。巨大なSet-Cookie、長大なJWT、大量のカスタムヘッダなどです。ALBにはレスポンスヘッダの合計サイズ上限があり、超えると502になります。上限値は公式ドキュメントのクォータを確認してください。
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を受けた直後に接続を即切断しているなら、新規受付を止めつつ処理中のリクエストを完了させる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)も候補です。
この記事のテーマをさらに学べる本
EC2やIAMなど基本サービスを含むAWS運用の手法が体系立てて解説されており、日々の運用知識を整理できます。
楽天ブックスで詳しく見るよくある質問
502 が出たとき ALB のアイドルタイムアウトを延ばせば直る?
逆効果です。バックエンドのKeep-AliveタイムアウトがALBのアイドルタイムアウト(デフォルト60秒)より短いとレースコンディションで502になるため、バックエンド側をALBより長くするのが正解です。nginxならkeepalive_timeout 75s、Node.jsならkeepAliveTimeoutを65秒にして数秒の余裕を持たせます。タイムアウト延長は504の対処であり、混同すると悪化させることがあります。
502 を出しているのが ALB かバックエンドかは見分けられる?
アクセスログのtarget_status_codeで判別できます。「-」ならALBが生成した502、「502」ならバックエンド(さらに背後のnginxや別サービス)が返した502の中継です。後者はALBの設定を変えても直らず、調べる先はその先になります。CloudWatchではHTTPCode_ELB_502_CountとHTTPCode_Target_5XX_Countを分けて見れば同じ判別ができます。
アクセスログの error_reason が「-」のままでも原因は絞れる?
絞れます。error_reasonに値が入るのは主にLambdaターゲットで、EC2やIPターゲットでは「-」のままが多い。その場合はtarget_status_code、request_processing_time / target_processing_time、classification_reasonの組み合わせで判断します。target_status_codeが「-」でtarget_processing_timeが-1なら接続切断・不正レスポンス系、classification_reasonにBadHeaderやMultipleContentLengthが出ていればそこが直接の原因です。
ECS や EKS のデプロイ時にだけ 502 が集中するのは何が原因?
コンテナがSIGTERM直後に接続を即切断している、ターゲットグループのDeregistration delayが短すぎて処理中リクエストが終わる前に切られている、といったケースが典型です。EKSではPod削除とEndpoint反映のラグがあるため、preStopに短いsleepを入れて登録解除の反映を待たせます。あわせてOOMでコンテナが落ちていないか(ECSのタスク停止理由やKubernetesのOOMKilled)も確認し、graceful shutdownの実装とセットで見直します。
再発防止:設定の見直しと監視の入れ方
原因を解消したら、再発を防ぐために次の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になる事象を減らせます。
ここで挙げた上限値は、サービスのアップデートで変わり得ます。設計に組み込む前に、Elastic Load Balancingの公式ドキュメントとクォータのページで現在値を確認してください。
さらに学ぶには
ALBの挙動は、ターゲットグループ、ヘルスチェック、セキュリティグループ、VPCルーティングの理解とあわせて初めて素早く切り分けられます。周辺知識の整理には以下も参考になります。