ECS와 Fargate — 컨테이너 배포
AWS 위에 컨테이너를 어떻게 올리는지 한 번에 정리합니다. ECS의 동작 방식(vs EKS), Cluster · Service · Task · Task Definition의 네 구성 요소, EC2 launch type과 Fargate의 차이, Execution Role과 Task Role의 분리, ALB · VPC 연결, 그리고 첫 배포부터 Auto Scaling · 비용까지 다룹니다.
이 책의 종착점은 ECS Fargate 위에서 풀스택 앱을 운영하는 것입니다(6부 풀스택 앱 AWS 배포하기). 그래서 본 챕터는 단순한 한 서비스가 아니라, 책 전체가 왜 컨테이너 우선으로 흘러가는지를 답하는 챕터입니다. 1부에서 계정·IAM·보안·CloudWatch의 토대를 잡고, 2부에서 8장 EC2/VPC부터 14장 CloudFront까지 핵심 자원을 익혔다면, 이제 EC2 한 대에 직접 올리는 방식에서 벗어나 한 단계 올라갈 차례입니다.
직전 14장 CloudFront 까지가 “자원 하나하나를 어떻게 만드는가” 였다면, 본 챕터부터는 그 자원들 위에 실제 애플리케이션을 어떻게 운영 가능한 형태로 올리는가로 시야가 바뀝니다. 도커로 만든 이미지를 AWS에서 돌리는 표준 패턴이 ECS와 Fargate이며, 4부 22장 ECS Fargate 배포 골격과 6부 캡스톤이 모두 본 챕터에서 잡은 모델 위에 세워집니다.
본 챕터에서는 ECS의 동작 방식과 EKS와의 차이, 네 가지 구성 요소, EC2와 Fargate라는 두 launch type, 가장 자주 헷갈리는 두 IAM 역할, 첫 배포의 전체 흐름, 그리고 배포 전략·Auto Scaling·비용까지 한 번에 정리합니다.
EC2 한 대에 직접 올리는 방식의 한계 #
9장 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는 8장 EC2/VPC 같은 토대 위에 k8s 자체 학습이 끝난 뒤로 미룹니다. 이 책은 컨테이너 운영을 ECS Fargate로 다루며, EKS · 쿠버네티스 노선은 쿠버네티스 책의 영역입니다.
ECS의 또 다른 서비스로 App Runner도 있습니다. ECS보다 더 간단합니다(이미지 → URL 한 번에). 하지만 옵션이 좁아 운영 영역은 ECS / Fargate가 차지하는 것이 현재 표준입니다.
ECS의 네 가지 구성 요소 #
ECS를 이해하려면 네 가지 구성 요소만 외우면 됩니다.
┌──────────────────────────────────────┐
│ Cluster — 묶음의 단위 │
│ ┌────────────────────────────────┐ │
│ │ Service — 항상 N 개 유지 │ │
│ │ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Task #1 │ │ Task #2 │ │ │
│ │ │ (컨테이너) │ │ (컨테이너) │ │ │
│ │ └────────────┘ └────────────┘ │ │
│ │ ↑ Task Definition (설계도) │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘Task Definition — 컨테이너의 설계도 #
JSON 한 장입니다. 무엇을 어떻게 띄울지가 다 들어 있습니다.
- 어떤 이미지 (
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:v1) - CPU / 메모리 (
512/1024 MB) - 환경 변수 / 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가 그것을 참조하게 바꾸는 식입니다.
Task — 실행 중인 인스턴스 #
Task Definition을 실제로 띄운 한 컨테이너(또는 컨테이너 묶음)입니다. EC2의 인스턴스에 해당합니다.
- 한 Task = 한 Task Definition revision의 실행입니다.
- Task 안에 컨테이너가 여러 개일 수도 있습니다(사이드카 패턴 — 메인 앱 + 로그 수집기 등).
- Task는 자체 ENI (네트워크 인터페이스)와 IP를 가집니다 (
awsvpc모드).
Service — N 개를 항상 유지 #
“Task를 한 번 띄워라"만 하면 그것이 죽으면 끝입니다. Service는 그 위에 붙는 역할입니다.
- “이 Task Definition의 Task를 항상 N 개 유지하라”
- 죽으면 자동 재시작
- ALB / NLB와 연결되어 트래픽 받기 (13장 ALB / NLB와 ACM)
- 배포 전략 (rolling, blue/green)
- Auto Scaling (CPU / 메모리 / 요청 수 기반)
운영 워크로드(웹 서버, API 등)는 거의 다 Service로 띄웁니다. 단발성 배치 작업만 Service 없이 Task를 직접 실행합니다 (RunTask).
Cluster — 묶음 #
Service와 Task가 사는 논리적 묶음입니다. 보통 환경 단위로 분리합니다.
prod-clusterstaging-clusterdev-cluster
Cluster는 무료입니다(Cluster 자체에는 비용이 없습니다). 안에서 도는 Task의 리소스가 비용입니다. 그래서 환경별로 자유롭게 나눠도 괜찮습니다.
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 + 자유 |
이 책의 3~4부와 6부 캡스톤은 모두 Fargate 기준으로 진행합니다. 운영 부담을 크게 줄여 주고 학습 곡선이 부드럽기 때문입니다.
두 IAM 역할 — Execution Role vs Task Role #
ECS 운영에서 가장 자주 헷갈리는 지점입니다.
Execution Role #
ECS 에이전트가 Task를 띄우는 데 필요한 권한입니다. Task가 시작되기 직전 AWS가 사용합니다.
- ECR에서 이미지 pull (16장 ECR)
- 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 #
16장 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 (13장 ALB / NLB와 ACM)을 미리 만들어 둔 상태로 진행합니다.
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 (또는 12장 Route 53의 도메인)로 접속하면 끝입니다.
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 비용이 클 수 있습니다. 비용 최적화는 27장 비용 최적화에서 본격적으로 다룹니다.
비용 절감 옵션 #
- 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장 CloudWatch 입문)에서 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 권한이 없는 것입니다. 자세히는 20장 Secrets Manager / Parameter Store에서 다룹니다.
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 간 트래픽 비용을 회피합니다.
연습문제 #
- 본인이 운영하려는 서비스의 트래픽 패턴(항상 일정한지, 변동이 큰지, 배치성인지)을 한 줄로 적고, §“Launch Type — EC2 vs Fargate"의 표에서 어느 launch type이 맞는지 골라 이유를 메모해 두세요. 22장 ECS Fargate 배포 골격에서 같은 선택을 Terraform으로 다시 내리게 됩니다.
- Execution Role과 Task Role이 각각 어떤 동작에 쓰이는지 보지 않고 한 문장씩 적어 보세요. 그리고 본인의 앱이 S3와 Secrets Manager를 모두 쓴다고 할 때, 어느 권한이 어느 역할에 들어가야 하는지 §“두 IAM 역할"을 근거로 연결해 두세요(6장 보안 기본의 최소 권한과 묶어 생각합니다).
- §“비용 — 어디서 나오는가"의 계산을 따라, 1vCPU + 2GB Fargate Task 2개를 한 달 운영할 때의 대략적인 비용을 직접 계산해 보세요. 여기에 ALB와 NAT Gateway가 더해진다면 어느 항목을 16장 ECR의 VPC Endpoint로 줄일 수 있는지 한 줄로 적어 두세요.
한 줄 요약: ECS는 AWS의 매니지드 컨테이너 오케스트레이터로, Cluster · Service · Task · Task Definition 네 요소로 구성된다. launch type은 운영 부담이 큰 EC2와 운영이 0 인 Fargate로 나뉘고 이 책은 Fargate 기준이다. Execution Role은 ECS가 Task를 띄우는 권한, Task Role은 코드가 AWS API를 호출하는 권한으로 절대 헷갈리면 안 된다. 첫 배포는 ECR push → Cluster → Task Definition → Service(ALB 연결) 순이며, rolling update가 기본이고 비용은 vCPU·메모리에 ALB·NAT가 더해지는데 NAT가 의외로 크다.
다음 챕터 #
다음 16장 ECR에서는 ECS가 띄우는 이미지가 어디서 오는지를 다룹니다. private repo 만들기, IAM 인증, push / pull, 이미지 스캔, 라이프사이클 정책, 멀티 아키텍처 이미지까지 — ECS의 동반자인 이미지 레지스트리를 한 번에 정리합니다.