풀스택 앱 AWS 배포하기 — ECS Fargate 캡스톤
1~31장의 모든 서비스를 하나로 엮는 종합 실습. modern-react의 Next.js 앱과 modern-python의 FastAPI 앱을 한 계정에 ECS Fargate + RDS + S3 + CloudFront + ALB + Secrets Manager + Terraform으로 배포하고, 단계별 Terraform 코드와 13단계 PR 흐름, 한 달 약 $10의 최소 비용 구성, 그리고 쿠버네티스 책의 EKS 배포와의 비교까지 정리합니다.
이 책의 종착점입니다. 1~31장에서 따로 익힌 서비스 — IAM · VPC · S3 · RDS · ALB · CloudFront · ECS Fargate · ECR · Secrets Manager · CloudWatch · Terraform — 가 이 챕터에서 하나의 동작하는 시스템 안으로 들어옵니다. 도메인은 모던 파이썬 책과 리액트 책에서 만든 것과 같은 풀스택 Todo 앱입니다.
이 캡스톤은 쿠버네티스 책 6부와 짝을 이룹니다. 같은 앱을 같은 도메인으로 배포하되, 쿠버네티스 책은 EKS 노선으로, 본 책은 ECS Fargate 노선으로 갑니다. 두 캡스톤을 비교해 읽으시면 “매니지드 컨테이너 vs 쿠버네티스"의 운영상 차이가 코드 수준에서 분명해집니다.
무엇을 배포하는가 #
| 구성 요소 | 출처 | AWS 배치 |
|---|---|---|
| 프런트엔드 | 리액트의 Next.js 앱 | ECS Fargate (SSR) + ALB, 정적 자산은 S3 + CloudFront |
| 백엔드 API | 모던 파이썬의 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 리포지터리 #
두 앱의 이미지를 담을 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 토픽으로 보내 슬랙·이메일로 받습니다.
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"로 단순해집니다.
비용 — 한 달 약 $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로 즉시 정리.
이 구성으로 한 달 약 $10 내외입니다. ALB · NAT · Aurora는 켜둔 만큼 과금 되니 실습이 끝나면 반드시 정리하고, 27장 비용 최적화의 청구 알림을 미리 걸어 둡니다.
terraform destroymodern-kubernetes 책과의 비교 #
쿠버네티스 책 6부가 같은 Todo 시스템을 EKS로 배포합니다. 같은 도메인을 두 플랫폼으로 구현했을 때의 차이는 다음과 같습니다.
| 결 | 본 책 (ECS Fargate) | 쿠버네티스 책 (EKS) |
|---|---|---|
| 오케스트레이션 | ECS Task 정의 | k8s Deployment / Service |
| 배포 단위 | Terraform + ECS 롤링 | Helm + ArgoCD GitOps |
| 컨트롤 플레인 비용 | 없음(Fargate) | EKS 컨트롤 플레인 시간당 |
| 학습 곡선 | 낮음 | 높음 |
| 이식성 | AWS 종속 | 멀티 클라우드 가능 |
| 적합 | 작은 팀, 단일 도메인, 빠른 출시 | 멀티 도메인, GitOps, 멀티 클라우드 옵션 |
결정 기준: 작은 팀 + 단일 도메인이면 ECS Fargate가 컨트롤 플레인 비용도 학습 곡선도 낮아 효율적입니다. 멀티 도메인 + GitOps + 멀티 클라우드 가능성이 필요해지면 그때 EKS를 검토합니다. 두 책의 6부가 같은 앱을 두 노선으로 보여 주는 이유입니다.
연습문제 #
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를 한 계정에 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로 한 달 약 $10에 따라하고 끝나면 destroy 한다. 같은 앱을 EKS로 배포하는 쿠버네티스 책 6부와 비교하면 작은 팀·단일 도메인엔 ECS Fargate, 멀티 도메인·GitOps·멀티 클라우드엔 EKS가 갈린다.
다음 챕터 #
본문은 여기서 끝납니다. 마지막으로 부록 A CLF-C02 자격증 가교에서는 본 책 27장이 AWS Cloud Practitioner(CLF-C02) 시험 범위와 어디서 겹치고 어디가 공백인지를 매핑합니다. 실전으로 익힌 내용을 자격증 트랙으로 잇고 싶은 분을 위한 다리입니다.