フルスタックアプリを AWS にデプロイする — ECS Fargate キャップストーン
1 ~ 31章のすべてのサービスを1つに織り上げる総合実習。modern-react の Next.js アプリと modern-python の FastAPI アプリを1つのアカウントに ECS Fargate + RDS + S3 + CloudFront + ALB + Secrets Manager + Terraform でデプロイし、ステップ別の Terraform コードと13ステップの PR の流れ、1か月約 $10 の最小コスト構成、そして Kubernetes 本の EKS デプロイとの比較までを整理します。
この本の終着点です。1 ~ 31章で別々に身につけたサービス — IAM · VPC · S3 · RDS · ALB · CloudFront · ECS Fargate · ECR · Secrets Manager · CloudWatch · Terraform — が、この章で 1つの動作するシステム の中へ入ってきます。ドメインは モダンPython 本と React 本で作ったのと同じ フルスタック Todo アプリ です。
このキャップストーンは Kubernetes 本の6部と対になります。同じアプリを同じドメインで デプロイしますが、Kubernetes 本は EKS 路線で、本書は ECS Fargate 路線で行きます。2つのキャップストーンを比べて読めば、「マネージドコンテナ vs Kubernetes」の運用上の違いがコードレベルで明確になります。
何をデプロイするか #
| 構成要素 | 出所 | AWS 上の配置 |
|---|---|---|
| フロントエンド | React の Next.js アプリ | ECS Fargate (SSR) + ALB、静的アセットは S3 + CloudFront |
| バックエンド API | モダンPython の FastAPI アプリ | ECS Fargate + ALB |
| データベース | — | RDS PostgreSQL (Aurora Serverless v2) |
| シークレット | — | Secrets Manager |
| ドメイン / TLS | — | Route 53 + ACM |
目標アーキテクチャ #
Route 53 (ドメイン)
│
┌────────┴─────────┐
▼ ▼
CloudFront ALB (HTTPS, ACM)
(静的アセット/S3) ╱ ╲
▼ ▼
ECS Fargate ECS Fargate
(Next.js SSR) (FastAPI API)
private subnet (2 AZ)
│
▼
RDS PostgreSQL
(Aurora Serverless v2)
isolated subnet
▲
Secrets Manager (DB 認証情報)第28章 VPC 深掘り の 3-tier Subnet の上に載せます。ALB と CloudFront は public、Fargate Task は private、RDS は isolated 層です。
リポジトリ構成 #
すべてのインフラは 第25章 Terraform 入門 のモジュール構成でコード化します。コンソールクリックはありません。
infra/
├── backend.tf # 1ステップ: state の保存先
├── providers.tf # provider + region
├── vpc.tf # 2ステップ: 3-tier VPC
├── ecr.tf # 3ステップ: イメージの保存先
├── rds.tf # 4·5ステップ: Aurora Serverless v2 + シークレット
├── alb.tf # 6ステップ: ALB + ACM + ターゲットグループ
├── ecs-api.tf # 7·8ステップ: FastAPI Task/Service
├── ecs-web.tf # 9ステップ: Next.js Task/Service
├── cdn.tf # 10ステップ: S3 + CloudFront
├── dns.tf # 11ステップ: Route 53
├── monitoring.tf # 13ステップ: CloudWatch アラーム
└── variables.tf1ステップ — Terraform バックエンド #
state をローカルではなく S3 に置き、DynamoDB で同時実行のロック(lock)をかけます。チームが同じインフラを安全に扱うための前提です。
terraform {
required_version = ">= 1.9"
backend "s3" {
bucket = "myapp-tfstate"
key = "capstone/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "myapp-tflock"
encrypt = true
}
required_providers {
aws = { source = "hashicorp/aws", version = "~> 6.0" }
}
}2ステップ — VPC 3-tier #
第28章 の 3-tier(public / private / isolated)設計をモジュールで載せます。database_subnets が isolated 層です。
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "myapp"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2c"]
public_subnets = ["10.0.0.0/20", "10.0.128.0/20"] # ALB, NAT
private_subnets = ["10.0.16.0/20", "10.0.144.0/20"] # Fargate
database_subnets = ["10.0.32.0/20", "10.0.160.0/20"] # RDS (isolated)
enable_nat_gateway = true
single_nat_gateway = false # AZ ごとに NAT (28章 推奨)
}
# S3 / ECR トラフィックは NAT ではなく Endpoint で (コスト削減)
module "vpc_endpoints" {
source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints"
version = "~> 5.0"
vpc_id = module.vpc.vpc_id
endpoints = {
s3 = { service = "s3", service_type = "Gateway", route_table_ids = module.vpc.private_route_table_ids }
ecr_api = { service = "ecr.api", subnet_ids = module.vpc.private_subnets }
ecr_dkr = { service = "ecr.dkr", subnet_ids = module.vpc.private_subnets }
}
}3ステップ — ECR リポジトリ #
2つのアプリのイメージを入れる 第16章 ECR の保存先です。
resource "aws_ecr_repository" "api" {
name = "myapp-api"
image_tag_mutability = "MUTABLE"
image_scanning_configuration { scan_on_push = true }
}
resource "aws_ecr_repository" "web" {
name = "myapp-web"
image_scanning_configuration { scan_on_push = true }
}4·5ステップ — Aurora Serverless v2 とシークレット #
第11章 RDS の Aurora Serverless v2 を isolated subnet に置きます。manage_master_user_password = true にすると、RDS がパスワードを Secrets Manager に直接生成・ローテーション します(第20章)。私たちが平文のパスワードを触ることはありません。
resource "aws_rds_cluster" "main" {
cluster_identifier = "myapp"
engine = "aurora-postgresql"
engine_version = "16.4"
database_name = "myapp"
master_username = "myapp"
manage_master_user_password = true # → Secrets Manager に自動生成
db_subnet_group_name = module.vpc.database_subnet_group_name
vpc_security_group_ids = [aws_security_group.rds.id]
serverlessv2_scaling_configuration {
min_capacity = 0 # 0 ACU 自動一時停止 (アイドル時のコンピューティング課金 0)
max_capacity = 4.0
}
skip_final_snapshot = true # 学習環境限定
}
resource "aws_rds_cluster_instance" "main" {
cluster_identifier = aws_rds_cluster.main.id
instance_class = "db.serverless"
engine = aws_rds_cluster.main.engine
}
# RDS が作ったシークレット ARN — ECS Task がこれを参照
output "db_secret_arn" {
value = aws_rds_cluster.main.master_user_secret[0].secret_arn
}6ステップ — ALB と ACM #
第13章 ALB / NLB と ACM の HTTPS 終端です。ホストヘッダで api.example.com は API ターゲットグループ、それ以外は Next.js ターゲットグループへ送ります。
resource "aws_lb" "main" {
name = "myapp-alb"
load_balancer_type = "application"
subnets = module.vpc.public_subnets
security_groups = [aws_security_group.alb.id]
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
certificate_arn = aws_acm_certificate.main.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn # デフォルト = Next.js
}
}
resource "aws_lb_listener_rule" "api" {
listener_arn = aws_lb_listener.https.arn
priority = 10
action { type = "forward", target_group_arn = aws_lb_target_group.api.arn }
condition { host_header { values = ["api.example.com"] } }
}7·8ステップ — FastAPI Task とマイグレーション #
第15章 ECS / Fargate · 第22章 インフラ骨格 の Task 定義です。DB シークレットを secrets で注入 するので、環境変数に平文がありません。
resource "aws_ecs_task_definition" "api" {
family = "myapp-api"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "512"
memory = "1024"
execution_role_arn = aws_iam_role.ecs_exec.arn # ECR pull, シークレット read
task_role_arn = aws_iam_role.app.arn # アプリが使う権限
container_definitions = jsonencode([{
name = "api"
image = "${aws_ecr_repository.api.repository_url}:latest"
portMappings = [{ containerPort = 8000 }]
secrets = [{
name = "DB_SECRET" # RDS が作ったシークレット(ホスト・パスワードの JSON)
valueFrom = aws_rds_cluster.main.master_user_secret[0].secret_arn
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/myapp-api"
"awslogs-region" = "ap-northeast-2"
"awslogs-stream-prefix" = "api"
}
}
}])
}
resource "aws_ecs_service" "api" {
name = "myapp-api"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2 # 2 AZ
launch_type = "FARGATE"
network_configuration {
subnets = module.vpc.private_subnets
security_groups = [aws_security_group.api.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8000
}
}DB マイグレーション(第23章)はサービスではなく 単発の Task で回します。同じイメージでコマンドだけ変えます。
aws ecs run-task \
--cluster myapp \
--task-definition myapp-api \
--launch-type FARGATE \
--overrides '{"containerOverrides":[{"name":"api","command":["alembic","upgrade","head"]}]}' \
--network-configuration "awsvpcConfiguration={subnets=[subnet-...],securityGroups=[sg-...]}"9·10·11ステップ — Next.js · 静的アセット · ドメイン #
Next.js SSR は API と同じ方式の Fargate サービスで載せます(ecs-web.tf)。ビルド成果の静的アセットは 第10章 S3 + 第14章 CloudFront でデプロイし、第12章 Route 53 でドメインを ALB / CloudFront に Alias で接続します。
resource "aws_route53_record" "web" {
zone_id = data.aws_route53_zone.main.zone_id
name = "example.com"
type = "A"
alias {
name = aws_cloudfront_distribution.web.domain_name
zone_id = aws_cloudfront_distribution.web.hosted_zone_id
evaluate_target_health = false
}
}
resource "aws_route53_record" "api" {
zone_id = data.aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
alias {
name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
evaluate_target_health = true
}
}12ステップ — CI/CD #
第24章 CI/CD の GitHub Actions でイメージを ECR にプッシュし、ECS をローリングアップデートします。
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: ap-northeast-2
- uses: aws-actions/amazon-ecr-login@v2
- run: |
docker build -t $ECR/myapp-api:$GITHUB_SHA ./api
docker push $ECR/myapp-api:$GITHUB_SHA
- run: |
aws ecs update-service --cluster myapp --service myapp-api \
--force-new-deploymentGitHub には長期の認証情報ではなく OIDC ロールの引き受け(role-to-assume)を使います(第6章 セキュリティ基本の最小権限の原則)。
13ステップ — モニタリング・アラーム #
第26章 モニタリング — CloudWatch アラームと X-Ray のアラームをかけたうえで、ALB 5xx と Fargate CPU を監視します。
resource "aws_cloudwatch_metric_alarm" "alb_5xx" {
alarm_name = "myapp-alb-5xx"
namespace = "AWS/ApplicationELB"
metric_name = "HTTPCode_Target_5XX_Count"
statistic = "Sum"
period = 60
evaluation_periods = 5
threshold = 10
comparison_operator = "GreaterThanThreshold"
alarm_actions = [aws_sns_topic.alerts.arn] # 19章 SNS で通知
dimensions = { LoadBalancer = aws_lb.main.arn_suffix }
}アラームは 第19章 EventBridge / SQS / SNS の SNS トピックに送って Slack・メールで受け取ります。
13ステップを一覧で #
| # | ステップ | ファイル | 関連章 |
|---|---|---|---|
| 1 | Terraform バックエンド | backend.tf | 第25章 |
| 2 | VPC 3-tier | vpc.tf | 第28章 |
| 3 | ECR | ecr.tf | 第16章 |
| 4·5 | Aurora + シークレット | rds.tf | 第11章 · 第20章 |
| 6 | ALB + ACM | alb.tf | 第13章 |
| 7·8 | FastAPI + マイグレーション | ecs-api.tf | 第15章 · 第22章 · 第23章 |
| 9 | Next.js | ecs-web.tf | 第15章 |
| 10·11 | S3/CloudFront · Route 53 | cdn.tf · dns.tf | 第10章 · 第14章 · 第12章 |
| 12 | CI/CD | deploy.yml | 第24章 |
| 13 | モニタリング・アラーム | monitoring.tf | 第26章 · 第19章 |
このコード化のおかげで、第30章 災害復旧 の Pilot Light DR が「同じコードを別のリージョンに apply」へと単純になります。
コスト — 1か月約 $10 でたどる #
学習用に最後までたどりながらコストを最小化する構成です。
- Aurora Serverless v2 を最小 0 ACU(自動一時停止)に置き、アイドル時のコンピューティング課金を 0(ストレージのみ)にします。最初の接続に約 15秒の再開が付くので、prod は 0 ではなく 0.5+ に置きます。
- Fargate Spot で Task を立ち上げて単価を下げます(学習限定、production は通常の Fargate と混合)。
- NAT の代わりに VPC Endpoint — ECR / S3 / Secrets Manager は Endpoint で(第28章)NAT 処理コストを回避します。
- 実習が終わったら
terraform destroyですぐに片付けます。
この構成で1か月約 $10 前後です。ALB · NAT · Aurora は付けておいた分だけ課金 されるので、実習が終わったら必ず片付け、第27章 コスト最適化 の請求アラートをあらかじめかけておきます。
terraform destroymodern-kubernetes 本との比較 #
Kubernetes 本の6部が同じ Todo システムを EKS でデプロイします。同じドメインを2つのプラットフォームで実装したときの違いは次のとおりです。
| 観点 | 本書 (ECS Fargate) | Kubernetes 本 (EKS) |
|---|---|---|
| オーケストレーション | ECS Task 定義 | k8s Deployment / Service |
| デプロイ単位 | Terraform + ECS ローリング | Helm + ArgoCD GitOps |
| コントロールプレーンのコスト | なし(Fargate) | EKS コントロールプレーンの時間単位 |
| 学習曲線 | 低い | 高い |
| 移植性 | AWS 依存 | マルチクラウド可能 |
| 適する場面 | 小さなチーム、単一ドメイン、速いリリース | マルチドメイン、GitOps、マルチクラウドオプション |
決定基準: 小さなチーム + 単一ドメインなら ECS Fargate がコントロールプレーンのコストも学習曲線も低く効率的です。マルチドメイン + GitOps + マルチクラウドの可能性 が必要になったら、そのとき EKS を検討します。二冊の6部が同じアプリを2つの路線で見せる理由です。
練習問題 #
terraform applyを13ステップの順に一度にせず、小さな PR に分ける理由を一段落で書いてみてください。VPC(2ステップ)なしで ECS(7ステップ)を apply すると何が止まるかを §「リポジトリ構成」の依存関係で説明します。- このキャップストーンの DB パスワードがコード · イメージ · Terraform state のどこにも平文で残らない経路を、
manage_master_user_passwordと Task のsecrets注入を根拠にした流れで書いてみてください(4·5·7ステップ)。 - 自分のサービスを ECS Fargate と EKS のどちらでデプロイするかを §「modern-kubernetes 本との比較」表の基準で決め、根拠を一段落で書いてみてください。
一行まとめ: 6部キャップストーンは modern-react の Next.js と modern-python の FastAPI を1つのアカウントに ECS Fargate + Aurora Serverless v2 + S3 + CloudFront + ALB + Secrets Manager で、28章の 3-tier VPC の上に13ステップでデプロイする。すべてのステップが Terraform ファイル(backend / vpc / ecr / rds / alb / ecs / cdn / dns / monitoring)でコード化されてコンソールクリックがなく、このコード化が30章 Pilot Light DR の前提になる。DB パスワードは
manage_master_user_passwordで RDS が Secrets Manager に作り、Task の secrets で注入されてどこにも平文で残らず、CI/CD は OIDC ロールの引き受けで長期キーなしにデプロイする。Aurora の最小 ACU + Fargate Spot + VPC Endpoint で1か月約 $10 でたどり、終わったら destroy する。同じアプリを EKS でデプロイする Kubernetes 本の6部と比べると、小さなチーム・単一ドメインには ECS Fargate、マルチドメイン・GitOps・マルチクラウドには EKS が分かれる。
次の章 #
本文はここで終わります。最後に 付録A CLF-C02 認定の橋渡し では、本書27章が AWS Cloud Practitioner(CLF-C02)試験範囲とどこで重なり、どこが空白かをマッピングします。実戦で身につけた内容を認定トラックへつなぎたい方のための橋です。