AWS 고급 #1 ECS와 Fargate: 컨테이너 배포
AWS 기초 7편으로 계정 / IAM / 보안 / CloudWatch의 토대를 잡고, AWS 중급 7편으로 EC2 / VPC / S3 / RDS / Route 53 / ALB / CloudFront에 익숙해졌다면, 이제 컨테이너로 한 단계 올라갑니다.
AWS 고급 7편은 EC2 한 대에 직접 올리는 방식에서 벗어나, 컨테이너 / 서버리스 / 메시지 / 시크릿 / 워크플로우 같은 운영 규모에서 만나게 되는 도구들을 정리합니다.
- #1 ECS와 Fargate: 컨테이너 배포 ← 이번 글
- #2 ECR: 이미지 레지스트리
- #3 Lambda 기초
- #4 API Gateway + Lambda
- #5 EventBridge / SQS / SNS
- #6 Secrets Manager / Parameter Store
- #7 Step Functions 입문
이번 글은 그 출발점인 ECS와 Fargate입니다. 도커로 만든 이미지를 AWS에서 어떻게 돌리는지의 표준 패턴을 잡겠습니다.
EC2 한 대에 직접 올리는 방식의 한계 #
중급 #2 EC2 운영의 흐름, EC2 인스턴스를 만들고 SSH로 들어가 nginx / docker / 코드를 직접 설치하고 systemd로 띄우는 방식은 단순한 경우에는 충분 합니다. 하지만 규모가 커지면 갈증이 옵니다.
| 갈증 | EC2 직접 운영 |
|---|---|
| 같은 환경 재현 | OS 패치, 의존성 드리프트로 매번 다름 |
| 스케일 아웃 | AMI 만들기 → ASG → 배포. 분 단위 |
| 무중단 배포 | 복잡한 셸 스크립트 / 별도 도구 |
| 롤백 | 스냅샷 → 부팅 → 트래픽 이동 |
| 헬스 체크 / 자동 복구 | systemd로는 한계 |
이 갈증을 컨테이너가 풀어 주는 것이 모던 인프라의 흐름입니다. AWS에서 그 입구가 ECS입니다.
ECS가 맡는 일 #
**Amazon ECS (Elastic Container Service)**는 AWS의 매니지드 컨테이너 오케스트레이터입니다. 도커 이미지를 받아 어떤 머신에서, 몇 개를 띄우고, 트래픽을 어떻게 보낼지를 정해 놓으면 ECS가 알아서 운영합니다.
ECS vs EKS: 한 줄 비교 #
| ECS | EKS | |
|---|---|---|
| 정체 | AWS 자체 오케스트레이터 | AWS가 관리하는 Kubernetes |
| 학습 곡선 | 얕음 (AWS 안에 잘 녹음) | 가파름 (k8s 자체 학습 필요) |
| 다른 클라우드 이식성 | 낮음 (AWS 전용) | 높음 (k8s 표준) |
| 생태계 | AWS 도구 + 일부 커뮤니티 | k8s 전체 생태계 (Helm, ArgoCD 등) |
| 운영 부담 | 낮음 | 높음 (Control Plane 비용 + 운영 지식) |
| 적합한 경우 | 작은 / 중간 규모, AWS 종속 OK | 큰 규모, 멀티 클라우드, k8s 표준 필요 |
처음 컨테이너 운영을 시작한다면 ECS부터. EKS는 중급 #1 EC2/VPC 같은 토대와 k8s 자체 학습이 끝난 뒤에 고려합니다.
ECS의 또 다른 서비스로 App Runner도 있습니다. ECS보다 더 간단 (이미지 → URL 한 번에). 하지만 옵션이 좁아 운영 영역은 ECS / Fargate가 현재 표준으로 자리 잡고 있습니다.
ECS의 4가지 구성 요소 #
ECS를 이해하려면 네 가지 구성 요소만 외우면 됩니다.
┌──────────────────────────────────────┐
│ Cluster : 묶음의 단위 │
│ ┌────────────────────────────────┐ │
│ │ Service : 항상 N 개 유지 │ │
│ │ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Task #1 │ │ Task #2 │ │ │
│ │ │ (컨테이너) │ │ (컨테이너) │ │ │
│ │ └────────────┘ └────────────┘ │ │
│ │ ↑ Task Definition (설계도) │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘1) Task Definition: 컨테이너의 설계도 #
JSON 한 장. 무엇을 어떻게 띄울지가 다 들어 있습니다.
- 어떤 이미지 (
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:v1) - CPU / 메모리 (
512/1024MB) - 환경 변수 / Secrets
- 포트 매핑
- 로그 드라이버 (보통 CloudWatch Logs)
- IAM 역할 (Task Role + Execution Role, 뒤에서 자세히)
- 헬스 체크
{
"family": "myapp",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789012:role/myapp-task-role",
"containerDefinitions": [
{
"name": "web",
"image": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:v1",
"essential": true,
"portMappings": [{ "containerPort": 8000, "protocol": "tcp" }],
"environment": [
{ "name": "ENV", "value": "production" }
],
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:myapp/db-AbCdEf"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/myapp",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "web"
}
}
}
]
}Task Definition은 revision (myapp:7처럼 번호)으로 누적됩니다. 새 이미지를 배포하려면 새 revision을 만들고 Service가 그걸 참조하게 바꾸는 식.
2) Task: 실행 중인 인스턴스 #
Task Definition을 실제로 띄운 한 컨테이너 (또는 컨테이너 묶음)입니다. EC2에서의 인스턴스에 해당하는 개념입니다.
- 한 Task = 한 Task Definition revision의 실행
- Task 안에 컨테이너가 여러 개 일 수도 있음 (사이드카 패턴, 메인 앱 + 로그 수집기 등)
- Task는 자체 ENI (네트워크 인터페이스) + IP를 가짐 (
awsvpc모드)
3) Service: N 개를 항상 유지 #
“Task를 한 번 띄워라"만 하면 그게 죽으면 끝입니다. Service는 그 위에서 다음을 맡습니다:
- “이 Task Definition의 Task를 항상 N 개 유지하라”
- 죽으면 자동 재시작
- ALB / NLB와 연결되어 트래픽 받기 (중급 #6)
- 배포 전략 (rolling, blue/green)
- Auto Scaling (CPU / 메모리 / 요청 수 기반)
운영 워크로드 (웹 서버, API 등)는 거의 다 Service로 띄웁니다. 단발성 배치 작업만 Service 없이 Task를 직접 실행 (RunTask).
4) Cluster: 묶음 #
Service / Task가 사는 논리적 묶음. 보통 환경 단위로 분리:
prod-clusterstaging-clusterdev-cluster
Cluster는 공짜입니다 (Cluster 자체엔 비용 없음). 안에서 도는 Task의 리소스가 비용. 그래서 환경별로 자유롭게 나눠 OK.
Launch Type: EC2 vs Fargate #
ECS가 Task를 실제로 어디에 띄울지 정하는 방식입니다. 두 모드가 있습니다.
EC2 Launch Type #
내가 EC2 인스턴스 묶음 (ASG)을 직접 운영하고, ECS는 그 위에 컨테이너를 스케줄링합니다.
ECS Service
│ (스케줄)
▼
EC2 #1 EC2 #2 EC2 #3 ← 내가 운영 (ASG, AMI, 패치, 보안)
▲ ▲ ▲
컨테이너 컨테이너 컨테이너장점:
- 인스턴스 비용 = EC2가격 (장기 절감 / Reserved / Spot)
- GPU / 큰 메모리 / 특수 인스턴스 자유
단점:
- EC2 자체를 운영해야 함. AMI 최신화, OS 보안 패치, ECS 에이전트 업데이트
- 인스턴스 채우기 (binpacking) 신경 써야 함
- 빈 인스턴스가 떠 있으면 그 시간만큼 낭비
Fargate Launch Type #
EC2가 안 보입니다. Task의 CPU / 메모리만 선언 하면 AWS가 그 작업을 알아서 처리합니다.
ECS Service
│ (스케줄)
▼
[AWS 관리 영역 : 보이지 않음]
│
▼
컨테이너 (Task)장점:
- EC2 운영 0. OS 패치, ASG, AMI 모두 AWS가 함
- Task 단위 결제 (분 단위, vCPU + 메모리)
- 빈 인스턴스 낭비 없음
단점:
- 단가가 EC2보다 높음 (관리 비용 포함)
- GPU / 특수 인스턴스 / 일부 네트워크 옵션 불가
- 컨테이너당 vCPU 0.25~16, 메모리 0.5~120GB 한도
어느 쪽을 고를까 #
| 경우 | 추천 |
|---|---|
| 작은 / 중간 트래픽 | Fargate. 운영 부담 0 |
| 비용이 매우 큰 경우 | EC2 + Reserved / Spot |
| GPU / 특수 워크로드 | EC2 |
| 변동 트래픽 / 배치 | Fargate Spot (최대 70% 할인) |
| k8s가 익숙한데 ECS만 가능 | EC2 + 자유 |
이 시리즈와 실전 7편은 모두 Fargate 기준으로 진행합니다. 운영 부담을 크게 줄여주고 학습 곡선이 완만하기 때문입니다.
두 IAM 역할: Execution Role vs Task Role #
ECS 운영에서 가장 자주 헷갈리는 지점.
Execution Role #
ECS 에이전트가 Task를 띄우는 데 필요한 권한. Task가 시작되기 직전 AWS가 사용.
- ECR에서 이미지 pull
- CloudWatch Logs 그룹 / 스트림 생성
- Secrets Manager / Parameter Store에서 secret 조회 (Task 시작 시점 주입)
기본적으로 한 계정에 ecsTaskExecutionRole 하나면 충분 (AWS 매니지드 정책 AmazonECSTaskExecutionRolePolicy 부여).
Task Role #
컨테이너 안의 코드가 AWS API를 호출할 때 쓰는 권한. 런타임에 사용.
- 코드 안에서
boto3.client("s3").get_object(...)→ S3 접근 - 코드 안에서
dynamodb.get_item(...)→ DynamoDB 접근
각 앱마다 최소 권한의 Task Role을 따로 만드는 게 원칙. 기초 #6 보안 기본의 최소 권한 패턴.
Execution Role → ECS가 사용 (이미지 pull, 로그 생성, secret 주입)
Task Role → 내 코드가 사용 (S3, DynamoDB, SQS 호출 등)이 둘을 헷갈려 한 곳에 몰아 넣으면 보안 사고로 이어집니다.
첫 배포: Hello, ECS #
완전한 흐름을 한 번 따라가 봅니다. 이미 도커 이미지가 있다고 가정합니다.
1) ECR에 이미지 push #
#2 ECR에서 자세히 다루지만, 미리 흐름:
# 인증
aws ecr get-login-password --region ap-northeast-2 \
| docker login --username AWS --password-stdin \
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
# 빌드 + 태그 + push
docker build -t myapp .
docker tag myapp:latest \
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:v1
docker push \
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:v12) Cluster 만들기 #
aws ecs create-cluster --cluster-name prod-cluster콘솔에서도 한 번 클릭. 다시 말하지만 무료.
3) Task Definition 등록 #
위의 JSON을 파일 (task-definition.json)로 두고:
aws ecs register-task-definition \
--cli-input-json file://task-definition.json성공 시 myapp:1 revision이 만들어집니다.
4) Service 생성 (ALB와 함께) #
ALB의 Target Group (중급 #6)을 미리 만들어둔 상태로:
aws ecs create-service \
--cluster prod-cluster \
--service-name myapp \
--task-definition myapp:1 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-xxx],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...,containerName=web,containerPort=8000"이 줄을 실행하는 순간 ECS가:
- Fargate 위에 컨테이너 2개 띄우기
- 각 컨테이너의 ENI를 Target Group에 등록
- ALB가 헬스 체크 통과 후 트래픽 라우팅
ALB의 DNS (또는 Route 53 (중급 #5)의 도메인)로 접속하면 끝.
5) 새 버전 배포 #
# 새 이미지 push (myapp:v2)
docker tag myapp:v2 ...; docker push ...
# Task Definition 새 revision (이미지 태그만 바꿔 다시 등록)
aws ecs register-task-definition --cli-input-json file://task-definition-v2.json
# → myapp:2
# Service가 새 revision을 쓰도록 업데이트
aws ecs update-service \
--cluster prod-cluster \
--service myapp \
--task-definition myapp:2ECS가 rolling update로 알아서, 새 Task 2개를 올리고 헬스 체크 통과하면 옛 Task 2개를 종료. 서비스 중단 없음.
Service의 배포 옵션 #
기본은 rolling update 지만 두 가지 더.
Rolling Update (기본) #
minimumHealthyPercent (기본 100)와 maximumPercent (기본 200) 두 노브로 조절.
minHealthy=100, maxPercent=200→ desired=2일 때 한순간 4개까지 (새 2 + 옛 2), 옛 거 종료. 무중단.minHealthy=50, maxPercent=100→ 옛 1개 종료 → 새 1개 → 옛 1개 종료 → 새 1개. 비용 절감.
Blue / Green (CodeDeploy 연동) #
새 환경 (green)을 통째로 만들고 ALB의 listener를 한순간 바꾸는 방식. 롤백이 즉시 가능.
External (Spinnaker / 자체 컨트롤러) #
ECS에 “어떻게 배포할지” 를 외부 도구에 위임. 큰 조직에서만.
Auto Scaling: 트래픽 따라 늘리기 #
Service 위에 Application Auto Scaling을 붙여 desired count를 자동 조절.
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/prod-cluster/myapp \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 --max-capacity 10
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/prod-cluster/myapp \
--scalable-dimension ecs:service:DesiredCount \
--policy-name cpu60 \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration file://cpu-60.jsoncpu-60.json 안에 PredefinedMetricSpecification: ECSServiceAverageCPUUtilization, TargetValue: 60.0.
스케일 트리거의 기본 후보:
- ECS Service 평균 CPU
- ECS Service 평균 메모리
- ALB의 RequestCountPerTarget (요청 수 기반)
Service Connect: 서비스 간 통신 #
여러 마이크로서비스가 ECS 위에서 서로 호출하는 방식에는 두 가지 옵션이 있습니다.
1) ALB / NLB 경유 #
각 서비스 앞에 ALB를 둡니다. 서비스 A → https://service-b.internal/ (Route 53 private hosted zone) → ALB → Service B 순으로 흐릅니다.
장점은 표준 HTTP를 쓰고 외부 구조와 일관된다는 점, 단점은 ALB 비용과 한 hop이 추가된다는 점입니다.
2) Service Connect (ECS 자체) #
ECS가 컨테이너 옆에 proxy 사이드카 (Envoy 기반)를 자동으로 끼워 넣어 mesh처럼 동작합니다. DNS는 Cluster 안에서 자동 등록됩니다 (web.myapp.local).
{
"serviceConnectConfiguration": {
"enabled": true,
"namespace": "myapp",
"services": [
{
"portName": "web",
"discoveryName": "web",
"clientAliases": [{ "port": 8000, "dnsName": "web" }]
}
]
}
}작은 시스템에선 ALB 한 번만으로 충분합니다. 마이크로서비스가 여러 개일 때 Service Connect를 검토하세요.
비용: 어디서 나오는가 #
Fargate 기준:
시간당 = (vCPU 시간) × $0.0506
+ (메모리 GB 시간) × $0.0055
+ (Data Transfer)
예: 0.5 vCPU + 1GB Fargate 1개 한 달 (730h)
= 0.5 × 0.0506 × 730 + 1 × 0.0055 × 730
= $18.5 + $4.0
= $22.5 / month (서울 리전 기준 대략)추가로:
- ALB: 시간당 + LCU 단위
- NAT Gateway (private subnet에서 인터넷 나갈 때): 시간당 + GB
- CloudWatch Logs: ingest GB + storage GB
NAT Gateway가 의외로 큽니다. 한 달 $30 수준. 작은 서비스에선 Fargate 자체보다 NAT 비용이 클 수 있습니다.
비용 절감 옵션 #
- Fargate Spot: 변동 / 배치 워크로드에 70% 할인. 갑자기 종료될 수 있어 stateless한 워크로드에서만
- Compute Savings Plans: 1~3년 약정으로 최대 50% 할인
- Right-sizing: CloudWatch Container Insights로 실제 사용량 확인 후 vCPU / 메모리 줄이기. 가장 큰 효과가 나는 항목
자주 만나는 함정 #
1) Task가 계속 죽고 다시 뜬다 #
Service가 자동 재시작하니 표면적으론 “동작하는 것 같지만” 사실은 컨테이너가 시작 직후 종료되고 있는 상황입니다. 주요 원인은 다음과 같습니다.
- 헬스 체크 실패 (앱이 늦게 떠서 ALB가 unhealthy 판단)
- 컨테이너 안에서 에러로 즉시 exit
- 메모리 부족 (OOM killed)
CloudWatch Logs (기초 #7)에서 stopped reason 확인:
aws ecs describe-tasks --cluster prod-cluster \
--tasks <task-id> --query 'tasks[0].stoppedReason'2) 이미지 pull 권한 부족 #
Task 시작 직후 “CannotPullContainerError” → 99%는 Execution Role에 ECR 권한 누락. AWS 매니지드 AmazonECSTaskExecutionRolePolicy를 붙였는지 확인.
3) Secret 주입이 안 된다 #
Task Definition의 secrets가 비어 들어옴 → Execution Role이 Secrets Manager / Parameter Store의 ARN에 secretsmanager:GetSecretValue / ssm:GetParameter 권한이 없음. 자세히는 #6.
4) ALB Target이 unhealthy #
배포는 됐는데 ALB 헬스 체크가 실패. 자주 보는 원인:
- 헬스 체크 path가 앱에 없음 (
/health엔드포인트 잊음) - Security Group이 ALB → Task 트래픽을 막음
- 앱이 0.0.0.0이 아닌 127.0.0.1에 바인딩 (컨테이너 안에서 외부에서 접근 불가)
5) Task Definition revision이 폭주 #
v1 → v2 → … → v847처럼 끝없이 쌓입니다. 직접 정리하지 않으면 콘솔이 무거워집니다. 운영 정책으로 30일 이상 미사용 revision을 자동 정리하거나, IaC가 정리하도록 설정하세요.
6) NAT Gateway 비용 폭발 #
private subnet의 Task가 외부 API를 자주 호출 → NAT Gateway의 Data Processing 요금이 EC2 요금을 넘어섬. 대안:
- VPC Endpoint (S3, ECR, Secrets Manager 등 자주 쓰는 서비스에). 트래픽이 NAT 안 거침
- 외부 API 호출이 많으면 같은 AZ의 NAT에서 같은 AZ의 Task → AZ 간 트래픽 비용 회피
정리 #
이번 글에서 잡은 것:
- EC2 직접 운영의 한계. 환경 재현, 스케일, 무중단 배포, 롤백, 헬스 체크가 컨테이너로 자연스럽게 풀린다
- ECS의 역할. AWS의 매니지드 컨테이너 오케스트레이터. EKS는 k8s 표준이 필요할 때
- 4가지 구성. Cluster (묶음) / Service (N 개 유지) / Task (실행 중인 컨테이너) / Task Definition (설계도)
- Launch Type. EC2 (직접 운영, 비용 최적화) vs Fargate (운영 0, 단가 높음). 시리즈는 Fargate 기준
- 두 IAM 역할. Execution Role (ECS가 Task를 띄우는 권한) vs Task Role (코드가 AWS API 호출 권한). 절대 헷갈리지 말 것
- 첫 배포 흐름. ECR push → Cluster → Task Definition → Service (ALB 연결)
- 배포. rolling (기본) / blue-green (CodeDeploy) / external
- Auto Scaling. Application Auto Scaling으로 CPU / 메모리 / 요청 수 기반
- Service Connect. Service 간 통신을 ALB 없이 mesh로
- 비용. vCPU + 메모리 + ALB + NAT. NAT가 의외로 큼. Spot, Savings Plans, Right-sizing
- 함정. Task 무한 재시작 (헬스 체크 / OOM), 이미지 pull 권한, Secret 권한, ALB unhealthy, revision 폭주, NAT 비용
다음: ECR #
ECS가 띄우는 이미지가 어디서 오는지가 다음 글의 주제입니다. **Amazon ECR (Elastic Container Registry)**은 다음 글에서 자세히 다룹니다.
#2 ECR: 이미지 레지스트리에서는 private repo 만들기, 인증, push / pull, 이미지 스캔, 라이프사이클 정책, 멀티 아키텍처 이미지까지, ECS의 동반자를 한 번에 정리하겠습니다.