この記事について
前回、クラスタ・ネットワーク・タスク定義・IAMロールという「土台」を作りました。今回はその土台の上に、常駐するappタスクをECSサービスとして動かす部分を扱います。構築順としては、タスク定義ができた後に「どう稼働させ続けるか」を決める段階にあたります。
- 基盤編: クラスタ、ネットワーク、タスク定義の分離設計、IAMロール
- デプロイ・可用性編(本記事): ECSサービス設定、Auto Scaling、ECS Exec、イメージ管理
- 監視・アラート編: CloudWatchアラーム、失敗通知、ログ監視
- バッチ・運用編: Step Functionsオーケストレーション、コスト管理、運用手順
ステップ1: タスク数と配置の基本方針を決める
最初に決めるべきは「最低何タスク動かすか」です。
variable "desired_count" {
type = number
default = 2 # 単一タスクだとデプロイ中・単一AZ障害時に可用性が落ちるため最低2
}
variable "min_capacity" {
type = number
default = 2
}
variable "max_capacity" {
type = number
default = 4
}
resource "aws_ecs_service" "app" {
name = "${var.project}-${var.env}-app-service"
cluster = aws_ecs_cluster.this.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids # 複数AZのサブネットを指定
security_groups = [aws_security_group.ecs.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = var.target_group_arn
container_name = "app"
container_port = 8080
}
# タスクをAZ間で常に均等配置(1AZ1タスク)に保つ
availability_zone_rebalancing = "ENABLED"
}
availability_zone_rebalancing = "ENABLED"により、タスクが特定AZに偏った場合にECSが自動で再配置します。private_subnet_idsに複数AZのサブネットを渡すことが前提になります。
ステップ2: デプロイ時に可用性を落とさない設定
タスクの起動・入れ替え中に何が起きるかを制御します。
resource "aws_ecs_service" "app" {
# (前項からの続き)
deployment_minimum_healthy_percent = 50
deployment_maximum_percent = 200
# 新タスクがヘルスチェックに失敗し続ける場合、自動的に直前のタスク定義へロールバック
deployment_circuit_breaker {
enable = true
rollback = true
}
# アプリ起動が完了する前にALBヘルスチェック失敗でタスクが強制終了されないよう、起動猶予期間を設ける
health_check_grace_period_seconds = 120
# CodePipelineによるデプロイ時にタスク定義が更新されるため
# Terraformがdesired_countやtask_definitionを上書きしないよう無視
lifecycle {
ignore_changes = [task_definition, desired_count]
}
depends_on = [aws_iam_role_policy_attachment.task_execution]
}
デプロイサーキットブレーカーは、新しいタスク定義でデプロイしたタスクがヘルスチェックに失敗し続けた場合に、自動的に直前の正常なタスク定義へロールバックする機能です。ロールバック判定の失敗数しきい値はdesired_countから動的に算出され、clamp(desired_count × 0.5, 最小3, 最大200)という計算式になります。desired_countが小さい環境でも最低3回の失敗を許容してから切り戻すため、一時的なヘルスチェック失敗で過敏にロールバックしません。2026年7月のAWSアップデートでこのしきい値はカスタマイズ可能になっており、より厳格な運用が必要な場合は個別設定を検討してください。
ただしこのサーキットブレーカーはヘルスチェック失敗ベースのロールバックのみであり、ヘルスチェックは通るがレイテンシ悪化やエラー率上昇が起きているケースは検知できません。CloudWatchアラームと連動したロールバックについては次回の監視編で扱います。
health_check_grace_period_seconds = 120は、アプリ起動直後にALBヘルスチェックが失敗してもタスクを強制終了しないための猶予期間です。Spring Bootアプリの起動時間(DBコネクションプール初期化、Redis接続確立を含む)を考慮して設定します。
lifecycle.ignore_changesでtask_definitionとdesired_countを無視しているのは、CodePipelineによる継続的デプロイがこれらの値を更新するため、Terraformとの二重管理による競合(perpetual diff)を避ける目的です。インフラの構造(SG、ロール、クラスタ設定など)はTerraformが、デプロイの中身(イメージタグ、タスク数)はCI/CDパイプラインが管理する、という責務分界をコード上で明示しています。
ステップ3: グレースフルシャットダウンのタイムアウトを揃える
デプロイでタスクが終了する際、3つのタイムアウト設定を連携させる必要があります。
| レイヤー | 設定 | 値 |
|---|---|---|
| ALB | deregistration_delay |
30秒 |
| ECSタスク定義 | stopTimeout |
30秒 |
| Spring Boot | timeout-per-shutdown-phase |
25秒 |
container_definitions = jsonencode([{
name = "app"
# SIGTERM送信からSIGKILLまでの猶予秒数。Fargateデフォルトと同値(30秒)だが、
# ALBのderegistration_delay・アプリのtimeout-per-shutdown-phase(25秒)との
# 整合が重要なため明示する。
stopTimeout = 30
# ...
}])
ALBが登録解除の猶予を最も長く取り(30秒)、アプリ側のグレースフルシャットダウン処理(25秒)がその範囲内で完了するように短く設定します。これにより、アプリで処理中のリクエストがALB、ECSのタイムアウトにより強制終了されないようにします。
ステップ4: ECS Execを実際に使える状態にする
前回、タスクロールにECS Exec用のssmmessages:*権限を付与しました。ただしこの権限設定だけではECS Execは動きません。サービス側とクラスタ側で追加設定が必要になります。
resource "aws_ecs_service" "app" {
# (前項からの続き)
# タスクロールの権限設定だけでは有効化されない。
# このフラグがないとECS Exec(execute-command)は使用できない
enable_execute_command = true
}
さらに、誰がいつコンテナに接続したかを追跡できるよう、セッションログの出力先をクラスタ側で設定します。
resource "aws_ecs_cluster" "this" {
name = "${var.project}-${var.env}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
configuration {
execute_command_configuration {
logging = "OVERRIDE"
log_configuration {
cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_exec.name
}
}
}
}
resource "aws_cloudwatch_log_group" "ecs_exec" {
name = "/ecs/${var.project}-${var.env}/exec"
retention_in_days = 90 # 監査目的のため他ログより長めに保持
}
ECS Execはコンテナ内へのシェル接続という強い権限であるため、「IAM権限」「サービスのフラグ」「セッションログ」の3点セットで初めて安全に使える状態になります。IAM権限だけ付与してこの2つを見落とすと、機能しないか、機能しても監査証跡が残らない状態になってしまいます。
ステップ5: Auto Scalingをメモリ観点でも見る
CPU使用率をターゲットとしたスケーリングに加え、メモリ使用率のポリシーも用意します。
resource "aws_appautoscaling_target" "ecs" {
max_capacity = var.max_capacity
min_capacity = var.min_capacity
resource_id = "service/${aws_ecs_cluster.this.name}/${aws_ecs_service.app.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
name = "${var.project}-${var.env}-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
scale_out_cooldown = 60
}
}
resource "aws_appautoscaling_policy" "memory" {
name = "${var.project}-${var.env}-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 = 75.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
CPUのみを見ていると、GCやコネクションプールでメモリ消費が先に張り付くワークロードでは、CPUが閾値に達する前にOOM Killでタスクが落ちるリスクがあります。CPU・メモリ両方のターゲット追跡ポリシーを設定しておくと、どちらか一方が閾値を超えた時点でスケールアウトが働きます。
スケールアウトのクールダウンを60秒、スケールインを300秒と非対称にしているのは、負荷急増への反応を速く、負荷低下後のスケールインは慎重に(短時間の変動でタスクを頻繁に増減させない)行うための一般的なパターンです。
ステップ6: イメージタグの運用方針を決める
variable "app_image_tag" {
type = string
default = "latest" # ローカル検証用のデフォルト。CI/CDでは必ず明示的なタグを渡す
}
default = "latest"のままCI/CDパイプラインを組むと、意図しないイメージでの再デプロイや、ロールバック時にどのビルド内容が動いていたか再現できない問題が起きます。パイプライン側では、コミットSHAやビルド番号を含む一意なタグ(またはイメージダイジェスト)を明示的に渡す運用にし、latestはローカルでの動作確認用途に限定します。