10. ECS / ECR / ALB
コンテナを Fargate(サーバ管理レス)で動かす標準構成。ECR(イメージ置き場)→ ECS(実行)→ ALB(外向きエンドポイント) の 3 点セット。
登場人物
| 用語 | 役割 |
|---|---|
| ECR | Docker イメージの置き場(プライベート) |
| ECS Cluster | 論理的な「実行環境のグループ」 |
| Task Definition | 「どのイメージを、どの CPU/Memory で、どの IAM で動かすか」の設計図 |
| ECS Service | 「Task Definition を N 個常時起動しておく」マネージャ |
| Fargate | EC2 不要のサーバレス実行モード |
| ALB | 外からの HTTP/HTTPS を受けて Task に振り分ける |
| Target Group | ALB の振り分け先プール(ECS Service が登録) |
構成図
[Internet] ──HTTPS──→ [ALB (public subnet)]
│
↓ Target Group
┌─────────────────────┐
│ ECS Service (count=3)│
│ Task A (Fargate) │
│ Task B (Fargate) │ ← private subnet
│ Task C (Fargate) │
└─────────────────────┘
│ pull image
↓
[ECR Repo]
ECR(コンテナレジストリ)
resource "aws_ecr_repository" "app" {
name = "myapp"
image_tag_mutability = "IMMUTABLE" # 同じタグの再 push 禁止(推奨)
image_scanning_configuration {
scan_on_push = true # 脆弱性スキャン
}
encryption_configuration {
encryption_type = "AES256"
}
}
# 古いイメージを自動削除
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [{
rulePriority = 1
description = "Keep last 30 images"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 30
}
action = { type = "expire" }
}]
})
}
ECS Cluster
resource "aws_ecs_cluster" "main" {
name = "main"
setting {
name = "containerInsights"
value = "enabled" # CloudWatch のメトリクス強化
}
}
# Fargate Capacity Provider を有効化
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy {
capacity_provider = "FARGATE"
weight = 100
}
}
Task Definition
2 つのロールに注意:
- execution_role_arn: ECS Agent(プラットフォーム側)が ECR から pull したり、CloudWatch Logs に書いたりするための権限
- task_role_arn: コンテナ 内のアプリ自身 が AWS API を叩く時に使う権限(S3、DynamoDB へのアクセス等)
data "aws_iam_policy_document" "ecs_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role" "task_execution" {
name = "ecs-task-execution"
assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}
resource "aws_iam_role_policy_attachment" "task_execution" {
role = aws_iam_role.task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# アプリが使うロール(必要に応じて権限を足す)
resource "aws_iam_role" "task" {
name = "ecs-task-app"
assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/myapp"
retention_in_days = 30
}
resource "aws_ecs_task_definition" "app" {
family = "myapp"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "512" # 0.5 vCPU
memory = "1024" # 1 GiB
execution_role_arn = aws_iam_role.task_execution.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([{
name = "app"
image = "${aws_ecr_repository.app.repository_url}:latest"
essential = true
portMappings = [{
containerPort = 8080
protocol = "tcp"
}]
environment = [
{ name = "LOG_LEVEL", value = "info" },
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.app.name
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "app"
}
}
}])
}
ALB(負荷分散)
resource "aws_security_group" "alb" {
name_prefix = "alb-"
vpc_id = aws_vpc.main.id
lifecycle { create_before_destroy = true }
}
resource "aws_vpc_security_group_ingress_rule" "alb_https" {
security_group_id = aws_security_group.alb.id
from_port = 443
to_port = 443
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
}
resource "aws_lb" "main" {
name = "main"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = [for s in aws_subnet.public : s.id]
enable_deletion_protection = true
}
resource "aws_lb_target_group" "app" {
name = "app"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip" # Fargate は ip ターゲット必須
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 3
interval = 30
timeout = 5
matcher = "200"
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
certificate_arn = aws_acm_certificate.app.arn
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
ECS Service
resource "aws_security_group" "task" {
name_prefix = "task-"
vpc_id = aws_vpc.main.id
lifecycle { create_before_destroy = true }
}
resource "aws_vpc_security_group_ingress_rule" "task_from_alb" {
security_group_id = aws_security_group.task.id
referenced_security_group_id = aws_security_group.alb.id
from_port = 8080
to_port = 8080
ip_protocol = "tcp"
}
resource "aws_ecs_service" "app" {
name = "app"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 3
launch_type = "FARGATE"
network_configuration {
subnets = [for s in aws_subnet.private : s.id]
security_groups = [aws_security_group.task.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "app"
container_port = 8080
}
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200 # ローリング更新(一時的に倍まで)
# ECS deploy circuit breaker(自動ロールバック)
deployment_circuit_breaker {
enable = true
rollback = true
}
# latest タグを使う場合、image 変更で再起動できないので task_definition の更新で再起動を促す
lifecycle {
ignore_changes = [desired_count] # オートスケーリングが管理する場合
}
}
CI/CD との連携
実運用では、image を
latest ではなく git SHA や semver タグ で push し、Task Definition の image を Terraform 変数 var.image_tag で受け取る。CI(GitHub Actions)で terraform apply -var image_tag=$GITHUB_SHA がデプロイの正体。