# AWS ECS on Fargate 本番運用ガイド：サーバーレスコンテナの設計・デプロイ・コスト・セキュリティを実コードで

> AWS公式ドキュメントに忠実なECS on Fargateの本番運用ガイド。タスクサイズ設計（CPU/メモリ表）、awsvpcネットワーキング、ローリング更新＋デプロイサーキットブレーカー、SIGTERMによるグレースフルシャットダウン、実行ロールとタスクロールの分離、Fargate Spotとコスト最適化までを、Terraform・タスク定義JSON・実コードで体系化します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: AWS, ECS, Fargate, コンテナ, Terraform, インフラ, コスト最適化, 可観測性
- URL: https://tomodahinata.com/blog/aws-ecs-fargate-production-guide

## 要点

- Fargateはサーバー管理を不要にするサーバーレスのコンテナ実行基盤で、タスクごとに分離境界を持ちカーネル・CPU・メモリ・ENIを他タスクと共有しない
- 本番品質の鍵は『実行ロールとタスクロールの分離』『awsvpc + ALB(target type=ip)』『ローリング更新＋デプロイサーキットブレーカーで自動ロールバック』『SIGTERMを受けてstopTimeout内に綺麗に終わる』の4点
- タスクサイズはCPUとメモリの組み合わせが固定（.25〜16 vCPU）。計測してright-sizingし、Application Auto Scalingのターゲット追跡で水平スケールするのが定石
- コストはvCPU秒・メモリ秒の従量課金。ARM64(Graviton)とFargate Spot（中断耐性ワークロードを2分前SIGTERM警告つきで割引実行）、Compute Savings Plansで最適化する
- シークレットはSecrets Manager/SSMからvalueForで注入し実行ロールに権限を寄せる。root実行とreadonlyRootFilesystem、ECS Execによるブレークグラス、Container Insightsまで含めて本番要件を満たす

---

「コンテナは本番で動かしたい。でもKubernetesクラスタの面倒は見たくないし、EC2のパッチ当てやスケーリングにも時間を割けない」——スタートアップや一人開発で本番のコンテナ基盤を組むとき、ほぼ必ずここに行き着きます。その答えが **AWS Fargate** です。

私は経済産業大臣賞を受賞した木材流通SaaSで、`API Gateway → NLB → ALB → ECS on Fargate` という構成の上に**221本のAPIエンドポイント**を本番運用してきました。決済基盤（[本番二重課金0件](/case-studies/payment-platform-reliability)）のワーカー群もFargateで動いています。サーバーを1台も触らずに、HTTPサービス・バッチ・イベント駆動ワーカーを同じ仕組みで回せることが、少人数で本番品質を出すための土台になっています。

この記事は、**[AWS公式ドキュメント](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html)に忠実でありながら、公式よりわかりやすく**、かつ「どの場面でどう使うか」を実コードで示すことを目的にしています。タスク設計・ネットワーキング・デプロイ・回復性・セキュリティ・コストまで、本番に出すために必要なことを一気通貫で扱います。技術選定そのもの（ECSかEKSか）は、別記事の [ECS on Fargate vs EKS：スタートアップの意思決定フレームワーク](/blog/aws-ecs-vs-eks-startup-decision-framework) を参照してください。本稿は**「ECS on Fargateを選んだ後、本番でどう作るか」**に集中します。

---

## Fargate とは何か：EC2起動タイプとの違い

Amazon ECS（Elastic Container Service）には、コンテナを動かす計算リソース（**起動タイプ / 容量**）として大きく2つあります。

- **EC2 起動タイプ**：自分でEC2インスタンス（コンテナインスタンス）の群れを用意し、その上にタスクを詰める。OSのパッチ、インスタンスのスケーリング、ビンパッキング（詰め込み効率）の管理が**自分の責任**。
- **Fargate**：CPUとメモリを指定するだけで、AWSがその裏側のインスタンスを用意・パッチ・スケールする**サーバーレス**方式。サーバーという概念自体が消える。

公式の定義はシンプルです。

> AWS Fargate is a technology that you can use with Amazon ECS to run containers without having to manage servers or clusters of Amazon EC2 instances.（— [AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html)）

セキュリティ上、最も重要な一文がこれです。

> Each Fargate task has its own isolation boundary and does not share the underlying kernel, CPU resources, memory resources, or elastic network interface with another task.

つまり **Fargateの各タスクは独立した分離境界**を持ち、カーネルもCPUもメモリもENI（Elastic Network Interface）も他タスクと共有しません。EC2起動タイプでは複数タスクが1インスタンスのカーネルを共有しますが、Fargateは「タスク=最小の隔離単位」です。マルチテナントや厳格な分離要件があるなら、これは決定的な利点になります。

### 比較表：いつFargateでいつEC2か

| 観点 | Fargate | EC2 起動タイプ |
|------|---------|---------------|
| サーバー管理 | **不要**（パッチ・AMI更新もAWS側） | 自分でOS/AMI/パッチを運用 |
| スケーリング | タスク数だけ考えればよい | インスタンス群とタスクの**二段スケール** |
| 分離境界 | **タスク単位で完全分離** | タスクはインスタンスのカーネルを共有 |
| 課金単位 | **vCPU秒・メモリ秒の従量**（割り当て量） | インスタンス時間（使用率に関わらず固定） |
| 起動の速さ | 数十秒（ENI割当含む） | インスタンスに空きがあれば速い |
| GPU / 特殊インスタンス | 非対応 | 対応（GPU、Inferentia等） |
| デーモン型（各ホスト1個） | 非対応（ホストの概念がない） | DAEMONスケジューリング可 |
| 常時高負荷の原価 | 使用率が高いと割高になりうる | 高使用率なら有利。Savings Plansも併用 |

**指針**：迷ったらFargate。サーバー管理コスト（=人件費）こそ最大のコストだからです。EC2に戻すべきなのは「GPUが要る」「常時CPU 80%超で回し続けるバッチ群があり原価が支配的」「各ホストに1個だけ常駐させるエージェント（DaemonSet相当）が必要」といった**明確な理由があるときだけ**です。

---

## どんな場面で使うか：3つの典型ワークロード

Fargateは「Webサーバー専用」ではありません。実務では次の3形態を同じ語彙（タスク定義）で扱えるのが強みです。

1. **常駐サービス（Service）**：ALB/NLBの背後でHTTP APIやWebアプリを常時起動。`desiredCount`で冗長化し、オートスケールする。← 最も一般的。
2. **スケジュールタスク（バッチ）**：EventBridge Schedulerでcron実行する日次集計・レポート生成・データ同期。サービスではなく**単発タスク（RunTask）**として走り、終わると課金が止まる。
3. **イベント駆動ワーカー**：SQSのキュー長に応じてタスク数をスケールさせる非同期処理。決済のWebhook処理や画像変換など。冪等性とグレースフルシャットダウンが本質になる。

決済基盤では「常駐APIサービス」と「SQS駆動の冪等ワーカー」を両方Fargateに載せ、Webhookの順不同・重複到達を[冪等性キーで吸収](/blog/dynamodb-payment-reliability-idempotency-zero-downtime)しました。**同じデプロイ基盤で3形態を回せる**ことが、運用の認知負荷を劇的に下げます。

---

## コアな構成要素：4つの登場人物の関係

ECSは用語が多くて最初に混乱します。本質は4つだけです。

```
Cluster（論理的な箱：複数サービスをまとめる名前空間）
└── Service（"常にN個のタスクを保つ"宣言＝望ましい状態のコントローラ）
    └── Task（実行中の1単位。1つ以上のコンテナの集合）
        └── Container（あなたのアプリのイメージ）
        ↑
        Task Definition（タスクの設計図：イメージ・CPU/メモリ・IAM・ログ・環境変数）
```

- **Task Definition（タスク定義）**：不変の「設計図」。リビジョン番号で版管理される（`my-app:1`, `my-app:2`...）。デプロイとは**新しいリビジョンを登録してサービスに差し替える**こと。
- **Task（タスク）**：タスク定義から起動した実体。1タスク＝1つのENI（`awsvpc`モード）＝1つのプライベートIP。
- **Service（サービス）**：「このタスク定義のタスクを常に`desiredCount`個、健全に保て」という**宣言的コントローラ**。タスクが落ちれば自動で再起動し、ALBへの登録/解除も面倒を見る。
- **Cluster（クラスタ）**：サービスやタスクを束ねる論理境界。Fargateではクラスタに「サーバー」は存在せず、ただの名前空間に近い。

この「Serviceが望ましい状態を保ち続ける」という宣言的モデルが、Kubernetesの`Deployment`と同じ発想です。だからこそ**手で`docker run`するのではなく、状態を宣言して任せる**のが正しい使い方になります。

---

## タスクサイズ設計：CPUとメモリは"組み合わせが固定"

Fargate最大の落とし穴がここです。**CPUとメモリは自由な組み合わせではなく、決められたペアからしか選べません**。公式の組み合わせ（[Task CPU and memory](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html)）はこうです。

| CPU（タスク全体） | 選べるメモリ | 刻み |
|------------------|------------|------|
| **256（.25 vCPU）** | 512 MiB / 1 GB / 2 GB | 固定3択 |
| **512（.5 vCPU）** | 1〜4 GB | 1 GB刻み |
| **1024（1 vCPU）** | 2〜8 GB | 1 GB刻み |
| **2048（2 vCPU）** | 4〜16 GB | 1 GB刻み |
| **4096（4 vCPU）** | 8〜30 GB | 1 GB刻み |
| **8192（8 vCPU）** | 16〜60 GB | 4 GB刻み（PV 1.4.0+） |
| **16384（16 vCPU）** | 32〜120 GB | 8 GB刻み（PV 1.4.0+） |

> CPUは`1024`（CPU単位）でも`1 vCPU`でも指定でき、メモリは`3072`（MiB）でも`3 GB`でも指定できます。登録時に内部単位へ変換されます。

**実務での効き方**：たとえば「メモリは512 MiBで足りるが、CPUは1 vCPU欲しい」というワークロードでも、`1024 CPU`を選んだ瞬間に**メモリは最低2 GB**を確保する（＝課金される）ことになります。逆も同様で、「8 GBメモリが要る」なら最低でも1 vCPUがついてきます。だから**サイズは"計測してから"決める**のが鉄則です。憶測で大きく取ると、使わないリソースに毎秒課金され続けます。

### エフェメラルストレージ（一時ディスク）

Fargateタスクには既定で **20 GB** のエフェメラルストレージが付きます。ビルド成果物・一時ファイル・キャッシュに使えます。足りなければタスク定義の`ephemeralStorage`で **最大200 GB** まで拡張できます（プラットフォームバージョン`1.4.0`以降）。タスク終了で消える揮発領域なので、永続化が要るならEFSやS3を使います。

### プラットフォームバージョンは `LATEST`（=Linux 1.4.0）

> The **LATEST** Linux platform version is `1.4.0`.（— [Fargate platform versions](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform-fargate.html)）

`1.4.0`はエフェメラルストレージ拡張・`systemControls`・UDP NLBなどに必要です。**特別な理由がなければ`LATEST`を使う**。新規タスクは常に最新リビジョンのインフラ（パッチ適用済み）で起動するため、セキュリティ的にもこれが既定の安全側です。ARM64（Graviton）ワークロードもサポートされ、`cpuArchitecture`に`ARM64`を指定できます（後述のコスト最適化で効きます）。

---

## ネットワーキング：awsvpc と ALB の正しい繋ぎ方

Fargateは**`awsvpc`ネットワークモード固定**です。各タスクが専用のENIとプライベートIPを持つため、EC2のように「ホストのポートをマッピングする」概念がありません。ここを誤解するとALB連携で必ず詰まります。

**重要な3点：**

1. **ALBのターゲットグループは`target_type = "ip"`**。`instance`ではありません。タスクはEC2インスタンスではなくENIに紐づくため、IPターゲットで登録されます（公式明記）。
2. **セキュリティグループはタスクのENIに付く**。「ALBのSG → タスクのSG（アプリのポート）だけ許可」と最小化する。タスクのSGはインバウンドをALBのSGに限定し、0.0.0.0/0を開けない。
3. **配置はプライベートサブネット + NAT Gateway**が本番の定石。`assignPublicIp=ENABLED`でパブリックサブネットに直接置くこともできるが、攻撃面が広がる。ECR/CloudWatch/Secrets ManagerへはVPCエンドポイント or NAT経由で到達させる。

リクエストの流れはこうなります。

```
Internet → ALB(public subnet) → Target Group(type=ip)
        → Task ENI(private subnet, SG=ALBのSGのみ許可) → container:8080
                                                   ↘ NAT GW → ECR / Secrets Manager / CloudWatch
```

サービス検出（サービス間通信）が必要なら、**ECS Service Connect**（または Cloud Map）でDNS名による名前解決を使い、内部通信にALBを増やさず疎結合にします。

---

## 実装①：最小構成のタスク定義（JSON）

まずは"何が必須か"を最小のタスク定義で掴みます。要点はコメントに記しました。

```json
{
  "family": "web-api",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "512",
  "memory": "1024",
  "runtimePlatform": {
    "cpuArchitecture": "ARM64",
    "operatingSystemFamily": "LINUX"
  },
  "executionRoleArn": "arn:aws:iam::111122223333:role/web-api-exec",
  "taskRoleArn": "arn:aws:iam::111122223333:role/web-api-task",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "111122223333.dkr.ecr.ap-northeast-1.amazonaws.com/web-api:1a2b3c4",
      "essential": true,
      "user": "10001:10001",
      "readonlyRootFilesystem": true,
      "linuxParameters": { "initProcessEnabled": true },
      "portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
      "environment": [{ "name": "NODE_ENV", "value": "production" }],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:111122223333:secret:prod/db-Ab12Cd"
        }
      ],
      "stopTimeout": 60,
      "healthCheck": {
        "command": ["CMD-SHELL", "wget -q -O - http://localhost:8080/healthz || exit 1"],
        "interval": 15,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 30
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/web-api",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "app"
        }
      }
    }
  ]
}
```

**本番で効くポイント：**

- **`image`はタグではなくダイジェスト相当の不変参照（CommitSHAタグ）**を使う。`latest`は再現性を壊す。ECSは既定で`versionConsistency: enabled`によりタグをダイジェストへ解決し、サービス内の全タスクが同一イメージで動くことを保証する。
- **`user`で非root実行 + `readonlyRootFilesystem: true`**。書き込みが要るパスだけ`volumes`でtmpfsを当てる。攻撃面を最小化する基本。
- **`secrets[].valueFrom`** でSecrets Manager / SSM Parameter Storeから機密を注入。環境変数に平文で書かない（後述）。
- **`stopTimeout`** は既定30秒、最大120秒。グレースフルシャットダウンの猶予（後述）。
- **`healthCheck.startPeriod`** で起動直後の猶予を与え、初期化中の誤検知による強制終了を防ぐ。

---

## 実装②：Terraformで本番サービス一式

タスク定義単体では本番になりません。**クラスタ・サービス・ALB・SG・ログ・オートスケール**をIaCで宣言してこそ「壊れない・再現できる」状態になります。Terraformで一式を組みます（要点に絞った構成）。Terraformのモジュール設計・state分離・ドリフト検知は[別記事](/blog/terraform-module-design-state-isolation-drift-detection-guide)に譲り、ここはECS固有部分に集中します。

```hcl
# --- クラスタ：Container Insights を有効化（可観測性の土台） ---
resource "aws_ecs_cluster" "main" {
  name = "prod"
  setting {
    name  = "containerInsights"
    value = "enhanced" # 拡張オブザーバビリティ。コスト許容なら本番推奨
  }
}

# --- タスクのSG：インバウンドは ALB のSGからのみ ---
resource "aws_security_group" "task" {
  name_prefix = "web-api-task-"
  vpc_id      = var.vpc_id
  lifecycle { create_before_destroy = true }
}
resource "aws_vpc_security_group_ingress_rule" "from_alb" {
  security_group_id            = aws_security_group.task.id
  referenced_security_group_id = aws_security_group.alb.id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080
}
resource "aws_vpc_security_group_egress_rule" "all_out" {
  security_group_id = aws_security_group.task.id
  ip_protocol       = "-1"
  cidr_ipv4         = "0.0.0.0/0" # NAT経由でECR/Secrets/CloudWatchへ
}

# --- ALB ターゲットグループ：Fargateは必ず target_type = "ip" ---
resource "aws_lb_target_group" "app" {
  name        = "web-api"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "ip"
  deregistration_delay = 30 # 接続ドレイン。既定300sは過剰なことが多い
  health_check {
    path                = "/healthz"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    interval            = 15
    timeout             = 5
    matcher             = "200"
  }
}

# --- サービス：ローリング更新＋デプロイサーキットブレーカー ---
resource "aws_ecs_service" "app" {
  name            = "web-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 2
  launch_type     = "FARGATE"
  platform_version = "LATEST"
  enable_execute_command = true # ECS Exec によるブレークグラス

  deployment_minimum_healthy_percent = 100
  deployment_maximum_percent         = 200
  deployment_circuit_breaker {
    enable   = true
    rollback = true # 失敗を検知したら前リビジョンへ自動ロールバック
  }

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.task.id]
    assign_public_ip = false
  }
  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = "app"
    container_port   = 8080
  }
  health_check_grace_period_seconds = 30
}
```

`desired_count = 2`・`minimum_healthy_percent = 100`・`maximum_percent = 200` の組み合わせは、「**常に2タスクを健全に保ったまま、最大4タスクまで一時的に増やして新版を立ち上げ、健全化を確認してから旧版を落とす**」という無停止ローリングを意味します。次節で正確に説明します。

---

## デプロイ：ローリング更新とデプロイサーキットブレーカー

ECSの既定デプロイは**ローリング更新（`ECS`タイプ）**です。動きは2つのパラメータで決まります（[公式](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html)）。

- **`minimumHealthyPercent`**：デプロイ中に「健全に動いていなければならない」タスク数の下限（％、切り上げ）。例：min 50%・desired 4なら、新版2個を起動する前に旧版を2個まで停止できる。
- **`maximumPercent`**：デプロイ中に「起動してよい」タスク数の上限（％、切り下げ）。例：max 200%・desired 4なら、旧版4個を止める前に新版4個を起動できる。

**`min 100% / max 200%`** は最も安全側で、可用性を一切落とさずに新版を立ち上げ切ってから旧版を落とします（その分、一時的にリソースが倍になる）。コスト優先なら`min 50% / max 100%`でリソースを増やさず入れ替える選択もあります。

### 失敗を"自動で巻き戻す"のがサーキットブレーカー

ここが本番品質の分かれ目です。新版がクラッシュループしているのに気付かずトラフィックを流す事故を、**デプロイサーキットブレーカー**が防ぎます。

> Both methods support rolling back to the previous service revision.（— 公式。サーキットブレーカーとCloudWatchアラームのいずれも前リビジョンへのロールバックに対応）

`deployment_circuit_breaker { enable = true, rollback = true }` を入れておけば、新タスクが規定回数立ち上がらない（ヘルスチェックを通らない）場合に**デプロイを失敗扱いにして自動で前の正常なリビジョンへ戻します**。さらにアプリのビジネスメトリクス（エラー率など）を基準にしたいなら、**CloudWatchアラーム連動**を併用できます。両方有効にすると、どちらかの条件を満たした時点で失敗・ロールバックされます。

### イメージダイジェストによる版の一貫性

ECSは既定で**タグをイメージダイジェストに解決**し、サービス内の全タスクが同一バイナリで動くことを保証します（`versionConsistency`）。「ビルドし直したら`latest`の中身が変わって一部タスクだけ別物」という事故を構造的に防ぎます。**CommitSHAをタグにし、`latest`に依存しない**運用と合わせて、再現性を担保しましょう。

CI/CDは**OIDCによる鍵レス**で組むのが2026年の標準です（長期アクセスキーを置かない）。具体は [GitHub Actions OIDC で鍵レスCI/CD](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide) を参照してください。デプロイ自体は新リビジョンを登録して`aws ecs update-service --force-new-deployment`で差し替えるか、`amazon-ecs-deploy-task-definition`アクションを使います。

---

## 回復性・冪等性：SIGTERMを受けて"綺麗に終わる"

Fargateで最も見落とされ、最も事故を生むのが**グレースフルシャットダウン**です。デプロイ・スケールイン・[Fargate Spot](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-capacity-providers.html)中断のたびにタスクは停止します。そのときECSは次の手順を踏みます。

1. タスクをALBターゲットから**登録解除**（新規リクエストを止め、`deregistration_delay`の間、処理中の接続を待つ）。
2. コンテナに **`SIGTERM`** を送る。
3. **`stopTimeout`（既定30秒・最大120秒）** 待つ。
4. それでも終わらなければ **`SIGKILL`** で強制終了。

> The SIGTERM signal must be received from within the container to perform any cleanup actions. Failure to process this signal results in the task receiving a SIGKILL signal after the configured `stopTimeout` and may result in **data loss or corruption**.（— 公式）

つまり**アプリがSIGTERMを握って、処理中のリクエストを捌き切り、DB接続やキュー受信を綺麗に閉じる責任がある**。これを怠ると、デプロイのたびに進行中の処理が`SIGKILL`で殺され、データ破損やWebhookの取りこぼしが起きます。Node.jsならこう書きます。

```ts
// graceful-shutdown.ts — SIGTERM を握って in-flight を捌き切る
import http from "node:http";

export function installGracefulShutdown(
  server: http.Server,
  opts: { drainMs: number; onClose: () => Promise<void> },
): void {
  let shuttingDown = false;

  const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
    if (shuttingDown) return; // 二重発火を冪等に無視
    shuttingDown = true;
    console.info({ msg: "shutdown:start", signal });

    // 1) 新規接続を止める。処理中のレスポンスは待つ
    server.close(() => console.info({ msg: "shutdown:http-closed" }));

    // 2) drain 上限を stopTimeout より短く張る（SIGKILL より先に終える）
    const deadline = new Promise<void>((r) => setTimeout(r, opts.drainMs));

    // 3) DB プール・キュー consumer など外部資源を閉じる
    await Promise.race([opts.onClose(), deadline]);
    console.info({ msg: "shutdown:done" });
    process.exit(0);
  };

  process.on("SIGTERM", shutdown); // ECS が送るのはこれ
  process.on("SIGINT", shutdown);  // ローカル Ctrl-C 用
}
```

`drainMs`は`stopTimeout`より**短く**設定するのが鉄則です（例：`stopTimeout: 60` に対し `drainMs: 50_000`）。SIGKILLが来る前に自分から綺麗に`exit(0)`するためです。`linuxParameters.initProcessEnabled: true`を入れておくと、PID 1のゾンビプロセス問題（シグナルが正しく伝播しない）も回避できます。

**冪等性との関係**：SQS駆動ワーカーなら「途中で殺されても、再配信されたメッセージを二重処理しない」設計が必須です。これはFargate固有ではなく分散処理一般の話で、[冪等な非同期処理](/blog/aws-sqs-lambda-eventbridge-idempotent-async-processing-guide)の原則がそのまま効きます。グレースフルシャットダウン（取りこぼさない）と冪等性（二重処理しない）は**両輪**です。

---

## オートスケーリング：計測値に追従させる

Fargateの水平スケールは **Application Auto Scaling のターゲット追跡（Target Tracking）** で組みます。「CPU使用率を60%に保て」のように**目標値を宣言**すると、超えれば`desiredCount`を増やし、下回れば減らします。

```hcl
resource "aws_appautoscaling_target" "app" {
  service_namespace  = "ecs"
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  min_capacity       = 2
  max_capacity       = 20
}

resource "aws_appautoscaling_policy" "cpu" {
  name               = "cpu-tt"
  policy_type        = "TargetTrackingScaling"
  service_namespace  = aws_appautoscaling_target.app.service_namespace
  resource_id        = aws_appautoscaling_target.app.resource_id
  scalable_dimension = aws_appautoscaling_target.app.scalable_dimension
  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
    target_value       = 60.0
    scale_in_cooldown  = 120
    scale_out_cooldown = 30 # 増やすのは速く、減らすのは慎重に
  }
}
```

**メトリクス選び**：CPUバウンドなら`ECSServiceAverageCPUUtilization`、メモリなら`...MemoryUtilization`。HTTPトラフィックに素直に追従させたいなら**`ALBRequestCountPerTarget`**（1ターゲットあたりのリクエスト数）が最も実感に合います。`scale_out_cooldown`を短く、`scale_in_cooldown`を長くするのが定石——**増やすのは即座に、減らすのは慎重に**（スパイク直後に縮めて再び慌てる「フラッピング」を防ぐ）。

---

## 可観測性：止まった処理を一目で追える状態に

少人数運用ほど可観測性が生命線です。Fargateでは次を最初から仕込みます。

- **Container Insights**：CPU/メモリ/ネットワーク/タスク数を自動収集。`enhanced`にすると、より細かいコンテナ単位のメトリクスが取れる。
- **ログ**：`awslogs`ドライバでCloudWatch Logsへ。**JSON構造化ログ**にして相関ID（リクエストID）を必ず通す。高度な要件（複数宛先・パース・フィルタ）には**FireLens（Fluent Bit）**を使い、CloudWatch/S3/OpenSearch等へルーティングする。
- **トレース**：分散トレーシングはOpenTelemetryでサイドカー収集する。ECS上でのSRE実践は [OpenTelemetry × ECS の可観測性](/blog/aws-observability-opentelemetry-sre-ecs) に詳述しました。

### ECS Exec：本番コンテナへの"ブレークグラス"

SSHもポート開放も鍵管理もなしに、**動いているコンテナの中へ入って調査**できるのがECS Execです。

> in production scenarios, you can use it to gain break-glass access to your containers to debug issues.（— [ECS Exec](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html)）

```bash
# サービス/タスクで enableExecuteCommand を有効化した上で：
aws ecs execute-command \
  --cluster prod \
  --task <task-id> \
  --container app \
  --interactive \
  --command "/bin/sh"
```

仕組みはSSM Session Managerで、**操作はCloudTrailに記録**され、コマンドと出力をCloudWatch/S3へ監査ログとして残せます。タスクロールに`ssmmessages:*`の4アクション（`CreateControlChannel`/`CreateDataChannel`/`OpenControlChannel`/`OpenDataChannel`）が必要です。IAMの条件キー（`ecs:container-name`等）で**本番コンテナへのExecだけ拒否**するといった細かな統制もかけられます。「誰が・いつ・どのタスクに入ったか」を残せるのが、SSHにはない監査性です。

---

## セキュリティ：実行ロールとタスクロールを混同しない

ここはFargate本番で**最も間違えられる**ポイントです。IAMロールが2種類あり、役割が全く違います。

| ロール | 誰が使う | 何のため | 典型的な権限 |
|--------|---------|---------|------------|
| **実行ロール**（`executionRoleArn`） | **ECS/Fargateエージェント** | タスクを"起動するため" | ECRからイメージpull、CloudWatch Logsへ書込、Secrets Manager/SSMから機密を取得して注入 |
| **タスクロール**（`taskRoleArn`） | **あなたのアプリコード** | 実行中にAWS APIを呼ぶため | S3読み書き、DynamoDB、SQS送受信など**アプリが必要な最小権限** |

公式の区別は明快です。

> The permissions granted in the IAM role are vended to containers running in the task. This role allows your application code to use other AWS services.（— タスクロール）
> These permissions aren't accessed by the Amazon ECS container and Fargate agents. For the IAM permissions that Amazon ECS needs to pull container images and run the task, see Amazon ECS task execution IAM role.（— 実行ロールとの違い）

**原則**：
- **シークレットの取得は実行ロール**に寄せる（`secrets[].valueFrom`の解決はエージェントが起動時に行うため）。アプリが実行中に直接Secrets Managerを叩くなら、その分はタスクロールに付ける。
- **アプリのAWSアクセスはタスクロール**。サービス／タスク定義ごとに**専用ロール**を作り、最小権限にする。「全タスク共通の何でもできるロール」は最大のアンチパターン。
- Fargateでは各タスクが独立した分離境界を持つため、EC2インスタンスプロファイルのような"同居タスクの資格情報が漏れる"問題は構造的に起きにくい。

### シークレットは環境変数の平文に置かない

`environment`に平文でDBパスワードを書くと、`DescribeTaskDefinition`できる全員に漏れます。**必ず`secrets`経由**でSecrets ManagerやSSM Parameter Storeから注入し、実行ロールに`secretsmanager:GetSecretValue`（とKMS復号権限）を最小スコープで付与します。これはこのポートフォリオの[ルート規約](/blog/typescript-type-safety-discipline-zod-nevererror-no-any)とも一貫する「秘密はenvに置き、コードに置かない」の延長です。

### さらに堅くする3点

- **非root実行**（`user: "10001:10001"`）＋ **`readonlyRootFilesystem: true`**。書き込みが要る箇所だけtmpfsを当てる。
- **イメージスキャン**：ECRのスキャン（拡張スキャン/Inspector連携）で既知脆弱性を出荷前に止める。
- **タスク定義は最小**：`privileged`やホスト系の共有はFargateでそもそも不可。必要のない`linuxParameters`を足さない。

WAF・多層防御まで含めた境界防御は [AWS WAF 多層防御](/blog/waf-defense-in-depth-aws-waf-cloud-armor-owasp-guide) を参照してください。

---

## コスト最適化：従量課金を"使った分だけ"に寄せる

Fargateは**割り当てたvCPUとメモリに対する秒単位課金**（最低1分）です。EC2のように「インスタンスを買って使用率で薄める」のではなく、**タスクが起動している間、割り当て量そのものに課金**されます。だから最適化の方向は明確です。

1. **Right-sizing**：Container Insightsで実使用を見て、過剰な割り当てを削る。前述の通りCPU/メモリは組み合わせ固定なので、"片方を上げると相方も上がる"前提で最小ペアを選ぶ。
2. **ARM64（Graviton）に寄せる**：`cpuArchitecture: ARM64`にするだけで、同等性能をx86より**約20%低い単価**で回せる（マルチアーキビルドが必要）。CPUバウンドな常駐サービスほど効く。
3. **Fargate Spot**：中断耐性のあるワークロード（バッチ、ステートレスワーカー、開発環境）を**大幅割引**で実行。代償は「AWSが容量を返せと言ったら**2分前の警告（SIGTERM）つきで中断される**」こと。グレースフルシャットダウンを実装済みなら、これは十分に受け入れられるトレードオフです。
4. **Compute Savings Plans**：常時動く本番サービスのベースライン分を1年/3年コミットして単価を下げる。Spotと併用し「ベースは割引コミット、バーストはオンデマンド/Spot」と層を分ける。

### 容量プロバイダ戦略：base と weight でSpotを安全に混ぜる

オンデマンドとSpotは**容量プロバイダ戦略**で混在させます（[公式](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-capacity-providers.html)）。

- **`base`**：そのプロバイダで**最低限確保するタスク数**（1つのプロバイダだけに設定可、既定0）。
- **`weight`**：base充足後、追加タスクを各プロバイダへ**何対何の比で割り振るか**。

```hcl
# 「最低2タスクは必ずオンデマンドで確保。それを超える分は Spot:オンデマンド = 4:1 で割る」
default_capacity_provider_strategy {
  capacity_provider = "FARGATE"
  base              = 2
  weight            = 1
}
default_capacity_provider_strategy {
  capacity_provider = "FARGATE_SPOT"
  base              = 0
  weight            = 4
}
```

これで**可用性のベースラインはオンデマンドで守りつつ、スケールアウト分を安く調達**できます。Spot中断時は、サービススケジューラが空き容量を見て自動で別タスクの起動を試みます（容量が枯渇していれば回復まで待つ）。中断は`SpotInterruption`としてEventBridgeのタスク状態変更イベントにも流れるので、監視に載せておきます。

FinOps全体の考え方（タグ・予算アラート・無駄の継続削減）は [AWS スタートアップのコスト最適化](/blog/aws-terraform-startup-cost-optimization-finops) にまとめています。

---

## 本番リリース前チェックリスト

出荷前に、私が必ず確認する項目です。

- [ ] **タスクサイズ**は計測値ベース。CPU/メモリの固定ペアで最小を選んだか
- [ ] `platform_version` は `LATEST`（=1.4.0）。`cpuArchitecture`をARM64にできないか検討したか
- [ ] **`awsvpc` + ALB `target_type=ip`**。タスクSGはALBのSGからのみ受ける（0.0.0.0/0を開けていない）
- [ ] プライベートサブネット配置。`assign_public_ip=false`、外向きはNAT/VPCエンドポイント
- [ ] **実行ロールとタスクロールを分離**。タスクロールはサービス専用＆最小権限
- [ ] **シークレットは`secrets[].valueFrom`**で注入。`environment`に平文を置いていない
- [ ] **非root実行 + `readonlyRootFilesystem`**。ECRイメージスキャンを通過
- [ ] イメージは**CommitSHAタグ**（`latest`非依存）。`versionConsistency`が効いている
- [ ] **ローリング更新 + `deployment_circuit_breaker {rollback=true}`** を有効化
- [ ] **SIGTERMハンドリング**を実装し`drainMs < stopTimeout`。`initProcessEnabled: true`
- [ ] `healthCheck`に`startPeriod`、サービスに`health_check_grace_period_seconds`
- [ ] **Application Auto Scaling**（ターゲット追跡）で`min/max`と非対称クールダウン設定
- [ ] **Container Insights**有効、構造化ログ＋相関ID、`enable_execute_command`でブレークグラス可
- [ ] コスト：**Spot + capacity provider strategy**（base/weight）／Savings Plansの層分け

---

## まとめ：Fargateは"サーバーを消して本番品質に集中する"ための道具

ECS on Fargateの本質は、**サーバー管理という最大のコスト（人件費）を消し、本番品質そのものに集中できる**ことです。そのために本稿で押さえた要点は4つでした。

1. **設計**：CPU/メモリは固定ペア。計測してright-sizingし、`awsvpc` + ALB(`target_type=ip`)で正しく繋ぐ。
2. **デプロイ**：ローリング更新＋**サーキットブレーカーで自動ロールバック**。版の一貫性をダイジェストで担保。
3. **回復性**：**SIGTERMを握って`stopTimeout`内に綺麗に終わる**。冪等性と両輪で取りこぼし・二重処理を防ぐ。
4. **安全とコスト**：**実行ロールとタスクロールを分離**し最小権限。ARM64・Spot・Savings Plansで従量課金を使った分だけに寄せる。

私はこの型で、221本のエンドポイントを持つ受賞SaaSと、二重課金0件の決済基盤を、少人数で本番運用してきました。**一人 × 生成AI**でも、公式ドキュメントに忠実な型を崩さなければ、世界最高峰の堅牢さは再現できます。あなたのプロダクトのコンテナ基盤を、速く・安く・安全に本番へ載せたいときは、ぜひご相談ください。
