# ECS on Fargate コスト最適化完全ガイド：料金モデル理解からGraviton・Fargate Spot・Savings Plansまで

> ECS on Fargateの料金モデルを正確に分解し、right-sizing・ARM64(Graviton)・Fargate Spot・Compute Savings Plansを効く順に適用するFinOps実践ガイド。Terraform付き。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: AWS, ECS, Fargate, コスト最適化, Fargate Spot, Graviton, Savings Plans, FinOps
- URL: https://tomodahinata.com/blog/aws-ecs-fargate-cost-optimization-spot-graviton-savings-plans-guide

## 要点

- Fargateの課金はvCPU秒＋メモリ秒（最低1分）で、使用率でなく割り当て量に対して発生するため、right-sizingが最初に効く最大のレバーになる
- CPU/メモリは固定ペアなので「片方だけ上げる」は不可。Container Insights / Compute Optimizerで実使用を計測してから最小ペアへ落とす
- ARM64(Graviton)はタスク定義のcpuArchitecture一行とマルチアーキビルドだけで切り替えられ、AWSはx86比で価格性能比の改善をうたう（割引率はAWSの主張値）
- Fargate Spotは2分前SIGTERM警告付きで中断される代わりに大幅割引。容量プロバイダ戦略のbase/weightでオンデマンドの最小ベースを守りつつSpotでスケールアウト分を安く調達できる
- Compute Savings Plansは常時稼働のベースラインを1年/3年コミットで単価削減。Spot・オンデマンドと層分けして全帯域をカバーするのがFinOpsの定石

---

「Fargateはサーバー管理が要らない。でもコストが読めない」——この不安は正当です。EC2なら「インスタンス時間 × 台数」で概算できますが、Fargateは割り当てたvCPUとメモリに対して秒単位で課金が走るため、タスク数・サイズ・稼働時間の三変数がすべて絡みます。しかし裏を返せば、**最適化の変数が整理されている**ということでもあります。

私は経済産業大臣賞を受賞した木材流通SaaS（`API Gateway → NLB → ALB → ECS on Fargate`、221エンドポイント）を少人数で本番運用してきました。コンテナ基盤のコストは「何にいくら払っているか」を可視化しないと改善できません。この記事はその可視化→削減のプロセスを、**料金モデルの正確な理解**から出発して**right-sizing → ARM64(Graviton) → Fargate Spot → Savings Plans**の順に体系化します。

[ECS on Fargateの本番構築の全体像](/blog/aws-ecs-fargate-production-guide)はピラー記事を参照してください。本稿は**コスト最適化の専論**です。

---

## なぜ「効く順」があるか：最適化のロジック

コスト最適化は手あたり次第に試すと効果が測れません。Fargateの場合、次の順序に従うと**後の手が乗算で効く**ようになります。

| 段階 | 手法 | 効く理由 |
|------|------|---------|
| 1 | Right-sizing | 過剰な割り当てを削る。後の全割引の**分母**が小さくなる |
| 2 | ARM64(Graviton) | 同一タスクサイズでも単価が下がる（常時稼働ほど効く） |
| 3 | Fargate Spot | 中断耐性ワークロードを大幅割引で実行 |
| 4 | Compute Savings Plans | 常時稼働ベースラインを長期コミットで単価削減 |

right-sizingで無駄を消してからSpotやSPを適用するのが正道です。過剰サイズのままSpotを使っても、割引が大きくなっても絶対コストは高くなりがちです。

---

## 料金モデルの分解：何に課金されているか

### 課金の単位

Fargateの課金はシンプルです（[公式料金ページ](https://aws.amazon.com/fargate/pricing/)）。

- **タスクレベルで割り当てたvCPU × 秒数**（vCPU-second）
- **タスクレベルで割り当てたメモリ × 秒数**（GB-second）
- 最低課金時間：**1分**（タスクが1秒で終わっても60秒分）
- エフェメラルストレージ：**20 GBまで無料**、超過分は別途GB-月で課金（最大200 GB）

**使用率は課金に無関係**です。1 vCPU/2 GBを割り当てたタスクが実際にCPU 5%しか使っていなくても、課金は1 vCPU/2 GBの秒数に対して発生します。EC2（インスタンス時間課金）と混同しないことが最初の理解です。

### CPU/メモリは固定ペア

Fargateのタスクサイズは**自由な組み合わせではなく、決められたペアからしか選べません**（[公式ドキュメント](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+） |

**落とし穴**：「メモリは512 MiBで十分だが、CPUは1 vCPU欲しい」場合でも、`1024 CPU`を選んだ瞬間にメモリは最低**2 GB**から始まります。「片方を上げると相方も上がる」前提で計測から入ることが必須です。

### EC2との課金構造の違い

| 観点 | Fargate | EC2（オンデマンド） |
|------|---------|----------------|
| 課金対象 | 割り当てたvCPU・メモリの秒数 | インスタンス起動時間（使用率無関係） |
| 使用率との関係 | 関係ない（5%使用でも100%使用でも同額） | 関係ない（インスタンス時間固定） |
| アイドル時 | タスクを落とせばゼロ | インスタンス停止しないと課金継続 |
| 最適化の方向 | 割り当てサイズを最小化 → 台数を最小化 | ビンパッキング + RI/SP |
| 管理コスト | 不要（人件費がゼロ） | OS/AMI/パッチ管理が発生 |

Fargateの「サーバー管理コストがゼロ」という価値は、単純な時間単価比較では見えません。**人件費まで含めたTotal Cost of Ownership（TCO）**で比較するのが正当です。常時CPU 80%超で大量台数を回す重いバッチ群でなければ、たいていFargate有利になります。

---

## Right-sizing：計測して最小ペアへ

### なぜ最初にやるか

Right-sizingはすべての割引手法の**乗数**に作用します。タスクを2 vCPU/4 GBから1 vCPU/2 GBへ縮小すると、Spot割引もSavings Plans割引も、より小さい元本に掛かります。後で割引を積んでも大きなサイズのままでは絶対コストが高どまりします。

### 計測ツール

**Container Insights**（クラスタ設定で`containerInsights: enhanced`）を有効にすると、タスク・コンテナ単位でCPU/メモリの実使用時系列が取れます。CloudWatch Metricsから「過去30日の99パーセンタイル使用率」を確認し、それに**20〜30%のヘッドルーム**を加えたサイズが最初の目標ペアです。

**AWS Compute Optimizer**はFargate対応しており、過去14日間のメトリクスを解析して「このタスクは現状オーバープロビジョニング」「推奨はXXX CPU/YYY GB」と示してくれます。判断の出発点として活用し、**人間が固定ペア制約と突き合わせて最終決定**するのが現実的なワークフローです。

### エフェメラルストレージは見落としやすいコスト

既定の20 GBは無料ですが、Dockerビルドキャッシュや一時ファイルが積み上がって不要に拡張している場合があります。タスク定義の`ephemeralStorage.sizeInGiB`を確認し、実際に必要な量だけに絞ります。ビルド成果物の永続化はS3へ、ランタイムキャッシュは最小化するのが原則です。

---

## ARM64(Graviton)：単価を下げる最も簡単な手

### Gravitonとは

AWS Graviton（ARM64アーキテクチャ）プロセッサは、AWSが設計したARM系チップです。Fargateでは**タスク定義の一行**で切り替えられます。AWSはGravitonにより価格性能比が改善する（最大約40%）とうたっています——本稿ではあくまで「AWSの主張値」として引用します。実際のワークロードでどう出るかは計測に依存します。

```json
{
  "runtimePlatform": {
    "cpuArchitecture": "ARM64",
    "operatingSystemFamily": "LINUX"
  }
}
```

Fargate料金表でもARM64（Graviton）はx86（X86_64）より低い単価が設定されています。同じvCPU/メモリサイズでもARM64を選ぶだけで単価が下がるため、**イメージのマルチアーキビルドさえ済ませれば、常時稼働サービスほどそのまま原価改善になります**。

### どんなワークロードで効くか

- **HTTPサービス（Node.js/Go/Python/Java）**: ARMに最適化されたランタイムが揃っており移行しやすい
- **データ処理バッチ**: CPUバウンドな処理ほど単価差が効く
- **ステートレスワーカー**: 依存ライブラリにネイティブバイナリが混じっていなければ素直に移行できる

注意が要るのは、**x86専用のネイティブバイナリ**（一部のCライブラリ、ベンダー提供のクローズドバイナリ等）を使うケースです。依存関係を洗い出してARM64ビルドが存在するか確認します。

### マルチアーキビルド（buildx）

ARM64で動かすには、**linux/arm64向けのDockerイメージ**が必要です。DockerのbuildxとBuildKitでマルチプラットフォームイメージを一度に作れます。

```bash
# QEMU エミュレーションのセットアップ（CI 環境で一度だけ）
docker run --privileged --rm tonistiigi/binfmt --install all

# linux/amd64 と linux/arm64 の両対応イメージをビルドして ECR へ push
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag 111122223333.dkr.ecr.ap-northeast-1.amazonaws.com/web-api:${COMMIT_SHA} \
  --push \
  .
```

ECRはマルチアーキテクチャマニフェストをサポートしているため、タグは一つのままで、Fargateが`ARM64`を指定していれば自動的にARM64レイヤーが取得されます。

GitHub Actionsで組む場合のスニペットです。

```yaml
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build and push multi-arch image
  uses: docker/build-push-action@v6
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    tags: ${{ env.ECR_REGISTRY }}/web-api:${{ github.sha }}
    push: true
    cache-from: type=gha
    cache-to: type=gha,mode=max
```

`cache-from: type=gha`によるキャッシュが効くと、マルチアーキビルドのCI時間増加を大きく抑えられます。

### Graviton移行の判断フロー

```text
依存ライブラリにARM64非対応のネイティブバイナリがあるか？
  → Yes: そのライブラリのARM64版を探す／迂回策を検討
  → No:  ローカルで linux/arm64 コンテナを動かしてスモークテスト
           → テスト通過: タスク定義をARM64に変更してステージング検証
              → 問題なし: 本番切り替え（ローリング更新で無停止）
```

---

## Fargate Spot：中断耐性ワークロードを大幅割引で

### Fargate Spotとは

Fargate Spotは、AWSのスペア容量（使われていないFargate基盤）を活用する仕組みです。AWSが容量を必要とした場合に**2分前の警告つきで中断**されるリスクと引き換えに、通常より大幅に低い料金で実行できます（割引率は変動します。AWSや一般の公表情報では大幅な割引として紹介されています）。

中断の仕組みは[公式](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-capacity-providers.html)の通りです。

1. AWSがスポット容量を回収する際、対象タスクに **EventBridgeのタスク状態変更イベント**（`TASK_STATE_CHANGE`）が飛ぶ
2. コンテナへ **`SIGTERM`** が送られる
3. **2分後**にタスクが強制終了される
4. サービスの場合、スケジューラは空き容量を使って別タスクの起動を試みる

グレースフルシャットダウン（SIGTERMを受けて処理を終わらせる）を実装済みであれば、2分は多くのワークロードで十分な猶予です。[本番運用ガイド](/blog/aws-ecs-fargate-production-guide)でSIGTERMハンドラの実装を詳述していますが、Spot利用の前提はこの実装が完成していることです。

### Spotに向くワークロード・向かないワークロード

| ワークロード | Spotへの適性 | 理由 |
|------------|------------|------|
| 日次バッチ・定期レポート | 向く | 中断されても次の実行で回収できる |
| SQS駆動の非同期ワーカー | 向く | メッセージは再配信される（冪等設計前提） |
| 開発・ステージング環境 | 向く | 中断してもビジネスインパクトが低い |
| 負荷試験タスク | 向く | 一時的な大量実行にコスト効率が高い |
| リアルタイムHTTP API（desiredCount=2以下） | 向かない | 中断が直接ユーザー影響になる |
| ステートフルなDB移行タスク | 向かない | 中断で中途半端な状態が残る可能性 |
| コミット済みSLAのある処理 | 向かない | 中断確率が品質保証を複雑にする |

SQS駆動ワーカーでSpotを使う場合の重要な前提は**冪等性**です。Spot中断でタスクがSIGKILLを受けた場合、処理中のメッセージはvisibility timeoutの期限切れ後に再配信されます。「同じメッセージを2回処理しても副作用が同じ」設計がないとSpotは危険です。

### 容量プロバイダ戦略：base と weight の設計

ECSサービスでSpotとオンデマンドを混在させるには**容量プロバイダ戦略**を使います。2つのパラメータを理解することが核心です。

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

```hcl
resource "aws_ecs_service" "worker" {
  name            = "async-worker"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.worker.arn

  # launch_type は指定しない（容量プロバイダ戦略と排他）
  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    # base = 1: 最低1タスクは必ずオンデマンドで確保（Spot枯渇時のフォールバック）
    base   = 1
    weight = 1
  }
  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    base   = 0
    weight = 4 # base 充足後の追加タスクは SPOT:オンデマンド = 4:1 で調達
  }
}
```

このstrategyは「**最初の1タスクはオンデマンドで保証し、それを超えるスケールアウト分の約80%をSpotから調達**」を意味します。Spot枯渇やサービス中断イベントが来ても、最低1タスクはオンデマンドが生き残ります。

ベースに置くタスク数は、サービスの最低限の可用性要件に合わせます。冗長性（2タスク以上）が必要なHTTPサービスに中断耐性を持たせる場合は`base = 2`にし、それを超えるバーストをSpotにするパターンもあります。

### Spot中断のモニタリング

Spot中断は`EventBridge`の`ECS Task State Change`イベントで取得できます。`stopReason: "Spot interruption"`でフィルタし、CloudWatch AlarmやSlack通知に繋ぐと運用可視性が上がります。

```json
{
  "source": ["aws.ecs"],
  "detail-type": ["ECS Task State Change"],
  "detail": {
    "lastStatus": ["STOPPED"],
    "stopCode": ["SpotInterruption"]
  }
}
```

このイベントにLambdaを繋いで集計すれば、「過去7日間のSpot中断率」などの指標を把握できます。中断率が高い（容量逼迫）リージョン・AZでは、`weight`配分を見直すか、オンデマンド比率を上げることを検討します。

---

## Compute Savings Plans：常時稼働ベースラインの単価削減

### Savings Plansとは

Compute Savings Plansは、**1年または3年の一定コンピュート使用量（$/時）をコミットすること**で、オンデマンド料金より低い単価を受ける仕組みです（[公式](https://docs.aws.amazon.com/savingsplans/latest/userguide/what-is-savings-plans.html)）。Compute SPはFargate・Lambda・EC2をカバーするため、ワークロードがどの形態に移っても割引が適用され続けます。

重要な特性：

- コミットは「金額（$/時）」であり、特定のリージョン・サービス・タスクサイズに縛られない
- コミット量を超えた使用分はオンデマンド料金にフォールバック
- 1年コミットより3年コミットの方が割引率が大きい（AWSの主張値）
- 前払い（All Upfront / Partial Upfront / No Upfront）を選べる

### ベースライン・オンデマンド・Spotの層分け

Savings Plansを最も効率的に使うには、**常時稼働するコンピュートの"ベースライン"をSPで確保**し、その上のバーストをオンデマンド・Spotで受けます。

```text
コスト層の設計（例：本番HTTPサービス）
────────────────────────────────────────
  高負荷バースト層    → Fargate Spot（中断耐性があれば）
  定常バースト層      → Fargate オンデマンド
  常時稼働ベース層    → Compute Savings Plans で単価削減
────────────────────────────────────────
```

ベースラインのコミット量を計算するには、過去30〜90日の**最低コンピュート消費量（$/時）**をAWS Cost Explorerで確認し、その90〜95%をコミットします（100%コミットはリスクが高い）。コミット不足はオンデマンドにフォールバックするだけなので、多少保守的でも損はしません。

### ECタイプのSPとコンピュートSPの違い

| 種別 | 対象 | 柔軟性 |
|-----|------|--------|
| EC2 Instance SP | 特定ファミリー・リージョンのEC2のみ | 低（縛りが強い分、割引大） |
| Compute SP | EC2 + Fargate + Lambda（全リージョン） | 高（縛りが少ない分、割引小） |

Fargateを主な対象にするならCompute SPを選びます。将来EC2やLambdaへワークロードが移ってもSPの消費先が変わるだけで無駄になりません。

---

## その他のコスト削減：ログ・不要リソース・タグ

### CloudWatch Logsの保持期間

デフォルトでCloudWatch Logsの保持期間は「無期限」です。本番で高頻度のアクセスログをそのまま流すとログストレージコストが積み上がります。

```hcl
resource "aws_cloudwatch_log_group" "ecs_app" {
  name              = "/ecs/web-api"
  retention_in_days = 30  # 要件に合わせて調整。デフォルト無期限は避ける
}
```

長期保存が必要なログはS3へアーカイブ（CloudWatch Logs → Kinesis Firehose → S3）し、S3のS3-IA / Glacier Instant Retrievalで保持するとコストを大幅に抑えられます。

### FireLensでログをフィルタリング

**FireLens（Fluent Bit サイドカー）**を使うと、CloudWatchに送る前にログをフィルタ・集約できます。ヘルスチェックやデバッグレベルのログをCloudWatch送出から除外し、コスト削減しながらS3には保持する、といった運用が実現できます。

```json
{
  "name": "log-router",
  "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable",
  "essential": true,
  "firelensConfiguration": {
    "type": "fluentbit"
  }
}
```

Fluent BitのFilterで`HEALTH`や`DEBUG`レベルをCloudWatch向けから除いてS3へ回す構成は、アクセスログが多い本番サービスで特に有効です。

### 不要タスク・環境の停止

開発・ステージング環境のECSサービスは、業務時間外に`desiredCount = 0`にするだけでコストがゼロになります。

```bash
# 夜間停止（EventBridge Scheduler から Lambda 経由で叩くか、直接 CLI）
aws ecs update-service \
  --cluster dev \
  --service web-api \
  --desired-count 0
```

朝の再起動・夜間停止をEventBridge Schedulerで自動化すると、開発環境のコストを劇的に削減できます。

### タグによるコスト配賦

ECSサービス・タスク定義にタグを付けておくとAWS Cost Explorerでサービス別・環境別の内訳が取れます。

```hcl
resource "aws_ecs_service" "app" {
  # ...
  tags = {
    Environment = "production"
    Service     = "web-api"
    Team        = "platform"
    CostCenter  = "api-platform"
  }
}
```

タグ配賦なしではFargateコスト全体の塊しか見えず、どのサービスが高いかわかりません。[FinOps全体の設計](/blog/aws-terraform-startup-cost-optimization-finops)と組み合わせて、コスト可視化を最初から仕込むことが大切です。

---

## コスト比較表（例）

以下は**完全に例示用の試算**です。実際の料金はリージョン・時期によって異なります。AWSの公式料金ページで最新値を確認してください。

前提（例）：東京リージョン、1 vCPU / 2 GB、720時間/月（常時稼働）、24タスクのスケール実績

| パターン | 構成 | 相対的なコストのイメージ | 備考 |
|--------|------|----------------------|------|
| ベースライン | x86 オンデマンド × 24タスク | 100（基準） | 全タスクがオンデマンド・x86 |
| Graviton のみ | ARM64 オンデマンド × 24タスク | AWSの主張値ではx86より低単価 | cpuArchitecture=ARM64 に変更するだけ |
| Spot混在 | ARM64 オンデマンド × 4 + ARM64 Spot × 20 | Spotの割引が効く分だけ下がる | Spot中断耐性設計が前提 |
| SP + Spot | ARM64 Spot（Spot分） + Compute SP（ベース分） | さらなる単価削減 | SPのベースラインコミットが前提 |

> **注意**：上記の「相対的なコストのイメージ」は構造的な傾向を示すためのもので、実際の数値を保証するものではありません。Savings Plansの具体的な割引率はAWS Cost Explorerの「Savings Plans推奨」から取得し、コミットの意思決定に使ってください。

---

## Terraform：容量プロバイダ戦略の完全スニペット

本番サービス（HTTP API）でGraviton + オンデマンドの組み合わせ、バッチワーカーでGraviton + Spotを使う構成です。

```hcl
# ── クラスタ ───────────────────────────────────────────────
resource "aws_ecs_cluster" "main" {
  name = "prod"
  setting {
    name  = "containerInsights"
    value = "enhanced"
  }
}

# ── タスク定義（ARM64 / Graviton） ────────────────────────
resource "aws_ecs_task_definition" "api" {
  family                   = "web-api"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "512"
  memory                   = "1024"

  runtime_platform {
    cpu_architecture        = "ARM64"   # Graviton に寄せる
    operating_system_family = "LINUX"
  }

  execution_role_arn = aws_iam_role.exec.arn
  task_role_arn      = aws_iam_role.task.arn

  container_definitions = jsonencode([{
    name      = "app"
    image     = "${aws_ecr_repository.api.repository_url}:${var.image_tag}"
    essential = true
    portMappings = [{ containerPort = 8080, protocol = "tcp" }]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"         = aws_cloudwatch_log_group.api.name
        "awslogs-region"        = data.aws_region.current.name
        "awslogs-stream-prefix" = "app"
      }
    }
    stopTimeout = 60
  }])
}

# ── 本番 HTTP サービス：base=2 オンデマンド保証 ─────────────
resource "aws_ecs_service" "api" {
  name            = "web-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = 4

  # launch_type は書かない（capacity_provider_strategy と排他）
  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    base              = 2  # 最低2タスクはオンデマンドで常時確保
    weight            = 1
  }
  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    base              = 0
    weight            = 1  # desiredCount > 2 の追加タスクは SPOT:OD = 1:1 で均等調達
  }

  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.api.arn
    container_name   = "app"
    container_port   = 8080
  }

  health_check_grace_period_seconds = 30
}

# ── バッチワーカー：ほぼ全量 Spot ─────────────────────────
resource "aws_ecs_task_definition" "worker" {
  family                   = "async-worker"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "1024"
  memory                   = "2048"

  runtime_platform {
    cpu_architecture        = "ARM64"
    operating_system_family = "LINUX"
  }

  execution_role_arn = aws_iam_role.exec.arn
  task_role_arn      = aws_iam_role.worker_task.arn

  container_definitions = jsonencode([{
    name      = "worker"
    image     = "${aws_ecr_repository.worker.repository_url}:${var.image_tag}"
    essential = true
    stopTimeout = 110  # Spot の 2 分前 SIGTERM に対して最大限使う
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"         = aws_cloudwatch_log_group.worker.name
        "awslogs-region"        = data.aws_region.current.name
        "awslogs-stream-prefix" = "worker"
      }
    }
  }])
}

resource "aws_ecs_service" "worker" {
  name            = "async-worker"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.worker.arn
  desired_count   = 2

  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    base              = 1  # フォールバック用に最低 1 はオンデマンド
    weight            = 1
  }
  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    base              = 0
    weight            = 4  # 追加タスクの 80% を Spot から調達
  }

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.worker_task.id]
    assign_public_ip = false
  }
}

# ── CloudWatch Logs（保持期間を明示する） ─────────────────
resource "aws_cloudwatch_log_group" "api" {
  name              = "/ecs/web-api"
  retention_in_days = 30
}

resource "aws_cloudwatch_log_group" "worker" {
  name              = "/ecs/async-worker"
  retention_in_days = 14  # ワーカーは短め
}
```

`stopTimeout = 110`（バッチワーカー）はFargate Spotの2分前SIGTERM通知を最大限活用するための値です。`SIGTERM`から`SIGKILL`まで110秒使えるため、SQSのメッセージ処理やDB書き込みを完了させる猶予があります。`stopTimeout`の上限は120秒です。

---

## コスト最適化チェックリスト

コスト最適化を本番に適用する前に確認する項目です。

### 計測・可視化
- [ ] Container Insightsが`enhanced`で有効になっているか
- [ ] 過去30日の**P99 CPU使用率・P99メモリ使用率**をタスク別に確認したか
- [ ] AWS Compute Optimizerの推奨を確認したか
- [ ] ECSサービス・タスク定義に`Environment`/`Service`/`Team`タグが付いているか
- [ ] AWS Cost ExplorerでECSコストをサービス別に分解できているか

### Right-sizing
- [ ] タスクサイズは計測値 + 20〜30%ヘッドルームで決めたか（憶測で決めていないか）
- [ ] CPU/メモリの固定ペア制約を確認した上で最小ペアを選んだか
- [ ] `ephemeralStorage`の拡張が本当に必要か確認したか（既定20 GBで不要なら指定しない）

### ARM64(Graviton)
- [ ] 依存ライブラリ・ベースイメージのARM64対応を確認したか
- [ ] `docker buildx`でlinux/arm64のビルドが通るか確認したか
- [ ] ステージング環境でARM64イメージを動かしてE2Eテストが通るか確認したか
- [ ] タスク定義の`runtimePlatform.cpuArchitecture`を`ARM64`に変更したか

### Fargate Spot
- [ ] 対象ワークロードが中断耐性を持つか（バッチ・ワーカー・開発環境か）
- [ ] SIGTERMハンドラを実装済みか（`stopTimeout`内に処理が終わるか）
- [ ] SQSワーカーの場合、冪等性設計が完了しているか（再配信で二重処理しないか）
- [ ] `capacity_provider_strategy`に`FARGATE`の`base`を設定してフォールバックを確保したか
- [ ] Spot中断イベント（EventBridge）を監視に載せたか

### Compute Savings Plans
- [ ] AWS Cost ExplorerでFargateの過去90日間の最低使用量（$/時）を確認したか
- [ ] SP推奨ツールで最適なコミット量を計算したか（最低消費量の90〜95%をコミット）
- [ ] EC2 Instance SPではなくCompute SPを選んでいるか（Fargate + 将来の柔軟性）
- [ ] SPのコミット期間（1年/3年）が事業計画と合っているか確認したか

### その他
- [ ] CloudWatch Logsの保持期間を明示したか（デフォルト無期限を避ける）
- [ ] 長期保存ログはS3アーカイブの設計があるか
- [ ] 開発・ステージング環境の夜間停止（`desiredCount=0`）を自動化したか

---

## まとめ：コストは"効く順"に構造的に削る

ECS on Fargateのコスト最適化は、**計測→right-sizing→ARM64→Spot→SP**という一方向の積み重ねです。

1. **計測なしの最適化は最適化ではない**。Container Insightsで実使用を見て、固定ペア制約の中で最小サイズを選ぶ。
2. **ARM64（Graviton）への切り替え**は、マルチアーキビルドさえ済ませれば常時稼働サービスほど効く。AWSは価格性能比の改善をうたっているが、計測で確認すること。
3. **Fargate Spot**は中断耐性の設計（グレースフルシャットダウン + 冪等性）が整っていることが前提。設計が整っていない状態で使うと、コスト削減より障害発生コストの方が高くつく。
4. **Compute Savings Plans**は常時稼働ベースラインのコミット。SPコミットをベースに、バーストをオンデマンド・Spotで受ける層分けが基本型。

木材流通SaaSでは `API Gateway → NLB → ALB → ECS on Fargate（221エンドポイント）` を少人数で運用しており、コスト最適化はグレースフルシャットダウンや冪等性の実装と一体で進めてきました。FinOpsはインフラの設計と切り離せません——[AutoScaling・SQSワーカーの設計](/blog/aws-ecs-fargate-auto-scaling-target-tracking-sqs-worker-guide)、[FargateとLambdaの選択フレームワーク](/blog/aws-ecs-fargate-vs-lambda-vs-app-runner-compute-selection-guide)も合わせて参照してください。

コンテナ基盤のコストと品質を両立させたい場合は、ぜひご相談ください。
