# ECS on Fargate オートスケーリング完全ガイド：ターゲット追跡・ステップ・SQSバックログパターンを本番品質で設計する

> ECS on Fargateのオートスケーリングを体系化。ターゲット追跡・ステップ・スケジュールの使い分けから、SQSバックログ・パー・タスクによるワーカースケーリングのカスタムメトリクス実装まで、Terraformと実コードで解説。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: AWS, ECS, Fargate, オートスケーリング, SQS, Application Auto Scaling, 可観測性, コスト最適化
- URL: https://tomodahinata.com/blog/aws-ecs-fargate-auto-scaling-target-tracking-sqs-worker-guide

## 要点

- Application Auto ScalingはECSサービスのdesiredCountを動かす層で、スケーラブルターゲット・ポリシー・クールダウンの3要素を正しく設定することが安定の鍵
- ターゲット追跡は目標値を宣言するだけで増減を自動制御。scale_out_cooldownを短く・scale_in_cooldownを長くする非対称設定がフラッピング防止の定石
- SQSワーカーのスケーリングにCPU使用率は不適。AWS推奨の『バックログ・パー・タスク』= ApproximateNumberOfMessagesVisible ÷ RunningTaskCountをカスタムメトリクスで発行してターゲット追跡する
- min_capacity=0のアイドル・ゼロ構成はコスト最適化に有効だが、初回スケールアウトの起動遅延（数十秒）を許容レイテンシと照合して使うかどうかを決める
- スケールインとグレースフルシャットダウン（SIGTERM→stopTimeout→SIGKILL）は必ず連動設計する。処理中タスクをSIGKILLで殺さないためにconnection_draining・stopTimeout・冪等性の三点セットが必須

---

「desiredCountを手で変えるのは限界がある。でもオートスケールの設定を雑にすると、逆にフラッピングして不安定になる」——ECS on Fargateを本番に持ち込むとき、必ずこの局面が来ます。

私は[決済基盤（本番二重課金0件）](/case-studies/payment-platform-reliability)でSQS駆動の冪等ワーカーをFargate上で運用し、木材流通B2B SaaSでは`API Gateway → NLB → ALB → ECS on Fargate`の上に221本のAPIエンドポイントを本番稼働させてきました。どちらも「速く増やし、慎重に減らす」という非対称スケーリングの考え方と、ワークロードに合ったメトリクス選択が安定の核心でした。

本稿は[ECS on Fargate 本番運用ガイド](/blog/aws-ecs-fargate-production-guide)の続編として、**オートスケーリング設計に特化**します。HTTPサービスとSQSワーカーという2つの代表的なワークロードで、それぞれ最適な設計を実コード付きで体系化します。

---

## なぜ手動 desiredCount では限界があるか

手動で`desired_count`を調整する運用には3つの根本的な限界があります。

1. **反応が遅い**：人間がアラートに気づき、Terraformを適用するまでにスパイクは終わっているか、すでにSLAを割っているかのどちらかです。
2. **縮小を忘れる**：増やしたタスクを減らし損ねると、コストが静かに膨らみ続けます。
3. **SQS長が読めない**：キューにメッセージが溜まっていても、CPUは動いていないため気づけません。

Application Auto Scalingは「計測値が閾値を超えたらdesiredCountを動かす」という仕組みをポリシーとして宣言し、ECSサービスコントローラに委譲します。あなたがやることは**目標値と上下限を決めるだけ**です。

---

## 仕組み：Application Auto Scaling が ECS の desiredCount を動かす

ECSのオートスケーリングは、**AWS Application Auto Scaling**というサービスが担います。これは単独のサービスで、ECS以外にDynamoDB・Aurora・Lambda・SageMakerなども同じAPIで扱います。ECS固有の話は3つの要素に整理されます。

### スケーラブルターゲット

まず「何をスケールするか」を登録します。これがスケーラブルターゲットです。

```hcl
resource "aws_appautoscaling_target" "app" {
  service_namespace  = "ecs"                         # ECS専用の名前空間
  scalable_dimension = "ecs:service:DesiredCount"    # 操作するのはdesiredCount
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
  min_capacity       = 2   # 最低タスク数（下限。0にするとアイドルゼロが可能）
  max_capacity       = 20  # 最大タスク数（上限）
}
```

`resource_id`の形式は`service/<cluster_name>/<service_name>`です。スペルミスしてもTerraformのapplyは通ってしまいますが、スケーリングが一切機能しなくなります。**applyした後は`aws application-autoscaling describe-scalable-targets`で実際に登録されたか確認する**のを習慣にしましょう。

### スケーリングポリシー

次に「いつ・どう動かすか」をポリシーで定義します。大きく3種類あります。

| ポリシー種別 | 判断基準 | 主な用途 |
|------------|---------|---------|
| **ターゲット追跡** | 目標メトリクス値を維持 | 定常サービス（CPU・リクエスト数） |
| **ステップスケーリング** | CloudWatchアラームの段階で増減幅を変える | バースト対応・非線形負荷 |
| **スケジュールスケーリング** | 時刻ベースでmin/max/desiredを変更 | 既知のピーク（業務時間・キャンペーン） |

### クールダウン

スケーリングアクション後に次のアクションを抑制する待機時間です。**スケールアウトは短く、スケールインは長く**——これが唯一の定石です。スパイク直後に縮めて再び慌てる「フラッピング」を防ぐためです。

---

## ターゲット追跡（Target Tracking）：目標値を宣言して任せる

最もシンプルで、ほとんどのHTTPサービスはこれで十分です。

### 事前定義メトリクス

ECSサービスに対して使える事前定義メトリクスは3つです（[公式](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-configure-auto-scaling.html)）。

| predefined_metric_type | 計測対象 | いつ使うか |
|------------------------|---------|----------|
| `ECSServiceAverageCPUUtilization` | サービス内タスクのCPU平均使用率（%） | CPUバウンドな処理（演算・エンコード） |
| `ECSServiceAverageMemoryUtilization` | サービス内タスクのメモリ平均使用率（%） | メモリバウンドな処理（大量データ展開） |
| `ALBRequestCountPerTarget` | ALBターゲット1台あたりのリクエスト数 | HTTPトラフィックに線形追従したい |

**選び方の原則**：ボトルネックになっているリソースを使う。分からなければContainer Insightsで実測してから決める。CPU/メモリとリクエスト数を**両方設定するとより安全**（どちらかがトリガーになった時点でスケールアウトされる）。

### 完全な Terraform 例（CPU + ALBRequestCountPerTarget）

```hcl
# --- スケーラブルターゲット（1回定義すれば複数ポリシーを紐付けられる） ---
resource "aws_appautoscaling_target" "app" {
  service_namespace  = "ecs"
  scalable_dimension = "ecs:service:DesiredCount"
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
  min_capacity       = 2
  max_capacity       = 20

  depends_on = [aws_ecs_service.app]  # サービスが先に存在していること
}

# --- CPU ターゲット追跡 ---
resource "aws_appautoscaling_policy" "cpu_tt" {
  name               = "cpu-target-tracking"
  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  # 60%を維持。70〜80%は高すぎてバースト余裕がなくなる
    scale_out_cooldown = 30    # スケールアウトは速く（秒）
    scale_in_cooldown  = 300   # スケールインは慎重に（秒）
    disable_scale_in   = false # スケールインも自動で行う（コスト管理）
  }
}

# --- ALBリクエスト数 ターゲット追跡 ---
resource "aws_appautoscaling_policy" "alb_tt" {
  name               = "alb-request-count-target-tracking"
  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 = "ALBRequestCountPerTarget"
      # ALBとターゲットグループのリソースラベルが必要
      resource_label = "${aws_lb.main.arn_suffix}/${aws_lb_target_group.app.arn_suffix}"
    }
    target_value       = 1000  # タスク1台あたり1000 req/min を目標
    scale_out_cooldown = 30
    scale_in_cooldown  = 300
  }
}
```

`resource_label`は`ALBRequestCountPerTarget`だけで必要な指定です。フォーマットは`<load-balancer-arn-suffix>/<target-group-arn-suffix>`で、`aws_lb`・`aws_lb_target_group`リソースの`arn_suffix`属性で取れます。

### target_value の選び方

- **CPU**：60〜70%が一般的。80%以上に設定するとバーストの余白がなく、スケールアウトが間に合わない。
- **ALBリクエスト数**：ローカルまたはステージング環境でタスク1台あたりの処理可能リクエスト数を計測し、その6〜7割を目標にする。憶測で設定せず**計測ファースト**。

---

## ステップスケーリング：段階的に増減幅を変える

バーストが激しいワークロードや「ちょっとした超過は小幅に、大幅な超過は一気に増やしたい」という非線形な需要には**ステップスケーリング**が適合します。

### ターゲット追跡との使い分け

| 観点 | ターゲット追跡 | ステップスケーリング |
|------|-------------|----------------|
| 設定の複雑さ | 低い（目標値だけ） | 高い（アラーム + ステップ定義） |
| スケール量の制御 | AWS自動計算 | 自分で段階を定義 |
| 向いているケース | 定常的なHTTPサービス | バースト・非線形・精密な制御が必要な場合 |
| 組み合わせ | 単独でOK | ターゲット追跡と共存も可能 |

ステップスケーリングはCloudWatchアラームと連動します。アラームが「ALARM状態」になるとスケールアウト、「OK状態に戻る」ときにスケールインのポリシーを定義します。

```hcl
# CloudWatchアラーム（スケールアウトトリガー）
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
  alarm_name          = "ecs-cpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = 60
  statistic           = "Average"
  threshold           = 70.0
  dimensions = {
    ClusterName = aws_ecs_cluster.main.name
    ServiceName = aws_ecs_service.app.name
  }
  alarm_actions = [aws_appautoscaling_policy.step_out.arn]
}

# ステップスケールアウトポリシー
resource "aws_appautoscaling_policy" "step_out" {
  name               = "step-scale-out"
  policy_type        = "StepScaling"
  service_namespace  = aws_appautoscaling_target.app.service_namespace
  resource_id        = aws_appautoscaling_target.app.resource_id
  scalable_dimension = aws_appautoscaling_target.app.scalable_dimension

  step_scaling_policy_configuration {
    adjustment_type          = "ChangeInCapacity"  # 絶対値で変える（他にPercentChangeInCapacityも可）
    cooldown                 = 60
    metric_aggregation_type  = "Average"

    step_adjustment {
      # CPU 70〜80%: +2タスク
      metric_interval_lower_bound = 0
      metric_interval_upper_bound = 10
      scaling_adjustment          = 2
    }
    step_adjustment {
      # CPU 80%超: +5タスク（バースト対応）
      metric_interval_lower_bound = 10
      scaling_adjustment          = 5
    }
  }
}
```

`metric_interval_lower_bound`と`upper_bound`は、アラームの**閾値からの差分**（ブリーチ量）で指定します。「閾値70%に対してCPUが73%」なら差分は+3——`0〜10`のステップに入ります。

---

## スケジュールスケーリング：ピークを先読みする

毎朝9時の業務開始、月次のキャンペーン、バッチ前のウォームアップなど、**いつ負荷が来るかわかっている**場合はスケジュールスケーリングで先回りします。

```hcl
# 平日9時にスケールアウト（JST = UTC+9、なのでUTCは0時）
resource "aws_appautoscaling_scheduled_action" "scale_up_business_hours" {
  name               = "scale-up-business-hours"
  service_namespace  = aws_appautoscaling_target.app.service_namespace
  resource_id        = aws_appautoscaling_target.app.resource_id
  scalable_dimension = aws_appautoscaling_target.app.scalable_dimension
  schedule           = "cron(0 0 ? * MON-FRI *)"  # UTC 0:00 = JST 9:00

  scalable_target_action {
    min_capacity = 5   # ピーク時の下限を引き上げる
    max_capacity = 30  # ピーク時の上限を広げる
  }
}

# 平日21時に縮小（JST = UTC 12:00）
resource "aws_appautoscaling_scheduled_action" "scale_down_off_hours" {
  name               = "scale-down-off-hours"
  service_namespace  = aws_appautoscaling_target.app.service_namespace
  resource_id        = aws_appautoscaling_target.app.resource_id
  scalable_dimension = aws_appautoscaling_target.app.scalable_dimension
  schedule           = "cron(0 12 ? * MON-FRI *)"  # UTC 12:00 = JST 21:00

  scalable_target_action {
    min_capacity = 2   # 夜間の下限に戻す
    max_capacity = 20  # 夜間の上限に戻す
  }
}
```

スケジュールスケーリングとターゲット追跡は**共存できます**。ピーク時間帯には`min_capacity`を引き上げてウォームな状態を保ち、ターゲット追跡がさらに細かく増減を調整するという組み合わせが本番でよく機能します。

---

## SQS 駆動ワーカーのスケーリング：「バックログ・パー・タスク」パターン

ここからが本稿の核心です。HTTPサービスとは全く異なる設計が必要です。

### なぜ CPU ではダメか

SQSワーカーは「キューにメッセージがあれば処理し、なければ待つ」という構造です。**キューが空でもワーカーは起動したまま待機している**ため、CPU使用率はほぼゼロになります。逆に、大量のメッセージが積まれていてもワーカーがIO待ち主体の処理（外部API呼び出し・DB書き込みなど）をしていれば、CPU使用率は低いままです。

つまり**CPUと処理待ちメッセージ数の間に相関がない**のです。CPU追跡でSQSワーカーをスケールするのは、ガソリン残量ではなくエンジン回転数で燃料補給タイミングを決めるようなものです。

### AWS 推奨：バックログ・パー・タスク

AWSが公式に推奨するパターンは、**「バックログ・パー・タスク（Backlog Per Task）」**です（[Scaling based on Amazon SQS](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-using-sqs-queue.html)、コンセプトはEC2 Auto ScalingのドキュメントですがECS Application Auto Scalingでも同じ原則が適用されます）。

計算式はシンプルです。

```text
バックログ・パー・タスク = ApproximateNumberOfMessagesVisible ÷ RunningTaskCount
```

これを**目標バックログ（Target Backlog）**に向けてターゲット追跡します。目標バックログの設定値は、許容レイテンシから逆算します。

### 目標バックログの算出（例）

以下は架空の例として、計算方法を示します。実際の値はワークロードの計測から求めてください。

```text
例（illustrative values — 実計測値ではありません）:
  - 1メッセージあたりの平均処理時間：5秒
  - タスク1台あたりの同時処理数（concurrency）：1（シングルスレッドワーカー）
  - タスク1台が1分間に処理できるメッセージ数：60秒 ÷ 5秒 = 12件/分
  - 許容メッセージ滞留時間（最大レイテンシ目標）：1分

  → 目標バックログ = 許容レイテンシ(秒) ÷ 1メッセージ処理秒
                   = 60秒 ÷ 5秒
                   = 12

  つまり「タスク1台あたり最大12件のバックログを目標に追跡する」と設定する。
  バックログが36件あればタスクを3台に、120件なら10台に増やす、という挙動になる。
```

### 0除算問題の扱い

RunningTaskCountが0のとき（アイドルでmin_capacity=0に縮んでいる状態）に除算するとゼロ除算になります。この場合、**メッセージ数そのものを目標値として使う**か、**RunningTaskCountを最低1として扱う**かのどちらかで対処します。カスタムメトリクスの発行ロジックで吸収するのが最も安全です。

### カスタムメトリクスの発行

SQS関連のメトリクス（`ApproximateNumberOfMessagesVisible`）はCloudWatchに自動で届きますが、RunningTaskCountとの比率（バックログ・パー・タスク）は**自分で計算してCloudWatchカスタムメトリクスとして発行**する必要があります。

**TypeScript（EventBridge Schedulerで定期実行するLambda）の例**：

```ts
import {
  CloudWatchClient,
  PutMetricDataCommand,
} from "@aws-sdk/client-cloudwatch";
import {
  SQSClient,
  GetQueueAttributesCommand,
} from "@aws-sdk/client-sqs";
import {
  ECSClient,
  DescribeServicesCommand,
} from "@aws-sdk/client-ecs";

const cw = new CloudWatchClient({});
const sqs = new SQSClient({});
const ecs = new ECSClient({});

const QUEUE_URL = process.env.QUEUE_URL!;
const CLUSTER = process.env.ECS_CLUSTER!;
const SERVICE = process.env.ECS_SERVICE!;
const NAMESPACE = "Custom/ECS";
const METRIC_NAME = "BacklogPerTask";

export async function handler(): Promise<void> {
  // 1) SQS の可視メッセージ数を取得
  const sqsRes = await sqs.send(
    new GetQueueAttributesCommand({
      QueueUrl: QUEUE_URL,
      AttributeNames: ["ApproximateNumberOfMessages"],
    }),
  );
  const visibleMessages = parseInt(
    sqsRes.Attributes?.ApproximateNumberOfMessages ?? "0",
    10,
  );

  // 2) ECS の Running タスク数を取得
  const ecsRes = await ecs.send(
    new DescribeServicesCommand({ cluster: CLUSTER, services: [SERVICE] }),
  );
  const runningCount = ecsRes.services?.[0]?.runningCount ?? 0;

  // 3) バックログ・パー・タスクを計算（0除算を安全に処理）
  //    runningCount=0 のときはメッセージ数をそのまま発行し、
  //    スケールアウトが起動するようにする
  const backlogPerTask =
    runningCount > 0 ? visibleMessages / runningCount : visibleMessages;

  console.log({ visibleMessages, runningCount, backlogPerTask });

  // 4) CloudWatch カスタムメトリクスへ発行
  await cw.send(
    new PutMetricDataCommand({
      Namespace: NAMESPACE,
      MetricData: [
        {
          MetricName: METRIC_NAME,
          Value: backlogPerTask,
          Unit: "Count",
          Dimensions: [
            { Name: "ClusterName", Value: CLUSTER },
            { Name: "ServiceName", Value: SERVICE },
          ],
        },
      ],
    }),
  );
}
```

このLambdaを**EventBridge Scheduler で1分ごとに**実行します。CloudWatchのカスタムメトリクスの解像度は最小1分なので、これで十分です。

**Bash（シェルスクリプトで手動確認・デバッグ用）**：

```bash
#!/usr/bin/env bash
set -euo pipefail

QUEUE_URL="${QUEUE_URL:?QUEUE_URL not set}"
CLUSTER="${ECS_CLUSTER:?ECS_CLUSTER not set}"
SERVICE="${ECS_SERVICE:?ECS_SERVICE not set}"
REGION="${AWS_REGION:-ap-northeast-1}"

# SQS 可視メッセージ数
VISIBLE=$(aws sqs get-queue-attributes \
  --queue-url "$QUEUE_URL" \
  --attribute-names ApproximateNumberOfMessages \
  --query 'Attributes.ApproximateNumberOfMessages' \
  --output text \
  --region "$REGION")

# ECS Running タスク数
RUNNING=$(aws ecs describe-services \
  --cluster "$CLUSTER" \
  --services "$SERVICE" \
  --query 'services[0].runningCount' \
  --output text \
  --region "$REGION")

if [[ "$RUNNING" -gt 0 ]]; then
  BACKLOG=$(echo "scale=2; $VISIBLE / $RUNNING" | bc)
else
  BACKLOG="$VISIBLE"
fi

echo "visible=$VISIBLE running=$RUNNING backlog_per_task=$BACKLOG"

# CloudWatch に発行
aws cloudwatch put-metric-data \
  --namespace "Custom/ECS" \
  --metric-name "BacklogPerTask" \
  --value "$BACKLOG" \
  --unit "Count" \
  --dimensions "Name=ClusterName,Value=$CLUSTER" "Name=ServiceName,Value=$SERVICE" \
  --region "$REGION"
```

### Terraform：カスタムメトリクスを使ったターゲット追跡

```hcl
resource "aws_appautoscaling_target" "worker" {
  service_namespace  = "ecs"
  scalable_dimension = "ecs:service:DesiredCount"
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.worker.name}"
  min_capacity       = 0   # アイドル時はゼロに縮む（コスト最適化）
  max_capacity       = 50  # 上限は処理能力と許容コストから設定
}

resource "aws_appautoscaling_policy" "worker_backlog" {
  name               = "sqs-backlog-per-task"
  policy_type        = "TargetTrackingScaling"
  service_namespace  = aws_appautoscaling_target.worker.service_namespace
  resource_id        = aws_appautoscaling_target.worker.resource_id
  scalable_dimension = aws_appautoscaling_target.worker.scalable_dimension

  target_tracking_scaling_policy_configuration {
    customized_metric_specification {
      metric_name = "BacklogPerTask"
      namespace   = "Custom/ECS"
      statistic   = "Average"
      dimensions {
        name  = "ClusterName"
        value = aws_ecs_cluster.main.name
      }
      dimensions {
        name  = "ServiceName"
        value = aws_ecs_service.worker.name
      }
    }
    target_value       = 12    # 上の計算例で求めた目標バックログ
    scale_out_cooldown = 60    # キュー急増への応答を速く
    scale_in_cooldown  = 300   # 処理しきるまでの余裕を持って縮小
    disable_scale_in   = false
  }
}
```

### min_capacity=0 の起動遅延

`min_capacity=0`（アイドルゼロ）にすると、タスクがゼロの状態からスケールアウトするとき、Fargateのタスク起動時間（ENI割り当て・イメージpull・アプリ起動込みで概ね**数十秒**）が加わります。この「コールドスタート」期間はメッセージが処理されないので、**許容レイテンシに対して起動時間が無視できない場合はmin_capacity=1以上にする**ことを検討してください。決済基盤のワーカーでは厳しいレイテンシSLAがあったため`min_capacity=1`を維持しました。

---

## アンチパターンとトラブルシューティング

### フラッピング（増減の繰り返し）

**症状**：スケールアウト直後にスケールインし、またスケールアウトするサイクルが止まらない。

**原因**：`scale_in_cooldown`が短すぎる。スケールアウト後の負荷が落ち着く前にスケールインが走り、また負荷が上がる。

**対処**：`scale_in_cooldown`を`scale_out_cooldown`の数倍（最低300秒）に設定する。`disable_scale_in = true`を一時的に使うのもフラッピング診断には有効ですが、コスト管理ができなくなるため本番では使わない。

### ヘルスチェック猶予を忘れる

スケールアウトで新しいタスクが起動し、ALBに登録されてもアプリの初期化が終わっていない間はヘルスチェックが失敗します。`health_check_grace_period_seconds`を設定しないと、起動中のタスクが即座に不健全扱いで落とされ、**スケールアウトがデスマーチになります**。

```hcl
resource "aws_ecs_service" "app" {
  # ...
  health_check_grace_period_seconds = 30  # アプリ起動時間に応じて調整
}
```

また、タスク定義の`healthCheck.startPeriod`も忘れずに設定します（[ピラー記事の実装①](/blog/aws-ecs-fargate-production-guide)参照）。

### スケールインで処理中タスクを殺さない

スケールインが発生すると、ECSはタスクにSIGTERMを送ります。SQSワーカーがメッセージを処理中にSIGTERMを受け、`stopTimeout`（既定30秒・最大120秒）を過ぎてSIGKILLで殺されると、**処理中のメッセージが中断されてvisibility timeoutまでリトライ不能**になります。

正しい対処は3点セットです。

1. **SIGTERMハンドラを実装**する（新規メッセージの受信を止め、処理中のメッセージを捌き切る）。
2. **`stopTimeout`を処理完了に十分な時間に設定**する（最大120秒。5秒かかるメッセージを1件処理している最中にSIGTERMが来る最悪ケースを想定して設定）。
3. **冪等性を担保**する（万が一SIGKILL後に再配信されても二重処理しない）。

```ts
// SQS ワーカーの SIGTERM ハンドリング（概念例）
let isShuttingDown = false;

process.on("SIGTERM", () => {
  console.log("SIGTERM received: stopping new message consumption");
  isShuttingDown = true;
  // 処理中のメッセージが完了するのを待ち、stopTimeout内にexitする
});

async function pollMessages(): Promise<void> {
  while (!isShuttingDown) {
    const messages = await receiveMessages();
    for (const msg of messages) {
      await processMessage(msg);      // 冪等な処理
      await deleteMessage(msg);       // 正常終了後にのみ削除
    }
  }
  console.log("Worker gracefully stopped");
  process.exit(0);
}
```

冪等な非同期処理の詳細な実装パターンは[SQS・Lambda・EventBridgeの冪等非同期処理ガイド](/blog/aws-sqs-lambda-eventbridge-idempotent-async-processing-guide)を、回路遮断・リトライの設計は[リトライ・バックオフ・サーキットブレーカー](/blog/retry-backoff-circuit-breaker-resilience-patterns-guide)を参照してください。

### スケーリング設定が効いているか確認する

```bash
# スケーラブルターゲットの確認
aws application-autoscaling describe-scalable-targets \
  --service-namespace ecs \
  --query 'ScalableTargets[*].{Resource:ResourceId,Min:MinCapacity,Max:MaxCapacity}'

# スケーリングアクティビティ（直近のスケール履歴）
aws application-autoscaling describe-scaling-activities \
  --service-namespace ecs \
  --resource-id "service/<cluster>/<service>" \
  --max-results 10
```

スケーリング履歴を定期的に確認し、フラッピングや想定外のスケールイン・アウトが起きていないかをモニタリングに組み込みます。[OpenTelemetry × ECS の可観測性](/blog/aws-observability-opentelemetry-sre-ecs)と合わせてダッシュボードに載せておくと、スケーリング挙動の異常に素早く気づけます。

---

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

- [ ] スケーラブルターゲットが`describe-scalable-targets`で正しく登録されているか確認した
- [ ] `min_capacity`と`max_capacity`はビジネス要件（コスト上限・SLA）から決めたか
- [ ] `scale_out_cooldown < scale_in_cooldown`（非対称）になっているか
- [ ] CPUターゲット追跡の`target_value`は計測ベースか（60〜70%が目安）
- [ ] `ALBRequestCountPerTarget`を使う場合、`resource_label`が正しく設定されているか
- [ ] SQSワーカーはCPU追跡ではなくバックログ・パー・タスクを使っているか
- [ ] カスタムメトリクス発行Lambdaは1分ごとに実行され、CloudWatch上でデータポイントが確認できるか
- [ ] `min_capacity=0`の場合、コールドスタート遅延を許容レイテンシと照合したか
- [ ] `health_check_grace_period_seconds`を設定し、起動時の誤検知による強制終了を防いでいるか
- [ ] SIGTERMハンドラを実装し、`stopTimeout`内に処理を完了できるか検証したか
- [ ] スケールイン後もメッセージの二重処理が起きないよう冪等性を担保しているか
- [ ] スケーリングアクティビティのログを可観測性ダッシュボードに組み込んだか
- [ ] コスト最適化視点でFargate Spotとの組み合わせを[コスト最適化ガイド](/blog/aws-ecs-fargate-cost-optimization-spot-graviton-savings-plans-guide)で確認したか

---

## まとめ

ECS on Fargateのオートスケーリングは、「メトリクスを正しく選ぶ」「非対称クールダウンでフラッピングを防ぐ」「グレースフルシャットダウンと連動させる」の3点が本番品質の土台です。

- **HTTPサービス**：ターゲット追跡（CPU + ALBRequestCountPerTarget）で十分。バーストがある場合はステップスケーリングを追加する。
- **SQSワーカー**：CPUは使わない。バックログ・パー・タスクをカスタムメトリクスで発行し、許容レイテンシから逆算した目標値でターゲット追跡する。
- **共通**：SIGTERMハンドリング・`stopTimeout`・冪等性の三点セットなしにスケールインは安全に運用できない。

私が[一人 × 生成AI](/case-studies/payment-platform-reliability)で決済基盤とB2B SaaSを本番運用してきた経験から言えるのは、「ツールを正しく使えば、少人数でも世界水準のインフラは手の届く場所にある」ということです。オートスケーリングの設計・見直し・トラブルシューティングについてご相談があれば、お気軽にどうぞ。
