ECS/Fargateのタスク定義・スケール・コストでハマらない設計術
Fargateのタスク定義、オートスケール、Spot、料金の落とし穴をMasaの失敗込みで解説。Claude Codeに実装させるコツも紹介。
「Fargateならサーバー管理いらないんでしょ、楽勝じゃん」
そう思ってNode.jsのAPIを移した僕は、デプロイ直後にタスクが起動→停止→起動→停止を延々と繰り返す画面を、コーヒー片手に眺めるハメになりました。コンテナは正しくビルドできている。ポートも合っている。なのに、ヘルスチェックが「起動40秒」のアプリを「30秒で応答しないからダメ」と判定して、生きているコンテナを片っ端から殺していたんです。
Fargateで詰まるのは、たいていDockerfileの中身じゃありません。タスク定義の数字、スケールの設定、そして月末に届く請求書——この3つです。今日はそこに絞って、僕が踏んだ地雷ごと共有します。
この記事の要点
- Fargateの事故の大半は「タスク定義の数字(CPU/メモリ/
startPeriod)」と「スケール設定」と「料金」で起きる。Dockerfileは入口にすぎない。 - ヘルスチェックの
startPeriodを入れないと、起動の遅いアプリは安定する前に殺され続ける。これが初心者の最頻ハマりどころ。 desiredCount固定ではなく、Application Auto Scalingで負荷に追従させると、ピークの取りこぼしと夜間の払いすぎを同時に減らせる。- 料金はFargate本体だけ見ても足りない。NAT Gateway・ALB・CloudWatch Logsの「常時課金」が地味に効く。
- Claude Codeには「数字の根拠」と「落とし穴」を先に渡す。長いJSONを速く書かせる道具として割り切ると強い。
なお、Lambda+API GatewayやCDKを含むデプロイ全体の流れはClaude CodeでAWSデプロイを自動化する実践ガイドにまとめてあります。この記事はそこから一歩進んで、ECS/Fargateのコンテナ運用そのものに集中します。
Fargateを選ぶ前に:そもそも向いてる?
最初に身も蓋もない話をします。小さなAPIなら、たいていLambdaのほうが楽で安いです。Fargateが効くのは、こういうときです。
- 既にDockerでアプリを動かしていて、その資産をそのまま運びたい
- リクエストが来ない時間も常時起動していてほしい(コールドスタートを避けたい)
- 1リクエストの処理が長め、もしくはWebSocketのような常時接続がある
- 言語ランタイムやライブラリを自由に選びたい
逆に「たまに叩かれるWebhook」みたいな処理ならClaude Code × AWS Lambda完全ガイドのほうが、固定費ゼロで始められます。ここを曖昧にしたままFargateを選ぶと、動かない時間にもお金を払う構成になりがちです。
向き不向きを、用途で並べておきます。
| ユースケース | Fargateが向く理由 | 先に決める数字 |
|---|---|---|
| SaaSのREST API | 常時2台以上+ALB配下でローリング更新できる | 最小タスク数・DB接続上限・ヘルスチェック猶予 |
| 管理画面のバックエンド | EC2管理なしで小さく始められる | 夜間に絞るなら最小1台か0台か |
| バッチ兼用の軽量サービス | 同じイメージをserviceとrun-taskで使い回せる | task roleの最小権限 |
「Fargate=サーバーレスのコンテナ実行環境」「タスク定義=コンテナの起動指示書」「task role=アプリ自身がAWSを触る権限」。この3語だけ先に飲み込んでおくと、以下が一気に読みやすくなります。
いちばん事故るのはタスク定義の数字
ここが本題です。Fargateのタスク定義で僕が事故った数字は、3つに集約できます。
startPeriodを入れないと起動の遅いアプリは死ぬ
冒頭の「起動→停止」ループの犯人がこれでした。ヘルスチェックにはstartPeriodという「起動猶予」があって、ここで指定した秒数のあいだは、失敗してもタスクを殺さずに待ってくれます。Node.js・Rails・Next.jsみたいに初期化が重いアプリは、ここがゼロだと「立ち上がる前に不合格」を食らい続けます。
下のタスク定義は、startPeriod: 60(60秒待つ)を入れた最小構成です。Secrets ManagerからDATABASE_URLを注入し、CloudWatch Logsへ流します。コピペでregister-task-definitionまで通る形にしてあります。
set -euo pipefail
export AWS_REGION="ap-northeast-1"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export IMAGE_URI="${IMAGE_URI:?先にECRへpushしてIMAGE_URIを設定してください}"
export EXECUTION_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole"
export TASK_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/myapp-task-role"
export SECRET_ARN="arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:prod/myapp/DATABASE_URL"
# ログは先に作って保持期間を30日に。放置すると保存料が地味に積み上がる
aws logs create-log-group --log-group-name /ecs/myapp --region "${AWS_REGION}" 2>/dev/null || true
aws logs put-retention-policy --log-group-name /ecs/myapp --retention-in-days 30 --region "${AWS_REGION}"
cat > ecs-task-definition.json <<EOF
{
"family": "myapp-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "${EXECUTION_ROLE_ARN}",
"taskRoleArn": "${TASK_ROLE_ARN}",
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
},
"containerDefinitions": [
{
"name": "app",
"image": "${IMAGE_URI}",
"essential": true,
"portMappings": [
{ "containerPort": 3000, "hostPort": 3000, "protocol": "tcp" }
],
"environment": [
{ "name": "NODE_ENV", "value": "production" },
{ "name": "PORT", "value": "3000" }
],
"secrets": [
{ "name": "DATABASE_URL", "valueFrom": "${SECRET_ARN}" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/myapp",
"awslogs-region": "${AWS_REGION}",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
]
}
EOF
aws ecs register-task-definition \
--cli-input-json file://ecs-task-definition.json \
--region "${AWS_REGION}"
地味ですが、startPeriodの有無でデプロイの成否が変わります。自分のアプリが「起動に何秒かかるか」をローカルで一度計っておき、その倍くらいを入れておくと安全です。
CPUとメモリは「組み合わせ」が決まっている
もうひとつ罠なのが、Fargateのcpuとmemoryは好きな値を入れられないこと。組み合わせが決められていて、たとえばcpu: 512(0.5 vCPU)ならmemoryは1024〜4096の範囲、といった具合です。ここを外すとregister-task-definitionが静かにエラーを返します。
代表的な対応を表にしておきます(よく使うところだけ抜粋。最新の正確な値は公式で確認してください)。
| cpu (vCPU相当) | 選べるmemory (MiB) | 向いている用途 |
|---|---|---|
| 256 (.25) | 512 / 1024 / 2048 | ごく軽いAPI・検証 |
| 512 (.5) | 1024〜4096 | 小〜中規模API(まずここ) |
| 1024 (1) | 2048〜8192 | 画像処理・やや重い処理 |
| 2048 (2) | 4096〜16384 | 重めのバックエンド |
最初から大きくしないこと。512 / 1024で出して、CloudWatchのメトリクスでCPU・メモリの使用率を見てから上げる。これが結局いちばん安く済みます。使用率の見方はClaude Code × AWS CloudWatch実践ガイドに書きました。
execution roleとtask roleを混ぜると沼る
3つ目は権限です。Fargateには似た名前のロールが2つあって、僕は初回ここで丸一日溶かしました。
- execution role: コンテナを「起動する側(ECSエージェント)」の権限。ECRからイメージをpullする、CloudWatch Logsに書く、Secrets Managerから値を読む——これらはこちら。
- task role: アプリの「コードが自分でAWSを叩く」権限。DynamoDBやS3にアクセスするならこちら。
DynamoDB権限をexecution roleに足しても、アプリからは一生使えません。逆に、secretが読めなくてタスクが起動しないときは、task roleではなくexecution role側を疑います。IAMの最小権限設計でつまずいたらClaude Code × AWS IAMガイドが近道です。
desiredCount固定をやめてオートスケールにする
ここからスケールの話です。最初はdesired-count 2の固定でいいんですが、本番で放っておくと2つの無駄が出ます。ピーク時に足りないのと、夜間に払いすぎです。
Fargateは「使った秒数 × vCPU × メモリ」で課金されるので、暇な時間に2台動かしっぱなしは、そのままお金を捨てているのと同じ。そこでApplication Auto Scalingを足して、CPU使用率に応じてタスク数を増減させます。下は「CPU平均50%を目標に、最小1〜最大6で勝手に増減」する設定です。
set -euo pipefail
export AWS_REGION="ap-northeast-1"
export CLUSTER_NAME="myapp-cluster"
export SERVICE_NAME="myapp-service"
export RESOURCE_ID="service/${CLUSTER_NAME}/${SERVICE_NAME}"
# タスク数の増減幅を登録(最小1・最大6)
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id "${RESOURCE_ID}" \
--min-capacity 1 \
--max-capacity 6 \
--region "${AWS_REGION}"
# 設定ファイル:CPU平均50%を目標にする
cat > scaling-policy.json <<'EOF'
{
"TargetValue": 50.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleInCooldown": 120,
"ScaleOutCooldown": 30
}
EOF
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id "${RESOURCE_ID}" \
--policy-name myapp-cpu-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration file://scaling-policy.json \
--region "${AWS_REGION}"
コツはScaleOutCooldown(増やす待ち時間)を短く、ScaleInCooldown(減らす待ち時間)を長めにすること。増やすのは素早く、減らすのは慎重に。逆にすると、瞬間的な負荷でタスクを減らした直後にまた急増、を繰り返して「フラッピング」します。
夜間バッチのように負荷の時間帯が読めるなら、scheduled-actionで「平日朝に最小2、夜は最小0」のように時刻で動かすほうが素直です。ただし最小0にすると、最初のリクエストでコンテナ起動を待たせる点だけ注意してください。
コストは本体より「常時課金の脇役」で漏れる
請求書で一番びっくりするのは、たいていFargate本体じゃありません。動かしているだけで課金され続ける脇役です。僕が検証環境を消し忘れて、月末に「あれ、こんな使ってないのに」となった内訳がこれです。
| 課金される対象 | 何で増えるか | 見落としがちな点 |
|---|---|---|
| Fargateタスク | vCPU × メモリ × 稼働秒数 | 暇な時間も常時2台だと払いっぱなし |
| NAT Gateway | 起動しているだけで時間課金+転送量 | 検証で1個立てて消し忘れがち。固定費が乗る |
| ALB | 起動時間+処理量(LCU) | タスクを止めてもALBは課金が続く |
| CloudWatch Logs | 取り込み量+保存量 | awslogsを絞らず全部流すと膨らむ |
| ECR | イメージの保存容量 | 古いタグが溜まる。ライフサイクルで掃除 |
対策はシンプルで、検証環境は「最小タスク1・ログ保持30日・使わないNAT Gatewayは即削除」を運用メモに固定するだけ。とくにNAT Gatewayは「assignPublicIp=DISABLED」のprivate subnet運用とセットで増えるので要注意です。private subnetからECR・Logs・Secrets Managerへ出る経路として、NAT Gatewayの代わりにVPCエンドポイントを使うと、転送量しだいでは安くなることもあります。料金の正確な計算はAWS Fargate料金で確認してください。
もう一段攻めるならFargate Spotです。最大で割引価格になりますが、AWS都合で2分前通知とともに止められる可能性があります。だから「落ちても困らない部分」に使う。サービスのcapacity providerをFARGATEとFARGATE_SPOTで混ぜ、たとえば「常時の土台はFARGATEで2台、上乗せ分はSPOT」のように分けると、止まりにくさと安さのバランスが取れます。本番のクリティカルなトラフィックを全部Spotに寄せるのはおすすめしません。
Claude Codeに任せる範囲、自分で決める範囲
ここまで読んで気づいたと思いますが、**Fargateで効いてくるのは「数字の判断」**です。そして数字の判断こそ、AIに丸投げしてはいけない部分です。
僕の分担はこうしています。人間が決めるのは、サービス選定(そもそもFargateか)、CPU/メモリの初期値、startPeriodの秒数、スケールの最小・最大、Spotを混ぜるか。Claude Codeに任せるのは、決めた数字を長いタスク定義JSONやCLIスクリプトに落とす作業、ECSイベントとログからの原因切り分け、それらの定型コードの量産です。
依頼するときは、数字の根拠と落とし穴を先に渡すと精度が段違いに上がります。たとえばこう。
AWS ECS/FargateへNode.js APIをデプロイするコードを作ってください。
決めた数字(この通りに):
- リージョン: ap-northeast-1 / ECRリポジトリ: myapp / コンテナポート: 3000
- launch type: FARGATE / network mode: awsvpc
- task CPU/memory: 512 / 1024(あとでCloudWatch使用率を見て調整する前提)
- 最小タスク1・最大6でCPU50%目標のオートスケール
- secrets: DATABASE_URLをSecrets Managerから注入
- logs: CloudWatch Logs /ecs/myapp, 保持30日
作ってほしいもの:
1. production用Dockerfile(/healthを持つ)
2. ECSタスク定義の登録script(startPeriod: 60を必ず入れる)
3. Fargate service作成script(private subnet・assignPublicIp=DISABLED)
4. Application Auto Scaling設定script
5. CloudWatch Logsとイベント確認script
制約:
- 疑似コード禁止。aws cliで実行できる形にする
- execution roleとtask roleを分けて、それぞれ何の権限かコメントで説明
- secret値を環境変数へ直書きしない/public subnetへ直接公開しない
- 公式AWSドキュメントと照合すべき注意点を最後に列挙する
「疑似コード禁止」と「数字を先に固定」が効きます。これを抜くと、Claude Codeはそれっぽいが運用で落ちる構成を平気で返してきます。
タスクが起動しないときの調べ方
最後に、デプロイがコケたときの動き方を。ECSは「なぜ起動しないか」をサービスイベント・タスク停止理由・CloudWatch Logsの3か所に分けて出します。1か所だけ見ても真相にたどり着けないので、3点セットで集めます。
export AWS_REGION="ap-northeast-1"
export CLUSTER_NAME="myapp-cluster"
export SERVICE_NAME="myapp-service"
# 1. サービスイベント(直近5件):配置の失敗理由が出やすい
aws ecs describe-services \
--cluster "${CLUSTER_NAME}" \
--services "${SERVICE_NAME}" \
--query "services[0].events[0:5].[createdAt,message]" \
--output table \
--region "${AWS_REGION}"
# 2. 停止したタスク一覧:このIDをdescribe-tasksに渡すと停止理由が読める
aws ecs list-tasks \
--cluster "${CLUSTER_NAME}" \
--service-name "${SERVICE_NAME}" \
--desired-status STOPPED \
--region "${AWS_REGION}"
# 3. アプリのログを追う
aws logs tail /ecs/myapp \
--follow \
--since 10m \
--region "${AWS_REGION}"
経験上、停止理由の9割はこの4つです。ECRからpullできない(execution roleかネットワーク)、secretを読めない(execution roleのGetSecretValue漏れ、KMS使用時はkms:Decryptも)、security groupでALBから届かない、アプリが0.0.0.0でなくlocalhostだけでlistenしている。この3点セットをそのままClaude Codeに貼ると、直すべきIAMポリシーと再デプロイ手順まで一気に整理してくれます。
なお、イメージのアーキテクチャ違いも定番です。Apple SiliconでARM64のイメージをビルドして、タスク定義がX86_64のままだと起動しません。docker buildx build --platform linux/amd64で揃えるか、タスク定義のruntimePlatformをARM64に合わせます。
よくある質問
Q. FargateとEC2起動タイプ、どっちを選べばいい? A. サーバー(EC2)の管理をしたくない、台数も多くないならFargateが楽です。GPUが要る、極端に大量のコンテナを高密度で詰めたい、EC2のリザーブドインスタンスで原価を絞りたい——こうした事情があるときだけEC2起動タイプを検討します。多くの個人・小規模チームはFargateで十分です。
Q. startPeriodは何秒にすればいい?
A. 自分のアプリの起動時間をローカルで計り、その2倍前後を入れておくと安全です。Node.jsの小さなAPIなら30〜60秒、フレームワークやDB接続の初期化が重いなら60〜120秒が目安。長すぎると「壊れたコンテナを長く生かす」ことになるので、無闇に大きくはしません。
Q. 検証用に作ったFargate、何を消せば課金が止まる?
A. serviceのdesiredCountを0にしてもタスク料金は止まりますが、NAT GatewayとALBは別途課金が続きます。検証を畳むなら、service削除に加えてNAT GatewayとALBの要否を必ず確認してください。ここが「使ってないのに請求が来る」の主犯です。
Q. Fargate Spotは本番で使って大丈夫? A. 「止まっても困らない部分」なら有効です。AWS都合で2分前通知とともに停止される前提なので、土台のタスクは通常Fargate、上乗せ分だけSpot、のように混ぜるのが現実的。クリティカルなトラフィックを全部Spotに寄せるのは避けます。
Q. Claude Codeにタスク定義を全部書かせていい?
A. JSONを書く作業は任せてOKです。ただしCPU/メモリの値、startPeriod、スケールの最小・最大といった「数字の判断」は人間が決め、依頼文に明記してから渡してください。数字を渡さないと、それっぽいが運用で落ちる構成が返ってきます。
実際に試した結果
小さなNode.js APIをFargateに載せて運用してみて、効果がいちばん大きかったのは凝ったDockerfileでも豪華な構成図でもなく、「数字を先に決めてからClaude Codeに渡す」というたった一手でした。
初回はstartPeriodなしでタスクが起動ループ、次にexecution roleのsecret権限漏れで停止——と、まさにこの記事に書いた地雷を順番に踏みました。でも、ECSイベント・停止理由・ログの3点セットをそのままClaude Codeに貼ったら、直すべきIAMポリシーと再デプロイ手順まで一度で出てきて、復旧が一気に速くなった。スケールもdesiredCount固定からCPU50%目標のオートスケールに変えただけで、夜間の払いすぎが目に見えて減りました。
Fargateは「Dockerを置けば終わり」ではなく、タスク定義の数字・スケール・料金を一緒に設計するサービスです。賢いAIに丸投げするより、自分で数字を決めて、その実装と調査をClaude Codeに高速化してもらう。これがいちばん事故らない、というのが今の実感です。
まずは手元の小さなAPIで、この記事のスクリプトを上から順に試してみてください。再利用できる依頼文やレビュー観点が欲しくなったら教材一覧、自分のリポジトリ前提で一気に整えたいなら研修・相談も役に立ちます。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。