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 が登録) |
構成図
ECR(コンテナレジストリ)
# ECR リポジトリ(Docker イメージのプライベート置き場)
resource "aws_ecr_repository" "app" {
name = "myapp"
# IMMUTABLE = 同じタグでの上書き push を禁止(バージョン管理事故防止)
image_tag_mutability = "IMMUTABLE" # 同じタグの再 push 禁止(推奨)
# push 時に自動で脆弱性スキャン(基本版は無料)
image_scanning_configuration {
scan_on_push = true # 脆弱性スキャン
}
encryption_configuration {
encryption_type = "AES256" # KMS にする場合は KMS + kms_key
}
}
# 古いイメージを自動削除(ストレージコスト管理)
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
# ライフサイクルルール:30 個より多いイメージを古い順に削除
policy = jsonencode({
rules = [{
rulePriority = 1
description = "Keep last 30 images"
selection = {
tagStatus = "any" # タグ有無問わず対象
countType = "imageCountMoreThan"
countNumber = 30
}
action = { type = "expire" }
}]
})
}
ECS Cluster
# ECS Cluster:論理的なグループ。Service / Task はこの中で動く
resource "aws_ecs_cluster" "main" {
name = "main"
setting {
name = "containerInsights"
value = "enabled" # CloudWatch のメトリクス強化(コンテナ単位の詳細メトリクス)
}
}
# Fargate Capacity Provider を有効化
# FARGATE_SPOT も入れておくと、開発環境などで weight 指定でコスト最適化できる
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
# Service 側で明示しなかった時のデフォルト戦略
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 へのアクセス等)
# 2 つのロールで共通の Trust Policy(ECS タスクサービス向け)
data "aws_iam_policy_document" "ecs_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
# 実行ロール:ECR からの pull、CloudWatch Logs への書き込み等
# AWS マネージドポリシーで一発(自前の権限は task ロール側に)
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"
}
# アプリ自身のロール(コンテナ内コードが使う)
# S3/DynamoDB アクセス等はこちらに権限を足す
resource "aws_iam_role" "task" {
name = "ecs-task-app"
assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}
# コンテナの stdout/stderr が流れるロググループ
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/myapp"
retention_in_days = 30
}
# Task Definition: 「どのコンテナをどう動かすか」の設計図(imutable のリビジョン管理)
resource "aws_ecs_task_definition" "app" {
family = "myapp" # リビジョンをグルーピングする名前
requires_compatibilities = ["FARGATE"] # 実行プラットフォーム
network_mode = "awsvpc" # Fargate は awsvpc 必須
cpu = "512" # 0.5 vCPU(256/512/1024/2048/4096 の選択肢)
memory = "1024" # 1 GiB(CPU と組み合わせの制約あり)
execution_role_arn = aws_iam_role.task_execution.arn # プラットフォーム側
task_role_arn = aws_iam_role.task.arn # アプリ側
# コンテナ定義は JSON 文字列(HCL ではなく JSON)。jsonencode で生成
container_definitions = jsonencode([{
name = "app"
image = "${aws_ecr_repository.app.repository_url}:latest"
essential = true # このコンテナが止まったらタスク全体停止
# コンテナのポート(ALB のターゲットがここを指す)
portMappings = [{
containerPort = 8080
protocol = "tcp"
}]
# 環境変数(秘匿情報は secrets で参照)
environment = [
{ name = "LOG_LEVEL", value = "info" },
]
# ログ出力先(CloudWatch Logs)
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.app.name
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "app" # ログストリーム名のプレフィックス
}
}
}])
}
ALB(負荷分散)
# ALB 用 SG(インターネットからの 443 を受ける)
resource "aws_security_group" "alb" {
name_prefix = "alb-"
vpc_id = aws_vpc.main.id
lifecycle { create_before_destroy = true }
}
# 0.0.0.0/0 から 443 を許可(HTTPS は ALB で終端)
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"
}
# ALB 本体(インターネット向け)
resource "aws_lb" "main" {
name = "main"
internal = false # false = インターネット向け
load_balancer_type = "application" # ALB(HTTP/HTTPS)
security_groups = [aws_security_group.alb.id]
subnets = [for s in aws_subnet.public : s.id] # public subnet に配置
enable_deletion_protection = true # 誤削除防止
}
# Target Group:ALB の振り分け先プール(ECS Service がここに自動登録)
resource "aws_lb_target_group" "app" {
name = "app"
port = 8080 # コンテナのポート
protocol = "HTTP" # ALB→Task 間は HTTP(内部通信)
vpc_id = aws_vpc.main.id
target_type = "ip" # Fargate は ip ターゲット必須
# ヘルスチェック設定
health_check {
path = "/health"
healthy_threshold = 2 # 2 回成功で正常判定
unhealthy_threshold = 3 # 3 回失敗で異常判定
interval = 30 # チェック間隔(秒)
timeout = 5
matcher = "200" # 200 を正常とみなす("200-299" も可)
}
}
# HTTPS リスナー:ALB の入り口
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
certificate_arn = aws_acm_certificate.app.arn # ACM 証明書
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" # 安全な TLS ポリシー
# 既定のアクション:上の Target Group に転送
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
ECS Service
# Task 用 SG(ALB からのみ 8080 を受ける)
resource "aws_security_group" "task" {
name_prefix = "task-"
vpc_id = aws_vpc.main.id
lifecycle { create_before_destroy = true }
}
# 「ALB の SG からだけ 8080 を許可」のルール(CIDR より安全)
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 # SG 参照
from_port = 8080
to_port = 8080
ip_protocol = "tcp"
}
# ECS Service:「Task Definition を desired_count 個常時起動」を維持
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"
# ネットワーク設定(awsvpc モードで必須)
network_configuration {
subnets = [for s in aws_subnet.private : s.id] # private subnet で実行
security_groups = [aws_security_group.task.id]
assign_public_ip = false # private 配置なので不要
}
# ALB との連携:Task 起動時に Target Group に自動登録
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "app" # container_definitions の name と一致
container_port = 8080
}
# デプロイ戦略(ローリング)
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200 # ローリング更新(一時的に倍まで)
# ECS deploy circuit breaker(自動ロールバック)
# デプロイ失敗を検知したら直前の Task Definition に自動戻し
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 がデプロイの正体。