はじめに
突然? App Runner の新規サービス登録終了のアナウンスが流れましたね。
約1ヶ月前という恐ろしいタイミングです。(一部フライングもあったので察しではありましたが)
以前公開されていたマイグレーション方法のページも 404 になってしまったし...。
背景
App Runner の新規サービス登録終了がアナウンスは以下。
移行先として ECS Express Mode を選定しました。
App Runner との主な違い
| 項目 | App Runner | ECS Express Mode |
|---|---|---|
| コンピュート | マネージド | ECS Fargate |
| ネットワーク | VPC Connector 経由 | パブリックサブネットに直接配置 |
| LB | 内蔵(不可視) | ALB(自動作成・可視) |
| スケーリング | MaxConcurrency ベース | CPU/Memory ターゲットベース |
| Scale to Zero | あり | なし(min_task_count >= 1) |
| デプロイ | ECR push で自動 | 外部パイプライン必要 |
| WAF / サイドカー | 不可 | 可 |
| Terraform リソース | aws_apprunner_service |
aws_ecs_express_gateway_service |
| Provider バージョン | v5 系で可 | v6 以上必須 |
概要
App Runner を稼働させたまま Express Mode を並行構築し、Route 53 加重ルーティングで段階的に切り替えます。バックエンド(RDS / ElastiCache 等)は共通なので並行稼働でき、問題があれば重みを戻すだけでロールバックできます。
移行イメージ
※ webapp と analytics の2つの App Runner で運用
# Before (App Runner)
Route 53 ── App Runner (ALB 内蔵)
(webapp / analytics)
# 移行中 (並行稼働)
┌── App Runner (既存)
Route 53 ──┤ (webapp / analytics)
└── ALB(自動作成) ── Express Mode
(webapp / analytics)
# After (ECS Express Mode)
Route 53 ── ALB(自動作成) ── Express Mode
(webapp / analytics)
移行手順
| フェーズ | 内容 |
|---|---|
| Phase 0 | Terraform 作成・IAM 設計 |
| Phase 1 | ECS Express 構築・疎通テスト |
| Phase 2 | Route 53 加重ルーティングで段階的移行(10% → 50% → 100%) |
| Phase 3 | 完全切替・安定性確認 |
| Phase 4 | App Runner リソース削除 |
移行対象は App Runner 上の 2 サービスのみ。既存 ALB や RDS 等には手を入れません。
内容
サービス構築自体は難しくありませんでした。大変だったのはカスタムドメインでの公開です。Route 53 の設定で以下の問題に順番にぶつかりました。
-
エイリアスレコードが設定できない
- App Runner と違い、Express Mode の ALB は Terraform から直接参照できず、エイリアスのターゲットに指定できない。ゾーンアペックスで運用している場合ここで詰みかける
→ 実際には ALB は内蔵ではなく独立したリソースなので、タグ逆引きで特定すればエイリアス設定は可能だった
- App Runner と違い、Express Mode の ALB は Terraform から直接参照できず、エイリアスのターゲットに指定できない。ゾーンアペックスで運用している場合ここで詰みかける
-
HTTPS が使えない
- ALB にカスタムドメイン用の ACM 証明書がアタッチされていない
-
ルーティングされない
- リスナールールがカスタムドメインを考慮していない
これらを Terraform で解決するためのワークアラウンドを以降で説明します。
0. 最も重要な前提:ALB は「自動生成」されるが「自動管理」ではない
Express Mode を検討する上で最初に理解すべきことがあります。
Express Mode は ALB を自動で作ってくれますが、証明書の追加やリスナールールのカスタマイズは自分でやる必要があります。App Runner のように「ドメインを設定したら全部よしなにやってくれる」わけではありません。ALB が作られた後の面倒は自分で見る、という前提で設計しないと後からひっくり返ることになります。
公式でもこれに関して以下のように触れています。
「Understanding the shared responsibility model」セクションに以下の記述があります。
While Express Mode creates and configures resources like Amazon ECS services, load balancers, and auto scaling policies, all resources remain in your AWS account and are fully accessible for direct management.
(Express Mode はロードバランサー等を作成・構成するが、すべてのリソースはユーザーのアカウントに残り、直接管理できる)
1. Express Mode 固有のリソースと IAM ロール
aws_ecs_service ではなく aws_ecs_express_gateway_service を使います。Terraform AWS Provider v6.38 以上が必須です。
v6 では ALB の mutual_authentication { mode = "off" } が属性ごと消えており、書くとエラーになります。既存の ALB 定義がある場合は先に削除が必要です。
IAM ロールは通常の ECS の 2 つ(Execution / Task)に加え、ALB や SG を自動構成するための Infrastructure Role が必要です。
| ロール | 何に使うか | Principal |
|---|---|---|
| Infrastructure Role | ALB / SG / 証明書の自動構成 | ecs.amazonaws.com |
| Execution Role | ECR pull / CloudWatch Logs / Secrets 読み取り | ecs-tasks.amazonaws.com |
| Task Role | アプリ実行時の権限(S3, SQS, SES 等) | ecs-tasks.amazonaws.com |
infrastructure_role_arn が Express Mode 固有の設定項目です。
resource "aws_ecs_express_gateway_service" "webapp" {
cluster = aws_ecs_cluster.express.name
service_name = local.prefix
infrastructure_role_arn = aws_iam_role.infrastructure.arn # Express Mode 固有
execution_role_arn = aws_iam_role.execution.arn
task_role_arn = aws_iam_role.task.arn
primary_container {
image = "${var.app_ecr_repository_url}:latest"
container_port = 3000
...
}
...
}
2. ALB 自動管理と Terraform の衝突
Express Mode は ALB を自動で作りますが、Terraform からはその ALB を直接参照できません。ここが一番苦労したところです。
Security Group の自動注入
Express Mode は ALB 用の SG をタスクの network_configuration に自動で追加します。Terraform は毎回差分を検出するので、lifecycle で握りつぶす必要があります。
resource "aws_ecs_express_gateway_service" "webapp" {
network_configuration {
subnets = var.public_subnet_ids
security_groups = [aws_security_group.task.id]
}
lifecycle {
ignore_changes = [
network_configuration[0].security_groups,
]
}
}
ALB のタグベース逆引き
ALB を Terraform から触るにはタグで逆引きするしかありません。Name タグは最初に作られたサービス名に依存して不安定だったので、AmazonECSManaged + environment + service の組み合わせに落ち着きました。
data "aws_lb" "express" {
tags = {
AmazonECSManaged = "true"
environment = var.env
service = local.prefix
}
depends_on = [
aws_ecs_express_gateway_service.webapp,
aws_ecs_express_gateway_service.analytics,
]
}
depends_on を忘れるとサービスより先に ALB を探しに行って失敗します。初回 apply でハマりました。
3. カスタムドメインのワークアラウンド
リスナールールにカスタムドメインの host-header 条件を足したかったのですが、Terraform のリソースでは実現できず、null_resource + local-exec で AWS CLI を直接叩いています。
resource "null_resource" "webapp_custom_domain" {
count = var.custom_domain_webapp != null ? 1 : 0
triggers = {
custom_domain = var.custom_domain_webapp
rule_arn = data.aws_lb_listener_rule.webapp[0].arn
conditions_hash = local_file.webapp_conditions[0].content_sha256
}
provisioner "local-exec" {
command = "aws elbv2 modify-rule --rule-arn ... --conditions file://..."
}
}
ルールの priority を固定値で指定しているので、サービスが増えたり AWS 側の採番が変わると壊れるリスクがあります。
4. カスタム証明書の追加
既存のワイルドカード ACM 証明書を Express Mode の ALB にも使い回したいのですが、これも ALB の逆引きが前提です。
resource "aws_lb_listener_certificate" "express_additional" {
listener_arn = data.aws_lb_listener.express_https[0].arn
certificate_arn = var.additional_certificate_arn
}
タグベース逆引きの安定性に依存する構造です。
5. Security Group 設計の制約
Express Mode が作る ALB の SG を Terraform から安定して参照できないため、タスク側の ingress は VPC CIDR 全体を許可しています。
resource "aws_security_group" "task" {
ingress = [{
cidr_blocks = [var.vpc_cidr_block]
from_port = 3000
protocol = "tcp"
to_port = 3000
...
}]
}
ALB の SG だけに絞りたいところですが、現状では妥協しています。
終わりに
ECS Express Mode は App Runner の移行先として悪くない選択肢ですが、Terraform との相性には苦労します。ALB を直接参照できないため、タグ逆引きや null_resource + CLI といった力技が必要になります。
その代わり、WAF の適用、Container Insights でのメトリクス監視、サイドカーの追加など、App Runner ではできなかったことができるようになりました。差し引きで考えれば移行してよかったと思っています。
aws_ecs_express_gateway_service から ALB ARN やリスナールール ARN を直接取れるようになれば、ここに書いたワークアラウンドの大半は不要になるはずです。Provider のアップデートに期待しつつ、それまでは力技で乗り切っていきます。
【追記】
以下に追加記事を書いていますので宜しければご確認ください。
一言
ECS Express Mode で一旦構築完了したら ALB関連のリソースを import してしまった方が良いのかもしれません。ここは皆さんの方でも試してみては如何でしょうか。
実際にここまでやってみて、もし自分が次に同じ機会があれば import を選ぶと思います^^;