概要
Application Load Balancer(ALB)の後ろにある Web アプリにアクセスしたら、502 Bad Gateway・503 Service Unavailable・504 Gateway Timeout のどれかが返ってきた。どれも「サーバー側のエラー」ですが、ALB が自分で返す 5XX は、ALB がターゲットとの間で何に失敗したかを表しています。コードを見分けられれば、調べる場所をすぐに絞れます。
- 502: ターゲットにつなごうとしたら拒否された(TCP RST)、または応答が壊れていた
- 503: 振り分け先のターゲットが 1 台も登録されていない
- 504: ターゲットにつながらないまま接続タイムアウト(10 秒)が過ぎた、またはつながったがアイドルタイムアウトまでに応答が無かった
ややこしいのは、アプリ自身が 502・503・504 を返すこともある点です。ALB はターゲットが返したエラーをそのままクライアントへ中継するので、ブラウザから見ると区別がつきません。どちらが返したかは、CloudWatch の HTTPCode_ELB_5XX_Count と HTTPCode_Target_5XX_Count で見分けます。
この記事では、ALB が 502・503・504 を返す条件と、エラーの出どころの見分け方を整理します。実践では、EC2 1 台の後ろに置いた ALB で、ターゲットの登録解除(503)・アプリの停止(502)・アイドルタイムアウトより遅い応答(504)を順に起こし、そのたびに直して元に戻ることを確かめます。最後に、アプリ自身が返した 503 と ALB が返した 503 が、メトリクスでは別のものとして数えられることを確認します。

クラウドおさる猿山のバナナ倉庫の窓口で「今日は渡せませぬ」と言われても、窓口番が言ったのか、奥の倉庫番が言ったのかで、探しに行く場所が変わるのでござる。今日は 502・503・504 を自分の手で起こして、誰が何に困ったのかを見分けるでござるよ。
この記事のメリット
- ALB が 502・503・504 を返す条件を、ターゲットとの間で起きたこととして説明できるようになる
- ターゲットがすべて
unhealthyでも 503 にはならず、すべてのターゲットに振り分けられる(フェールオープン)ことが分かる HTTPCode_ELB_5XX_CountとHTTPCode_Target_5XX_Countで、エラーを返したのが ALB かアプリかを見分けられるようになる- 登録解除・アプリの停止・遅い応答で 503・502・504 を実際に起こし、直して戻るまでを体験できる
執筆者とキャラクター紹介
執筆者:土肥(とひ)
株式会社ジェニュインの代表であり元エンジニア。これまでオンプレミスと AWS 環境のインフラ業務を担当。AWS 認定資格の学習で得た知識を、ハンズオンとともに解説するのが得意。直近では新人の教育も行っており、これから技術を学ぶ方が納得できる解説を心がけています。基礎編・Cloud Practitioner の記事では、解説役としても登場します。


クラウドおさる



はじめましてでござる。弊猿はクラウドおさる。IT インフラが異常に発達した猿山から、雲に乗って地上の AWS を学びに降りてきたのでござる。



基礎編や Cloud Practitioner の記事では、とひさんに素朴な疑問をぶつける役でござるな。Security - Specialty などの記事では、要所でワンポイントや注意点をひとこと添えるでござる。



猿山にもデータセンターや監視当番があって、地上で起きることはだいたい猿山にも同じ仕組みがあるのでござる。例え話がつい猿山に寄るのは、弊猿の癖でござる。
技術解説
ALB が返す 5XX と、ターゲットが返す 5XX
ALB を通るリクエストのエラーには、出どころが 2 つあります。
| 出どころ | どういうときか | 数えられるメトリクス |
|---|---|---|
| ALB 自身 | ターゲットへの接続や応答の受け取りに失敗したとき、ALB がエラーを作って返す | HTTPCode_ELB_5XX_Count(内訳として HTTPCode_ELB_500_Count・HTTPCode_ELB_502_Count・HTTPCode_ELB_503_Count・HTTPCode_ELB_504_Count など) |
| ターゲット(アプリ) | アプリが 5XX を返し、ALB はそれをそのまま中継する | HTTPCode_Target_5XX_Count |
HTTPCode_ELB_5XX_Count にはターゲットが返したコードは含まれず、HTTPCode_Target_5XX_Count には ALB が返したコードは含まれません。たとえばアプリが自分で 504 を返した場合、HTTPCode_ELB_504_Count は増えず、HTTPCode_Target_5XX_Count が増えます。どちらのメトリクスも、意味のある統計は Sum(合計)です。
アクセスログを有効にしている場合は、elb_status_code(ALB がクライアントに返したコード)と target_status_code(ターゲットが返したコード)を見比べます。elb_status_code が 502 で target_status_code が - なら、ターゲットから応答を受け取れずに ALB が返したエラーです。
503: 振り分け先のターゲットが無い
ALB が 503 を返すのは、ターゲットグループに登録されたターゲットが無いか、登録されたターゲットがすべて unused の状態のときです。unused になるのは、ターゲットが停止・終了している、ターゲットグループがリスナーから使われていない、ALB で有効にしていない AZ にターゲットがある、といった場合です。
注意したいのは、ターゲットがすべて unhealthy になっただけでは 503 にならないことです。ターゲットグループに異常なターゲットしか無い場合、ALB はヘルスステータスにかかわらず、すべてのターゲットにリクエストを振り分けます(フェールオープン)。その結果、ターゲットの状態に応じて 502 や 504 が返ったり、たまたま処理できたリクエストは成功したりします。
| ターゲットグループの状態 | ALB の動き |
|---|---|
| 登録済みのターゲットが 0 台 | 503 を返す |
登録済みのターゲットがすべて unused(停止中など) | 503 を返す |
一部が healthy | healthy のターゲットだけに振り分ける |
登録済みのターゲットがすべて unhealthy | すべてのターゲットに振り分ける(フェールオープン)。503 にはならない |
Auto Scaling グループのインスタンスをターゲットにしている場合は、ターゲットグループが Auto Scaling グループに関連付けられているかも確認します。関連付けが外れていると、インスタンスが動いていてもターゲットは登録されません。



「全部 unhealthy だから 503」と思い込むと、調べる場所を間違えるのでござる。ALB はターゲットが全滅すると、だめ元で全員に仕事を回すのでござるよ。503 を見たら、まずターゲットグループに誰か登録されているかを確かめるのでござる。
502: ターゲットに拒否された、応答が壊れていた
ALB が 502 を返すのは、ターゲットとのやり取りが異常な形で終わったときです。主な原因は次のとおりです。
| 原因 | よくある状況 | 確認するところ |
|---|---|---|
| 接続しようとしたら、ターゲットが TCP RST を返した | アプリのプロセスが止まっていて、ポートが閉じている | アプリのプロセス。TargetConnectionErrorCount |
接続しようとしたら、ICMP Destination unreachable などの予期しない応答が返った | ネットワークの経路の問題 | ALB のサブネットからターゲットのポートへの通信が許可されているか |
| リクエストの処理中に、ターゲットが TCP RST / FIN で接続を閉じた | アプリのキープアライブのタイムアウトが ALB のアイドルタイムアウトより短い | アプリのキープアライブの設定(ALB のアイドルタイムアウトより長くする) |
| ターゲットの応答の形式が不正、HTTP ヘッダーが無効 | アプリやミドルウェアの不具合 | ターゲットでのパケットキャプチャ |
| 登録解除の遅延が過ぎた | 処理中のリクエストが残っているうちにターゲットを登録解除した | 登録解除の遅延の長さ |
| HTTPS のターゲットとの SSL ハンドシェイクに失敗した | ターゲットの TLS の設定 | ターゲットが対応する暗号スイート |
ターゲットが Lambda 関数の場合は、応答本文が 1 MB を超えた、関数がタイムアウトやエラーで終わった、といったときにも 502 になります。
504: ターゲットが時間内に応えない
ALB が 504 を返すのは、時間切れのときです。
| 原因 | よくある状況 | 確認するところ |
|---|---|---|
| 接続タイムアウト(10 秒)までにターゲットへの接続を確立できなかった | ターゲットのセキュリティグループが ALB からの通信を許可していない | セキュリティグループ、ネットワーク ACL(ターゲットから ALB へのエフェメラルポート 1024〜65535 の戻り)。TargetConnectionErrorCount |
| 接続はできたが、アイドルタイムアウトまでにターゲットが応答しなかった | アプリの処理が遅い。DB などの依存先が遅い | TargetResponseTime の最大値、ターゲットの CPU 使用率 |
ターゲットが本文より大きい Content-Length を返した | アプリの不具合 | アプリの応答 |
ALB のアイドルタイムアウト(接続アイドルタイムアウト)は既定で 60 秒で、1〜4000 秒の範囲で変えられます。応答が遅いからといってすぐにアイドルタイムアウトを延ばすのではなく、まずはターゲットの処理やリソースを見直し、どうしても時間がかかる処理のときだけ延ばすのがよいとされています。





503 は「倉庫番がいない」、502 は「倉庫番に門前払いされた」、504 は「倉庫番がいつまでも返事をしない」でござる。誰もいないのか、断られたのか、待たされたのか、と読み替えると覚えやすいのでござるな。
実践
EC2 1 台(soa-5xx-web)をターゲットにした ALB(soa-5xx-alb)に CloudShell からリクエストを送り、次の順に 3 つのエラーを起こしては直します。
- ターゲットを登録解除して 503 を起こし、登録し直して戻す
- アプリを止めて 502 を起こし、起動し直して戻す
- アイドルタイムアウトを 10 秒に縮め、20 秒かかる
/slowで 504 を起こし、60 秒に戻す
最後に、アプリ自身が返す 503(/app-error)を呼び、CloudWatch のメトリクスで ALB が返したエラーとアプリが返したエラーを見比べます。
前提条件
- リージョン:
ap-northeast-1(東京) - EC2・Systems Manager・CloudWatch・CloudShell の操作権限を持つ IAM ユーザーまたはロールでコンソールにログイン済みであること
以下のリソースが作成済みであることを前提とします。まだ用意していない場合は、「作り方の参考」の記事の手順で作れます。リソース名は、この記事の名前に読み替えてください。
| リソース種別 | リソース名 | 内容 | 作り方の参考 |
|---|---|---|---|
| VPC | soa-5xx-vpc | パブリックサブネット soa-5xx-public-1a / soa-5xx-public-1c の 2 つ。0.0.0.0/0 → インターネットゲートウェイ | Amazon VPC で仮想ネットワークを構築しサブネットを設定する ※AZ を 2 つにする |
| IAM ロール | soa-5xx-ec2-role | AmazonSSMManagedInstanceCore をアタッチ(Session Manager で接続するため) | AWS Systems Manager でインスタンスをリモート管理する |
| セキュリティグループ | soa-5xx-alb-sg | ALB 用。HTTP 80 をどこからでも許可 | Elastic Load Balancing でトラフィックを複数のターゲットに分散する |
| セキュリティグループ | soa-5xx-web-sg | EC2 用。HTTP 80 を soa-5xx-alb-sg からだけ許可 | 同上 |
| EC2 インスタンス | soa-5xx-web | Amazon Linux 2023・t3.micro・soa-5xx-public-1a。IAM ロール soa-5xx-ec2-role。下記のアプリをサービス soa-5xx-app としてポート 80 で動かしている | Amazon EC2 でインスタンスを起動しSSH接続する |
| ターゲットグループ | soa-5xx-tg | インスタンス、HTTP 80。ヘルスチェックはパス /・間隔 10 秒・正常/非正常のしきい値 2。登録解除の遅延は 10 秒。soa-5xx-web を登録済み | Elastic Load Balancing でトラフィックを複数のターゲットに分散する |
| Application Load Balancer | soa-5xx-alb | インターネット向け、soa-5xx-public-1a / 1c、soa-5xx-alb-sg。リスナー HTTP:80 → soa-5xx-tg。アイドルタイムアウトは既定の 60 秒 | 同上 |
soa-5xx-web で動かしているアプリは、Python の標準ライブラリだけで書いた小さな Web サーバーです。パスによって次のように応答します。
| パス | 応答 |
|---|---|
/ | すぐに 200 で ok from <ホスト名> を返す(ヘルスチェックもここ) |
/slow | 20 秒待ってから 200 で slow ok from <ホスト名> を返す |
/app-error | すぐに 503 で 503 from app (<ホスト名>) を返す(アプリ自身が返す 503) |
import socket
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOST = socket.gethostname()
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/slow":
time.sleep(20) # 20 秒かかる処理のつもり
self.reply(200, "slow ok from " + HOST)
elif self.path == "/app-error":
self.reply(503, "503 from app (" + HOST + ")") # アプリ自身が返す 503
else:
self.reply(200, "ok from " + HOST)
def reply(self, code, body):
data = (body + "\n").encode()
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
ThreadingHTTPServer(("", 80), Handler).serve_forever()このハンズオンで変更するリソースは以下のとおりです。新しく作るリソースはありません。
| リソース種別 | リソース名 | 内容 |
|---|---|---|
| ターゲットの登録 | soa-5xx-tg の soa-5xx-web | 登録解除して、登録し直す |
| サービス | soa-5xx-web の soa-5xx-app | 停止して、起動し直す |
| ロードバランサーの属性 | soa-5xx-alb の接続アイドルタイムアウト | 60 秒 → 10 秒 → 60 秒 |
ALB は起動している時間と処理量(LCU)に応じて課金されます。EC2 インスタンスの起動時間と、パブリック IPv4 アドレス(EC2 と ALB の各ノード)にも課金されます。検証が終わったら ALB と EC2 インスタンスを削除してください。
手順 1: 正常な状態を確かめる
AWS マネジメントコンソールにログインし、リージョンが ap-northeast-1(東京)であることを確認します。上部の検索バーに EC2 と入力し、「EC2」を選択します。
左側ナビゲーションの「ロードバランシング」→「ロードバランサー」を選択し、soa-5xx-alb の「DNS 名」をコピーします。
コンソール上部のツールバーにある CloudShell のアイコンをクリックし、CloudShell を開きます。コピーした DNS 名を変数に入れ、ステータスコードと応答までの秒数を表示する関数を用意します。
ALB='soa-5xx-alb-xxxxxxxxxx.ap-northeast-1.elb.amazonaws.com'
hit() { curl -s -o /dev/null -w "%{http_code} %{time_total}s $1\n" "http://$ALB$1"; }
hit /
curl -s "http://$ALB/"200 が返り、本文に ok from とインスタンスのホスト名が表示されることを確認します。


手順 2: ターゲットを登録解除して 503 を起こす
EC2 コンソールに戻り、左側ナビゲーションの「ロードバランシング」→「ターゲットグループ」を選択して soa-5xx-tg を開きます。「ターゲット」タブで soa-5xx-web にチェックを入れ、「登録解除」をクリックします。
登録解除の遅延(10 秒)が過ぎ、ターゲットの一覧から soa-5xx-web が消えたことを確認します。


CloudShell に戻り、もう一度リクエストを送ります。
hit /
curl -s "http://$ALB/"503 が返り、本文に 503 Service Temporarily Unavailable が含まれることを確認します。この本文はアプリではなく ALB が作ったものです。インスタンスもアプリも動いたままですが、ターゲットグループに振り分け先が無いため、ALB はリクエストをどこにも送れません。


ターゲットグループ soa-5xx-tg の「ターゲット」タブで「ターゲットを登録」をクリックします。soa-5xx-web にチェックを入れ、ポートが 80 であることを確かめて「保留中として以下を含める」をクリックし、「保留中のターゲットを登録」をクリックします。
ヘルスステータスが Healthy になるまで待ち(画面右上の更新ボタンで再読み込み)、CloudShell で hit / を実行して 200 に戻ったことを確認します。
手順 3: アプリを止めて 502 を起こす
EC2 コンソールの左側ナビゲーションの「インスタンス」から soa-5xx-web を選択し、「接続」→「Session Manager」タブ→「接続」をクリックします。開いたターミナルで、アプリのサービスを止めます。
sudo systemctl stop soa-5xx-app
sudo systemctl is-active soa-5xx-appinactive と表示されることを確認します。インスタンスは動いていますが、ポート 80 で待ち受けているプロセスがいなくなりました。
CloudShell に戻り、リクエストを送ります。
hit /
curl -s "http://$ALB/"502 がすぐに返り、本文に 502 Bad Gateway が含まれることを確認します。ALB が接続しようとしたところ、閉じたポートからは TCP RST が返り、リクエストを渡せなかったためです。


ターゲットグループ soa-5xx-tg の「ターゲット」タブを開き、しばらく待って soa-5xx-web のヘルスステータスが Unhealthy になったことを確認します。CloudShell で hit / を何度か実行しても、503 ではなく 502 が返り続けることを確認します。ターゲットがすべて unhealthy になると、ALB はフェールオープンしてすべてのターゲットに振り分けるので、返るのは「ターゲットが無い」503 ではなく「接続を拒否された」502 のままです。


Session Manager のターミナルに戻り、アプリを起動し直します。
sudo systemctl start soa-5xx-app
sudo systemctl is-active soa-5xx-appactive と表示されたら、CloudShell で hit / を実行し、200 に戻ったことを確認します。ヘルスステータスもしばらくすると Healthy に戻ります。



インスタンスは動いているのに 502、というのが今回のミソでござる。門番(ALB)は倉庫の前まで行けたのに、戸が閉まっていて追い返されたのでござるな。こういうときは、インスタンスではなくアプリのプロセスとポートを見に行くのでござる。
手順 4: アイドルタイムアウトより遅い応答で 504 を起こす
まず、既定のアイドルタイムアウト(60 秒)のまま、20 秒かかる /slow を呼びます。
hit /slow20 秒ほど待ってから 200 が返ることを確認します。応答までの時間はアイドルタイムアウトより短いので、ALB は待ってくれます。
EC2 コンソールの左側ナビゲーションの「ロードバランサー」から soa-5xx-alb を選択し、「属性」タブで「編集」をクリックします。「トラフィックの設定」の「接続アイドルタイムアウト」を、入力欄の分を 0、秒を 10 にして 10 秒に変更し、「変更内容の保存」をクリックします。


CloudShell で、もう一度 /slow と / を呼びます。
hit /slow
hit //slow は 10 秒ほどで 504 が返り、/ はすぐに 200 が返ることを確認します。ALB はターゲットへの接続には成功していますが、アイドルタイムアウト(10 秒)のあいだにアプリから応答が無かったため、待つのをやめて 504 を返しています。アプリは動いているので、速く応答できるパスは成功します。


soa-5xx-alb の「属性」タブで「編集」をクリックし、「接続アイドルタイムアウト」を 60 秒(分が 1、秒が 0)に戻して「変更内容の保存」をクリックします。CloudShell で hit /slow を実行し、20 秒ほどで 200 に戻ったことを確認します。
実際の運用では、アイドルタイムアウトを延ばす前に、アプリの処理が遅くなった原因(CPU 使用率、DB などの依存先の遅れ)を確認します。
手順 5: アプリ自身が返す 503 を呼ぶ
CloudShell で、アプリが自分で 503 を返す /app-error を 5 回呼びます。
for i in 1 2 3 4 5; do hit /app-error; done
curl -s "http://$ALB/app-error"ステータスコードは手順 2 と同じ 503 ですが、本文は 503 from app (<ホスト名>) で、ALB の 503 Service Temporarily Unavailable とは違うことを確認します。ALB はターゲットから正しい形式の応答を受け取ったので、その中身が 503 でもそのままクライアントへ渡しています。


手順 6: ALB が返した 5XX とアプリが返した 5XX をメトリクスで見分ける
メトリクスが CloudWatch に反映されるまで数分待ちます。上部の検索バーに CloudWatch と入力し、「CloudWatch」を選択します。左側ナビゲーションの「メトリクス」→「クラシックメトリクス」を選択します。
「参照」タブで「ApplicationELB」→ ロードバランサーごとのメトリクスを選び、検索ボックスに soa-5xx-alb と入力します。次のメトリクスにチェックを入れます。
HTTPCode_ELB_502_CountHTTPCode_ELB_503_CountHTTPCode_ELB_504_CountHTTPCode_Target_5XX_Count
「グラフ化したメトリクス」タブで、統計をすべて「合計」、期間を「1 分」にします。手順 2〜5 を実行した時間帯で、手順 2 の 503 は HTTPCode_ELB_503_Count、手順 3 の 502 は HTTPCode_ELB_502_Count、手順 4 の 504 は HTTPCode_ELB_504_Count に数えられ、手順 5 のアプリが返した 503 は HTTPCode_ELB_503_Count ではなく HTTPCode_Target_5XX_Count に数えられていることを確認します。撮影時は、手順 4 の 504 と手順 5 のアプリの 503 を続けて実行したため、HTTPCode_ELB_504_Count と HTTPCode_Target_5XX_Count は同じ 1 分に立ちました。それでも、アプリが返した 503 が HTTPCode_ELB_503_Count には数えられていないことは確かめられます。グラフの時刻は、画面の設定によって UTC かローカル時刻で表示されます。


同じ「503」でも、HTTPCode_ELB_503_Count が増えたならターゲットグループの登録状況を、HTTPCode_Target_5XX_Count が増えたならアプリのログを見に行く、と調べる場所が分かれます。アラームを作るときも、ALB が返すエラーとアプリが返すエラーは別々のメトリクスで見張ります。



502・503・504 を「どれも 5XX」でひとまとめにすると、原因を取り違えやすいのでござる。ターゲットが全滅しても 503 にはならないこと、アプリが返した 5XX は HTTPCode_Target_5XX_Count に数えられることを押さえておけば、症状から確認する場所を選べるのでござるよ。
まとめ
| コード | ALB が返す条件 | まず確認するところ |
|---|---|---|
| 503 | ターゲットグループに登録済みのターゲットが無い、またはすべて unused | ターゲットグループの登録状況、Auto Scaling グループとの関連付け、ターゲットの停止 |
| 502 | 接続時にターゲットが TCP RST を返した、処理中に接続を閉じられた、応答の形式が不正 | アプリのプロセスとポート、キープアライブのタイムアウト(ALB のアイドルタイムアウトより長く)、TargetConnectionErrorCount |
| 504 | 接続タイムアウト(10 秒)までに接続できない、アイドルタイムアウトまでに応答が無い | セキュリティグループとネットワーク ACL、TargetResponseTime、ターゲットの負荷、アイドルタイムアウトの値 |
- ALB が作ったエラーは
HTTPCode_ELB_5XX_Count(とHTTPCode_ELB_502_Countなどの内訳)、アプリが返したエラーはHTTPCode_Target_5XX_Countに数えられる。統計はSumで見る - ターゲットがすべて
unhealthyになると、ALB はすべてのターゲットに振り分ける(フェールオープン)。503 にはならない - アプリのプロセスが止まっているだけなら、インスタンスが動いていても 502 になる
- アイドルタイムアウトは既定 60 秒(1〜4000 秒)。延ばす前に、ターゲットの処理が遅い原因を確認する
参照先
- Application Load Balancer のトラブルシューティング – Elastic Load Balancing
- Application Load Balancer の CloudWatch メトリクス – Elastic Load Balancing
- Application Load Balancer ターゲットグループのヘルスチェック – Elastic Load Balancing
- Application Load Balancer の属性を編集する – Elastic Load Balancing
- Application Load Balancer で発生する HTTP 502 エラーのトラブルシューティング | AWS re:Post
- Application Load Balancer からの 503 エラーのトラブルシューティング | AWS re:Post
- Application Load Balancer での 504 エラーのトラブルシューティング | AWS re:Post













