はじめに
AWS ECS Fargate にデプロイしたアプリへアクセスすると 504 Gateway Timeout が出た。
最初は ECR への接続エラーが原因に見えたが、調べると別の原因が潜んでいた。
その切り分け手順と最終的な解決方法をまとめます。
前提条件
| カテゴリ | 項目 | 値 |
|---|---|---|
| 基盤 | AWS サービス | Amazon ECS(Fargate 起動タイプ) / Application Load Balancer / Amazon ECR |
| 基盤 | リージョン | us-west-2(オレゴン) |
| ECS | クラスタ名 | my-app-cluster |
| サービス名 | my-sm-service |
|
| desired count | 3 | |
| ALB | ALB 名 |
my-sm-alb(インターネット向け) |
| ターゲットグループ名 | my-sm-tg |
|
| プロトコル / ポート | HTTP / 8080 | |
| コンテナ | コンテナ名 | sessionmanagement |
| コンテナポート | 8080 | |
| ヘルスチェック | パス | /api/users/status |
| 期待 HTTP ステータス | 200 | |
| ネットワーク | サブネット配置 | パブリックサブネット2つ |
| パブリック IP 割り当て | assignPublicIp: ENABLED |
|
| アプリ | 種別 | REST API |
| フレームワーク / 言語 | Spring Boot / Java | |
| 作業ツール | 操作環境 | AWS CLI v2(コンソール GUI と等価の操作を CLI で実施) |
発生していた状況
-
ECS Fargate(us-west-2)にデプロイ - ブラウザでアクセスすると 504
- CloudWatch のイベントログに ECR への接続エラー
service my-sm-service was unable to place a task.
Reason: ResourceInitializationError: unable to pull secrets or registry auth:
... dial tcp 34.223.26.179:443: i/o timeout.
切り分けの流れ
1. ECR への到達性
エラー文面そのままに、タスクが ECR からイメージを取得できていない可能性 を疑った。
Fargate タスクが ECR に届かない典型例は次の3つ。
- パブリックサブネットに置いているが
assignPublicIpが無効 - プライベートサブネットで
NATもVPCエンドポイントも無い - セキュリティグループのアウトバウンドで 443 が閉じている
2. ネットワーク設定を順に確認
サービスのネットワーク設定を見る。
aws ecs describe-services \
--cluster my-app-cluster \
--services my-sm-service \
--region us-west-2 \
--query "services[0].networkConfiguration.awsvpcConfiguration"
結果は assignPublicIp: ENABLED、サブネット2つ、SG1つ。パブリック IP は付くようになっている。
次にルートテーブル。
aws ec2 describe-route-tables \
--region us-west-2 \
--filters "Name=association.subnet-id,Values=subnet-...,subnet-..." \
--query "RouteTables[].{RouteTableId:RouteTableId,Routes:Routes}"
0.0.0.0/0 → igw-...(active)があり、両サブネットとも本物のパブリックサブネットだった。
SG のアウトバウンドも全許可、NACL もデフォルトで全許可。
ネットワーク設定は全て正しい
3. 現状を再確認
設定上は問題ないので、サービスの今の状態を確認した。
aws ecs describe-services \
--cluster my-app-cluster --services my-sm-service \
--region us-west-2 \
--query "services[0].{Desired:desiredCount,Running:runningCount,Deployments:deployments[].{Rollout:rolloutState}}"
{ "Desired": 3, "Running": 3, "Deployments": [{ "Rollout": "FAILED" }] }
Running: 3。タスクはすでに起動していた。最初の ECR エラーは過去のもので、その後解消していた。本当の 504 の原因はここから先にある。
4. ALB のターゲットヘルスを確認
Rollout: FAILED が引っかかる。これはヘルスチェックの失敗で起こる典型的なパターン。
ALB の 504 は「ターゲットに転送はできたが応答が無い」時に発生する(ターゲット皆無の時は 503)。
つまり ALB → タスク間に問題がある。
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-west-2:...:targetgroup/my-sm-tg/... \
--region us-west-2
3つすべてのターゲットが unhealthy / Target.Timeout / Request timed out。
ヘルスチェック設定は /api/users/status(HTTP 8080、期待コード 200)。
Target.Timeout はパスのミスマッチではなく、パケットが無言で破棄されている 状態を意味する。
SG が ALB → タスクの 8080 を通していない可能性が濃厚。
5. 真の原因 — SG インバウンドの送信元不一致
タスク SG のインバウンドを確認すると、8080 を許可するルール自体は存在していた。
ただし 送信元が、現役のALBのSGではない別の SGを指していた。
その送信元 SG を調べると、現在どの ENI にも紐づいていない「残骸 SG」だった。
おそらく ALB を作り直した際に SG が変わり、タスク SG 側は古い SG を参照したままだった、というパターン。
タスク SG が許可していた送信元: sg-cccc3333(旧/残骸 SG)
実際の ALB が持っている SG: sg-bbbb2222(ALB SG)
→ ALB の通信は許可対象に該当せず、無言で破棄 → ヘルスチェック失敗 → 504
検証した項目と結果
切り分けで確認した各設定を実際の値と本来あるべき値で並べて比較する。
不一致は 1箇所だけ だったことが一目で分かる。
| 確認項目 | 実際の設定 | あるべき値 | 判定 |
|---|---|---|---|
サブネットの assignPublicIp
|
ENABLED |
ENABLED |
✓ |
| サブネットのルートテーブル |
0.0.0.0/0 → igw-...(active) |
IGW への経路あり | ✓ |
| タスク SG のアウトバウンド | 全許可(-1, 0.0.0.0/0) |
443 が外部に出られる | ✓ |
| ネットワーク ACL | デフォルト(全許可) | 全許可で問題なし | ✓ |
| タスク SG インバウンド プロトコル | TCP | TCP | ✓ |
| タスク SG インバウンド ポート | 8080 | 8080(コンテナポートと一致) | ✓ |
| タスク SG インバウンド 送信元 | sg-cccc3333(旧/残骸 SG) |
sg-bbbb2222(ALB の SG) |
✗ |
| ALB ターゲットグループ ポート | 8080 | 8080 | ✓ |
| ヘルスチェックパス | /api/users/status |
アプリが 200 を返すパス | ✓ |
| ヘルスチェック期待コード | 200 | 200 | ✓ |
解決
タスク SG に「ALB の SG からの TCP 8080」を1本追加するだけ。
aws ec2 authorize-security-group-ingress \
--group-id sg-aaaa1111(タスク SG) \
--protocol tcp \
--port 8080 \
--source-group sg-bbbb2222(ALB SG) \
--region us-west-2
画面から行う場合は、
EC2 コンソール → セキュリティグループ → タスク SG を選択 → インバウンドルールを編集 → カスタム TCP / 8080 / ソースに ALB の SG を指定 → 保存。
1〜2分待つと ALB が再判定して全ターゲットが healthy になり、ブラウザからのアクセスでも 504 が消えた。
ハマりどころと学び
-
ALB の 504 と 503 は意味が違う。
503 は「ターゲットなし」、504 は「ターゲットには到達したが応答なし」。
504 ならアプリ側か SG インバウンドを疑う -
Target.Timeout(Request timed out)は SG が無言ドロップしているサイン
Connection refused や 404 とは区別する -
エラーログは過去のものかもしれない
診断時はまず「今どうなっているか」を確認する -
SG ルールはポート番号だけでなく送信元 SG の妥当性も見る
古い SG を参照していると一見正しく見えて実は無効。リソースを作り直した後は要注意
まとめ
- 表面のエラー(ECR タイムアウト)は古いログで、現在の真因ではなかった
- 真因は タスク SG のインバウンドが、現役の ALB の SG ではない古い SG を許可していた こと
- ALB の SG をタスク SG のインバウンドに追加するだけで解決
- 504 を見たら、ECR → ネットワーク → ALB → SG → アプリの順で切り分けると見落としが少ない
参考リンク
-
Amazon ECS タスクネットワーキングのオプション(awsvpc / パブリック IP)
└ Fargate タスクが外部(ECR 含む)に到達するためのネットワーク要件 -
Application Load Balancer のトラブルシューティング
└HTTP 503とHTTP 504の違い、原因と対処 -
ターゲットグループのヘルスチェック
└Target.Timeoutを含む各Reasonの意味 -
Amazon ECS サービスでのロードバランサーの使用
└ ECS タスク SG と ALB SG の関係(インバウンド許可の必要性) -
Amazon VPC のセキュリティグループ
└ SG の基本(ステートフル、送信元に SG を参照する考え方) -
AWS CLI
authorize-security-group-ingressリファレンス
└ 本記事で使用した CLI コマンドの仕様 -
AWS CLI
describe-target-healthリファレンス
└ ターゲットの健全性確認に使った CLI コマンド