# ECS on Fargate トラブルシューティング完全ガイド：タスクが起動しない・すぐ落ちる原因を停止理由コード別に診断・修復する

> ECS Fargate のタスク停止理由（CannotPullContainerError・OutOfMemory・ヘルスチェック失敗など）を describe-tasks の読み方から停止コード別に体系的に診断・修復する実務ガイド。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: AWS, ECS, Fargate, トラブルシューティング, 可観測性, デバッグ, コンテナ, 運用
- URL: https://tomodahinata.com/blog/aws-ecs-fargate-troubleshooting-task-stopped-reasons-guide

## 要点

- タスクが起動しない・落ちる原因は必ず stoppedReason / stopCode / exitCode に現れる。最初の一手は aws ecs describe-tasks で全フィールドを確認すること
- CannotPullContainerError の8割はプライベートサブネットでのネットワーク到達性不足（VPCエンドポイントまたはNAT不足）か、実行ロールへのECR権限漏れのどちらか
- 実行ロール（executionRoleArn）はECSエージェントが使う。タスクロール（taskRoleArn）はアプリが使う。この2つを混同すると secrets/pull/log が連鎖して壊れる
- ELBヘルスチェック失敗の大半はstartPeriod・health_check_grace_period_seconds不足と target_type=ip 忘れ。SGの許可漏れが見落とされやすい
- ECS Exec（execute-command）はSSM Session Manager経由でCloudTrailに記録されるブレークグラス手段。enableExecuteCommand と ssmmessages の4アクションが必須

---

「デプロイしたらタスクが起動しない」「起動してもすぐ落ちる」「ALBのヘルスチェックを永遠に通らない」——ECS on Fargate の運用で必ず一度は踏む問題です。

私は木材流通B2B SaaSで `API Gateway → NLB → ALB → ECS on Fargate` の構成に221本のエンドポイントを乗せ、決済基盤（[本番二重課金0件](/case-studies/payment-platform-reliability)）のワーカー群も同じ基盤で動かしてきました。タスク停止の問題は「運用の注意深さ」ではなく、**診断の型と構造的な予防**で潰します。CloudWatch を凝視して「何かおかしい」と感じる前に、`stoppedReason` と `stopCode` から**論理的に原因を絞る**のが最短ルートです。

この記事は、ECS Fargate のタスク停止理由を**カテゴリ別に体系化**し、「どこを見るか → 何が原因か → どう直すか → どう再発防止するか」を一気通貫で示す実務ガイドです。本番設計の全体像は [ECS on Fargate 本番運用ガイド](/blog/aws-ecs-fargate-production-guide) を先に読んでおくと理解が深まります。

---

## まず見るべき場所：診断の起点

「タスクが落ちた」と気づいた瞬間から、確認する場所は4つです。

| 確認場所 | 何がわかるか |
|---------|------------|
| コンソール「Stopped tasks」タブ | stoppedReason のサマリ、exitCode、最終ステータス |
| `aws ecs describe-tasks` | 全フィールドの構造化データ（最も詳細） |
| CloudWatch Logs（awslogs） | アプリが自分で書いたログ（パニック・起動失敗など） |
| EventBridge「ECS Task State Change」イベント | 非同期の停止通知。アラート連携・自動対応の起点 |

**最初の一手はこれ一択です。**

```bash
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フィールドを必ず確認する

```json
{
  "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 など）。アプリのログを追う前に、エージェント側の原因を潰してください。

---

## 診断フローチャート

```text
タスクが 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` に以下が出る。

```text
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に格納されているため必須）

```bash
# 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` が付いていないか、カスタムポリシーが不足。

```json
{
  "Effect": "Allow",
  "Action": [
    "ecr:GetAuthorizationToken",
    "ecr:BatchCheckLayerAvailability",
    "ecr:GetDownloadUrlForLayer",
    "ecr:BatchGetImage"
  ],
  "Resource": "*"
}
```

> 注意: `ecr:GetAuthorizationToken` はリソースを `*` にしないと機能しません。

**③ タグ・ダイジェスト誤り**

指定したタグが ECR に存在しないか、digest が変更されている。

```bash
# 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 にミラーしてプルする運用に切り替える。

### 修復フロー

```text
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` に下記のようなメッセージ。

```text
ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: ...
```

あるいは:

```text
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のアウトバウンドルールを確認 |

```bash
# サブネットの空きIPを確認
aws ec2 describe-subnets \
  --subnet-ids <subnet-id> \
  --query 'Subnets[*].{CIDR:CidrBlock,Available:AvailableIpAddressCount}'
```

### 実行ロールとタスクロールの混同が連鎖エラーを生む

これが最も見落とされるパターンです。[ECS on Fargate 本番運用ガイド](/blog/aws-ecs-fargate-production-guide)でも詳述していますが、改めて整理します。

| ロール | 設定キー | 使用主体 | 典型的な権限 |
|--------|---------|---------|------------|
| **実行ロール** | `executionRoleArn` | ECSエージェント（起動時） | ECR pull、CloudWatch Logs書込、SecretsManager取得 |
| **タスクロール** | `taskRoleArn` | アプリコード（実行中） | S3・DynamoDB・SQSなどアプリ固有のAWSリソース |

シークレット注入（`secrets[].valueFrom`）はエージェントが起動時に取得するため、権限は**実行ロール**に付ける。アプリが実行中に `aws secretsmanager get-secret-value` を呼ぶ場合だけ、タスクロールに付ける。この原則を守らないと ResourceInitializationError として現れます。

---

## OutOfMemoryError：メモリ上限超過

### 症状

タスクの `stoppedReason` に:

```text
OutOfMemoryError: Container killed due to memory usage
```

または `containers[].exitCode = 137`（Linux の OOMKill は exit code 137）。

### 原因と診断

Fargateのメモリ制限はコンテナレベルで設定します。コンテナのメモリ使用量がタスク定義の `memory`（上限）を超えると、Linuxのカーネル OOM Killer がプロセスを強制終了します。

```bash
# 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つのレベルがあります。

```json
{
  "name": "app",
  "memory": 1024,
  "memoryReservation": 512
}
```

- `memory`（ハードリミット）: これを超えるとOOMKill
- `memoryReservation`（ソフトリミット）: スケジューリング時の目安。超えても即Killはしない

スパイク耐性を持たせるには、`memoryReservation` を通常使用量より少し上に、`memory` をその1.5〜2倍程度に設定して、余裕のバッファを持たせます。ただし、**Fargateのタスクメモリ上限は `task.memory` と等しく、コンテナの合計が超えると起動できません。**

---

## Essential container exited / 非ゼロ exitCode：アプリ起動失敗

### 症状

```text
Essential container in task exited
```

`stopCode = EssentialContainerExited`、`containers[].exitCode` が非ゼロ（0以外）。

### 診断手順

**Step 1: CloudWatch Logs を確認する（最重要）**

```bash
# ログストリームの最新を取得
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が間違っていたり、該当バージョンが存在しない場合、起動に必要な環境変数が空になってアプリがクラッシュする。

```bash
# シークレットの存在確認
aws secretsmanager describe-secret \
  --secret-id arn:aws:secretsmanager:ap-northeast-1:111122223333:secret:prod/db-Ab12Cd
```

**Step 3: CMD / ENTRYPOINT の確認**

タスク定義の `command` が間違っている、または存在しないパスを参照しているケース。

```bash
# ローカルでイメージを起動して再現確認
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サービスのイベントに以下が出る。

```text
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 タスクを正しく登録できない。

```bash
aws elbv2 describe-target-groups \
  --query 'TargetGroups[*].{Name:TargetGroupName,TargetType:TargetType}'
```

必ず `ip` であることを確認する。`instance` になっていたら作り直し（変更不可）。

**② ヘルスチェックパスとポートの確認**

ALBのターゲットグループに設定したヘルスチェックパス（例 `/healthz`）がアプリで実装されていて、正しいポートを返すか。

```bash
# タスクの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のヘルスチェック送信元** と一致していなければ通らない。

```bash
# タスクSGのインバウンドルールを確認
aws ec2 describe-security-groups \
  --group-ids <task-sg-id> \
  --query 'SecurityGroups[*].IpPermissions'
```

**④ startPeriod と grace period の設定**

起動に時間がかかるアプリ（DBマイグレーション実行・大きなモデルロードなど）は、ヘルスチェックが始まる前の猶予を設定しないと、初期化中に「不健全」と判定されてKillされる。

```json
{
  "healthCheck": {
    "startPeriod": 60,
    "interval": 15,
    "timeout": 5,
    "retries": 3
  }
}
```

加えて、ECSサービス側にも `health_check_grace_period_seconds` が必要です。

```hcl
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中断は"障害"ではない

### 症状

```text
stopCode: "SpotInterruption"
stoppedReason: "Your Spot Task was interrupted."
```

### 正しい理解

Spot 中断はAWSが容量を回収するための**設計された動作**であり、ソフトウェアのバグでも運用ミスでもありません。対処すべきは「中断が起きたとき壊れない設計になっているか」です。

**Spot 中断の仕組み:**

1. AWSが容量回収を決定（通常の通知より2分前）
2. EventBridge に `ECS Task State Change` イベント発行（stopCode=SpotInterruption）
3. タスクに **SIGTERM** を送信（`stopTimeout` の猶予あり、最大120秒）
4. 猶予後に SIGKILL

**設計で吸収する3点セット:**

```text
① グレースフルシャットダウン：SIGTERMを受けてin-flightを捌き切る
② 冪等な処理：途中でKillされても、再起動後に二重処理しない
③ 容量プロバイダ戦略：baseをオンデマンドで守り、追加分をSpotに割り当て
```

```hcl
# 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 本番運用ガイド](/blog/aws-ecs-fargate-production-guide) の「SIGTERMを受けて綺麗に終わる」節を参照してください。`stopTimeout` の既定は30秒、最大は120秒です。本番ではアプリの drain 時間に合わせて明示的に設定します。

---

## CannotStartContainerError / ContainerRuntimeTimeoutError

### 症状

```text
CannotStartContainerError: ...
ContainerRuntimeTimeoutError: Timeout waiting for container to start
```

exitCode は null（コンテナプロセス起動に到達していない）。

### 原因と診断

**CannotStartContainerError:**

- `command` / `entryPoint` に実行権限のないバイナリを指定
- `volumes` のマウント先に書き込めない（`readonlyRootFilesystem: true` のとき特に）
- `user` で指定した UID がコンテナ内に存在しない
- `linuxParameters` の依存設定が Fargate でサポート外

```bash
# ローカルで同じ設定を再現
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`**

```hcl
resource "aws_ecs_service" "app" {
  enable_execute_command = true
  # ...
}
```

注意: `enableExecuteCommand` は**新しく起動するタスクにのみ有効**です。既存タスクには後付けできません。設定変更後に force-new-deployment でタスクを差し替えてください。

**② タスクロールに ssmmessages の4アクション**

```json
{
  "Effect": "Allow",
  "Action": [
    "ssmmessages:CreateControlChannel",
    "ssmmessages:CreateDataChannel",
    "ssmmessages:OpenControlChannel",
    "ssmmessages:OpenDataChannel"
  ],
  "Resource": "*"
}
```

これは**タスクロール**（`taskRoleArn`）に付ける権限です（実行ロールではない）。

**③ SSM Session Manager プラグイン（クライアント側）**

```bash
# macOS
brew install session-manager-plugin
```

### 実際のコマンド

```bash
# タスク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"
```

シェルに入ったら、環境変数・ファイル存在・ネットワーク疎通などを直接確認できます。

```bash
# コンテナ内で環境変数の確認
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 の可観測性](/blog/aws-observability-opentelemetry-sre-ecs) を参照してください。

---

## 予防：可観測性で再発を止める

問題を一度解決したら、同じ問題で再度時間を使わないよう構造的な予防を入れます。

### 1. 構造化ログ + 相関ID

全ログを JSON で出力し、`requestId` / `traceId` を必ず含める。CloudWatch Logs Insights でフィルタできるようになります。

```ts
// 構造化ログの最小実装
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 + アラート

```hcl
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. デプロイサーキットブレーカー

新タスクが連続してヘルスチェックに失敗したとき、自動で前リビジョンへロールバックします。

```hcl
resource "aws_ecs_service" "app" {
  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }
}
```

これだけで「ミスデプロイが本番に刺さり続ける」事故を構造的に防げます。CI/CDパイプラインとの統合は [ECS on Fargate CI/CD ガイド](/blog/aws-ecs-fargate-cicd-blue-green-codedeploy-github-actions-guide) で詳述しています。

### 4. ヘルスチェックの startPeriod を必ず設定

```json
{
  "healthCheck": {
    "startPeriod": 30,
    "interval": 15,
    "timeout": 5,
    "retries": 3
  }
}
```

`startPeriod` なしのデフォルトは0秒。起動中の一時的な不健全状態でタスクが落とされます。

### 5. ネットワーク設計は Terraform でコード化

VPCエンドポイントの欠如が CannotPullContainer / ResourceInitializationError の最多原因です。[ECS on Fargate ネットワーキングガイド](/blog/aws-ecs-fargate-networking-alb-service-connect-vpc-guide) のパターンを 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つに集約されます。

1. **ネットワーク到達性**（プライベートサブネットでVPCエンドポイントまたはNAT不足）
2. **実行ロールとタスクロールの混同**（権限が間違ったロールに付いている）
3. **ヘルスチェックの猶予不足**（startPeriod と grace period が設定されていない）

決済基盤で二重課金0件を実現できたのも、「障害が起きてから直す」ではなく、**構造と可観測性で問題を事前に潰す**設計を一貫して持っていたからです。Fargateの本番環境を速く・安全に安定させたい場合は、お気軽にご相談ください。
