AWS ECS・Fargateでコンテナアプリを本番運用するための設計手順|タスク定義・ALB連携・マルチAZ配置の構成パターン入門

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > AWS(Amazon Linux) > AWS ECS・Fargateでコンテナアプリを本番運用するための設計手順|タスク定義・ALB連携・マルチAZ配置の構成パターン入門
「Dockerでアプリをコンテナ化したはいいが、AWSで本番運用する構成をどう設計すればいいかわからない」
「ECSとFargateの違いは?ALBとの連携方法や、マルチAZ設計のポイントが整理できていない」

こうした悩みを持つLinuxエンジニアは少なくありません。コンテナ技術そのものはDockerで学べても、AWSで止まらない本番構成を組み立てるには、ECS固有の設計ルールを体系的に把握する必要があります。

この記事では、AWS ECS・Fargateを使ったコンテナ本番環境の設計手順を、タスク定義・ALBとの連携・マルチAZ分散配置・Auto Scaling・IAMロール設計まで順を追って解説します。セキュリティグループ設計・デプロイ失敗時の自動ロールバック設定・ゼロダウンタイムデプロイ・VPCエンドポイントによるコスト最適化に加え、タスク定義のバージョン管理とTerraformによるIaC管理のポイント、本番で頻出するエラーの対処法も網羅しています。動作確認済みのAWS CLIコマンドと設定例を交えながら進めるので、設計の全体像を掴みながら読み進めてください。

動作確認環境: Amazon Linux 2023 / AWS CLI 2.x / Terraform v1.9 / AWS Provider v5.x

この記事のポイント

・FargateはサーバーレスでEC2管理が不要。本番用途では最初の選択肢になる
・タスク定義でCPU・メモリ・コンテナ定義・ログ設定を一元管理する
・ECSサービス+ALBでマルチAZに分散配置してヘルスチェックを自動化する
・maximumPercent=200/minimumHealthyPercent=100でゼロダウンタイムデプロイを実現する
・force_new_deployment=trueを設定するとタスク定義更新時にサービスの再デプロイが自動でトリガーされる
・Auto ScalingはCPU使用率70%をトリガーにするターゲットトラッキングが基本
・実行ロール(起動権限)とタスクロール(アプリ権限)を分離して最小権限を実現する
・デプロイ失敗時はcircuit breakerで自動ロールバックしサービス停止を防ぐ
・TerraformでIaC管理する場合はjsonencodeかテンプレートファイルでコンテナ定義を管理し、lifecycle.ignore_changesでCI/CDと共存する
・ECSはVPC・ALBと別のState(ディレクトリ)に分割して変更頻度の違いを吸収するのが本番設計の鉄則


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

ECSとFargateとは?起動タイプの違いと選び方

ECS(Elastic Container Service)はAWSのコンテナオーケストレーションサービスです。
DockerコンテナをAWS上で管理・実行するための基盤として機能します。

ECSには起動タイプが2つあります。

・EC2起動タイプ: コンテナを実行するEC2インスタンスを自分で管理する
・Fargate起動タイプ: コンテナの実行基盤をAWSがすべて管理するサーバーレス

ほとんどの新規構築ではFargateを選ぶのが正解です。EC2起動タイプは、コンテナホストのOSパッチ管理・ECSエージェントの更新・スケール時のインスタンス管理が必要になります。FargateであればこれらをすべてAWSに任せられます。

EC2起動タイプが適しているのは、GPUを使う機械学習コンテナや、ARM64など特定のアーキテクチャが必要な場合、または特権コンテナが必要な場合に限られます。Fargateは特権コンテナをサポートしていないため、この点だけEC2起動タイプが優位です。

2つの起動タイプの管理責任の違いを整理しておきます。
管理項目 EC2起動タイプ Fargate起動タイプ
コンテナホストのOSパッチ 自分で管理 AWSが担当
ECSエージェントの更新 自分で管理 AWSが担当
インスタンスのスケーリング Auto Scaling Groupを自分で設計 タスク単位で自動管理
コスト構造 EC2常時稼働コスト(アイドルコストあり) タスク実行時間ベース(アイドルコストなし)
特権コンテナ 使用可能 使用不可
コンテナ定義・起動 ECSが管理 ECSが管理
アプリケーションコード 自分で管理 自分で管理
Fargateを選ぶと「コンテナホスト以下の管理をAWSに委ねて、アプリケーション設計に集中できる」のが本質的なメリットです。TerraformなどのIaCツールで管理する際も、EC2起動タイプに比べてEC2・ASG・ECSエージェントの設定が不要なため、HCLのリソース数が大幅に少なくなります。

ECSの主要コンポーネントの整理

ECSを設計する前に、主要コンポーネントの役割を整理しておきます。

・クラスター: ECSリソースの論理的なグループ。Fargateタスクが実行される空間
・タスク定義: コンテナの設定テンプレート(CPU・メモリ・イメージ・環境変数・ログ設定)
・タスク: タスク定義を元に実際に起動したコンテナのインスタンス
・サービス: 指定した数のタスクを常時稼働させ、ALBと連携する管理エンティティ

「タスク定義=設計図」「タスク=インスタンス」「サービス=台数管理と外部公開」というイメージで捉えると整理しやすいです。Terraformでは aws_ecs_cluster・aws_ecs_task_definition・aws_ecs_service の3リソースが対応します。

クラスターとContainer Insightsの設定

ECSクラスターを作成する際に、Container Insightsを有効化することを強く推奨します。Container Insightsを有効にすると、CPU・メモリ使用率・タスク数・ネットワーク転送量などのメトリクスがCloudWatch Metricsに自動収集されます。本番障害の原因調査にほぼ必ず使うことになります。

# ECSクラスターを作成する(Container Insights有効化) aws ecs create-cluster --cluster-name myapp-cluster --settings "name=containerInsights,value=enabled" --region ap-northeast-1 # 既存クラスターにContainer Insightsを後から有効化する aws ecs update-cluster-settings --cluster myapp-cluster --settings "name=containerInsights,value=enabled" --region ap-northeast-1

TerraformでIaC管理する場合は aws_ecs_cluster リソースで同様に設定できます。

resource "aws_ecs_cluster" "main" { name = "myapp-cluster" setting { name = "containerInsights" value = "enabled" } tags = { Environment = var.environment } }

FargateのキャパシティプロバイダーにはFARGATEとFARGATE_SPOTの2種類があります。FARGATE_SPOTはEC2スポットインスタンスと同様の仕組みで、通常FARGATEと比べて最大70%のコスト削減が可能です。ただし中断リスクがあるため、常時稼働が必要な本番サービスにはFARGATEを使い、バッチ処理など中断されても再実行できるワークロードにFARGATE_SPOTを組み合わせるのが定石です。TerraformでIaC管理する場合は aws_ecs_cluster_capacity_providers リソースで両者を明示的に設定できます。

タスク定義の設計ポイント

タスク定義はECS設計の核心部分です。以下の5点を押さえておけば、現場レベルの設計ができます。

1. CPUとメモリの割り当て基準

Fargateでは、タスク全体のCPUとメモリを事前に決める必要があります。
使えるサイズの組み合わせは固定されています。代表的なパターンは次のとおりです。
タスクCPU(vCPU) 選択できるメモリ(GB) 用途の目安
0.25 vCPU 0.5 ~ 2 GB 軽量なAPIやバッチ処理
0.5 vCPU 1 ~ 4 GB 標準的なWebアプリ(小規模)
1 vCPU 2 ~ 8 GB 中規模Webアプリ・マイクロサービス
2 vCPU 4 ~ 16 GB 高トラフィックAPI・画像処理
4 vCPU 8 ~ 30 GB 大規模バッチ・データ処理
最初は小さめのサイズから始め、CloudWatch MetricsのCPUUtilization・MemoryUtilizationを見ながら調整するのが現場の鉄則です。最初から大きいサイズを確保しても、コストが増えるだけです。
Fargateに設定するCPU値は、CLIのJSON・Terraform HCLではそれぞれ整数文字列("256"・"512"・"1024"・"2048"・"4096")で指定します。AWSコンソールでは「0.25 vCPU」と表示されますが、コード上は上表のvCPU値に対応する整数を使います。制約外の組み合わせ(例: cpu=256でmemory=256)を指定するとデプロイ時にInvalidParameterExceptionが即座に発生するため、上表の組み合わせを必ず守ってください。

2. コンテナ定義と環境変数・シークレットの設計

タスク定義のコンテナ定義で重要なのは、環境変数とシークレットの扱い方です。

・一般的な設定値(非機密): environmentに直接指定
・パスワード・APIキー(機密情報): AWS Secrets ManagerまたはParameter StoreをsecretsセクションでARN参照する

以下はAWS CLIでタスク定義を登録するJSON例です(抜粋)。

# タスク定義のJSON(コンテナ定義部分の抜粋) { "family": "myapp-task", "networkMode": "awsvpc", "requiresCompatibilities": ["FARGATE"], "cpu": "512", "memory": "1024", "containerDefinitions": [ { "name": "myapp", "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:latest", "portMappings": [ { "containerPort": 8080, "protocol": "tcp" } ], "environment": [ {"name": "APP_ENV", "value": "production"}, {"name": "LOG_LEVEL", "value": "info"} ], "secrets": [ { "name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:myapp/db-password" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/myapp", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "ecs" } } } ] }

機密情報をSecretsManagerで管理するポイントは2点あります。
コンテナ側には環境変数として渡されるため、アプリケーションのコードを変更する必要がないこと。そしてタスク定義のJSONにパスワードが平文で残らないことです。

3. awslogsでCloudWatch Logsにログを転送する

Fargateではコンテナの標準出力・標準エラー出力を、awslogsドライバー経由でCloudWatch Logsに転送します。
タスクが終了してもログが残るため、障害調査に不可欠な設定です。

ロググループは事前に作成しておく必要があります。

# CloudWatch Logsのロググループを作成する aws logs create-log-group --log-group-name /ecs/myapp --region ap-northeast-1 # ロググループの保持期間を30日に設定する(必ず設定すること) aws logs put-retention-policy --log-group-name /ecs/myapp --retention-in-days 30

ロググループの保持期間を設定しないとログが永続的に積み上がり、予想外のコストが発生します。本番環境では30日や90日などの保持期間を必ず設定してください。TerraformでIaC管理する場合は aws_cloudwatch_log_group リソースで retention_in_days = 30 を設定しておくのが定番です。

コンテナ定義の awslogs-stream-prefix も必ず設定してください。複数のFargateタスクが同時に動いている状態でこの設定が抜けると、各タスクのCloudWatchログストリーム名が衝突し、どのタスクのログか判別できなくなります。障害発生時のログ調査で大幅な時間ロスにつながるため、設定漏れがないかタスク定義を作成したときに確認する習慣をつけてください。

4. タスク定義でコンテナのヘルスチェックを設定する

ALBのヘルスチェックとは別に、タスク定義レベルでコンテナのヘルスチェックを設定できます。
コンテナ自体の死活監視をECSが行うことで、ALBよりも早い段階で異常なタスクを検出して入れ替えられます。

# タスク定義のコンテナヘルスチェック設定(JSON抜粋) { "healthCheck": { "command": [ "CMD-SHELL", "curl -f http://localhost:8080/health || exit 1" ], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 60 } } # startPeriodは起動直後のヘルスチェック猶予時間(秒) # アプリの起動に時間がかかる場合は大きめに設定する # 例: Javaアプリで起動に45秒かかるなら startPeriod を 60~90 に設定する

startPeriodの設定を忘れると、アプリが完全に起動する前にヘルスチェックが失敗し、タスクが繰り返し再起動されるループに陥ることがあります。アプリの起動時間を計測してから設定してください。

5. タスク定義JSONをファイルで管理してバージョン管理に乗せる

コンソール上でタスク定義の変更を手動で積み重ねていくと、ステージングと本番の設定が少しずつずれていく「構成ドリフト」が発生します。「本番だけなぜか動かない」という問題の多くは、このずれが原因です。コンソール変更はGitに残らないため、「誰が何をいつ変えたか」の追跡にCloudTrailを掘る必要が生じます。

対策として、タスク定義のJSONをファイルとして切り出してGitで管理する方法を推奨します。

# 既存のタスク定義をJSONファイルとして書き出す(初回) aws ecs describe-task-definition --task-definition myapp-task --query 'taskDefinition' --output json > task-definition.json # JSONファイルを編集した後、新しいリビジョンとして登録する aws ecs register-task-definition --cli-input-json file://task-definition.json # Gitで変更を管理する git add task-definition.json git commit -m "Update myapp-task: increase memory from 1024 to 2048"

JSONファイルをGitで管理することで、変更差分をPRでレビューできるようになります。障害対応でクラスターを作り直す場面でも、同じファイルから構成を再現できます。GitHub ActionsなどCI/CDパイプラインと連携する場合、このJSONファイルをベースにコンテナイメージのタグだけを差し替えてサービスを更新するワークフローを組むと、Terraform等のインフラ自動化ツール導入に向けた良い足がかりになります。

TerraformでECSをIaC管理する場合は、本番のECSサービスリソースにlifecycle { prevent_destroy = true }を設定しておくと、terraform destroyによる誤削除を防止できます。terraform plan -refresh-onlyを定期実行することで、コンソールでの手動変更(構成ドリフト)もコードレベルで即座に検出でき、JSONファイルとGitだけで管理するよりも早い段階でずれに気づけます。
Terraformでコンテナ定義を書くときの推奨パターン:
container_definitions に長いJSONをHCL内に記述する場合は、jsonencode([{...}]) 関数を使うのが実務のベストプラクティスです。変数展開が自然に使え、terraform plan の差分も見やすくなります。以下はTerraformでのタスク定義例です。

resource "aws_ecs_task_definition" "app" { family = "myapp-task" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" cpu = "512" memory = "1024" execution_role_arn = aws_iam_role.ecs_task_execution.arn task_role_arn = aws_iam_role.ecs_task.arn container_definitions = jsonencode([ { name = "myapp" image = "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:latest" essential = true portMappings = [ { containerPort = 8080 protocol = "tcp" } ] environment = [ { name = "APP_ENV", value = var.environment }, { name = "LOG_LEVEL", value = "info" } ] logConfiguration = { logDriver = "awslogs" options = { "awslogs-group" = "/ecs/myapp" "awslogs-region" = "ap-northeast-1" "awslogs-stream-prefix" = "ecs" } } } ]) }

サイドカーパターン(ログ収集エージェントなどの補助コンテナ)を追加する場合は、補助コンテナに essential = false を設定してください。essential = true のコンテナが終了するとタスク全体が停止しますが、essential = false にしておくとサイドカーの終了でメインコンテナが道連れにされるのを防げます。
Auto ScalingとTerraformを併用する場合は、ECSサービスリソースにlifecycle { ignore_changes = [task_definition, desired_count] }を設定してください。task_definition も含めることで、CI/CDパイプラインがデプロイしたタスク定義リビジョンをTerraformが誤って古いバージョンに戻す問題を防げます。desired_count を含めないと、Auto Scalingが変更したタスク数が terraform apply 実行のたびに強制リセットされます。

6. Terraformのtemplatefileでコンテナ定義を外部ファイル化する

jsonencode([{...}])をHCLインラインで書く方法はシンプルな構成に適しています。コンテナ数が増えてサイドカーパターンを組んだり、環境変数の数が多くなってHCLが長大になってきた場合は、templatefile関数を使ってコンテナ定義を独立した.json.tplファイルに切り出す方法が有効です。

# container_def.json.tpl (コンテナ定義テンプレートファイル) [ { "name": "${container_name}", "image": "${image_uri}", "essential": true, "portMappings": [ { "containerPort": ${container_port}, "protocol": "tcp" } ], "environment": [ { "name": "APP_ENV", "value": "${app_env}" } ], "secrets": [ { "name": "DB_PASSWORD", "valueFrom": "${ssm_db_password_arn}" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "${log_group}", "awslogs-region": "${region}", "awslogs-stream-prefix": "ecs" } } } ]

# task.tf (templatefileを使ったタスク定義) resource "aws_ecs_task_definition" "app" { family = "${var.env}-${var.app_name}" network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] cpu = var.task_cpu memory = var.task_memory execution_role_arn = aws_iam_role.ecs_task_execution.arn task_role_arn = aws_iam_role.ecs_task.arn container_definitions = templatefile("${path.module}/container_def.json.tpl", { container_name = var.app_name image_uri = "${var.ecr_repository_url}:${var.image_tag}" container_port = var.container_port app_env = var.env ssm_db_password_arn = var.ssm_db_password_arn log_group = aws_cloudwatch_log_group.app.name region = var.region }) }

templatefileの利点は、コンテナ定義ファイルをHCLと分離して管理できることです。Terraform HCLに不慣れなアプリ開発者でもJSONテンプレートは読めるため、コンテナの環境変数やイメージURIのレビューがしやすくなります。コンテナ数が1~2つで環境変数が少ない段階ではjsonencodeのほうがファイルをまたがず見通しがよく、複雑な構成になってきたらtemplatefileに切り替えるのが実務での判断基準です。

7. TerraformのState分割でタスク定義変更の影響を局所化する

タスク定義はapplyのたびにリビジョン番号が増えます。VPCやALBと同じStateに詰め込むと、コンテナイメージを更新するたびにVPCの差分チェックも走り、applyが遅くなるうえ誤ってVPCを変更してしまうリスクが生まれます。本番環境では変更頻度の違いに応じてリソースを分割するのが鉄則です。

terraform/ ├── environments/ │ ├── prd/ │ │ ├── vpc/ # VPC・サブネット(変更頻度: 低) │ │ ├── alb/ # ALB・Target Group(変更頻度: 低) │ │ └── ecs/ # Cluster・Task Def・Service(変更頻度: 高) │ └── stg/ │ └── ...(同じ構造) └── modules/ ├── vpc/ ├── alb/ └── ecs-service/

ECS(変更頻度: 高)をVPC・ALB(変更頻度: 低)と分けるポイントは、Stateの独立性にあります。同じStateに詰め込むと、デプロイ中にVPCやALBの設定が予期せず影響を受けるリスクと常に隣り合わせになります。特にCI/CDパイプラインとTerraformを組み合わせる場合、ECSのみを独立したStateで管理することで、タスク定義のリビジョン増分による影響範囲をコンテナ層だけに局所化できます。

既存のVPC・ALBリソースを別のStateから参照する場合は、data "aws_vpc"・data "aws_subnets"などのdataソースでARNやIDを取得するのが実務の定番パターンです(前述のサブネット設計パターン参照)。

ECSサービスとALBの連携設定

ECSサービスはタスクの台数を管理し、ALBと組み合わせることで外部からのHTTPリクエストを各Fargateタスクに分散します。

1. セキュリティグループの設計方針

ALBとECSタスクのセキュリティグループは、それぞれ役割を分けて設計します。正しい構成の原則は「FargateタスクのインバウンドはALBのセキュリティグループIDのみを許可する」ことです。0.0.0.0/0(全インターネット)を開けてしまうと、タスクに直接アクセスできてしまいます。

# ALB用セキュリティグループ(インターネットからの80/443を受け入れる) aws ec2 create-security-group --group-name myapp-alb-sg --description "Security group for ALB" --vpc-id vpc-0123456789abcdef0 # ALB SGにインターネットからのHTTPS(443)を許可する aws ec2 authorize-security-group-ingress --group-id sg-alb-xxxxxxxx --protocol tcp --port 443 --cidr 0.0.0.0/0 # ECSタスク用セキュリティグループ(ALBのSGからのみコンテナポートを受け入れる) aws ec2 create-security-group --group-name myapp-ecs-sg --description "Security group for ECS Fargate tasks" --vpc-id vpc-0123456789abcdef0 # ECSタスクSGへの許可:ソースをALBのSGに限定する(0.0.0.0/0は絶対に使わない) aws ec2 authorize-security-group-ingress --group-id sg-ecs-xxxxxxxx --protocol tcp --port 8080 --source-group sg-alb-xxxxxxxx

このセキュリティグループ設計がポイントです。ECSタスクのインバウンドルールに「CIDRで0.0.0.0/0を使わず、ALBのセキュリティグループIDをソースに指定する」ことで、ALBを経由したリクエストのみがタスクに届く構成になります。IPアドレス管理も不要になるため、TerraformなどのIaCツールとも相性が良い設計です。

2. ターゲットグループの作成

ALBとECSサービスを接続するために、まずターゲットグループを作成します。
Fargateの場合はターゲットタイプを「ip」に設定することが必要です(EC2タイプではなく)。

# ターゲットグループを作成する(FargateはIPターゲット) aws elbv2 create-target-group --name myapp-tg --protocol HTTP --port 8080 --vpc-id vpc-0123456789abcdef0 --target-type ip --health-check-path /health --health-check-interval-seconds 30 --healthy-threshold-count 2 --unhealthy-threshold-count 3 # 出力例 { "TargetGroups": [ { "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/myapp-tg/abc123def456", "TargetGroupName": "myapp-tg", "Protocol": "HTTP", "Port": 8080, "HealthCheckPath": "/health", ... } ] }

ターゲットグループのプロトコルは必ず HTTP または HTTPS を指定してください。TCP を指定するとALBとの連携でエラーになります(後述のトラブルシュート参照)。ヘルスチェックパス(/health)はアプリケーション側で必ず実装してください。200 OKを返すだけの軽量なエンドポイントで十分です。
TerraformでALBとターゲットグループをIaC管理する場合は、aws_lb・aws_lb_listener・aws_lb_target_groupの3リソースを定義します。

# alb.tf (ALB・リスナー・ターゲットグループのTerraform定義) resource "aws_lb" "main" { name = "${var.env}-alb" internal = false load_balancer_type = "application" security_groups = [aws_security_group.alb.id] subnets = var.public_subnet_ids # 本番環境では誤削除防止のため必ずtrueに設定する enable_deletion_protection = var.environment == "prod" ? true : false tags = { Env = var.environment } } resource "aws_lb_listener" "http" { load_balancer_arn = aws_lb.main.arn port = 80 protocol = "HTTP" default_action { type = "forward" target_group_arn = aws_lb_target_group.app.arn } } resource "aws_lb_target_group" "app" { name = "${var.env}-${var.app_name}-tg" port = var.container_port protocol = "HTTP" target_type = "ip" # Fargateはawsvpcモードなので "ip" 固定 vpc_id = var.vpc_id health_check { path = var.health_check_path matcher = "200" interval = 30 timeout = 5 healthy_threshold = 2 unhealthy_threshold = 3 } # デプロイ時に古いタスクの登録解除を速くする(デフォルト300秒) deregistration_delay = 30 tags = { Env = var.environment } }

本番環境のALBにはenable_deletion_protection = trueを必ず設定してください。設定がないとterraform destroyの誤実行でALBが削除されてしまいます。var.environment == "prod" ? true : falseの三項演算子で環境ごとに自動切り替えするのが実務の定番パターンです。
ターゲットグループのderegistration_delayはデフォルト300秒です。30秒に短縮するとローリングデプロイ時に古いタスクの登録解除が素早くなり、デプロイ全体の所要時間を大幅に削減できます。ただしアプリのリクエスト最大処理時間より短くすると処理中の接続が切断されるため、アプリのレイテンシ特性に合わせて調整してください。

3. ECSサービスでロードバランサーを設定する

ECSサービスを作成する際にALBのターゲットグループを紐付けます。

# ECSサービスを作成する(ALBと連携・マルチAZ・デプロイcircuit breaker有効) aws ecs create-service --cluster myapp-cluster --service-name myapp-service --task-definition myapp-task:1 --desired-count 2 --launch-type FARGATE --network-configuration "awsvpcConfiguration={ subnets=[subnet-0a1b2c3d4e,subnet-0f1e2d3c4b], securityGroups=[sg-ecs-xxxxxxxx], assignPublicIp=DISABLED }" --load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/myapp-tg/abc123,containerName=myapp,containerPort=8080" --deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true},maximumPercent=200,minimumHealthyPercent=100"

4点押さえてください。

・assignPublicIp=DISABLED: Fargateタスクはプライベートサブネットに配置し、インターネットからの直接アクセスを遮断します。外部公開はALBをパブリックサブネットに置いて行うのが本番設計の基本パターンです
・maximumPercent=200/minimumHealthyPercent=100: ゼロダウンタイムデプロイの核心設定です。「旧タスクを落とす前に新タスクを起動して切り替える」ブルーグリーン風のデプロイが実現します。たとえばdesired-count=2の場合、更新時に最大4タスク(200%)まで起動を許可し、常に2タスク(100%)の稼働を保証するため、ユーザーリクエストを止めずにデプロイできます
・deploymentCircuitBreaker: デプロイ中にタスクが連続して起動失敗した場合、自動的に以前のタスク定義にロールバックします。IAM権限不足やヘルスチェック失敗でデプロイが詰まった際に、サービス全体が止まる事態を防げます
・リソースの作成順序: ECSサービスはALBとリスナーが完全に作成されてから作成してください。ALBリスナーが存在しない状態でECSサービスを作成すると、ロードバランサーへの登録が失敗します。CLIで手動作成する場合は、aws elbv2 describe-listeners でリスナーの存在を確認してからECSサービスのコマンドを実行してください

subnetsには2つの異なるAZのサブネットを指定することで、自動的にマルチAZ分散配置されます。

TerraformでIaC管理する場合は、depends_on と lifecycle の設定が重要です。

resource "aws_ecs_service" "app" { name = "myapp-service" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.app.arn desired_count = 2 launch_type = "FARGATE" network_configuration { subnets = data.aws_subnets.private.ids security_groups = [aws_security_group.ecs_tasks.id] assign_public_ip = false } load_balancer { target_group_arn = aws_lb_target_group.app.arn container_name = "myapp" # タスク定義のname と完全一致させること container_port = 8080 } depends_on = [aws_lb_listener.https] # ALBリスナーが先に存在することを保証 deployment_minimum_healthy_percent = 100 deployment_maximum_percent = 200 # Javaアプリなど起動に時間がかかるコンテナは猶予時間を設定する health_check_grace_period_seconds = 60 # タスク定義更新時に自動で再デプロイをトリガーする force_new_deployment = true lifecycle { ignore_changes = [task_definition, desired_count] } }

force_new_deployment = true は、タスク定義のリビジョンARNが変わった際にECSサービスのローリング更新を自動でトリガーします。これを設定しないと、タスク定義のHCLを変更してapplyしても、サービスが古いリビジョンのコンテナを使い続けることがあります。CI/CDパイプライン(GitHub Actionsのecspresso等)がイメージ更新を担当する運用ではignore_changes = [task_definition]に切り替え、Terraformはインフラ構成専任にする分担が現場での標準的なパターンです。

container_name はタスク定義のコンテナ名と完全一致させることが必要です。スペルミスがあると terraform apply は正常終了しますが、サービスがDEPLOYINGのまま進まなくなります。lifecycle { ignore_changes = [task_definition, desired_count] } を設定することで、CI/CDパイプラインがデプロイしたタスク定義リビジョンやAuto Scalingが変更したタスク数をTerraformが戻そうとしなくなり、TerraformとデプロイパイプラインとAuto Scalingが共存できます。

depends_on = [aws_lb_listener.https] は省略できません。ECSサービスはALBリスナーを直接参照する式を持たないため、Terraformが暗黙的な依存関係を推論できません。明示的に指定しないとALBリスナーの作成完了前にECSサービスが起動しようとし、ターゲットグループへの登録が失敗します。

health_check_grace_period_seconds は、タスク起動直後のALBヘルスチェック猶予時間です。この設定がないと、アプリが完全に起動する前にALBがヘルスチェック失敗と判定し、タスクが繰り返し入れ替わるループに陥ることがあります。Javaアプリや起動の重いWebサーバーでは60~120秒を目安に設定してください。

サービス作成後はタスクが正常に起動しているかを確認してください。RunningカウントとDesiredカウントが一致し、Pendingが0になっている状態が正常です。

# ECSサービスの起動状態を確認する aws ecs describe-services --cluster myapp-cluster --services myapp-service --query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount,Pending:pendingCount}' # 正常な出力例 { "Status": "ACTIVE", "Running": 2, "Desired": 2, "Pending": 0 }

PendingカウントがDesiredと一致したままRunningが増えない場合は、タスクの起動に失敗している可能性があります。後述のトラブルシュートを参照してください。
terraform applyがECSサービスの起動待ちでタイムアウトする場合は、timeoutsブロックを追加して待機時間を延ばせます(デフォルトは作成・更新ともに10分)。ただし待機時間の延長は根本解決にはならないため、まずサービスのイベントログで起動失敗の原因を確認してください(後述のトラブルシュート参照)。

# aws_ecs_serviceにtimeoutsブロックを追加する例 resource "aws_ecs_service" "app" { # ... 既存の設定 ... timeouts { create = "20m" update = "20m" delete = "20m" } }

4. デプロイ戦略の選択:Rolling UpdateとBlue/Greenデプロイ

ECSサービスのデプロイ戦略はデフォルトのRolling Updateで十分なケースがほとんどです。最も重要なのがdeployment_circuit_breakerの有効化で、新しいタスクが連続して起動失敗した場合に直前の安定リビジョンへ自動ロールバックします。

resource "aws_ecs_service" "app" { # ... deployment_minimum_healthy_percent = 100 deployment_maximum_percent = 200 deployment_circuit_breaker { enable = true # デプロイ失敗時に自動ロールバックを有効化 rollback = true } }

ロールバックが発動したかどうかは aws ecs describe-services のdeployments[].rolloutStateで確認できます。rolloutState: "FAILED"のエントリが残っていれば、circuit_breakerによるロールバックの証拠です。

Blue/Greenデプロイ(CodeDeploy連携)が必要になるのは、APIに破壊的変更があり「古いコンテナと新しいコンテナが同時に稼働する期間」を完全に排除したい場合です。Terraformでの設定は次のとおりです。

resource "aws_ecs_service" "app" { # ... deployment_controller { type = "CODE_DEPLOY" # Rolling Updateの場合は"ECS"(デフォルト) } lifecycle { # CodeDeployが管理するためTerraformでは差分を無視する ignore_changes = [task_definition, load_balancer] } }

ただしBlue/GreenではTerraformの管理範囲が狭まり、CodeDeployのappspec.yaml・デプロイグループ・承認ルールを別途管理する必要が出てきます。可用性要件が特別高くない場合はRolling Update + circuit_breakerのほうがシンプルで運用コストが低いのが実態です。新規構築であれば、まずRolling Updateで運用し、APIの破壊的変更が実際に問題になった段階でBlue/Greenへ移行するのが現実的な判断です。

5. マルチAZのサブネット設計パターン

本番設計でFargateタスクとALBを配置するサブネットの基本構成は次のとおりです。
用途 サブネット種別 配置するリソース
10.0.1.0/24(ap-northeast-1a) パブリック ALB、NATゲートウェイ
10.0.2.0/24(ap-northeast-1c) パブリック ALB、NATゲートウェイ
10.0.11.0/24(ap-northeast-1a) プライベート Fargateタスク
10.0.12.0/24(ap-northeast-1c) プライベート Fargateタスク
Fargateタスクをプライベートサブネットに配置した場合、ECRからのイメージpullやSecretsManagerへのアクセスにはNATゲートウェイまたはVPCエンドポイントが必要です。コスト最適化が重要な場合はVPCエンドポイント(ecr.dkr・ecr.api・s3・secretsmanager・logs)を設定することでNATゲートウェイの転送費用を削減できます。

TerraformでIaCとして管理する場合、既存VPCやサブネットをdata sourceで参照する構成が一般的です。

# 既存VPC・サブネットをTagベースで参照する(Terraform data source) data "aws_vpc" "main" { tags = { Name = "${var.project}-vpc" } } data "aws_subnets" "private" { filter { name = "vpc-id" values = [data.aws_vpc.main.id] } tags = { Tier = "private" } } data "aws_subnets" "public" { filter { name = "vpc-id" values = [data.aws_vpc.main.id] } tags = { Tier = "public" } }

6. タスク配置戦略でAZ均等分散を明示する

ECSサービスはデフォルトでAZ均等分散(SPREAD)のタスク配置戦略を取りますが、明示的に設定することが推奨されます。

# タスク配置戦略を確認する aws ecs describe-services --cluster myapp-cluster --services myapp-service --query 'services[0].placementStrategy' # AZ均等分散の配置戦略を設定する(update-service) aws ecs update-service --cluster myapp-cluster --service myapp-service --placement-strategy "[{"type":"spread","field":"attribute:ecs.availability-zone"}]"

desired-countを2以上にしたうえでAZ均等分散を設定することで、1つのAZで障害が発生しても残りのAZで処理を継続できます。desired-countを1にした場合は意味がないため、必ず2以上に設定してください。

AWSのインフラ設計をLinuxエンジニアとして体系的に学びたい方は、
AWSをLinuxエンジニアが学ぶためのロードマップもあわせてご覧ください。

Auto ScalingでFargateを自動スケールさせる

ECSサービスのAuto Scalingを設定することで、トラフィックの増減に応じてFargateタスクの台数を自動調整できます。

1. Application Auto Scalingの設定

ECSのAuto ScalingはApplication Auto Scalingを通じて設定します。

# ECSサービスをAuto Scalingのターゲットとして登録する aws application-autoscaling register-scalable-target --service-namespace ecs --scalable-dimension ecs:service:DesiredCount --resource-id service/myapp-cluster/myapp-service --min-capacity 2 --max-capacity 10

min-capacityは本番環境では最低2にしてください。1にすると単一タスクが落ちた瞬間に全サービスが停止します。max-capacityはトラフィックのピーク時に必要な最大タスク数を設定します。TerraformでIaC管理する場合は aws_appautoscaling_target リソースに相当します。

2. スケールアウト・スケールインのポリシー設定

CPU使用率をトリガーにするターゲットトラッキングポリシーが、最もシンプルで安定した設定です。

# CPU使用率70%を目標にするターゲットトラッキングポリシーを設定する aws application-autoscaling put-scaling-policy --service-namespace ecs --scalable-dimension ecs:service:DesiredCount --resource-id service/myapp-cluster/myapp-service --policy-name myapp-cpu-scaling --policy-type TargetTrackingScaling --target-tracking-scaling-policy-configuration '{ "TargetValue": 70.0, "PredefinedMetricSpecification": { "PredefinedMetricType": "ECSServiceAverageCPUUtilization" }, "ScaleOutCooldown": 60, "ScaleInCooldown": 300 }' # メモリ使用率80%でもスケールするポリシーを追加する(CPU・メモリの両方を監視) aws application-autoscaling put-scaling-policy --service-namespace ecs --scalable-dimension ecs:service:DesiredCount --resource-id service/myapp-cluster/myapp-service --policy-name myapp-memory-scaling --policy-type TargetTrackingScaling --target-tracking-scaling-policy-configuration '{ "TargetValue": 80.0, "PredefinedMetricSpecification": { "PredefinedMetricType": "ECSServiceAverageMemoryUtilization" }, "ScaleOutCooldown": 60, "ScaleInCooldown": 300 }'

ScaleOutCooldown(スケールアウト後のクールダウン)は60秒程度にしてトラフィック急増に素早く対応できるようにします。ScaleInCooldown(スケールイン後のクールダウン)は300秒以上に設定して、過剰なスケールイン(タスク削減)による不安定な状態を防ぎます。

CPUとメモリの両方にポリシーを設定しておくと、CPU負荷よりも先にメモリが逼迫するタイプのアプリ(JVMヒープを多く使うJavaアプリなど)でもスケールアウトが機能します。現場では「スケールアウトは素早く、スケールインは慎重に」が鉄則です。

3. TerraformでAuto ScalingをIaC管理する

CLIで設定したAuto ScalingをTerraformで管理する場合は、aws_appautoscaling_target と aws_appautoscaling_policy の2リソースを定義します。ECSサービスの lifecycle { ignore_changes = [desired_count] } と組み合わせることで、Auto Scalingが変更したタスク数をTerraformが次回applyで上書きしないように保護できます。

# Auto ScalingターゲットとしてECSサービスを登録する resource "aws_appautoscaling_target" "ecs" { max_capacity = 10 min_capacity = 2 resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}" scalable_dimension = "ecs:service:DesiredCount" service_namespace = "ecs" } # CPU使用率70%を目標にするターゲットトラッキングポリシー resource "aws_appautoscaling_policy" "cpu" { name = "myapp-cpu-scaling" policy_type = "TargetTrackingScaling" resource_id = aws_appautoscaling_target.ecs.resource_id scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension service_namespace = aws_appautoscaling_target.ecs.service_namespace target_tracking_scaling_policy_configuration { predefined_metric_specification { predefined_metric_type = "ECSServiceAverageCPUUtilization" } target_value = 70.0 scale_in_cooldown = 300 # スケールインは慎重に(5分) scale_out_cooldown = 60 # スケールアウトは素早く(60秒) } } # メモリ使用率80%でのスケールポリシーも追加する場合 resource "aws_appautoscaling_policy" "memory" { name = "myapp-memory-scaling" policy_type = "TargetTrackingScaling" resource_id = aws_appautoscaling_target.ecs.resource_id scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension service_namespace = aws_appautoscaling_target.ecs.service_namespace target_tracking_scaling_policy_configuration { predefined_metric_specification { predefined_metric_type = "ECSServiceAverageMemoryUtilization" } target_value = 80.0 scale_in_cooldown = 300 scale_out_cooldown = 60 } }

resource_id はリソース参照(aws_ecs_cluster.main.name・aws_ecs_service.app.name)で構築することがポイントです。文字列リテラルで書くとリソース名を変更したときに不整合が生じます。ECSサービス側の lifecycle { ignore_changes = [desired_count] } も合わせて設定しておかないと、terraform apply のたびにAuto Scalingが調整したタスク数が元の desired_count 値に戻されてしまいます。

IAMロール設計|タスクロールと実行ロールの違い

FargateのIAM設計で最も混乱しやすいのが、タスクロールと実行ロールの違いです。

・実行ロール(Task Execution Role): ECSがタスクを起動・管理するために必要な権限。ECRからイメージをpullする、SecretsManagerからシークレットを取得する、CloudWatch Logsにログを書き込む権限が含まれる。タスクの「起動作業」を行うECSエージェントが使うロール。
・タスクロール(Task Role): コンテナの中で動くアプリケーションが他のAWSサービスを呼び出すための権限。S3にファイルをアップロードする、DynamoDBを参照する等の権限をここで設定する。

混同しやすいですが、実行ロールはタスクを「起動する」ための権限、タスクロールはコンテナが「実際に使う」権限と覚えてください。

# 実行ロールに必要な最低限のポリシー(AWSマネージドポリシー) # AmazonECSTaskExecutionRolePolicy に以下が含まれる # - ecr:GetAuthorizationToken # - ecr:BatchGetImage # - logs:CreateLogStream # - logs:PutLogEvents # SecretsManagerを使う場合は追加で以下が必要 # - secretsmanager:GetSecretValue # - kms:Decrypt(KMSで暗号化している場合) # 実行ロールの信頼ポリシー(ecs-tasks.amazonaws.com が引き受ける) { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "ecs-tasks.amazonaws.com"}, "Action": "sts:AssumeRole" }] } # タスクロールの作成例(S3とDynamoDBを使うアプリケーション用) aws iam create-role --role-name myapp-task-role --assume-role-policy-document '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "ecs-tasks.amazonaws.com"}, "Action": "sts:AssumeRole" }] }' # タスクロールに最小権限ポリシーをアタッチする # 例: S3バケットへのPUTのみ許可(GetやDeleteは付与しない) aws iam put-role-policy --role-name myapp-task-role --policy-name myapp-s3-put --policy-document '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:PutObject"], "Resource": "arn:aws:s3:::myapp-uploads/*" }] }'

タスクロールは最小権限の原則で設計してください。S3のPUTだけが必要ならPutObjectのみを許可し、バケット全体への全権限は付けません。アプリケーションが侵害された場合の被害範囲を最小化できます。

実行ロールとタスクロールの両方に、信頼ポリシーで ecs-tasks.amazonaws.com を Principal に指定することが必要です。ec2.amazonaws.com を誤って指定するとロールのアタッチ自体は通っても権限が機能しないため注意してください。
TerraformでIaC管理する場合は、aws_iam_policy_documentデータソースを使ってポリシーをHCLで記述するのが推奨です。JSONを文字列で渡すよりもTerraformの型チェックが効き、terraform planの差分も見やすくなります。信頼ポリシー(Trust Policy)も同様にdata.aws_iam_policy_documentで定義し、assume_role_policyに渡すと、変更履歴をGitのdiffで追跡できます。aws_iam_role_policy_attachmentでAWSマネージドポリシー(AmazonECSTaskExecutionRolePolicy)をアタッチし、アプリ固有の権限はaws_iam_role_policyのインラインポリシーで最小権限を付与するパターンが現場の定石です。

以下はTerraform HCLでの実行ロール・タスクロール定義の例です。

# タスク実行ロール(ECR pull・CloudWatch Logs書き込み用) resource "aws_iam_role" "ecs_task_execution" { name = "${var.project}-ecs-task-execution" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ecs-tasks.amazonaws.com" } }] }) } resource "aws_iam_role_policy_attachment" "ecs_task_execution" { role = aws_iam_role.ecs_task_execution.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } # タスクロール(コンテナ内アプリがAWSサービスを呼び出す場合) resource "aws_iam_role" "ecs_task" { name = "${var.project}-ecs-task" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ecs-tasks.amazonaws.com" } }] }) } # アプリ固有の権限をインラインポリシーで付与する(例: S3・DynamoDB) resource "aws_iam_role_policy" "ecs_task_app" { name = "${var.project}-ecs-task-app" role = aws_iam_role.ecs_task.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:PutObject"] Resource = "${aws_s3_bucket.app_uploads.arn}/*" }, { Effect = "Allow" Action = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query"] Resource = aws_dynamodb_table.app.arn } ] }) }

TerraformでSecretsManagerのシークレットをコンテナに注入する場合、実行ロールにsecretsmanager:GetSecretValueの権限追加が必要です。既存のシークレットをdata "aws_secretsmanager_secret"データソースで参照することで、ARNをハードコードせずに安全に管理できます。

信頼ポリシーの定義にはjsonencode()を使う方法のほかに、data "aws_iam_policy_document"データソースを使う方法もあります。データソース方式はTerraformの型チェックが有効になり、terraform planでポリシーの変更差分がより見やすくなるため、チームで管理する場合は検討してください。

# aws_iam_policy_documentデータソースを使った信頼ポリシー定義(jsonencode代替) data "aws_iam_policy_document" "ecs_assume" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } } } # 既存のSecretsManagerシークレットをデータソースで参照する data "aws_secretsmanager_secret" "db" { name = "prod/app/db-password" } # 実行ロールにsecretsmanager:GetSecretValueを追加する(対象ARNのみに限定) resource "aws_iam_role_policy" "ecs_execution_secrets" { name = "${var.project}-ecs-execution-secrets" role = aws_iam_role.ecs_task_execution.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = ["secretsmanager:GetSecretValue"] Resource = [data.aws_secretsmanager_secret.db.arn] }] }) }

KMSで暗号化したシークレットを使う場合はkms:Decrypt権限も対象ARNを限定したうえで追加してください。"Resource": "*"で全シークレットを対象にするのは最小権限の原則に反します。

本番でよくあるトラブルシュート

「ResourceInitializationError: unable to pull secrets or registry auth」が出る

ECRからコンテナイメージをpullできない状態です。Fargateタスクが起動する前段階で失敗しているため、CloudWatch Logsにもエラーが残りません。原因はほぼ2択です。

・Task Execution RoleにAmazonECSTaskExecutionRolePolicyが付与されていない
・プライベートサブネット内のFargateタスクからECRへの経路がない(NATゲートウェイまたはVPCエンドポイントが未設定)

VPCエンドポイントでNATゲートウェイを省いてECRに接続する場合は、以下の3つのエンドポイントが最低限必要です。

# ECRイメージpullに必要なVPCエンドポイントを確認する aws ec2 describe-vpc-endpoints --filters "Name=vpc-id,Values=vpc-0123456789abcdef0" --query 'VpcEndpoints[*].{service:ServiceName,state:State}' # ECRのイメージpullに必要な3つのエンドポイント # - com.amazonaws.ap-northeast-1.ecr.dkr → Dockerイメージの取得 # - com.amazonaws.ap-northeast-1.ecr.api → ECR API呼び出し # - com.amazonaws.ap-northeast-1.s3 → ECRレイヤーの取得(S3バケット経由) # CloudWatch Logsへの書き込みに必要 # - com.amazonaws.ap-northeast-1.logs # SecretsManagerを使う場合も必要 # - com.amazonaws.ap-northeast-1.secretsmanager

タスクが起動直後にSTOPPEDになる

ECSサービスでタスクが起動してもすぐにSTOPPEDになる場合、原因の大半は以下の3つです。

・ECRへのアクセス権限不足(実行ロールの権限漏れ)
・コンテナの起動コマンドが即時終了している(プロセスがフォアグラウンドで起動していない)
・SecretsManagerからシークレットが取得できない

停止理由はコンソールまたはCLIで確認できます。
停止理由の大まかな原因と対処は次の表で素早く絞り込めます。
エラーメッセージ 原因と対処
CannotPullContainerError ECRのイメージURIが間違っている、またはタスク実行ロールにecr:GetAuthorizationTokenが不足している
exitCode: 1 / 137 アプリの起動エラー(CloudWatch Logs /ecs/myapp を確認)またはメモリ不足(OOMKill)。memoryを増やして再デプロイする
ResourceInitializationError NATゲートウェイ経由のインターネット疎通がなくECRに届かない。VPCエンドポイント(ecr.dkr・ecr.api・s3)の設定を確認する
Essential container exited essential: true のコンテナが終了した。まずCloudWatch Logsでアプリのエラーログを確認する

# 停止したタスクの停止理由を確認する aws ecs describe-tasks --cluster myapp-cluster --tasks arn:aws:ecs:ap-northeast-1:123456789012:task/myapp-cluster/a1b2c3d4 --query 'tasks[0].{status:lastStatus,stopCode:stopCode,reason:stoppedReason}' # 出力例(実行ロールの権限不足の場合) { "status": "STOPPED", "stopCode": "TaskFailedToStart", "reason": "CannotPullContainerError: pull image manifest has been retried 5 time(s): ..." } # CloudWatch Logsでコンテナのエラーログを確認する aws logs get-log-events --log-group-name /ecs/myapp --log-stream-name ecs/myapp/a1b2c3d4 --limit 50

タスク定義を更新してもECSサービスが再デプロイされない

TerraformでタスクのHCLを変更して terraform apply しても、ECSサービスが古いリビジョンのコンテナを使い続ける場合があります。最も多い原因は task_definition の参照に文字列リテラルを使っているケースです。

# NG: 文字列リテラルで固定するとapply後もリビジョンが変わらない task_definition = "myapp-task:5" # NG: :LATESTのような曖昧な参照も同様に問題になる task_definition = "myapp-task:LATEST" # OK: リソース参照にすると常に最新リビジョンのARNが使われる task_definition = aws_ecs_task_definition.app.arn

aws_ecs_task_definition.app.arn でリソース参照にすると、Terraformが新しいリビジョンを登録するたびにECSサービスのタスク定義ARNも更新され、次の terraform apply でサービスが新しいリビジョンで再デプロイされます。文字列リテラルで書いた場合は terraform plan に差分が出ないため、変更が反映されていないことに気づきにくいです。

また、lifecycle { ignore_changes = [task_definition] } をECSサービスに設定している場合は、この設定がTerraformによるタスク定義の更新を完全にスキップします。CI/CDパイプラインが別途デプロイを担う構成では意図的な設定ですが、Terraformでタスク定義変更を反映させたい場合は一時的に ignore_changes から task_definition を除外してapplyし、その後設定を戻す手順が必要です。

ALBのヘルスチェックが通らない

ECSサービスとALBを連携させた際に、タスクが起動してもすぐにDRAININGになる場合は、ALBのヘルスチェックが失敗しています。

よくある原因と確認コマンドは次のとおりです。

・セキュリティグループでALBからコンテナポートへのアクセスが許可されていない
・ヘルスチェックパスが間違っている(/health が404を返している)
・コンテナのアプリが起動完了前にヘルスチェックが来てタイムアウトしている

# ターゲットグループのヘルスチェック状態を確認する aws elbv2 describe-target-health --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/myapp-tg/abc123 --query 'TargetHealthDescriptions[*].{id:Target.Id,port:Target.Port,state:TargetHealth.State,reason:TargetHealth.Reason}' # 出力例(unhealthyの場合) [ { "id": "10.0.1.45", "port": 8080, "state": "unhealthy", "reason": "Target.ResponseCodeMismatch" } ]

Reasonフィールドで原因を素早く絞り込めます。

・Target.Timeout: ヘルスチェックのタイムアウト時間内にアプリが応答できていません。アプリの起動時間が長い場合やヘルスチェックのintervalが短すぎる場合に発生します。interval・timeout・unhealthyThresholdCount を大きめの値に設定してから再確認してください
・Target.ResponseCodeMismatch: ヘルスチェックのパスはアクセスできているが、HTTPステータスコードが期待値(デフォルト200)と異なります。ターゲットグループの matcher 設定とアプリが実際に返すステータスコードを照合してください
・Target.FailedHealthChecks: ヘルスチェックのパス自体に到達できていません。セキュリティグループのインバウンドルール(ALBのSGからコンテナポートへの許可)を確認してください

Linuxのポート疎通確認の詳細はLinuxのポート確認コマンド(ss・lsof)が参考になります。コンテナが期待するポートで待ち受けているかを確認する際にも同じ考え方が使えます。

セキュリティグループの設定では、ALBのセキュリティグループをソースとして、コンテナポートへのインバウンドを許可する設定が正しい構成です。0.0.0.0/0を許可する設定は本番環境では避けてください。

ALBのヘルスチェック失敗でタスクが入れ替わり続ける

ECSサービスのdesired_count = 2なのに、コンソールで見ると常に3台以上のタスクが起動・停止を繰り返している場合、ヘルスチェック失敗によるタスク入れ替えループが疑われます。確認はサービスのeventsフィールドから入ります。

# ECSサービスの直近イベントを確認する aws ecs describe-services --cluster myapp-cluster --services myapp-service --query 'services[0].events[:5]' # 出力例(ヘルスチェック失敗で登録・解除が繰り返されている場合) [ { "createdAt": "2026-10-05T09:23:41+09:00", "message": "service myapp-service deregistered 1 targets in target-group myapp" }, { "createdAt": "2026-10-05T09:23:10+09:00", "message": "service myapp-service registered 1 targets in target-group myapp" } ]

register → deregisterが繰り返されているのはヘルスチェック失敗の典型パターンです。Terraformのaws_lb_target_group設定を見直してください。

・ヘルスチェックパス(health_check.path)がアプリの実際のエンドポイントと一致しているか確認する。不一致の場合は404が返り続ける
・health_check.matcherとアプリが実際に返すHTTPステータスコードが一致しているか確認する
・deregistration_delayがデフォルト300秒のままになっていないか確認する。30秒に短縮するとローリングデプロイの所要時間も大幅に減る

Fargateタスクから外部への通信が失敗する

Fargateタスクをプライベートサブネットに配置した場合、ECRへのイメージpullや外部APIへのHTTPアクセスが失敗することがあります。

よくある原因は次の2つです。

・NATゲートウェイが設定されていない(またはルートテーブルがNATを向いていない)
・VPCエンドポイントが未設定でNATゲートウェイも存在しない

# プライベートサブネットのルートテーブルを確認する # 0.0.0.0/0 がNATゲートウェイ(nat-xxxxxxxxx)に向いているか確認 aws ec2 describe-route-tables --filters "Name=association.subnet-id,Values=subnet-0a1b2c3d4e" --query 'RouteTables[0].Routes[*].{dest:DestinationCidrBlock,gw:GatewayId,nat:NatGatewayId}' # 出力例(正常な場合) [ {"dest": "10.0.0.0/16", "gw": "local", "nat": null}, {"dest": "0.0.0.0/0", "gw": null, "nat": "nat-0abc12345def67890"} ] # VPCエンドポイント経由でECRにアクセスする場合は以下のエンドポイントを作成する # - com.amazonaws.ap-northeast-1.ecr.dkr (イメージpull) # - com.amazonaws.ap-northeast-1.ecr.api (API呼び出し) # - com.amazonaws.ap-northeast-1.s3 (ECRレイヤーの取得) # - com.amazonaws.ap-northeast-1.logs (CloudWatch Logsへの書き込み) # - com.amazonaws.ap-northeast-1.secretsmanager (シークレット取得) aws ec2 describe-vpc-endpoints --filters "Name=vpc-id,Values=vpc-0123456789abcdef0" --query 'VpcEndpoints[*].{service:ServiceName,state:State}'

NATゲートウェイとVPCエンドポイントは用途が異なります。外部インターネットへのアクセスが必要な場合はNATゲートウェイが必要です。AWSサービス(ECR・SecretsManager・CloudWatch Logs)へのアクセスのみであればVPCエンドポイントだけで完結でき、NATゲートウェイのコストを節約できます。

「ECS service failed to stabilize」でDesired Countに達しない

サービスを作成・更新してもタスクがDesired Countに達しない状態です。AWSコンソールの「サービスイベント」タブにエラーメッセージが出るため、まずそこを確認してください。CLIで確認する場合は次のコマンドを使います。

# ECSサービスのイベントを確認する(直近の10件) aws ecs describe-services --cluster myapp-cluster --services myapp-service --query 'services[0].events[:10].{createdAt:createdAt,message:message}' # 出力例(ヘルスチェック失敗でタスクが入れ替わり続けている場合) [ { "createdAt": "2024-01-15T10:23:45+09:00", "message": "(service myapp-service) (task a1b2c3d4) failed container health checks." }, { "createdAt": "2024-01-15T10:23:20+09:00", "message": "(service myapp-service) registered 1 targets in (target-group arn:...)" } ]

よくある原因3つ:
・ヘルスチェックパスの設定ミス:/healthで200が返るかcurlで事前に確認する。アプリ側に/healthエンドポイントが実装されていないケースが多い
・セキュリティグループの許可漏れ:ALBのSGからECSタスクのSGへのインバウンドが開いているか確認する。ポート番号の指定ミスにも注意
・ECRイメージ取得の失敗:実行ロール(Task Execution Role)にAmazonEC2ContainerRegistryReadOnlyまたはAmazonECSTaskExecutionRolePolicyがアタッチされているか確認する

「InvalidParameterException」でターゲットグループのプロトコルが合わない

ターゲットグループ作成時にプロトコルを TCP にするとALBとの連携でエラーになります。NLB(Network Load Balancer)向けの設定をALBに適用してしまった場合に発生します。ALBのターゲットグループには必ず HTTP または HTTPS を指定してください。

# ターゲットグループのプロトコルを確認する aws elbv2 describe-target-groups --names myapp-tg --query 'TargetGroups[0].{Protocol:Protocol,TargetType:TargetType,Port:Port}' # 出力例(間違ってTCPになっている場合) { "Protocol": "TCP", "TargetType": "ip", "Port": 8080 } # 修正方法: ターゲットグループを削除して再作成する(protocolはHTTPを指定) aws elbv2 delete-target-group --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/myapp-tg/abc123 aws elbv2 create-target-group --name myapp-tg --protocol HTTP --port 8080 --vpc-id vpc-0123456789abcdef0 --target-type ip --health-check-path /health

「terraform apply」でECSサービスの作成・更新がタイムアウトする

TerraformでECSサービスを作成・更新した際に「Still creating... (10m elapsed)」のままタイムアウトエラーになる場合、タスクが起動に失敗し続けています。まずサービスのイベントログで原因を確認してください。

# ECSサービスのイベントを確認する(直近の10件) aws ecs describe-services --cluster myapp-cluster --services myapp-service --query 'services[0].events[:10].{createdAt:createdAt,message:message}' # よくあるイベントメッセージ例(ヘルスチェック失敗でタスクが入れ替わり続けている場合) [ { "createdAt": "2024-01-15T10:23:45+09:00", "message": "(service myapp-service) (task a1b2c3d4) failed container health checks." } ]

よくある原因と対処の優先順位は次のとおりです。

・ヘルスチェックが通らない:/healthで200が返るかcurlで事前に確認する。ターゲットグループのmatcherとアプリが実際に返すステータスコードが一致しているか照合する
・ECRからイメージを取得できない:実行ロール(Task Execution Role)にAmazonECSTaskExecutionRolePolicyがアタッチされているか確認する。プライベートサブネットではNATゲートウェイまたはVPCエンドポイント(ecr.dkr・ecr.api・s3)が必要
・セキュリティグループの許可漏れ:ALBのSGからECSタスクのSGへのインバウンドが開いているか確認する

根本原因を修正してからterraform applyを再実行してください。原因特定を急がず一時的に待機時間を延ばしたい場合はtimeoutsブロックを使いますが、タイムアウトを延ばすだけでは問題は解消されません。

plan時に「known after apply」が多すぎる

terraform planで次のような出力が頻発することがあります。

# aws_ecs_service.app will be updated in-place ~ resource "aws_ecs_service" "app" { ~ task_definition = "arn:.../myapp:3" -> (known after apply) ... }

task_definition = (known after apply)は、aws_ecs_task_definition.app.arnの値がapply前に確定しないために出ます。タスク定義のリビジョン番号がapply実行後にしか決まらないECS固有の仕様から来ており、エラーではなく正常な挙動です。

変更内容を事前に確認してからapplyしたい場合は、planをファイルに保存してから適用する手順が安全です。

# planをファイルに保存して内容を確認してからapplyする terraform plan -out=tfplan terraform show tfplan # 変更内容を確認 terraform apply tfplan # 確認済みのplanを適用

terraform plan -out=tfplanでplanを保存し、terraform showで実際に変更されるリソースの全属性を確認してから適用するのは、本番環境でのapply前の標準手順として推奨します。

「TaskDefinition is inactive」でサービス更新が失敗する

TerraformでECSサービスを更新した際に次のエラーが出ることがあります。

Error: error updating ECS Service: InvalidParameterException: TaskDefinition is inactive.

タスク定義がINACTIVE状態になっているケースです。terraform destroyを実行するとTerraformはタスク定義をINACTIVEに変更します。この状態で同名のfamilyで再度applyすると新しいリビジョンが作られますが、サービスが古いARNを参照している場合にこのエラーが発生します。

解決策:ECSサービスのタスク定義参照には固定値ではなく aws_ecs_task_definition.app.arn を使ってください。この記法であればapply時点の最新リビジョンのARNが自動的に使われます。また、lifecycle { ignore_changes = [task_definition] } をECSサービスに設定している場合は、一度その設定を外してapplyし、最新のARNを反映させてから設定し直す手順が必要になります。

本記事のまとめ

AWS ECS・Fargateを使ったコンテナ本番環境の設計手順をまとめます。
設計項目 ポイント
起動タイプの選定 新規構築はFargate一択。EC2管理が不要でコスト最適化しやすい
クラスター設定 Container Insightsを必ず有効化。メトリクスが障害調査の基礎になる
タスク定義のCPU・メモリ 小さく始めてCloudWatchで監視しながら調整する
シークレット管理 機密情報はSecretsManagerのARN参照。環境変数への平文埋め込みは禁止
ログ設定 awslogsドライバーでCloudWatch Logsへ転送。保持期間とawslogs-stream-prefixを必ず設定する
コンテナヘルスチェック タスク定義でhealthCheckを設定しstartPeriodを適切に設定する
タスク定義のバージョン管理 JSONファイルをGitで管理して構成ドリフトを防ぐ。IaCツール導入の起点にもなる
コンテナ定義のTerraform管理 jsonencode([{...}])で動的生成する。サイドカーコンテナはessential=falseに設定する
TerraformのState分割 ECSはVPC・ALBと別のディレクトリ・Stateに分割する。変更頻度の違いを吸収して影響範囲を局所化できる
セキュリティグループ設計 ECSタスクのインバウンドはALBのSG IDに限定。0.0.0.0/0は使わない
サブネット設計 Fargateはプライベートサブネット、ALBはパブリックサブネットに配置する
マルチAZ分散 サブネットを複数AZに配置しdesired-countを2以上にする
ゼロダウンタイムデプロイ maximumPercent=200/minimumHealthyPercent=100で旧タスク停止前に新タスクを起動する
デプロイ自動ロールバック deploymentCircuitBreaker有効化。デプロイ失敗時のサービス停止を防ぐ
デプロイ戦略の選択 通常はRolling Update + circuit_breakerで十分。APIの破壊的変更がある場合のみBlue/Green(CodeDeploy)を検討する
force_new_deployment タスク定義更新時にサービスの再デプロイを自動トリガー。CI/CDが担当する場合はignore_changesに切り替える
ヘルスチェック猶予時間 health_check_grace_period_secondsをECSサービスに設定。起動の重いアプリで60~120秒を目安にする
Auto Scaling CPU・メモリ使用率でターゲットトラッキング。min=2、スケールインは慎重に
Auto ScalingのTerraform管理 aws_appautoscaling_targetとaws_appautoscaling_policyをリソース参照で定義する
IAMロール 実行ロール(起動時の権限)とタスクロール(アプリの権限)を分離する
アウトバウンド設計 プライベートサブネットにはNATゲートウェイまたはVPCエンドポイントが必要
IaCによる構成管理 depends_on=[aws_lb_listener.https]で作成順序を保証し、lifecycle.ignore_changes=[task_definition, desired_count]でCI/CDと共存させる
task_definitionの参照方法 文字列リテラルではなくaws_ecs_task_definition.app.arnのリソース参照を使う。リビジョン更新が自動で反映される

ECS・Fargateの設計はVPC設計と密接に連動しています。
プライベートサブネット・NATゲートウェイ・セキュリティグループの設計と組み合わせることで、より堅牢な本番環境が完成します。

AWSのインフラ設計をLinuxエンジニアとして体系的に学びたい方は、
AWSをLinuxエンジニアが学ぶためのロードマップもご覧ください。

マルチAZを含む冗長設計の実践については、
AWSの冗長設計を体系的に学ぶ上級ガイドでさらに詳しく解説しています。

AWSのコンテナ設計を「実務の型」として身につけませんか?

ECSやFargateの設定方法は調べれば分かります。でも「なぜその構成を選ぶのか」を説明できますか?
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。

※ 「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。