前提条件
- 第1回から第6回までの構築が完了していること
- Dockerイメージがローカルでbuildできていること
- AWS CLIが設定済みで、ECRへのアクセス権限があること
- こちらからDockerコンテナでの実行環境が準備できていること
Terraform実行環境の起動
前回と同様に、Dockerコンテナ内で作業を進めます。
# コンテナを起動してbashに入る
docker-compose run --rm terraform
これ以降のコマンドは、すべてこのコンテナ内で実行します。
基礎知識
Task Definitionとは
Task Definitionは、ECSでコンテナを実行するための設計図として認識しておきましょう。
WEBアプリケーション開発者の視点から見ると
docker-compose.yml
のようなものだと理解してください。ローカル開発ではdocker-compose
でコンテナの設定を定義しますが、ECSではTask Definitionがその役割を担います。
docker-compose.yml | Task Definition |
---|---|
image | container_definitions.image |
ports | portMappings |
environment | environment |
volumes | mountPoints |
command | command |
ECS Serviceとは
Serviceは、Task Definitionに基づいてタスクを常に希望数だけ実行・管理するリソースです。
なぜServiceが必要か
機能 | 説明 |
---|---|
タスク数の維持 | 指定した数のタスクを常に維持 |
ヘルスチェック連携 | 異常なタスクを自動的に再起動 |
ロードバランサー連携 | ALBへの自動登録・解除 |
デプロイ管理 | 新バージョンへの安全な更新 |
ヘルスチェックの階層構造
ECSでは3つのレベルでヘルスチェックが実行されます。それぞれ異なる役割を持ち、組み合わせることで高可用性を実現できます。
レベル | 設定場所 | 監視主体 | 失敗時の動作 | 必須/推奨 |
---|---|---|---|---|
コンテナレベル | Dockerfile HEALTHCHECK | Docker Engine | コンテナステータスがunhealthy |
推奨 |
タスクレベル | Task Definition healthCheck | ECS Agent | タスクの再起動 | オプション |
ALBレベル | Target Group Health Check | ALB | トラフィックから除外 | 必須 |
本番環境では、DockerfileでHEALTHCHECK
を定義するため、Task Definitionのhealthcheckは省略します。
デプロイ戦略
ECSは複数のデプロイ戦略をサポートしています。
戦略 | 説明 | 使用場面 |
---|---|---|
Rolling Update | 古いタスクを段階的に新しいタスクに置き換え | 一般的なアップデート |
Blue/Green | 新旧環境を並行稼働させて切り替え | ダウンタイムゼロが必須の場合 |
External | 外部ツールでデプロイを管理 | CI/CDツール連携時 |
今回はRolling Updateを使用します。
今回作成するリソース
リソース | 説明 | 数量 |
---|---|---|
ECS Task Definition | コンテナ実行の設計図 | 1個 |
ECS Service | タスクの実行・管理 | 1個 |
Fargate Tasks | 実行されるコンテナインスタンス | 2個 |
ECRイメージ | デプロイするDockerイメージ | 1個 |
Terraformコードの実装
変数の追加
Task DefinitionとService用の変数を追加します。
# terraform/variables.tf に追加
variable "task_cpu" {
description = "タスクのCPU単位(256 = 0.25 vCPU)"
type = string
default = "256"
}
variable "task_memory" {
description = "タスクのメモリ(MB)"
type = string
default = "512"
}
variable "app_count" {
description = "実行するタスク数"
type = number
default = 2
}
Fargate CPU/メモリの組み合わせ
CPU | 利用可能なメモリ |
---|---|
256 (.25 vCPU) | 512MB, 1GB, 2GB |
512 (.5 vCPU) | 1GB〜4GB (1GB刻み) |
1024 (1 vCPU) | 2GB〜8GB (1GB刻み) |
Task Definition
コンテナの実行設定を定義します。Secrets Managerとの連携も設定します。
# terraform/ecs_service.tf
# ECS Task Definition
resource "aws_ecs_task_definition" "app" {
family = "${var.project_name}-${var.environment}-app"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.task_cpu
memory = var.task_memory
execution_role_arn = aws_iam_role.ecs_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_role.arn
container_definitions = jsonencode([
{
name = "${var.project_name}-${var.environment}-app"
image = "${aws_ecr_repository.app.repository_url}:latest"
essential = true
portMappings = [
{
containerPort = 3000
protocol = "tcp"
}
]
environment = [
{
name = "NODE_ENV"
value = "production"
},
{
name = "PORT"
value = "3000"
}
]
secrets = [
{
name = "DB_HOST"
valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:host::"
},
{
name = "DB_PORT"
valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:port::"
},
{
name = "DB_NAME"
valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:dbname::"
},
{
name = "DB_USER"
valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:username::"
},
{
name = "DB_PASSWORD"
valueFrom = "${aws_secretsmanager_secret.db_credentials.arn}:password::"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.ecs.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
# DockerfileのHEALTHCHECKがあるため、ここでの定義は省略
# 本番環境でより厳密な制御が必要な場合のみ追加
}
])
tags = {
Name = "${var.project_name}-${var.environment}-app-task"
}
}
healthCheckを省略する理由
DockerfileにHEALTHCHECK
が定義されているため、Task Definitionでの重複定義は不要です。これにより、ヘルスチェックのロジックが一元管理され、メンテナンス性が向上します。
ECS Service
タスクの実行とALBとの連携を管理するServiceを作成します。
# terraform/ecs_service.tf
# ECS Service
resource "aws_ecs_service" "app" {
name = "${var.project_name}-${var.environment}-app-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.app_count
launch_type = "FARGATE"
network_configuration {
security_groups = [aws_security_group.ecs.id]
subnets = aws_subnet.private[*].id
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.ecs.arn
container_name = "${var.project_name}-${var.environment}-app"
container_port = 3000
}
deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 100
deployment_circuit_breaker {
enable = true
rollback = true
}
lifecycle {
ignore_changes = [task_definition]
}
depends_on = [
aws_lb_listener.http,
aws_iam_role_policy.ecs_task_role_policy
]
tags = {
Name = "${var.project_name}-${var.environment}-app-service"
}
}
deployment_circuit_breakerの役割
デプロイに失敗した際、自動でロールバックする機能です。新しいタスクが正常に起動しない場合、自動的にデプロイ前の安定したバージョンに戻します。
NAT Gateway 再登場
現状、NAT Gatewayが無効化されているため、プライベートサブネットのECSタスクがAWSサービスに接続できないので、覚悟を決めて有効化します。
# terraform/terraform.tfvars
enable_nat_gateway = true
※ 学習中でもこれだけで月額コスト約$45発生するため、コストが気になる場合は検証が済んだらすぐにリソースを消しましょう
IAM権限の追加
ECS Execution RoleからSecrets Managerへのアクセスを可能にするように、IAM権限を追加していきます。
# terraform/ecs.tf
# ECS Execution Role用のカスタムポリシー
resource "aws_iam_role_policy" "ecs_execution_role_policy" {
name = "${var.project_name}-${var.environment}-ecs-execution-policy"
role = aws_iam_role.ecs_execution_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = aws_secretsmanager_secret.db_credentials.arn
}
]
})
}
出力の追加
作成したリソースの情報を出力に追加します。
# terraform/outputs.tf に追加
# ECS Service関連の出力
output "ecs_service_name" {
description = "ECS Service名"
value = aws_ecs_service.app.name
}
output "ecs_task_definition_arn" {
description = "Task Definition ARN"
value = aws_ecs_task_definition.app.arn
}
output "ecs_task_definition_family" {
description = "Task Definition Family"
value = aws_ecs_task_definition.app.family
}
リソースを作成
Terraformコンテナ内で以下のコマンドを実行します。
# terraformディレクトリへ移動
cd terraform
# 実行計画の確認
terraform plan
# リソースの作成
terraform apply
この時点では、ECRにコンテナイメージが存在しないため、ECS Serviceはタスクを正常に起動できません。これは想定通りの動作です。
ECRへのイメージプッシュ
ECRリポジトリURLの確認
Terraformコンテナ内でECRリポジトリのURLを確認します。
# ECRリポジトリURLの確認
terraform output ecr_repository_url
出力例
123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/my-project-dev-app
イメージのビルドとプッシュ
別ターミナル(ローカル環境)で作業します。
AWS Management ConsoleのECRサービスから、作成したリポジトリを選択し、「プッシュコマンドの表示」をクリックします。表示された手順に従って、以下の作業を行います。
- ECRへのログイン
- Dockerイメージのビルド
- イメージのタグ付け
- ECRへのプッシュ
APIリポジトリ(aws-hands-on)のbackend
ディレクトリで作業することを前提としていますが、コンテナ化の知識があれば独自の実装を利用しても問題ありません。(Terraformリポジトリとは別のリポジトリです)
プッシュ完了後、ECRでイメージが登録されていることを確認します。
Docker ビルドエラー
docker build
実行時に「buildx プラグインが見つからない」エラー- レガシービルダー使用の警告メッセージ
- パッケージ依存関係エラーでビルド失敗
Dockerのプラグインファイルが破損している可能性がある
レガシービルダーを強制使用する
DOCKER_BUILDKIT=0 docker build -t xxx-dev-app .
# Apple Silicon の場合
DOCKER_BUILDKIT=0 docker build --platform linux/amd64 -t xxx-dev-app .
ECS Serviceの再起動
イメージをプッシュした後、ECS Serviceを更新してタスクを起動します。
# Terraformコンテナ内で実行
cd terraform
# サービスの強制更新(新しいタスクを起動)
aws ecs update-service \
--cluster $(terraform output -raw ecs_cluster_id) \
--service $(terraform output -raw ecs_service_name) \
--force-new-deployment
# デプロイ状況の確認
aws ecs describe-services \
--cluster $(terraform output -raw ecs_cluster_id) \
--services $(terraform output -raw ecs_service_name) \
--query 'services[0].{Status:status,RunningCount:runningCount,DesiredCount:desiredCount}' \
--output table
3〜5分程度でタスクが起動し、RunningCount
が2
になればデプロイ完了です。
動作確認
ALB経由でのアクセス確認
ALBのDNS名を取得し、APIにアクセスします。
# ALB DNS名の取得
ALB_DNS=$(terraform output -raw alb_dns_name)
# ルートエンドポイントへのアクセス
curl http://$ALB_DNS/
期待されるレスポンス
{
"message":"Hono CRUD API Server",
"version":"1.0.0",
"environment":"production",
"endpoints": {
"health":"/health",
"users":"/api/users"
}
}
ヘルスチェックの確認
# ヘルスチェックエンドポイント
curl http://$ALB_DNS/health
期待されるレスポンス
{
"status":"healthy",
"timestamp":"2025-07-28T16:19:23.881Z"
}
CRUD操作の確認
ユーザー管理APIの動作を確認します。
# ユーザー作成
curl -X POST http://$ALB_DNS/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "Test User",
"email": "[email protected]"
}'
# ユーザー一覧取得
curl http://$ALB_DNS/api/users
# 特定ユーザー取得(IDは作成時のレスポンスから取得)
curl http://$ALB_DNS/api/users/1
CloudWatch Logsの確認
アプリケーションログが正しく出力されていることを確認します。
# ログストリームの一覧
aws logs describe-log-streams \
--log-group-name $(terraform output -raw cloudwatch_log_group_name) \
--order-by LastEventTime \
--descending \
--query 'logStreams[0:3].logStreamName' \
--output table
# 最新ログの確認(Ctrl+Cで停止)
aws logs tail $(terraform output -raw cloudwatch_log_group_name) \
--follow \
--format short
トラブルシューティング
タスクが起動しない場合
タスクの停止理由を確認
# 停止したタスクの理由確認
aws ecs describe-tasks \
--cluster $(terraform output -raw ecs_cluster_id) \
--tasks $(aws ecs list-tasks \
--cluster $(terraform output -raw ecs_cluster_id) \
--desired-status STOPPED \
--query 'taskArns[0]' \
--output text) \
--query 'tasks[0].{StopCode:stopCode,StoppedReason:stoppedReason}' \
--output table
よくある原因
- ECRイメージが存在しない(プッシュ忘れ)
- Secrets Managerへのアクセス権限不足
- ECRイメージのpull権限不足
- メモリ不足(タスクのメモリ設定を増やす)
ヘルスチェックが失敗する場合
Target Groupのヘルスステータスを確認
# ターゲットヘルスの確認
aws elbv2 describe-target-health \
--target-group-arn $(terraform output -raw target_group_arn) \
--query 'TargetHealthDescriptions[*].{Target:Target.Id,Health:TargetHealth.State,Description:TargetHealth.Description}' \
--output table
その他
タスクは起動するもののDB接続がうまくいかない場合はコンテナ側の問題説がある。
今回、サンプルコードではDrizzleを使用していましたが、drizzle.config.ts
を認識しない問題やRDS用の設定などの考慮漏れが発生し少しハマりました。
そんな時はコンテナのbuild -> push -> 「ECS Serviceの再起動」セクションに立ち返って確認しましょう。
次のステップ
次回は、VPCエンドポイントを設定することで、NAT Gatewayを使用せずに完全なプライベート通信を実現します。
重要:次回の作業前に、コスト削減のため一度 terraform destroy
でリソースを削除してから再構築します。
- ECR、CloudWatch Logs、S3のVPCエンドポイント作成
- NAT Gatewayを使用しないプライベート通信
- VPC Flow Logsによる通信の可視化
- ネットワークコストの最適化