ALB が 502 Bad Gateway を返す原因と診断手順 — ターゲットが Healthy でも発生するケース
ターゲットグループのヘルスチェックは全台 Healthy なのに、ALB が 502 を返し続ける。このパターンは本番障害の中でも特に混乱しやすい。ヘルスチェックが通っているという事実が「アプリは正常」という誤った確信を生み、実際の原因であるHTTPプロトコル違反やコネクション管理の問題を見落とさせる。
TL;DR — ALB 502 の主要原因と対処
| 原因カテゴリ | 具体的な症状 | 対処の方向性 |
|---|---|---|
| 不正な HTTP レスポンス | ステータス行・ヘッダーの形式違反 | アプリのレスポンス形式を修正 |
| Keep-Alive タイムアウトのミスアライン | 断続的な 502、ピーク時に増加 | アプリ側の idle timeout を ALB より短く設定 |
| 接続リセット (RST) | ALB がレスポンス受信前に RST を受け取る | アプリのクラッシュ・OOM を調査 |
| プロトコルミスマッチ | HTTPS ターゲットに HTTP で応答、またはその逆 | ターゲットグループのプロトコル設定を確認 |
| レスポンスヘッダーサイズ超過 | 特定リクエストのみ 502 | レスポンスヘッダーを削減 |
ALB 502 の仕組み — なぜ Healthy でも発生するか
ALB のヘルスチェックとリクエスト転送は独立したコネクションで動作する。ヘルスチェックは設定したパス(例: /health)に対して単純な HTTP GET を発行し、期待するステータスコードが返れば Healthy と判定する。これはアプリが 生きているか を確認するだけで、任意のリクエストに対して正しい HTTP レスポンスを返せるかは検証しない。
502 は ALB がバックエンドから有効な HTTP レスポンスを受け取れなかったことを意味する。具体的には以下のいずれかが発生している。
- バックエンドが接続を確立したが、整形式の HTTP レスポンスを返さなかった
- バックエンドがレスポンスを返す前に接続をリセット (TCP RST) した
- ALB がレスポンスを受信する前にバックエンドが接続を閉じた
- クライアント → ALB: HTTPS リクエストが到着。ALB で TLS を終端する。
- ALB → ターゲット: ALB はターゲットグループのプロトコル設定に従い HTTP または HTTPS でバックエンドに転送する。
- ターゲット → ALB: バックエンドが不正なレスポンスを返すか、RST を送信すると ALB は 502 を生成する。
- ALB → クライアント: 502 がクライアントに返される。アクセスログには
502と-(バックエンドからのステータスなし)が記録される。
ALB アクセスログで 502 の種類を特定する
診断の起点は必ずアクセスログだ。ALB のアクセスログを有効化していない場合、まずそこから始める。ログなしで 502 を追うのは暗闇の中で作業するようなものだ。
アクセスログの有効化
# S3 バケットへのアクセスログ配信を有効化
aws elbv2 modify-load-balancer-attributes \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
--attributes Key=access_logs.s3.enabled,Value=true \
Key=access_logs.s3.bucket,Value=my-alb-logs-bucket \
Key=access_logs.s3.prefix,Value=my-alb
502 ログエントリの読み方
ALB アクセスログの各フィールドのうち、502 診断で重要なのは elb_status_code、target_status_code、error_reason の3つだ。
# S3 からログをダウンロードして 502 エントリを抽出
aws s3 cp s3://my-alb-logs-bucket/my-alb/ ./alb-logs/ --recursive
grep ' 502 ' ./alb-logs/*.log | head -50
ログ内の target_status_code フィールドに注目する。
502 -: ターゲットからステータスコードなし → バックエンドが接続をリセットしたか、不正なレスポンスを返した502 200: ターゲットは 200 を返したが ALB がレスポンスを解析できなかった → ヘッダー形式の問題
Step 1: ターゲットグループのプロトコル設定を確認する
最初に確認すべきは設定ミスだ。ターゲットグループのプロトコルがアプリの実際のリスニングプロトコルと一致していない場合、ALB は TLS ハンドシェイクを試みるが失敗し、即座に 502 になる。コンソールで見た目が正しそうでも CLI で確認する。
aws elbv2 describe-target-groups \
--target-group-arns arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-tg/1234567890abcdef \
--query 'TargetGroups[*].{Protocol:Protocol,Port:Port,HealthCheckProtocol:HealthCheckProtocol,HealthCheckPath:HealthCheckPath}'
ターゲットグループが HTTPS に設定されているのにアプリが HTTP で待ち受けている場合、ヘルスチェックも HTTPS で行われていれば両方失敗するはずだが、ヘルスチェックプロトコルを HTTP に設定していると Healthy のまま 502 が出続けるという矛盾した状態になる。
Step 2: Keep-Alive タイムアウトのアライメントを検証する
断続的な 502 でログの target_status_code が - の場合、Keep-Alive タイムアウトのミスアラインが最も疑わしい。ALB はバックエンドとの接続を再利用する。バックエンドが接続を閉じるタイミングと ALB がその接続にリクエストを送るタイミングが重なると、ALB は閉じられた接続にリクエストを送り、RST を受け取って 502 を生成する。
これは競合状態だ。バックエンドが 'もう閉じる' と決めた瞬間に ALB が 'これ使う' と判断する。タイムアウト値を数秒ずらすだけで解消する。
ALB のアイドルタイムアウトのデフォルトは 60 秒だ。バックエンドアプリのKeep-Aliveタイムアウトは必ず ALB のアイドルタイムアウトより短く設定する。
# ALB の現在のアイドルタイムアウトを確認
aws elbv2 describe-load-balancer-attributes \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
--query 'Attributes[?Key==`idle_timeout.timeout_seconds`]'
# ALB のアイドルタイムアウトを変更する場合
aws elbv2 modify-load-balancer-attributes \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
--attributes Key=idle_timeout.timeout_seconds,Value=60
アプリ側の設定例(Nginx の場合): keepalive_timeout を ALB のアイドルタイムアウトより小さい値(例: 55秒)に設定する。Node.js の場合は server.keepAliveTimeout と server.headersTimeout の両方を調整する必要がある。headersTimeout は keepAliveTimeout より大きい値にしなければならない点に注意。
- 正常ケース: バックエンドの Keep-Alive タイムアウト (55s) が ALB のアイドルタイムアウト (60s) より短いため、バックエンドが先に接続を閉じ、ALB は新しい接続を確立する。
- 502 発生ケース: バックエンドのタイムアウト (60s) が ALB と同じかそれ以上の場合、ALB がリクエストを送った瞬間にバックエンドが接続を閉じる競合が発生する。
Step 3: アプリのレスポンス形式を直接検証する
ALB を経由せずにバックエンドに直接リクエストを送り、レスポンスの形式を確認する。これはヘルスチェックでは検出できない HTTP プロトコル違反を特定するためだ。
# EC2 インスタンスに SSM セッションを開始して直接 curl
aws ssm start-session \
--target i-1234567890abcdef0 \
--region us-east-1
# セッション内でアプリに直接リクエストを送り、レスポンスヘッダーを確認
curl -v http://localhost:8080/api/endpoint 2>&1 | head -50
確認すべきポイント:
- ステータス行が
HTTP/1.1 200 OKの形式になっているか Content-LengthまたはTransfer-Encoding: chunkedが含まれているか- ヘッダー名に不正な文字(スペース、コロン以外の区切り文字)が含まれていないか
- レスポンスヘッダーの合計サイズが過大でないか
Step 4: CloudWatch メトリクスで 502 のパターンを分析する
502 が特定の時間帯やターゲットに集中しているかを確認する。全ターゲットで均等に発生しているなら設定やプロトコルの問題、特定インスタンスに集中しているならそのインスタンスのアプリ状態(OOM、デッドロック等)を疑う。
# ALB の HTTPCode_ELB_502_Count メトリクスを取得
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-15T00:00:00Z \
--end-time 2024-01-15T23:59:59Z \
--period 300 \
--statistics Sum
# ターゲット別の 502 を確認(ターゲットグループ単位)
aws cloudwatch get-metric-statistics \
--namespace AWS/ApplicationELB \
--metric-name HTTPCode_Target_5XX_Count \
--dimensions Name=TargetGroup,Value=targetgroup/my-tg/1234567890abcdef \
Name=LoadBalancer,Value=app/my-alb/1234567890abcdef \
--start-time 2024-01-15T00:00:00Z \
--end-time 2024-01-15T23:59:59Z \
--period 300 \
--statistics Sum
Step 5: ターゲットの登録状態とドレイン設定を確認する
ターゲットが draining 状態に移行中の場合、新規リクエストは受け付けないが登録解除遅延の設定によっては ALB がリクエストを送り続けることがある。また、ターゲットが initial 状態から healthy になる前にトラフィックが流れることも稀にある。
# ターゲットの詳細な状態を確認
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-tg/1234567890abcdef \
--query 'TargetHealthDescriptions[*].{Id:Target.Id,Port:Target.Port,State:TargetHealth.State,Reason:TargetHealth.Reason,Description:TargetHealth.Description}'
実際の障害パターン — Node.js の headersTimeout 設定ミス
本番で遭遇した典型的なケースを記録しておく。
症状: デプロイ直後から断続的に 502 が発生。アクセスログの target_status_code は常に -。CloudWatch で見ると 502 は数分おきにスパイクし、その間は正常。
最初の誤診: ヘルスチェックが Healthy だったため、セキュリティグループやネットワーク ACL を疑い、1時間調査した。何も変わっていなかった。
実際の原因: Node.js 18 へのアップグレード後、server.keepAliveTimeout のデフォルト値が変更されていた。新しいデフォルトが ALB のアイドルタイムアウト (60秒) と同じ値になり、競合状態が頻発していた。さらに server.headersTimeout が keepAliveTimeout より小さく設定されていたため、Keep-Alive 接続上のリクエストでヘッダー受信前にタイムアウトが発生していた。
修正: keepAliveTimeout を 55000ms、headersTimeout を 60000ms に設定。502 は即座に消えた。
Node.js のバージョンアップ時はデフォルト値の変更を必ず確認する — これは公式ドキュメントに記載されているが、見落としやすい。
IAM 権限 — ログアクセスと診断に必要な最小権限
🔽 IAM ポリシーを展開
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ALBDiagnostics",
"Effect": "Allow",
"Action": [
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeLoadBalancerAttributes",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetGroupAttributes",
"elasticloadbalancing:DescribeTargetHealth",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics"
],
"Resource": "*"
},
{
"Sid": "ALBLogAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-alb-logs-bucket",
"arn:aws:s3:::my-alb-logs-bucket/*"
]
},
{
"Sid": "SSMForDirectAccess",
"Effect": "Allow",
"Action": [
"ssm:StartSession",
"ssm:DescribeInstanceInformation"
],
"Resource": "*"
}
]
}
診断フロー — ALB 502 トラブルシューティング
target_status_code を確認"] B --> C{"target_status_code"} C -->|"- (なし)"| D["プロトコルミスマッチを確認
Step 1"] C -->|"200 など"| E["レスポンスヘッダー形式を確認
Step 3"] D --> F{"プロトコル一致?"} F -->|"不一致"| G["ターゲットグループの
プロトコルを修正"] F -->|"一致"| H["Keep-Alive タイムアウト確認
Step 2"] H --> I{"タイムアウト
アライン済み?"} I -->|"未調整"| J["アプリ側 timeout を
ALB より短く設定"] I -->|"調整済み"| K["アプリ直接 curl で
レスポンス検証 Step 3"] K --> L["CloudWatch で
特定ターゲット集中確認 Step 4"] L --> M["対象インスタンスの
OOM / クラッシュ調査"]
- アクセスログで
target_status_codeを確認し、502 の種類を分類する。 -の場合はプロトコルミスマッチまたは Keep-Alive タイムアウトを調査する。- ターゲットから直接レスポンスを取得し、HTTP 形式を検証する。
- CloudWatch メトリクスで 502 が特定ターゲットに集中しているか確認する。
- タイムアウト設定を修正し、変更後のアクセスログで改善を確認する。
ALB 502 診断のまとめと次のステップ
ALB の 502 Bad Gateway は、ターゲットが Healthy であっても HTTP プロトコルレベルの問題やコネクション管理の設定ミスで発生する。診断の優先順位は: アクセスログ確認 → プロトコル設定検証 → Keep-Alive タイムアウトのアライメント → アプリ直接検証の順で進める。
関連する公式ドキュメントとして、ALB トラブルシューティングガイド および ALB アクセスログのリファレンス を参照すること。アクセスログを有効化していない環境では、まずそれを最優先で対応する。
用語集
| 用語 | 説明 |
|---|---|
| Keep-Alive タイムアウト | HTTP 持続接続をアイドル状態で維持する最大時間。ALB とバックエンドの両方に設定が存在する。 |
| TCP RST | TCP 接続を即座に強制終了するリセットパケット。ALB がこれを受け取ると 502 を生成する。 |
| target_status_code | ALB アクセスログのフィールド。バックエンドが返した HTTP ステータスコード。- はバックエンドからレスポンスなしを意味する。 |
| 登録解除遅延 (Deregistration Delay) | ターゲットをターゲットグループから削除する際、進行中のリクエストを完了させるための待機時間。 |
| アイドルタイムアウト | ALB がフロントエンド・バックエンド接続をアイドル状態で維持する最大時間。デフォルト 60 秒。 |
コメント
コメントを投稿