「デプロイしたらタスクが起動しない」「起動してもすぐ落ちる」「ALBのヘルスチェックを永遠に通らない」——ECS on Fargate の運用で必ず一度は踏む問題です。
私は木材流通B2B SaaSで API Gateway → NLB → ALB → ECS on Fargate の構成に221本のエンドポイントを乗せ、決済基盤(本番二重課金0件)のワーカー群も同じ基盤で動かしてきました。タスク停止の問題は「運用の注意深さ」ではなく、診断の型と構造的な予防で潰します。CloudWatch を凝視して「何かおかしい」と感じる前に、stoppedReason と stopCode から論理的に原因を絞るのが最短ルートです。
この記事は、ECS Fargate のタスク停止理由をカテゴリ別に体系化し、「どこを見るか → 何が原因か → どう直すか → どう再発防止するか」を一気通貫で示す実務ガイドです。本番設計の全体像は ECS on Fargate 本番運用ガイド を先に読んでおくと理解が深まります。
まず見るべき場所:診断の起点
「タスクが落ちた」と気づいた瞬間から、確認する場所は4つです。
| 確認場所 | 何がわかるか |
|---|---|
| コンソール「Stopped tasks」タブ | stoppedReason のサマリ、exitCode、最終ステータス |
aws ecs describe-tasks | 全フィールドの構造化データ(最も詳細) |
| CloudWatch Logs(awslogs) | アプリが自分で書いたログ(パニック・起動失敗など) |
| EventBridge「ECS Task State Change」イベント | 非同期の停止通知。アラート連携・自動対応の起点 |
最初の一手はこれ一択です。
aws ecs describe-tasks \
--cluster prod \
--tasks <task-id> \
--query 'tasks[0].{lastStatus:lastStatus,stoppedReason:stoppedReason,stopCode:stopCode,containers:containers[*].{name:name,reason:reason,exitCode:exitCode,lastStatus:lastStatus}}' \
--output json
describe-tasks の読み方:5フィールドを必ず確認する
{
"lastStatus": "STOPPED",
"stoppedReason": "Essential container in task exited",
"stopCode": "EssentialContainerExited",
"containers": [
{
"name": "app",
"lastStatus": "STOPPED",
"exitCode": 1,
"reason": ""
}
]
}
| フィールド | 意味 | 注目ポイント |
|---|---|---|
lastStatus | タスク全体の現在状態 | STOPPED が確定 |
stoppedReason | 人間向けの停止理由テキスト | 「CannotPullContainerError」「Your Spot Task was interrupted.」などカテゴリが読める |
stopCode | 機械判別用の停止コード | EventBridgeフィルタやアラートに使う |
containers[].exitCode | コンテナプロセスの終了コード | 0=正常、1=アプリエラー、137=OOMKill、null=起動すら到達していない |
containers[].reason | コンテナ個別の理由 | pullエラーなどはここに詳細が出る |
exitCode が null の場合は、アプリが起動するより前にFargateエージェント側で失敗しています(CannotPullContainer / ResourceInitializationError など)。アプリのログを追う前に、エージェント側の原因を潰してください。
診断フローチャート
タスクが STOPPED になった
|
+-- stoppedReason に "CannotPullContainer" ?
| YES → § CannotPullContainerError へ
|
+-- stoppedReason に "ResourceInitializationError" ?
| YES → § ResourceInitializationError へ
|
+-- stopCode = "EssentialContainerExited" ?
| YES → containers[].exitCode を確認
| exitCode=137 → § OutOfMemoryError へ
| exitCode≠0, null 以外 → § Essential container exited へ
| exitCode=null → § CannotStartContainerError へ
|
+-- stoppedReason に "health check" / "ELB" ?
| YES → § ELBヘルスチェック失敗 へ
|
+-- stopCode = "SpotInterruption" ?
| YES → § Spot中断 へ(障害ではない)
|
+-- stoppedReason に "timeout" ?
YES → § ContainerRuntimeTimeoutError へ
CannotPullContainerError:イメージが pull できない
症状
タスクが PROVISIONING → PENDING で止まり、stoppedReason に以下が出る。
CannotPullContainerError: pull image manifest has been retried 1 time(s): failed to resolve ref ...
exitCode は null(アプリ起動に到達していない)。
原因別チェックリスト
① VPCエンドポイント / NAT が不足(最多)
プライベートサブネットで assignPublicIp=false の構成なのに、以下のいずれもない状態。
- NAT Gateway(アウトバウンド経路)
- ECR 用 VPC エンドポイント(
com.amazonaws.<region>.ecr.api+com.amazonaws.<region>.ecr.dkr) - S3 ゲートウェイエンドポイント(ECRのレイヤーはS3に格納されているため必須)
# VPCエンドポイント一覧を確認
aws ec2 describe-vpc-endpoints \
--filters "Name=vpc-id,Values=<vpc-id>" \
--query 'VpcEndpoints[*].{Service:ServiceName,State:State}'
必要な最小セット(プライベートサブネット構成):
| エンドポイント | 種別 | 用途 |
|---|---|---|
ecr.api | Interface | タスク定義のイメージURL解決 |
ecr.dkr | Interface | レイヤーのpull(Docker Registry API) |
s3 | Gateway | ECRレイヤーの実体(S3に格納) |
logs | Interface | awslogs ドライバ(CloudWatch Logs) |
secretsmanager | Interface | secrets valueFrom(使う場合) |
ssmmessages | Interface | ECS Exec(使う場合) |
② 実行ロールに ECR 権限が無い
executionRoleArn に AmazonECSTaskExecutionRolePolicy が付いていないか、カスタムポリシーが不足。
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "*"
}
注意:
ecr:GetAuthorizationTokenはリソースを*にしないと機能しません。
③ タグ・ダイジェスト誤り
指定したタグが ECR に存在しないか、digest が変更されている。
# ECR でタグ一覧を確認
aws ecr describe-images \
--repository-name web-api \
--query 'imageDetails[*].{Tags:imageTags,Pushed:imagePushedAt}' \
--output table
④ Docker Hub レート制限
Docker Hub の公式イメージ(node:20など)を直接使っている場合、匿名プルのレート制限に引っかかることがある。ECR Public または ECR にミラーしてプルする運用に切り替える。
修復フロー
1. プライベートサブネット構成か確認
YES → VPCエンドポイント(ecr.api / ecr.dkr / s3)を追加
または NAT Gateway を確認
2. 実行ロールのポリシーを確認
→ AmazonECSTaskExecutionRolePolicy がアタッチされているか
3. イメージURIを確認
→ ECR にそのタグが存在するか describe-images で確認
4. SGを確認
→ VPCエンドポイントのSGがタスクのSGからの443を許可しているか
予防
Terraform で VPC エンドポイントをコード管理し、Plan 段階で経路欠如を検知する。ECR プッシュをCIパイプラインで行い、タグの存在を push 直後に確認してからデプロイをトリガーする。
ResourceInitializationError:リソース初期化失敗
症状
タスク起動の初期フェーズでエラー。stoppedReason に下記のようなメッセージ。
ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: ...
あるいは:
ResourceInitializationError: failed to configure ENI: ...
原因と診断
ResourceInitializationError は CannotPullContainer の親カテゴリに近い位置にあります。Fargateエージェントがネットワーク設定・シークレット取得・ログ初期化を行う「起動前の初期化フェーズ」全体で起きます。
主な原因:
| 原因 | チェックポイント |
|---|---|
| secrets 取得失敗(Secrets Manager / SSM 到達不能) | VPCエンドポイント secretsmanager / ssm の有無 |
実行ロールに GetSecretValue / GetParameter 権限なし | 実行ロールのポリシーを確認 |
| ログ初期化失敗(CloudWatch Logs 到達不能) | VPCエンドポイント logs の有無、ロールに logs:CreateLogStream |
| ENI 割当失敗(サブネットのIPアドレス枯渇) | サブネットの空きIP数を describe-subnets で確認 |
| SG がアウトバウンドを塞いでいる | タスクSGのアウトバウンドルールを確認 |
# サブネットの空きIPを確認
aws ec2 describe-subnets \
--subnet-ids <subnet-id> \
--query 'Subnets[*].{CIDR:CidrBlock,Available:AvailableIpAddressCount}'
実行ロールとタスクロールの混同が連鎖エラーを生む
これが最も見落とされるパターンです。ECS on Fargate 本番運用ガイドでも詳述していますが、改めて整理します。
| ロール | 設定キー | 使用主体 | 典型的な権限 |
|---|---|---|---|
| 実行ロール | executionRoleArn | ECSエージェント(起動時) | ECR pull、CloudWatch Logs書込、SecretsManager取得 |
| タスクロール | taskRoleArn | アプリコード(実行中) | S3・DynamoDB・SQSなどアプリ固有のAWSリソース |
シークレット注入(secrets[].valueFrom)はエージェントが起動時に取得するため、権限は実行ロールに付ける。アプリが実行中に aws secretsmanager get-secret-value を呼ぶ場合だけ、タスクロールに付ける。この原則を守らないと ResourceInitializationError として現れます。
OutOfMemoryError:メモリ上限超過
症状
タスクの stoppedReason に:
OutOfMemoryError: Container killed due to memory usage
または containers[].exitCode = 137(Linux の OOMKill は exit code 137)。
原因と診断
Fargateのメモリ制限はコンテナレベルで設定します。コンテナのメモリ使用量がタスク定義の memory(上限)を超えると、Linuxのカーネル OOM Killer がプロセスを強制終了します。
# Container Insights でメモリ使用量を確認(クエリ例)
aws cloudwatch get-metric-statistics \
--namespace ECS/ContainerInsights \
--metric-name MemoryUtilized \
--dimensions Name=ClusterName,Value=prod Name=ServiceName,Value=web-api \
--start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Average Maximum
原因の仕分け:
| パターン | 見分け方 | 対処 |
|---|---|---|
| タスクサイズの見積もり不足 | 常時OOMKill。最大値がlimitに達している | タスクサイズを上のペアに変更 |
| メモリリーク | 起動直後は正常だが徐々に増加して落ちる | ヒープダンプ/プロファイラで調査 |
| スパイク時の瞬間超過 | 特定時間帯や高負荷時のみ | ソフトリミット(memoryReservation)とハードリミット(memory)を分けて設定 |
タスク定義のメモリ設定には2つのレベルがあります。
{
"name": "app",
"memory": 1024,
"memoryReservation": 512
}
memory(ハードリミット): これを超えるとOOMKillmemoryReservation(ソフトリミット): スケジューリング時の目安。超えても即Killはしない
スパイク耐性を持たせるには、memoryReservation を通常使用量より少し上に、memory をその1.5〜2倍程度に設定して、余裕のバッファを持たせます。ただし、Fargateのタスクメモリ上限は task.memory と等しく、コンテナの合計が超えると起動できません。
Essential container exited / 非ゼロ exitCode:アプリ起動失敗
症状
Essential container in task exited
stopCode = EssentialContainerExited、containers[].exitCode が非ゼロ(0以外)。
診断手順
Step 1: CloudWatch Logs を確認する(最重要)
# ログストリームの最新を取得
aws logs get-log-events \
--log-group-name /ecs/web-api \
--log-stream-name app/<task-id> \
--limit 50 \
--query 'events[*].message'
Fargate は awslogs ドライバ経由でアプリのSTDOUT/STDERRをCloudWatch Logsに書きます。アプリが「設定ファイルが読めない」「DB接続失敗」「ポートが既に使われている」などで panic/exit した記録が必ずここに残ります。
Step 2: 環境変数・シークレットの不足を確認
secrets[].valueFrom で参照しているシークレットのARNが間違っていたり、該当バージョンが存在しない場合、起動に必要な環境変数が空になってアプリがクラッシュする。
# シークレットの存在確認
aws secretsmanager describe-secret \
--secret-id arn:aws:secretsmanager:ap-northeast-1:111122223333:secret:prod/db-Ab12Cd
Step 3: CMD / ENTRYPOINT の確認
タスク定義の command が間違っている、または存在しないパスを参照しているケース。
# ローカルでイメージを起動して再現確認
docker run --rm \
-e DATABASE_URL=<test-url> \
111122223333.dkr.ecr.ap-northeast-1.amazonaws.com/web-api:latest
よくある exitCode 一覧:
| exitCode | 意味 | 対処の方向 |
|---|---|---|
| 1 | 一般的なアプリエラー | CloudWatch Logs でエラー内容を確認 |
| 2 | Bash の誤用 / シェルエラー | CMD/ENTRYPOINT の構文確認 |
| 127 | コマンドが見つからない | パス / バイナリ名の誤り |
| 137 | SIGKILL(OOMKillまたは手動kill) | メモリ設定またはStopTask操作 |
| 143 | SIGTERM(グレースフルシャットダウン正常受理) | 正常停止。意図的な停止かを確認 |
Task failed ELB health checks:ヘルスチェック通過失敗
症状
タスクは起動しているのにサービスが不健全と判定され、タスクが繰り返し入れ替わる。ECSサービスのイベントに以下が出る。
service web-api (port 8080) is unhealthy in target-group arn:... due to (reason Health checks failed)
診断フロー
① target_type = "ip" か確認(Fargate固有の落とし穴)
ALBのターゲットグループが target_type = "instance" のままだと、Fargate タスクを正しく登録できない。
aws elbv2 describe-target-groups \
--query 'TargetGroups[*].{Name:TargetGroupName,TargetType:TargetType}'
必ず ip であることを確認する。instance になっていたら作り直し(変更不可)。
② ヘルスチェックパスとポートの確認
ALBのターゲットグループに設定したヘルスチェックパス(例 /healthz)がアプリで実装されていて、正しいポートを返すか。
# タスクのENI IPを取得してヘルスチェックを手動実行
TASK_ENI_IP=$(aws ecs describe-tasks \
--cluster prod --tasks <task-id> \
--query 'tasks[0].attachments[0].details[?name==`privateIPv4Address`].value' \
--output text)
# VPC内の踏み台から確認(直接疎通できる場合)
curl -v http://${TASK_ENI_IP}:8080/healthz
③ SG がヘルスチェックをブロックしていないか
タスクのSGインバウンドルールが「ALBのSGのみ許可」になっていても、ALBのSGのヘルスチェック送信元 と一致していなければ通らない。
# タスクSGのインバウンドルールを確認
aws ec2 describe-security-groups \
--group-ids <task-sg-id> \
--query 'SecurityGroups[*].IpPermissions'
④ startPeriod と grace period の設定
起動に時間がかかるアプリ(DBマイグレーション実行・大きなモデルロードなど)は、ヘルスチェックが始まる前の猶予を設定しないと、初期化中に「不健全」と判定されてKillされる。
{
"healthCheck": {
"startPeriod": 60,
"interval": 15,
"timeout": 5,
"retries": 3
}
}
加えて、ECSサービス側にも health_check_grace_period_seconds が必要です。
resource "aws_ecs_service" "app" {
# ...
health_check_grace_period_seconds = 60
}
healthCheck.startPeriod(タスク定義内コンテナのヘルスチェック猶予)と health_check_grace_period_seconds(ECSサービスが ALB ヘルスチェック失敗を無視する猶予)は別物です。ALBに登録されてからの猶予は後者で設定します。
ヘルスチェック診断チェックリスト:
-
target_type = "ip"になっているか - ヘルスチェックパスがアプリで
200を返すか - ヘルスチェックのポート番号がコンテナポートと一致しているか
- タスクSGがALBのSGからのインバウンドを許可しているか
- タスク定義の
healthCheck.startPeriodが設定されているか - ECSサービスの
health_check_grace_period_secondsが設定されているか
SpotInterruption:Spot中断は"障害"ではない
症状
stopCode: "SpotInterruption"
stoppedReason: "Your Spot Task was interrupted."
正しい理解
Spot 中断はAWSが容量を回収するための設計された動作であり、ソフトウェアのバグでも運用ミスでもありません。対処すべきは「中断が起きたとき壊れない設計になっているか」です。
Spot 中断の仕組み:
- AWSが容量回収を決定(通常の通知より2分前)
- EventBridge に
ECS Task State Changeイベント発行(stopCode=SpotInterruption) - タスクに SIGTERM を送信(
stopTimeoutの猶予あり、最大120秒) - 猶予後に SIGKILL
設計で吸収する3点セット:
① グレースフルシャットダウン:SIGTERMを受けてin-flightを捌き切る
② 冪等な処理:途中でKillされても、再起動後に二重処理しない
③ 容量プロバイダ戦略:baseをオンデマンドで守り、追加分をSpotに割り当て
# Spot 中断をEventBridgeで検知してアラートへ
resource "aws_cloudwatch_event_rule" "spot_interruption" {
name = "ecs-spot-interruption"
event_pattern = jsonencode({
source = ["aws.ecs"]
detail-type = ["ECS Task State Change"]
detail = {
stopCode = ["SpotInterruption"]
}
})
}
グレースフルシャットダウンの実装パターンは ECS on Fargate 本番運用ガイド の「SIGTERMを受けて綺麗に終わる」節を参照してください。stopTimeout の既定は30秒、最大は120秒です。本番ではアプリの drain 時間に合わせて明示的に設定します。
CannotStartContainerError / ContainerRuntimeTimeoutError
症状
CannotStartContainerError: ...
ContainerRuntimeTimeoutError: Timeout waiting for container to start
exitCode は null(コンテナプロセス起動に到達していない)。
原因と診断
CannotStartContainerError:
command/entryPointに実行権限のないバイナリを指定volumesのマウント先に書き込めない(readonlyRootFilesystem: trueのとき特に)userで指定した UID がコンテナ内に存在しないlinuxParametersの依存設定が Fargate でサポート外
# ローカルで同じ設定を再現
docker run --rm \
--user 10001:10001 \
--read-only \
--tmpfs /tmp \
my-image:tag
ContainerRuntimeTimeoutError:
コンテナの起動シーケンス(ENTRYPOINT/CMD の実行開始)がタイムアウト。依存サービスへの接続待ちが長すぎる場合など。起動時の初期化処理で外部サービスを待っているなら、ヘルスチェックの startPeriod を延ばすか、起動シーケンスから外部依存を切り離すことを検討する。
ECS Exec:動いているコンテナの中で調査する
タスクが動いているとき(または起動直後のデバッグ中)に、コンテナの中に直接入って調査できるのが ECS Exec です。SSHもポート開放も鍵管理も不要です。
前提条件
① ECS サービス / タスクで enableExecuteCommand: true
resource "aws_ecs_service" "app" {
enable_execute_command = true
# ...
}
注意: enableExecuteCommand は新しく起動するタスクにのみ有効です。既存タスクには後付けできません。設定変更後に force-new-deployment でタスクを差し替えてください。
② タスクロールに ssmmessages の4アクション
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
}
これはタスクロール(taskRoleArn)に付ける権限です(実行ロールではない)。
③ SSM Session Manager プラグイン(クライアント側)
# macOS
brew install session-manager-plugin
実際のコマンド
# タスクIDを確認
TASK_ID=$(aws ecs list-tasks \
--cluster prod \
--service-name web-api \
--query 'taskArns[0]' \
--output text | awk -F/ '{print $NF}')
# コンテナに入る
aws ecs execute-command \
--cluster prod \
--task ${TASK_ID} \
--container app \
--interactive \
--command "/bin/sh"
シェルに入ったら、環境変数・ファイル存在・ネットワーク疎通などを直接確認できます。
# コンテナ内で環境変数の確認
env | grep DATABASE
# DB への疎通確認
nc -zv db.internal 5432
# プロセスの確認
ps aux
ECS Exec の監査
ECS Exec の全操作は CloudTrail に記録されます(ExecuteCommand API 呼び出しとして)。さらに logging: OVERRIDE で設定すると、セッションの入出力を CloudWatch Logs または S3 に保存できます。本番コンテナへのアクセスには必ずこの監査ログを有効にしておくことを推奨します。
可観測性のより深い実装については OpenTelemetry × ECS の可観測性 を参照してください。
予防:可観測性で再発を止める
問題を一度解決したら、同じ問題で再度時間を使わないよう構造的な予防を入れます。
1. 構造化ログ + 相関ID
全ログを JSON で出力し、requestId / traceId を必ず含める。CloudWatch Logs Insights でフィルタできるようになります。
// 構造化ログの最小実装
const log = (level: string, msg: string, ctx: Record<string, unknown> = {}) => {
process.stdout.write(
JSON.stringify({ level, msg, timestamp: new Date().toISOString(), ...ctx }) + "\n"
);
};
// リクエストIDを全ログに通す
log("info", "server:start", { port: 8080, env: process.env.NODE_ENV });
2. Container Insights + アラート
resource "aws_cloudwatch_metric_alarm" "task_stopped" {
alarm_name = "ecs-task-stopped-abnormally"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "RunningTaskCount"
namespace = "ECS/ContainerInsights"
period = 60
statistic = "Minimum"
threshold = 0
alarm_description = "全タスクが停止した(desired > 0 にもかかわらず)"
dimensions = {
ClusterName = "prod"
ServiceName = "web-api"
}
}
3. デプロイサーキットブレーカー
新タスクが連続してヘルスチェックに失敗したとき、自動で前リビジョンへロールバックします。
resource "aws_ecs_service" "app" {
deployment_circuit_breaker {
enable = true
rollback = true
}
}
これだけで「ミスデプロイが本番に刺さり続ける」事故を構造的に防げます。CI/CDパイプラインとの統合は ECS on Fargate CI/CD ガイド で詳述しています。
4. ヘルスチェックの startPeriod を必ず設定
{
"healthCheck": {
"startPeriod": 30,
"interval": 15,
"timeout": 5,
"retries": 3
}
}
startPeriod なしのデフォルトは0秒。起動中の一時的な不健全状態でタスクが落とされます。
5. ネットワーク設計は Terraform でコード化
VPCエンドポイントの欠如が CannotPullContainer / ResourceInitializationError の最多原因です。ECS on Fargate ネットワーキングガイド のパターンを Terraform で管理し、環境間の設定差分をなくします。
早見表:停止理由 → 真っ先に見る場所 → 典型原因 → 修復
| 停止理由 / stopCode | 真っ先に見る場所 | 典型原因 | 修復 |
|---|---|---|---|
CannotPullContainerError | VPCエンドポイント一覧、実行ロール | プライベートサブネットでNAT/VPCエンドポイント欠如、実行ロールのECR権限不足 | ECR用エンドポイント追加 or NAT確認、ポリシー付与 |
ResourceInitializationError | 実行ロール、VPCエンドポイント(secretsmanager/logs/ssm) | シークレット取得不能、ログ書込不能、ENI割当失敗 | 実行ロールに権限追加、VPCエンドポイント追加、サブネットIP枯渇確認 |
EssentialContainerExited (exitCode≠0) | CloudWatch Logs | アプリ起動失敗、env/secrets不足、CMD誤り | ログでエラー内容確認、環境変数・コマンドを修正 |
EssentialContainerExited (exitCode=137) | Container Insights メモリグラフ | OOMKill(メモリ上限超過) | タスクメモリを上のペアに変更、リークを調査 |
EssentialContainerExited (exitCode=null) | describe-tasks の containers[].reason | CannotStartContainer(起動前失敗) | CMD/ENTRYPOINTのパス・権限確認 |
| ELBヘルスチェック失敗 | ALBターゲットグループ設定、タスクSG | target_type=instance、SG設定ミス、startPeriod不足 | target_type=ipに変更、SG許可追加、startPeriod設定 |
SpotInterruption | EventBridgeイベント | Spot容量回収(正常動作) | グレースフルシャットダウン実装、冪等設計を確認 |
ContainerRuntimeTimeoutError | タスク定義のCMD/ENTRYPOINT | 起動処理が長すぎる、依存サービスへの接続待ち | 起動シーケンスの最適化、startPeriod延長 |
CannotCreateVolumeError | タスク定義のvolumes、EFS設定 | EFSマウントターゲット不達、SG設定 | EFSエンドポイントとSGを確認 |
まとめ
ECS on Fargate のトラブルシューティングは、「何かがおかしい」という感覚から始めるのではなく、stoppedReason → stopCode → exitCode → CloudWatch Logs という順序で論理的に絞り込む型が最速です。
私が実務で積み重ねてきた経験から言うと、タスクが起動しない問題の大多数は次の3つに集約されます。
- ネットワーク到達性(プライベートサブネットでVPCエンドポイントまたはNAT不足)
- 実行ロールとタスクロールの混同(権限が間違ったロールに付いている)
- ヘルスチェックの猶予不足(startPeriod と grace period が設定されていない)
決済基盤で二重課金0件を実現できたのも、「障害が起きてから直す」ではなく、構造と可観測性で問題を事前に潰す設計を一貫して持っていたからです。Fargateの本番環境を速く・安全に安定させたい場合は、お気軽にご相談ください。