Claude CodeのECS Fargateタスク定義を実測で下げる手順
claude -pをECS Fargateで回すときのCPU・メモリを、Container Insightsの実測値からFargateの許容組み合わせに丸めてTerraformに反映する手順です。
claude -p をECS Fargateで定期実行している場合、タスク定義の cpu と memory は最初に余裕を見て大きめに置いたまま、見直されないことがよくあります。Fargateのタスクサイズは「実際に使った量」ではなく「宣言した組み合わせ」で決まるため、使われない余白もそのまま確保されます。
見直しの流れは4段階です。Container Insightsを有効にして実測値を集め、ピーク値に余裕を足し、Fargateの許容組み合わせに丸め、Terraformのタスク定義を書き換えます。そのあとに、下げた結果を確かめます。この記事は、その計測から確認までを、コピーして使えるコマンドとスクリプトで示します。Claude apps gatewayをAWSにデプロイするのようなゲートウェイの構成例とは別に、ここでは claude -p を1回きりのバッチタスクとして走らせる場合のサイズ決定だけを扱います。
Fargateのサイズはどう決まるか
Fargateのタスクでは、タスクレベルの cpu と memory が必須です。この2つはハード上限として扱われ、任意の値は選べません。cpu の値ごとに、指定できる memory の範囲が決まっています。
| タスクのCPU | 指定できるメモリ(Linux) |
|---|---|
| 256(0.25 vCPU) | 指定できるメモリ(Linux)512 MiB、1 GB、2 GB |
| 512(0.5 vCPU) | 指定できるメモリ(Linux)1 GBから4 GB(1 GB刻み) |
| 1024(1 vCPU) | 指定できるメモリ(Linux)2 GBから8 GB(1 GB刻み) |
| 2048(2 vCPU) | 指定できるメモリ(Linux)4 GBから16 GB(1 GB刻み) |
| 4096(4 vCPU) | 指定できるメモリ(Linux)8 GBから30 GB(1 GB刻み) |
8192(8 vCPU)以上の行もありますが、ここでは扱いません。CPUは1024ユニットが1 vCPUです。JSONの memory はMiBで書く(たとえば3 GBなら 3072)こともGBで書くこともできるので、後述の計算ではMiBに揃えます。
この表から言えることは、片方を下げるともう片方の選択肢も変わる点です。メモリを2 GBに収めたくてもCPUが256では2 GBが上限ぎりぎりになり、CPUが513ユニット以上必要なら512には収まらず1024に上がって、メモリの下限も2 GBに跳ね上がります。丸めの計算は、CPUとメモリを別々にではなく、組み合わせの表に対して一度に当てます。
手順1: Container Insightsを有効にする
実測値を取るには、ECSのContainer Insightsが必要です。アカウント設定 containerInsights の既定値は disabled で、有効にするコマンドは2種類あります。
# 拡張オブザーバビリティ(推奨とされている方)
aws ecs put-account-setting --name containerInsights --value enhanced
# 従来のContainer Insights
aws ecs put-account-setting --name containerInsights --value enabled特定のクラスターだけに設定する場合は aws ecs update-cluster-settings --cluster <名前> --settings name=containerInsights,value=enhanced を使います。AWSは拡張オブザーバビリティの利用を推奨していて、こちらはタスクとコンテナ単位のメトリクスも追加で取れます。
収集されるメトリクスは、実行中のタスクがある期間の分だけです。サービスにRUNNINGのタスクがなければ、そのサービスのメトリクスは送られません。1日に数回だけ動くバッチ型の claude -p では、タスクが動いていない時間帯に値が欠けるのが普通で、欠測は異常ではありません。
費用の面では、Container Insightsが収集するメトリクスはカスタムメトリクスとして課金されます。例外は RunningTaskCount で、2026年10月1日以降は課金されません。計測期間を決めて有効にし、サイズが決まったら設定を戻すかどうかは、CloudWatchの料金ページで見積もってから決められます。
手順2: ピーク値を取り出す
使うのは名前空間 ECS/ContainerInsights の4つです。CpuUtilized と MemoryUtilized が実使用量、CpuReserved と MemoryReserved がタスク定義で宣言した量です。ディメンションは ClusterName と TaskDefinitionFamily の組、ServiceName と ClusterName の組などが選べます。
注意が2点あります。1つ目は単位です。MemoryUtilized と MemoryReserved はUnitが「Megabytes」と表示されますが、実際にはMiBです。2つ目は集計の単位で、CpuUtilized は「ディメンションで指定したリソースのタスクが使ったCPUユニット」と説明されていますが、同じファミリーのタスクが並列で動くときに合算になるのか、タスクごとの値なのかは、Container Insightsのメトリクス一覧には書かれていません。確実に1タスク分を測るには、計測期間中は同じタスク定義を同時に1つしか動かさないか、拡張オブザーバビリティの ContainerCpuUtilized などを TaskId ディメンション付きで取ります。
aws cloudwatch get-metric-statistics \
--namespace ECS/ContainerInsights \
--metric-name CpuUtilized \
--dimensions Name=ClusterName,Value=claude-batch \
Name=TaskDefinitionFamily,Value=claude-worker \
--start-time "$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 60 --statistics Maximum \
--query 'max(Datapoints[].Maximum)'上のコマンドはmacOSの date -v-7d を使っています。GNU系のLinuxでは date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ に置き換えます。--metric-name を MemoryUtilized にしてもう一度実行すれば、メモリのピークも同じ形で取れます。
計測期間に入れておくべきタスク
claude -p のサイズは、プロンプトが同じでも、Claudeが途中で何を実行するかで変わります。テストランナーやビルドを Bash ツールで走らせる運用なら、ピークはClaude Code本体ではなく、その子プロセスが作ります。ピークがいちばん高くなる種類のジョブ(最大のリポジトリ、最長のテストスイート)を計測期間に含めないと、通常の日の値だけでサイズを決めて、重い日にメモリ不足で落ちます。
Maximum を取るのはそのためです。平均値や95パーセンタイルで丸めると、重い日の1回が収まらなくなります。
手順3: 余裕を足して許容組み合わせに丸める
ピーク値にどれだけ余裕を持たせるかは、AWSの文書には指針がありません。以下では1.3倍を例にしますが、これは筆者の置いた仮の値で、ジョブの重さのばらつきと、落ちたときの再実行コストから自分で決める部分です。
次のスクリプトは、ピーク値と余裕率を受け取り、条件を満たすFargateの組み合わせを全部出します。どれを選ぶかは、リージョンの料金表の単価で最終判断します。料金の式はこの記事の範囲外なので、スクリプトは「収まる組み合わせの列挙」までにしています。
python3 fit_fargate.py --cpu-peak 310 --mem-peak-mib 1480 --headroom 1.3#!/usr/bin/env python3
import argparse
# Fargate (Linux) の許容組み合わせ。memory は MiB。
FARGATE = {
256: [512, 1024, 2048],
512: [1024 * n for n in range(1, 5)],
1024: [1024 * n for n in range(2, 9)],
2048: [1024 * n for n in range(4, 17)],
4096: [1024 * n for n in range(8, 31)],
}
p = argparse.ArgumentParser()
p.add_argument("--cpu-peak", type=float, required=True)
p.add_argument("--mem-peak-mib", type=float, required=True)
p.add_argument("--headroom", type=float, default=1.3)
a = p.parse_args()
need_cpu = a.cpu_peak * a.headroom
need_mem = a.mem_peak_mib * a.headroom
print(f"必要量: CPU {need_cpu:.0f} ユニット / メモリ {need_mem:.0f} MiB")
fits = [(c, m) for c, mems in FARGATE.items() if c >= need_cpu
for m in mems if m >= need_mem]
if not fits:
print("4 vCPU までの組み合わせには収まりません")
for c, m in fits[:6]:
print(f"cpu={c} memory={m}")例として、CpuUtilized のピークが310ユニット、MemoryUtilized のピークが1,480 MiBだったとします(数値は説明用の仮の値です)。1.3倍すると、必要量はCPU 403ユニット、メモリ約1,924 MiBです。CPUは256では足りず512、そのとき選べるメモリは1,024から4,096 MiBで、1,924以上の最小は2,048 MiBです。出力の先頭は cpu=512 memory=2048 になります。
もし元の定義が cpu=2048、memory=8192 だったなら、この例では4分の1のCPUと4分の1のメモリまで下がります。実測値に対する宣言の比率は CpuReserved と CpuUtilized、MemoryReserved と MemoryUtilized を並べると一目で出ます。
一点だけ、JavaのZGCを使うアプリではContainer Insightsの MemoryUtilized が不正確になることがある、とドキュメントに注記があります。コンテナ内でそうしたJVMのツールを動かすジョブなら、この値だけを根拠にしません。
手順4: Terraformのタスク定義を書き換える
サイズを変数にしておくと、丸めた結果の反映が1行の差分で済みます。Fargateの aws_ecs_task_definition では cpu と memory が必須で、どちらも文字列で渡します。memory の単位はMiBです。
variable "task_cpu" {
type = string
default = "512"
}
variable "task_memory" {
type = string
default = "2048"
}
resource "aws_ecs_task_definition" "claude_worker" {
family = "claude-worker"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.task_cpu
memory = var.task_memory
execution_role_arn = aws_iam_role.execution.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([{
name = "claude"
image = "${aws_ecr_repository.worker.repository_url}:latest"
essential = true
stopTimeout = 60
command = ["claude", "-p", "テストを実行して失敗を要約する",
"--permission-prompts", "none"]
}])
}family を計測時と同じ名前のまま保つ点が大事です。Container Insightsのメトリクスは TaskDefinitionFamily ディメンションで引くので、名前を変えると過去の計測と見比べられなくなります。
terraform apply で新しいリビジョンが登録されるだけで、動いているタスクには影響しません。次にタスクが起動されるときから、新しいサイズが使われます。
CPUとメモリの外にある「作業ディスク」
cpu と memory を下げても、一時ストレージ(ephemeral storage)の量は変わりません。リポジトリをタスク内でcloneして claude -p に読ませる運用では、容量の不足がCPUやメモリより先に表面化することがあります。広げたいときは ephemeralStorage を指定します。これはプラットフォームバージョン1.4.0以降(Linux)の機能です。Terraformでは ephemeral_storage ブロックの size_in_gib で書き、指定できる値は21から200 GiBです。
ephemeral_storage {
size_in_gib = 40
}この値は CpuUtilized と MemoryUtilized では測れないため、cloneしたリポジトリとビルド成果物の合計サイズを別に見積もります。
下げたあとに見る値と、サイズに効く挙動
サイズを下げたあとは、同じメトリクスで MemoryUtilized の最大値が MemoryReserved に対してどこまで迫ったかを確認します。タスクのメモリはハード上限として宣言されています。余裕率を1.3倍にしたのなら、実測の最大値は宣言量の約77パーセント前後に収まるはずです。95パーセントを超える日が出たら、一段上の組み合わせに戻す合図です。
claude -p の側にも、サイズ設計に効く挙動が3つあります。
- 終了待ちでタスクが延びます。Claudeが起動したバックグラウンドのサブエージェントやコマンドがあると、
claude -pは完了まで開いたままです。待機が続けば、その分の時間もFargateの課金対象になります。待ちの上限は既定で10分で、CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MSで変えられます。 - 停止はSIGTERMで来ます。ECSがタスクを止めるとき、
claude -pがSIGTERMを受けて終了コード143で抜けます。このとき実行中のBashコマンドのプロセスツリーを止め、SessionEndフックだけを実行して終わります。コンテナのstopTimeoutは既定で30秒、最大120秒です。SessionEndフックに重い処理を置くなら、この値も一緒に見直します。詳しい挙動はclaude -pをSIGTERMで止めるとどうなるにまとめています。 - 費用の上限はフラグで切れます。
--max-budget-usdに達すると、残りのバックグラウンド処理も止まります。Fargateの時間課金とモデルの費用は別の請求で、片方を絞ってももう片方は減りません。モデル側のコストはOpus 5.5のタスクコストを試算する方法で分解できます。
非対話実行の基本オプションはclaude -pオプションとはで扱っています。
落とし穴
| 症状 | 原因 | 見直す点 |
|---|---|---|
| 計測期間中、値がゼロや欠測になる | 原因RUNNINGのタスクがない時間はメトリクスが送られない | 見直す点欠測を0として平均しない。Maximum だけを見る |
| 丸めた後に特定のジョブだけ落ちる | 原因計測期間に重いジョブが含まれていなかった | 見直す点最大のジョブを1回実行して値を足し直す |
| メモリが想定の1.05倍ずれる | 原因MiBとMBを取り違えた | 見直す点MemoryUtilized はMiB。GB指定の memory と混ぜない |
| 同じファミリーを並列実行して値が大きい | 原因複数タスクが合算されている可能性がある | 見直す点計測中は1タスクだけ動かすか、TaskId 付きで取る |
| 下げたのに請求が減らない | 原因待機時間やバックグラウンド処理でタスクが長引いている | 見直す点実行時間そのものを claude -p 側で短くする |
まとめ
Fargateのタスクサイズは、宣言した組み合わせでしか選べません。まず Maximum で重い日を含めたピークを取り、余裕率を掛け、許容組み合わせの表に対してCPUとメモリを同時に当てはめるのが、最短の道筋です。丸めたあとの1週間は、MemoryUtilized の最大値が宣言量の95パーセントを超えないかを見ます。超える日があれば、余裕率が足りなかったことになるので、一段上の組み合わせに戻します。claude -p の待機時間とSIGTERMの扱いは、サイズとは別にタスクの寿命を決めるため、タスクサイズを下げる作業の最後に併せて確認します。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。