AWS 실전 #1 인프라 골격: FastAPI/Django를 ECS Fargate에 배포
기초 7 편에서 계정 / 리전 / IAM / 비용 / CLI / 보안 / 로그를, 중급 7 편에서 EC2 / VPC / S3 / RDS / DNS / ALB / CloudFront를, 고급 7 편에서 ECS / Lambda / 메시징 / Secrets / Step Functions를 차례로 다뤘습니다. 21 편의 도구를 모았고, 이제 진짜 백엔드를 하나의 프로젝트로 올립니다.
이 시리즈는 FastAPI 실전 또는 장고 DRF 시리즈에서 만든 블로그 API (Post + Comment + User)를 기반으로, 6 편을 거쳐 운영 가능한 형태까지 끌어올립니다.
큰 그림 #
이번 글에서 만들 인프라:
Internet
│
▼
┌──────────────┐
│ Route 53 │ blog.example.com
└──────┬───────┘
│
▼
┌────────────────┐
│ ALB │ :443 → :8000
│ (HTTPS, ACM) │
└────────┬───────┘
│
┌──────────┴──────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ AZ-a │ │ AZ-c │
│ Fargate │ │ Fargate │
│ Task #1 │ │ Task #2 │
│ (Blog) │ │ (Blog) │
└─────┬─────┘ └─────┬─────┘
│ │
└──────────┬──────────┘
▼
┌──────────────┐
│ RDS Postgres│ (Multi-AZ, Private)
└──────────────┘구성별 정리:
| 구성 요소 | 역할 | 출처 |
|---|---|---|
| Route 53 | 도메인 → ALB | 중급 #5 |
| ALB | TLS 종단, 라우팅, 헬스체크 | 중급 #6 |
| ACM | TLS 인증서 발급/갱신 | 중급 #6 |
| ECR | 이미지 저장 | 고급 #2 |
| ECS Fargate | 컨테이너 실행 (서버리스) | 고급 #1 |
| RDS | DB | 중급 #4, #2 |
| VPC + Subnet | 네트워크 분리 | 중급 #1 |
| Secrets Manager | DB 비밀번호 | 고급 #6, #2 |
이번 글은 DB를 제외한 모든 구성 요소를 한 번에 셋업합니다. RDS는 #2에서 별도로 다룹니다.
도메인: 블로그 API 컨테이너 한 줄 요약 #
이 시리즈가 가정하는 컨테이너는 FastAPI 실전 #6 또는 DRF #6의 산출물로, 다음 모양입니다.
FROM python:3.14-slim AS base
WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 curl && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app /app/app
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD curl -fsS http://127.0.0.1:8000/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]핵심 약속 셋:
- 포트 8000에서 듣기
/health가 200을 반환 (DB의존 없는 가벼운 체크)/ready가 DB 연결 OK 면 200, 아니면 503. ALB / ECS가 트래픽 라우팅을 결정
장고라면 gunicorn -w 4 myproject.wsgi로 같은 약속을 만들면 됩니다.
1) VPC와 서브넷: 네트워크 골격 #
ECS / RDS / ALB는 모두 VPC 안에 삽니다. VPC가 없으면 한 줄도 못 띄웁니다. 다행히 새 계정엔 default VPC가 리전마다 있어서, 빠르게 시작할 땐 그걸 써도 됩니다. 운영은 직접 만든 VPC를 권장합니다.
권장 구조 #
Public Subnet (10.0.0.0/24, AZ-a) ← ALB, NAT GW
Public Subnet (10.0.1.0/24, AZ-c) ← ALB, NAT GW
Private Subnet (10.0.10.0/24, AZ-a) ← Fargate Task
Private Subnet (10.0.11.0/24, AZ-c) ← Fargate Task
DB Subnet (10.0.20.0/24, AZ-a) ← RDS
DB Subnet (10.0.21.0/24, AZ-c) ← RDS세 가지 구성 요소:
| Subnet | 트래픽 방향 | 누가 사는가 |
|---|---|---|
| Public | 인터넷 ↔ | ALB, NAT Gateway |
| Private | 인터넷 X (NAT 통해 outbound만) | Fargate, EC2 |
| DB | 인터넷 X, Fargate만 접근 | RDS |
이 글에선 default VPC의 public subnet만 사용해서 빠르게 띄웁니다 (Fargate task에 public IP 부여). 운영 모양은 #4 Terraform에서 코드로.
Security Group 두 개 #
sg-alb 80, 443 ← 0.0.0.0/0
(인터넷이 ALB에)
sg-fargate 8000 ← sg-alb
(ALB만 Fargate에)중요한 패턴. SG는 다른 SG를 source로 받을 수 있습니다. IP 대역이 아니라 “이 SG가 붙은 리소스만” 을 의미합니다. ALB의 IP가 바뀌어도 규칙이 자동 따라옵니다.
2) ECR에 이미지 올리기 #
고급 #2 ECR에서 이미 다뤘지만 빠르게 다시.
aws ecr create-repository \
--repository-name blog-api \
--image-scanning-configuration scanOnPush=true \
--region ap-northeast-2scanOnPush=true. 이미지 푸시 시 자동 취약점 스캔 (#5 모니터링에서 결과 확인).
빌드와 푸시 #
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-northeast-2
REPO=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/blog-api
# 1) 로그인
aws ecr get-login-password --region $REGION | \
docker login --username AWS --password-stdin $REPO
# 2) 빌드 (linux/amd64 : Fargate 표준 아키텍처)
docker build --platform=linux/amd64 -t blog-api:v1 .
# 3) 태그
docker tag blog-api:v1 $REPO:v1
docker tag blog-api:v1 $REPO:latest
# 4) 푸시
docker push $REPO:v1
docker push $REPO:latestApple Silicon (M1/M2/M3) 맥에서 그냥
docker build로 만들면 arm64이미지가 생기고, Fargate (x86_64 표준)에선 안 돕니다.--platform=linux/amd64를 항상 명시. Fargate가 ARM도 지원하지만 별도 설정이 필요합니다.
이미지 태그 전략 #
| 태그 | 의미 |
|---|---|
latest | 최신. 운영에서 쓰지 마세요 (롤백 불가) |
v1, v2, ... | 사람이 읽는 버전 |
<git-sha> | 추적. CI가 자동 발행 (#3) |
<git-sha>-prod | 환경별 별칭 |
latest는 개발자 편의용입니다. 운영 작업 정의는 항상 git SHA 또는 semver로 고정합니다. 그래야 “어떤 코드가 돌고 있는지"를 한 번에 확인할 수 있습니다.
3) 작업 정의: 컨테이너의 “주민등록” #
ECS의 가장 중요한 구성 요소입니다. 이미지 + CPU/메모리 + 환경 변수 + 포트 + 로그 설정이 한 JSON에 묶입니다.
{
"family": "blog-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789012:role/blog-api-task-role",
"containerDefinitions": [
{
"name": "api",
"image": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/blog-api:v1",
"portMappings": [
{ "containerPort": 8000, "protocol": "tcp" }
],
"essential": true,
"environment": [
{ "name": "ENVIRONMENT", "value": "production" },
{ "name": "LOG_LEVEL", "value": "info" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/blog-api",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "api",
"awslogs-create-group": "true"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -fsS http://127.0.0.1:8000/health || exit 1"],
"interval": 10,
"timeout": 3,
"retries": 3,
"startPeriod": 30
}
}
]
}핵심 항목:
| 키 | 의미 |
|---|---|
cpu / memory | Fargate가 정해진 조합만 허용 (e.g. 256/512, 512/1024, 1024/2048) |
executionRoleArn | ECS agent가 ECR pull / Logs / Secrets 접근에 쓸 역할 |
taskRoleArn | 컨테이너 코드가 쓸 IAM 역할. boto3가 이걸로 sign |
awslogs | 로그가 자동으로 CloudWatch로 (#5) |
healthCheck | 컨테이너 자체 헬스체크 (Dockerfile와 별개) |
두 IAM 역할의 차이가 자주 헷갈림 #
executionRoleArn | taskRoleArn | |
|---|---|---|
| 누가 쓰는가 | ECS agent (시작 단계) | 컨테이너 안 코드 (실행 중) |
| 권한 | ECR pull, CloudWatch 쓰기, Secrets 읽기 | S3 접근, RDS, SQS 등. 앱 로직 |
executionRoleArn 누락 → 이미지 pull 실패. taskRoleArn 누락 → boto3가 NoCredentialsError.
등록 #
aws ecs register-task-definition \
--cli-input-json file://task-definition.json \
--region ap-northeast-2등록할 때마다 revision 번호 (blog-api:1, blog-api:2, …)가 올라갑니다. 롤백은 이전 revision 번호로 #3에서.
4) ALB + Target Group: 트래픽을 받는 쪽 #
중급 #6에서 만든 ALB 패턴 그대로. 핵심:
ALB:443 (HTTPS, ACM 인증서)
│
▼
Listener: 443 → forward → tg-blog-api
│
▼
Target Group: tg-blog-api
- Protocol: HTTP / 8000
- Target type: ip ← Fargate는 무조건 ip
- Health check: GET /health
- Healthy threshold: 2
- Interval: 15sTarget type은 반드시 ip. Fargate task는 매번 IP가 바뀌어 instance 모드가 안 됩니다.
aws elbv2 create-target-group \
--name tg-blog-api \
--protocol HTTP --port 8000 \
--vpc-id $VPC_ID \
--target-type ip \
--health-check-path /health \
--healthy-threshold-count 2 \
--health-check-interval-seconds 15ALB Listener 규칙은 중급 #6 참조. HTTPS 443 → forward → tg-blog-api, HTTP 80 → 443 redirect.
5) ECS 서비스: 컨테이너의 “회사” #
서비스는 항상 desired count만큼의 작업을 유지하고, 작업이 죽으면 새로 만들고, 배포 시 점진 교체합니다.
aws ecs create-cluster --cluster-name blog-clusteraws ecs create-service \
--cluster blog-cluster \
--service-name blog-api \
--task-definition blog-api:1 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={
subnets=[subnet-aaa, subnet-bbb],
securityGroups=[sg-fargate],
assignPublicIp=ENABLED
}" \
--load-balancers "targetGroupArn=$TG_ARN,containerName=api,containerPort=8000" \
--health-check-grace-period-seconds 60 \
--deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true},maximumPercent=200,minimumHealthyPercent=100"핵심 옵션 정리:
| 옵션 | 의미 |
|---|---|
desired-count 2 | 최소 2개. Multi-AZ 배포로 한 AZ 장애 견딤 |
assignPublicIp=ENABLED | private subnet + NAT가 없을 때 사용 (간이 셋업). 운영은 NAT 권장 |
health-check-grace-period | Service가 task를 띄운 직후 ALB 헬스체크를 기다리는 유예 (앱 부팅 시간) |
deploymentCircuitBreaker | 새 배포가 N 회 연속 실패하면 자동 롤백 (#3에서 자세히) |
maximumPercent=200 | 배포 중 최대 task 수 (200% = 기존 + 새 동시) |
minimumHealthyPercent=100 | 배포 중 최소 healthy 비율 (100% = 다운타임 0) |
이 두 % 값이 롤링 업데이트의 모양을 결정합니다.
자동 스케일링 #
서비스가 떴다고 자동 확장이 켜지지는 않습니다. 별도로:
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/blog-cluster/blog-api \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 --max-capacity 10aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/blog-cluster/blog-api \
--scalable-dimension ecs:service:DesiredCount \
--policy-name cpu-target \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 60.0,
"PredefinedMetricSpecification": {"PredefinedMetricType": "ECSServiceAverageCPUUtilization"},
"ScaleOutCooldown": 30,
"ScaleInCooldown": 120
}'CPU 평균 60% 를 기준으로 자동 scale out / in. 운영에서 처음엔 보수적으로 (40~60%) 두고 트래픽 패턴 보면서 조정.
6) 첫 배포 검증 #
서비스가 안정 상태에 들어갈 때까지 대기 #
aws ecs wait services-stable \
--cluster blog-cluster \
--services blog-api헬스체크 직접 확인 #
ALB_DNS=$(aws elbv2 describe-load-balancers \
--names blog-alb \
--query 'LoadBalancers[0].DNSName' --output text)
curl -i https://$ALB_DNS/health
# HTTP/2 200
# {"status": "ok"}로그 확인 #
aws logs tail /ecs/blog-api --follow --since 5m요청 한 번 보내고 로그에 access log가 뜨면 이번 시리즈의 첫 도착점에 온 겁니다 🎉.
함정: 첫 배포가 안 뜨는 5가지 원인 #
1) STOPPED 상태로 끝없이 재시작
#
ECS 콘솔의 Tasks 탭에서 STOPPED 행 클릭 → “Stopped reason” 확인. 흔한 원인:
| 메시지 | 원인 |
|---|---|
CannotPullContainerError | ECR 권한 누락 → executionRole |
ResourceInitializationError: ... secret manager | Secrets ARN 오타 / 권한 |
Essential container ... exited | 컨테이너 자체가 죽음 → CloudWatch logs |
Task failed ELB health checks | ALB가 healthy 판정 못 함 → 다음 항목 |
2) ALB 헬스체크 실패 #
가장 흔합니다. 점검 포인트:
- 컨테이너 포트 (
8000)가 Target Group 포트 (8000)와 일치하는가 /health엔드포인트가 진짜 200을 반환하는가 (DB의존 X)health-check-grace-period가 앱 부팅 시간 (FastAPI 5s, Django 20~40s)보다 긴가- Fargate Security Group inbound가 ALB SG만 허용하는가
- ALB가 그 task의 subnet까지 라우팅 가능한가 (같은 VPC)
3) awsvpc networkMode의 ENI 한도
#
Fargate task는 ENI (Elastic Network Interface)를 한 개씩 잡아먹습니다. AZ / 서브넷의 IP가 부족하면 새 task를 못 띄웁니다. CIDR를 너무 좁게 잡지 말기 (위 예시 /24 = 256 IP).
4) Public IP 없이 ECR pull 실패 #
Private subnet에 task를 띄우는데 NAT Gateway도 VPC Endpoint도 없으면, ECR / Secrets Manager / CloudWatch로 가는 트래픽이 막혀 시작이 실패합니다.
해결 셋:
- NAT Gateway 추가 (시간당 ~$0.045 + 데이터 전송)
- ECR / Logs / Secrets의 Interface VPC Endpoint 추가 (NAT보다 저렴)
- Public subnet +
assignPublicIp=ENABLED(학습용)
5) 배포 정체: 새 task가 healthy가 안 됨 #
deploymentCircuitBreaker가 켜져 있으면 N 분 후 자동 롤백. 꺼져 있으면 service가 영원히 IN_PROGRESS. aws ecs describe-services로 deployments 배열 확인.
정리 #
이번 글에서 잡은 것:
- 큰 그림. Route 53 → ALB → Fargate (× 2 AZ) → RDS, Multi-AZ 운영의 표준
- VPC 골격. public / private / db subnet의 역할, SG 두 개로 ALB ↔ Fargate
- ECR. 이미지 빌드 시
--platform=linux/amd64, 태그는 git SHA 또는 semver,latest운영 금지 - Task Definition. Fargate CPU/메모리 조합, executionRole vs taskRole의 분리, awslogs로 자동 로깅
- ALB Target Group. Fargate는
target-type ip, 헬스체크는/health - ECS Service. desired count, deployment circuit breaker, maximum/minimum % 가 롤링 모양 결정
- Auto Scaling.
application-autoscaling으로 CPU/요청 수 기반 target tracking - 검증.
services-stablewait, ALB DNS curl, CloudWatch Logs tail - 함정. STOPPED 원인 분석 / ALB 헬스체크 실패 5종 / ENI IP 부족 / NAT/Endpoint 누락 / 배포 정체
다음: RDS #
ALB 뒤로 트래픽이 들어오기 시작했지만, 우리 API는 아직 DB가 없어 메모리 안에서만 살고 있습니다.
#2 RDS 연동과 마이그레이션 운영에서는 VPC 안에 RDS Postgres Multi-AZ를 띄우고, Secrets Manager로 비밀번호를 주입하고, Alembic / Django migrations의 운영 패턴, 그리고 운영 트래픽에 영향을 주지 않는 blue/green 마이그레이션 패턴까지 정리하겠습니다.