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 がレスポンスを受信する前にバックエンドが接続を閉じた
sequenceDiagram participant C as クライアント participant ALB as ALB participant T as ターゲット (EC2) C->>ALB: HTTPS リクエスト ALB->>T: HTTP/HTTPS 転送 alt 正常レスポンス T-->>ALB: HTTP 200 OK ALB-->>C: HTTP 200 OK else 不正レスポンス / RST T-->>ALB: TCP RST または不正な HTTP ALB-->>C: 502 Bad Gateway end
  1. クライアント → ALB: HTTPS リクエストが到着。ALB で TLS を終端する。
  2. ALB → ターゲット: ALB はターゲットグループのプロトコル設定に従い HTTP または HTTPS でバックエンドに転送する。
  3. ターゲット → ALB: バックエンドが不正なレスポンスを返すか、RST を送信すると ALB は 502 を生成する。
  4. 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_codetarget_status_codeerror_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.keepAliveTimeoutserver.headersTimeout の両方を調整する必要がある。headersTimeoutkeepAliveTimeout より大きい値にしなければならない点に注意。

sequenceDiagram participant ALB as ALB (idle timeout: 60s) participant App as バックエンドアプリ note over ALB,App: 正常ケース — アプリ timeout 55s ALB->>App: リクエスト送信 App-->>ALB: 200 OK note over App: 55s 経過 → 接続クローズ App->>ALB: FIN (接続終了通知) ALB->>App: 新規接続を確立 note over ALB,App: 502 発生ケース — アプリ timeout 60s ALB->>App: リクエスト送信 note over App: 同時に 60s 経過 → RST 送信 App->>ALB: TCP RST ALB-->>ALB: 502 生成
  1. 正常ケース: バックエンドの Keep-Alive タイムアウト (55s) が ALB のアイドルタイムアウト (60s) より短いため、バックエンドが先に接続を閉じ、ALB は新しい接続を確立する。
  2. 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.headersTimeoutkeepAliveTimeout より小さく設定されていたため、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 トラブルシューティング

graph TD A["502 発生を確認"] --> B["アクセスログを確認
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 / クラッシュ調査"]
  1. アクセスログで target_status_code を確認し、502 の種類を分類する。
  2. - の場合はプロトコルミスマッチまたは Keep-Alive タイムアウトを調査する。
  3. ターゲットから直接レスポンスを取得し、HTTP 形式を検証する。
  4. CloudWatch メトリクスで 502 が特定ターゲットに集中しているか確認する。
  5. タイムアウト設定を修正し、変更後のアクセスログで改善を確認する。

ALB 502 診断のまとめと次のステップ

ALB の 502 Bad Gateway は、ターゲットが Healthy であっても HTTP プロトコルレベルの問題やコネクション管理の設定ミスで発生する。診断の優先順位は: アクセスログ確認 → プロトコル設定検証 → Keep-Alive タイムアウトのアライメント → アプリ直接検証の順で進める。

関連する公式ドキュメントとして、ALB トラブルシューティングガイド および ALB アクセスログのリファレンス を参照すること。アクセスログを有効化していない環境では、まずそれを最優先で対応する。

用語集

用語説明
Keep-Alive タイムアウトHTTP 持続接続をアイドル状態で維持する最大時間。ALB とバックエンドの両方に設定が存在する。
TCP RSTTCP 接続を即座に強制終了するリセットパケット。ALB がこれを受け取ると 502 を生成する。
target_status_codeALB アクセスログのフィールド。バックエンドが返した HTTP ステータスコード。- はバックエンドからレスポンスなしを意味する。
登録解除遅延 (Deregistration Delay)ターゲットをターゲットグループから削除する際、進行中のリクエストを完了させるための待機時間。
アイドルタイムアウトALB がフロントエンド・バックエンド接続をアイドル状態で維持する最大時間。デフォルト 60 秒。

コメント

このブログの人気の投稿

EC2 SSH接続タイムアウトの原因と修正方法 — セキュリティグループのインバウンドルール完全ガイド

S3パブリックアクセス拒否の原因と解決策:バケットレベルの「Block Public Access」が優先される仕組み

カスタムVPCのEC2インターネット接続不可を解決する — Internet GatewayとRoute Tableの設定手順