목차
15 장

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에서 돌리는 표준 패턴이 ECSFargate이며, 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 #

ECSEKS
정체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를 이해하려면 네 가지 구성 요소만 외우면 됩니다.

ECS의 4가지 구성 — 위에서 아래로
┌──────────────────────────────────────┐
│  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 — 뒤에서 자세히 다룹니다)
  • 헬스 체크
task-definition.json (Fargate)
{
  "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-cluster
  • staging-cluster
  • dev-cluster

Cluster는 무료입니다(Cluster 자체에는 비용이 없습니다). 안에서 도는 Task의 리소스가 비용입니다. 그래서 환경별로 자유롭게 나눠도 괜찮습니다.

Launch Type — EC2 vs Fargate #

ECS가 Task를 실제로 어디에 띄울지 정하는 방식입니다. 두 모드가 있습니다.

EC2 Launch Type #

내가 EC2 인스턴스 묶음 (ASG)을 운영하고 ECS는 그 위에 컨테이너를 스케줄링합니다.

EC2 Launch Type
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가 그 작업을 알아서 처리합니다.

Fargate Launch Type
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에서 자세히 다루지만, 미리 흐름만 봅니다.

ECR push
# 인증
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:v1

2) Cluster 만들기 #

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)을 미리 만들어 둔 상태로 진행합니다.

Service
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가 다음을 합니다.

  1. Fargate 위에 컨테이너 2개 띄우기
  2. 각 컨테이너의 ENI를 Target Group에 등록
  3. 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:2

ECS가 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를 자동 조절합니다.

평균 CPU 60% 유지하도록
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.json

cpu-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).

Service Connect 설정 (요약)
{
  "serviceConnectConfiguration": {
    "enabled": true,
    "namespace": "myapp",
    "services": [
      {
        "portName": "web",
        "discoveryName": "web",
        "clientAliases": [{ "port": 8000, "dnsName": "web" }]
      }
    ]
  }
}

작은 시스템에서는 ALB 한 번만으로 충분합니다. 마이크로서비스가 여러 개일 때 Service Connect를 검토합니다.

비용 — 어디서 나오는가 #

Fargate 기준입니다.

비용 = vCPU + 메모리 + 네트워크
시간당 = (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이 폭주 #

v1v2 → … → 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 간 트래픽 비용을 회피합니다.

연습문제 #

  1. 본인이 운영하려는 서비스의 트래픽 패턴(항상 일정한지, 변동이 큰지, 배치성인지)을 한 줄로 적고, §“Launch Type — EC2 vs Fargate"의 표에서 어느 launch type이 맞는지 골라 이유를 메모해 두세요. 22장 ECS Fargate 배포 골격에서 같은 선택을 Terraform으로 다시 내리게 됩니다.
  2. Execution Role과 Task Role이 각각 어떤 동작에 쓰이는지 보지 않고 한 문장씩 적어 보세요. 그리고 본인의 앱이 S3와 Secrets Manager를 모두 쓴다고 할 때, 어느 권한이 어느 역할에 들어가야 하는지 §“두 IAM 역할"을 근거로 연결해 두세요(6장 보안 기본의 최소 권한과 묶어 생각합니다).
  3. §“비용 — 어디서 나오는가"의 계산을 따라, 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의 동반자인 이미지 레지스트리를 한 번에 정리합니다.

X