メインコンテンツまでスキップ

ECS後編: web/apiサービスの公開とAuto Scaling

前章でRDSが加わり、TasukuのTerraformコードはネットワーク・DNS・実行基盤・データベースがひととおり揃いました。ただし第8章のECSは、hello-worldイメージの単発run-taskで起動を確かめただけで止まっています。実際のWebアプリのように、複数のタスクが常時稼働し続ける状態にはまだなっていません。この章では、web・apiの2つのECSサービスを作り、ALB経由で公開するところまで組み上げます。

10.1. タスク定義からサービスへ

第8章で使ったrun-taskは、タスクを1回だけ起動する操作です。タスクが終了すれば、それきりです。対してECSサービスは、指定した数のタスクを維持し続ける仕組みです。1 タスクが異常終了すれば新しいタスクを起動し直し、常に指定数が動いている状態を保ちます。hello-worldのような「起動してログを出して終わる」タスクではなく、HTTPリクエストを待ち受け続けるWebアプリやAPIサーバーには、サービスが必要です。

サービスには、タスクの入れ替え方を制御するdeploymentMaximumPercentdeploymentMinimumHealthyPercentという設定があります。2 既定のデプロイタイプ (rolling update) では、新しいタスク定義への切り替え時、この2つの値がデプロイ中に許容する実行タスク数の上限・下限をパーセントで決めます。標準スケジューラの既定値はそれぞれ200%・100%で、2 たとえば希望タスク数2のサービスなら、最大4タスクまで一時的に増やしながら、常に2タスク以上が実行された状態を保って入れ替えます。この章では既定値のまま使います。

10.2. web/apiの2サービス構成

第1章1.3節で、Tasukuのアプリは「web: ポート3000で待ち受けるブラウザ向け画面」「api: ポート8080で待ち受けるJSON API」の2つのコンテナイメージがある前提だと決めています。同じ節で、イメージ名とポート番号はTerraformの変数にすると予告しました。ここでその通りに実装します。

イメージはvar.web_imagevar.api_imageとし、第7章var.domain_nameと同じ理由で既定値を設定しません。Tasukuのアプリは実在せず、読者が自分のイメージに置き換える前提のためです。ポートはvar.web_port(既定3000)・var.api_port(既定8080)とし、こちらは1.3節で確定済みの値のため既定値を付けます。

タスク定義は、実行ロール・タスクロール (いずれも第8章で作成済み) をwebとapiで共有します。共有できるのは、両者とも同じECRリポジトリ・同じロググループ・同じRDSインスタンス・同じS3バケットにアクセスするためです。個別に用意するのはポート番号とコンテナ名だけです。

10.3. ターゲットグループとヘルスチェック

ALBがECSタスクへリクエストを転送するには、ターゲットグループが要ります。第8章のタスクはawsvpcネットワークモードで動いており、EC2インスタンスでなくElastic Network Interfaceに直接紐づきます。このため、ターゲットグループのtarget_typeは既定のinstanceではなくipを選ぶ必要があります。3 Fargate起動タイプを使う以上、この選択に迷う余地はありません。

ヘルスチェックのパスには/healthzを使います。第1章1.3節で、Tasukuのアプリが満たす前提として「GET /healthzに200を返す口がある」と決めています。web・apiそれぞれに専用のターゲットグループを作り、ヘルスチェックのパスは共通で/healthzとします。

10.4. ホストベースルーティングとセキュリティグループの疎通

第6章以来、443番リスナーはfixed-responseで固定文字列を返すだけでした。この章で、ようやく実際のターゲットグループへ転送するように切り替えます。振り分け先は、第1章1.4節の全体図どおり、ホスト名で決めます。tasuku.example宛てはweb、api.tasuku.example宛てはapiです。

デフォルトの転送先はwebにし、apiだけをリスナールールで上書きします。リスナールールのhost_header条件には、ワイルドカードと大文字小文字を区別しないマッチングが使えます。4 今回は完全一致のapi.${var.domain_name}だけで足ります。証明書は第7章で発行したワイルドカードSAN付きの1枚がそのままカバーするため、apiサブドメイン用に追加発行する必要はありません。Route 53にはapi.${var.domain_name}のAliasレコードを追加し、同じALBを指します。

ルーティングを切り替えただけでは、まだ実際の通信は通りません。第6章でALB用のセキュリティグループのegressを意図的に空のままにしていたのは、当時ターゲットグループが存在せず、ALBが転送先に疎通する必要がまだ生じていなかったためです。ここでターゲットグループが2つできたので、ALB用SGにweb・apiそれぞれのポートへのegressを追加します。ECSタスク用SGの側にも、第8章で作ったweb向け3000番のingressに加え、api向け8080番のingressを追加します。これで、ALBからECSタスクへの経路がセキュリティグループのレベルでもつながります。

10.5. Secrets Manager注入

第9章9.9節では、動作確認のためにSecrets Managerからパスワードを取得し、jqで手動で取り出してコンテナに渡しました。サービスとして常時稼働するweb・apiには、この手作業を毎回行わせるわけにはいきません。ECSタスク定義のsecretsパラメータを使い、パスワードをコンテナ起動時に自動で注入します。

secretsvalueFromは、シークレットの完全なARNの末尾に:<キー名>::を付けると、JSON形式のシークレットから特定のキーだけを取り出せます。5 第9章のシークレットはpasswordキーを含むことがわかっているため、<secret_arn>:password::という値をDATABASE_PASSWORDという環境変数名に紐づけます。ホスト名・ポート番号・データベース名・ユーザー名は秘密情報ではないため、通常のenvironmentでRDSの各属性から直接渡します。

secretsの解決には、実行ロールにsecretsmanager:GetSecretValue権限が要ります。第8章で作った実行ロール用のカスタムポリシーは、この用途を想定しておらず、しかも編集できません。第8章8.5節の制約通り、AWS管理ポリシーのアタッチが選べない以上、既存ポリシーへのiam:CreatePolicyVersionもterraform実行者には許可されていないためです。第9章と同じく、別の新規ポリシーとして追加します。

このシークレットを暗号化するKMSキーが、カスタマー管理キーかAWS管理キーかによって、必要な権限は変わります。第9章aws_db_instancemaster_user_secret_kms_key_idを指定していないため、AWS管理キー (Secrets Manager既定のキー) が使われます。6 kms:Decrypt権限は、シークレットがカスタマー管理キーで暗号化されている場合にのみ必要で、7 既定のAWS管理キーを使う今回の構成には要りません。実行ロールへの新規ポリシーはsecretsmanager:GetSecretValueだけを許可します。

10.6. タスクロールへのS3権限

第3章3.4節の予告表で、タスクロールへの権限追加はこの章で行うと決めています。第8章時点のタスクロールは信頼ポリシーだけでアタッチポリシーがなく、コンテナからS3の添付バケットへアクセスする権限がありませんでした。

必要なS3アクションは、オブジェクトの取得・追加・削除にあたるs3:GetObjects3:PutObjects3:DeleteObjectです。8 これらはオブジェクトARN (<バケットARN>/*) を対象にします。バケット内の一覧が必要ならs3:ListBucketも加えますが、こちらはバケットARN自体を対象にするため、Resourceの形が異なる点に注意が必要です。8 共有タスクロールにこの権限を持つ新規ポリシーをアタッチします(10.2節の通りタスクロールはweb・apiで共有するため、この権限は両方のタスクに付与されます)。

10.7. デプロイ戦略: rolling + circuit breaker

新しいタスク定義へ切り替えるとき、切り替え後のタスクが起動に失敗し続けると、サービスは新旧のタスクが入り乱れたまま止まらなくなります。デプロイサーキットブレーカーを有効にすると、デプロイが失敗と判断された時点で自動的に直前の正常な状態へロールバックします。9 enablerollbackをどちらも有効にし、失敗したデプロイを自動的に巻き戻す設定にします。

もう1つ、health_check_grace_period_secondsも設定します。この値は、タスク起動直後の期間、ロードバランサーのヘルスチェック失敗を無視する猶予です。既定値は0で、猶予がありません。10 コンテナの起動に数秒かかる場合、ヘルスチェックの初回結果が届く前にタスクが不健全と判断され、起動と停止を繰り返しかねません。60秒の猶予を設定し、起動直後の不安定な期間をやり過ごします。

10.8. Auto Scaling

第8章では、タスクのCPU・メモリを256・512MiBという固定値で決め打ちしました。この章では、実行するタスクのを負荷に応じて自動的に増減させます。垂直方向のサイズでなく、水平方向の台数を動かす形です。

Application Auto Scalingのtarget trackingは、指定したメトリクスが目標値に近づくよう、タスク数を自動的に調整する仕組みです。ECSサービス向けの定義済みメトリクスにはECSServiceAverageCPUUtilizationがあります。11 目標値を70%とし、この値を超えるとタスクを増やし、下回るとタスクを減らします。第1章1.2節の規模想定 (数十テナント、同時アクセス数百ユーザー程度) を踏まえ、最小2・最大4の範囲でスケールさせます。最小値を2にするのは、複数のアベイラビリティゾーンにまたがるサブネットを指定したサービスに対し、ECSが新規タスクをアベイラビリティゾーン間で均等に配置しようとするためです。12 最小1では、常時稼働するタスクが1つのアベイラビリティゾーンに偏りかねません。

Auto Scalingがタスク数を動かすようになると、Terraformが管理するdesired_countの値と、実際にAuto Scalingが設定した値がずれます。このままでは、次のterraform planのたびに、Auto Scalingによる変更が差分として検出されてしまいます。aws_ecs_servicelifecycleブロックでdesired_countignore_changesに指定すると、この値への外部からの変更 (Auto Scalingを含む) をTerraformが無視するようになります。13

10.9. Terraform実装

ここまでの内容をecs-service.tfという新しいファイルに書きます。この連載で確立してきた「章の主なリソースを1つのファイルにまとめる」方針を踏襲します。ターゲットグループ・リスナー・Route 53レコード・セキュリティグループのルールといった既存リソースへの変更は、それぞれalb.tfecs.tfroute53-acm.tfに置きます。実行ロール・タスクロールへの新規ポリシーとタスク定義・サービス・Auto Scalingという新規リソースだけを、このecs-service.tfに書きます。まず新規ポリシー2つです。10.5節・10.6節で決めた権限を、それぞれ別のポリシーとして追加します。

resource "aws_iam_policy" "ecs_secrets_read" {
name = "${local.name_prefix}-ecs-secrets-read-policy"
path = "/${var.project}/"
description = "RDSマスターパスワードのSecrets Managerシークレットを読み取る権限。デフォルトのAWS管理KMSキーを使うためkms:Decryptは含めない"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ReadRdsMasterSecret"
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = aws_db_instance.main.master_user_secret[0].secret_arn
},
]
})
}

resource "aws_iam_role_policy_attachment" "ecs_secrets_read" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = aws_iam_policy.ecs_secrets_read.arn
}

resource "aws_iam_policy" "ecs_task_s3" {
name = "${local.name_prefix}-ecs-task-s3-policy"
path = "/${var.project}/"
description = "添付ファイル用S3バケットへの読み書き削除・一覧権限"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AttachmentsObjectAccess"
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
]
Resource = "${aws_s3_bucket.attachments.arn}/*"
},
{
Sid = "AttachmentsListBucket"
Effect = "Allow"
Action = ["s3:ListBucket"]
Resource = aws_s3_bucket.attachments.arn
},
]
})
}

resource "aws_iam_role_policy_attachment" "ecs_task_s3" {
role = aws_iam_role.ecs_task.name
policy_arn = aws_iam_policy.ecs_task_s3.arn
}

10.4節のターゲットグループとホストベースルーティングです。443番リスナーのデフォルトをforwardへ切り替え、apiだけをリスナールールで振り分けます。

resource "aws_lb_target_group" "web" {
name = "${local.name_prefix}-web-tg"
port = var.web_port
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip"

health_check {
path = "/healthz"
}

tags = {
Name = "${local.name_prefix}-web-tg"
}
}

resource "aws_lb_target_group" "api" {
name = "${local.name_prefix}-api-tg"
port = var.api_port
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip"

health_check {
path = "/healthz"
}

tags = {
Name = "${local.name_prefix}-api-tg"
}
}

aws_lb_listener.https(第7章で作成済み) のdefault_actionを、fixed-responseからwebへのforwardに書き換えます。

resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09"
certificate_arn = aws_acm_certificate_validation.main.certificate_arn

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
}

resource "aws_lb_listener_rule" "api" {
listener_arn = aws_lb_listener.https.arn
priority = 100

action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}

condition {
host_header {
values = ["api.${var.domain_name}"]
}
}
}

ALB用SGのegressと、ECSタスク用SGのapi向けingressを追加します (web向けingressは第8章で作成済み)。

resource "aws_vpc_security_group_egress_rule" "alb_to_web" {
security_group_id = aws_security_group.alb.id

referenced_security_group_id = aws_security_group.ecs_tasks.id
from_port = var.web_port
to_port = var.web_port
ip_protocol = "tcp"

description = "ECSタスク用SG (webサービス) への転送"
}

resource "aws_vpc_security_group_egress_rule" "alb_to_api" {
security_group_id = aws_security_group.alb.id

referenced_security_group_id = aws_security_group.ecs_tasks.id
from_port = var.api_port
to_port = var.api_port
ip_protocol = "tcp"

description = "ECSタスク用SG (apiサービス) への転送"
}

resource "aws_vpc_security_group_ingress_rule" "ecs_tasks_from_alb_api" {
security_group_id = aws_security_group.ecs_tasks.id

referenced_security_group_id = aws_security_group.alb.id
from_port = var.api_port
to_port = var.api_port
ip_protocol = "tcp"

description = "ALBからのみ許可 (第10章のapi service化・host-basedルーティングで使う)"
}

Route 53にapi.${var.domain_name}のAliasレコードを追加します。

resource "aws_route53_record" "api" {
zone_id = aws_route53_zone.main.zone_id
name = "api.${var.domain_name}"
type = "A"

alias {
name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
evaluate_target_health = true
}
}

タスク定義です。webのDB接続用環境変数・secretsは、apiと共有できるようlocalsにまとめます。ユーザー名は文字列を組み立てず、aws_db_instance.main.usernameを直接参照します。RDS側の値が将来変わっても、無言で追従できるようにするためです。

locals {
db_environment = [
{ name = "DATABASE_HOST", value = aws_db_instance.main.address },
{ name = "DATABASE_PORT", value = tostring(aws_db_instance.main.port) },
{ name = "DATABASE_NAME", value = aws_db_instance.main.db_name },
{ name = "DATABASE_USER", value = aws_db_instance.main.username },
]

db_secrets = [
{ name = "DATABASE_PASSWORD", valueFrom = "${aws_db_instance.main.master_user_secret[0].secret_arn}:password::" },
]
}

resource "aws_ecs_task_definition" "web" {
family = "${local.name_prefix}-web"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn

runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}

container_definitions = jsonencode([
{
name = "web"
image = var.web_image
essential = true

portMappings = [
{
containerPort = var.web_port
protocol = "tcp"
}
]

# WHOAMI_PORT_NUMBERは動作確認用プレースホルダ(traefik/whoami)がcontainerPortで
# 待ち受けるための設定。Tasukuの実アプリイメージはこの環境変数を参照しないため無視される
environment = concat(local.db_environment, [
{ name = "WHOAMI_PORT_NUMBER", value = tostring(var.web_port) },
])
secrets = local.db_secrets

logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.region
"awslogs-stream-prefix" = "web"
}
}
}
])

tags = {
Name = "${local.name_prefix}-web"
}
}

apiのタスク定義は、ポート番号とS3バケット名の環境変数以外、webと同じです。差分だけを示します。

resource "aws_ecs_task_definition" "api" {
family = "${local.name_prefix}-api"
# requires_compatibilities / network_mode / cpu / memory / execution_role_arn /
# task_role_arn / runtime_platform はwebと同じ

container_definitions = jsonencode([
{
name = "api"
image = var.api_image
essential = true

portMappings = [
{
containerPort = var.api_port
protocol = "tcp"
}
]

# WHOAMI_PORT_NUMBERは動作確認用プレースホルダ(traefik/whoami)がcontainerPortで
# 待ち受けるための設定。Tasukuの実アプリイメージはこの環境変数を参照しないため無視される
environment = concat(local.db_environment, [
{ name = "ATTACHMENTS_BUCKET", value = aws_s3_bucket.attachments.id },
{ name = "WHOAMI_PORT_NUMBER", value = tostring(var.api_port) },
])
secrets = local.db_secrets

logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.region
"awslogs-stream-prefix" = "api"
}
}
}
])

tags = {
Name = "${local.name_prefix}-api"
}
}

サービス本体です。ターゲットグループがまだリスナー・リスナールールに関連付けられていない状態でサービスを作成すると、AWS API側のタイミングの都合でエラーになることがあります。14 depends_onでリスナー・リスナールールへの依存を明示し、この順序を保証します。

resource "aws_ecs_service" "web" {
name = "${local.name_prefix}-web"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.web.arn
desired_count = 2
launch_type = "FARGATE"

network_configuration {
subnets = [for k, v in aws_subnet.this : v.id if local.subnets[k].tier == "private-app"]
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}

load_balancer {
target_group_arn = aws_lb_target_group.web.arn
container_name = "web"
container_port = var.web_port
}

health_check_grace_period_seconds = 60

deployment_circuit_breaker {
enable = true
rollback = true
}

lifecycle {
ignore_changes = [desired_count]
}

tags = {
Name = "${local.name_prefix}-web"
}

depends_on = [aws_lb_listener.https]
}

resource "aws_ecs_service" "api" {
name = "${local.name_prefix}-api"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2
launch_type = "FARGATE"

network_configuration {
subnets = [for k, v in aws_subnet.this : v.id if local.subnets[k].tier == "private-app"]
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}

load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = var.api_port
}

health_check_grace_period_seconds = 60

deployment_circuit_breaker {
enable = true
rollback = true
}

lifecycle {
ignore_changes = [desired_count]
}

tags = {
Name = "${local.name_prefix}-api"
}

depends_on = [aws_lb_listener_rule.api]
}

最後にAuto Scalingです。web・apiで同じ構成を2つ分繰り返します。

resource "aws_appautoscaling_target" "web" {
service_namespace = "ecs"
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.web.name}"
scalable_dimension = "ecs:service:DesiredCount"
min_capacity = 2
max_capacity = 4
}

resource "aws_appautoscaling_policy" "web" {
name = "${local.name_prefix}-web-cpu-tracking"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.web.resource_id
scalable_dimension = aws_appautoscaling_target.web.scalable_dimension
service_namespace = aws_appautoscaling_target.web.service_namespace

target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}

target_value = 70
}
}

# aws_appautoscaling_target.api / aws_appautoscaling_policy.api も同じ構成

outputs.tfには、動作確認で使うターゲットグループのARNとサービス名を追加します。

output "web_target_group_arn" {
description = "webサービス用ターゲットグループのARN (動作確認でのヘルスチェック状態確認等に使用)"
value = aws_lb_target_group.web.arn
}

output "api_target_group_arn" {
description = "apiサービス用ターゲットグループのARN (動作確認でのヘルスチェック状態確認等に使用)"
value = aws_lb_target_group.api.arn
}

output "web_service_name" {
description = "webサービス名 (aws ecs describe-services等で参照)"
value = aws_ecs_service.web.name
}

output "api_service_name" {
description = "apiサービス名 (aws ecs describe-services等で参照)"
value = aws_ecs_service.api.name
}

10.10. 動作確認

terraform fmt -check -recursive -diffterraform validateを実行し、構文とプロバイダスキーマの整合を確認します。ただし、この章のvalidateが保証する範囲は、これまでの章より狭いことに注意が必要です。container_definitionsはJSON文字列として渡すため、secretsのキー名やportMappingsの値に誤りがあっても、validateはそれを検出しません。タスク定義自体の登録 (RegisterTaskDefinition) やタスクの起動を試みた段階で、初めて誤りが表面化します。この章のTerraformコードの正しさは、validateの通過でなく、10.5節・10.9節で示した一次情報との突き合わせによって担保しています。

実際にapplyして確認する場合の手順です。まず、/healthzに200を返すことを保証する広く使われた公開イメージが見当たらないため、traefik/whoamiというデモイメージをweb・apiの代わりに使います。第8章のhello-worldと同じくECRへ再ホストし、var.web_imagevar.api_imageにそのタグを指定します。このイメージは/healthのような固定パスにはハンドラを持ちますが、/healthzという個別のパスは用意していません。15 ただし、登録されていないパスへのリクエストは、ルートパス/に登録されたハンドラへフォールバックする形でGoの標準ルーターが処理するため、16 結果として/healthzにも200が返ります。/healthz固有の応答を保証しているわけではなく、未登録パスのフォールバック挙動を借りているだけである点に注意してください。

待受ポートも、10.9節のタスク定義で環境変数WHOAMI_PORT_NUMBERを渡すことで、containerPortに指定した3000・8080番に合わせています。このイメージの既定の待受ポートは80番です。17

docker pull traefik/whoami
docker tag traefik/whoami <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/tasuku-dev-app:web
docker tag traefik/whoami <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/tasuku-dev-app:api
docker push <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/tasuku-dev-app:web
docker push <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/tasuku-dev-app:api

applyが終わったら、aws ecs describe-servicesweb_service_nameapi_service_namedeployments[].rolloutStateCOMPLETEDになっていることを確認します。次に、aws elbv2 describe-target-healthweb_target_group_arnapi_target_group_arnそれぞれに対して実行し、登録されたターゲットのTargetHealth.Stateがすべてhealthyであることを確認します。ここまでで、サービスとしてタスクが起動し、ALBからも正常と判断されている状態です。

最後に、実際にHTTP経由で確認します。tasuku.exampleapi.tasuku.exampleそれぞれへHTTPSでアクセスし、200が返ること、レスポンスの内容が想定通りwebとapiで別のコンテナから返っていることを確認します。ホストベースルーティングが実際に機能しているかは、この2つのレスポンスを見比べて初めてわかります。

この手順が確認するのは、あくまでタスクが起動し、ALB経由でHTTPが通ることまでです。traefik/whoamiDATABASE_PASSWORDのようなアプリ固有の環境変数・secretsを参照しないため、10.5節のSecrets Manager注入についても「IAM権限とARNの参照が有効で、タスクの起動自体は妨げない」ことまでしか確認できません。第9章9.9節のpsql-checkのように、注入された値でデータベースへ実際に接続できることまでは確認しません。

この章のECSサービスも、これまでと同じ検証水準です。実際にAWSへapplyしてこの手順を実行した結果は保証しません。

本章のまとめ

  • ECSサービスは指定した数のタスクを維持し続ける仕組みで、run-taskによる単発起動とは役割が異なります
  • 第1章1.3節の予告通り、web (ポート3000)・api (ポート8080) の2サービスを実装しました。イメージ・ポートともにTerraformの変数にし、イメージはvar.domain_nameと同じ理由で既定値を設定していません
  • ターゲットグループのtarget_typeipが必須です。awsvpcネットワークモードのFargateタスクはEC2インスタンスでなくENIに紐づくためです
  • 443番リスナーをfixed-responseからforwardへ切り替え、ホストヘッダでapi.サブドメインだけをapi用ターゲットグループへ振り分けました。証明書は第7章のワイルドカードSANがそのままカバーします
  • 第6章で意図的に空にしていたALB用SGのegressを追加し、ALB→ECSタスクの疎通経路を完成させました
  • Secrets Manager統合済みのRDSパスワードを、ECSタスク定義のsecretsで自動注入する形に切り替え、第9章の手動jq抽出をサービス向けに置き換えました。KMSキーはデフォルトのAWS管理キーのため、実行ロールにkms:Decryptは不要です
  • 第3章の予告通り、タスクロールにS3添付バケットへのアクセス権限を新規ポリシーとして追加しました
  • デプロイサーキットブレーカーで失敗デプロイの自動ロールバックを有効にし、health_check_grace_period_secondsで起動直後のヘルスチェック猶予を設けました
  • Auto Scalingで、CPU使用率に応じてタスク数を2〜4の範囲で自動調整する設定にしました。第8章の固定cpu/memoryが垂直方向のサイズなら、こちらは水平方向の台数です
  • Terraformが管理するdesired_countとAuto Scalingの変更が競合しないよう、lifecycle.ignore_changesで除外しました

次に読む

Footnotes

  1. 出典: Amazon ECS services。サービスは指定した数のタスクを維持し続ける仕組みであるとしています。

  2. 出典: Deploy Amazon ECS services by replacing tasks、およびDeploymentConfigurationPropertyREPLICAサービススケジューラの既定maximumPercentは200%、minimumHealthyPercentの既定値は100%だとしています。 2

  3. 出典: AWS::ECS::TaskSet LoadBalancerawsvpcネットワークモードを使うタスク定義では、ターゲットグループのtarget_typeinstanceでなくipを選ぶ必要があり、この仕様はFargate起動タイプで必須だとしています。

  4. 出典: terraform MCP (hashicorp/aws provider v6.53.0、aws_lb_listener_rule)。host_header条件はワイルドカードに対応し、大文字・小文字を区別しないマッチングをサポートするとしています。

  5. 出典: Specifying sensitive data using Secrets Manager (AWS Batch User Guide、ECSと同じsecretsコンテナ定義パラメータの仕組みを使うため適用可能)。シークレットARNの末尾にJSONキー名を付けることで、特定のキーだけを参照できる構文を例示しています。

  6. 出典: Password management with Amazon RDS and AWS Secrets Manager。カスタマー管理キーを指定しない限り、Secrets Manager提供のデフォルトKMSキーが使われるとしています。

  7. 出典: Amazon ECS task execution IAM rolekms:Decryptは、シークレットがカスタマー管理キーを使っている場合にのみ必要だとしています。

  8. 出典: How Amazon S3 works with IAMGetObjectPutObjectDeleteObjectはオブジェクトARNを対象とするオブジェクト操作、ListBucketはバケットレベルの操作であるとしています。 2

  9. 出典: terraform MCP (hashicorp/aws provider v6.53.0、aws_ecs_service)。rollbackを有効にすると、デプロイが失敗した場合に直前の正常完了デプロイへロールバックするとしています。

  10. 出典: Health check grace period。既定値は0秒で、猶予がないとしています。

  11. 出典: PredefinedMetricSpecificationECSServiceAverageCPUUtilizationが有効な定義済みメトリクスタイプの1つであるとしています。

  12. 出典: Balancing an Amazon ECS service across Availability Zones。Fargateで動くサービスは、利用可能なアベイラビリティゾーン間でタスクを自動的に再分散するとしています。

  13. 出典: terraform MCP (hashicorp/aws provider v6.53.0、aws_ecs_service「Ignoring Changes to Desired Count」example)。lifecycle.ignore_changesdesired_countを指定すると、Application Auto Scaling等の外部要因による変更を無視できるとしています。

  14. 出典: terraform-provider-aws公式GitHubリポジトリのissue (#3495hashicorp/terraform #32908ほか、AWS公式ドキュメントでの明示的な言及はなし)。ターゲットグループがリスナーに未関連の状態でECSサービスを作成しようとすると、タイミング依存のエラーになりうるとする複数の報告があります。

  15. 出典: traefik/whoami (GitHub公式リポジトリ、README.mdの「Paths」節)。//api/bench/data/echo/health/openapi.yamlにハンドラを持つとしています。/healthzは含まれません。

  16. 出典: traefik/whoami app.gohttp.NewServeMux()で各パスのハンドラを登録したうえで、mux.Handle("/", handle(whoamiHandler, verbose))としてルートパスにwhoamiHandlerを登録しています。Go標準のhttp.ServeMuxは、/に登録したハンドラを他のどのパターンにもマッチしないパスの受け皿として扱う仕様のため、/healthzのような未登録パスもこのハンドラで処理されます。whoamiHandler自体はステータスコードを明示的に設定しておらず、最初の書き込み時にGoが自動的に200を設定します。

  17. 出典: traefik/whoami README.md の「Flags」節、およびapp.go。待受ポートは--portフラグまたはWHOAMI_PORT_NUMBER環境変数で指定でき、既定値は80番だとしています。