AWS 고급 #1 ECS와 Fargate: 컨테이너 배포

11 분 소요

AWS 기초 7편으로 계정 / IAM / 보안 / CloudWatch의 토대를 잡고, AWS 중급 7편으로 EC2 / VPC / S3 / RDS / Route 53 / ALB / CloudFront에 익숙해졌다면, 이제 컨테이너로 한 단계 올라갑니다.

AWS 고급 7편은 EC2 한 대에 직접 올리는 방식에서 벗어나, 컨테이너 / 서버리스 / 메시지 / 시크릿 / 워크플로우 같은 운영 규모에서 만나게 되는 도구들을 정리합니다.

이번 글은 그 출발점인 ECSFargate입니다. 도커로 만든 이미지를 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: 한 줄 비교 #

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

ECS의 4가지 구성 : 위에서 아래로
┌──────────────────────────────────────┐
│  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, 뒤에서 자세히)
  • 헬스 체크
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가 그걸 참조하게 바꾸는 식.

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-cluster
  • staging-cluster
  • dev-cluster

Cluster는 공짜입니다 (Cluster 자체엔 비용 없음). 안에서 도는 Task의 리소스가 비용. 그래서 환경별로 자유롭게 나눠 OK.

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 + 자유

이 시리즈와 실전 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에서 자세히 다루지만, 미리 흐름:

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 (중급 #6)을 미리 만들어둔 상태로:

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 (또는 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: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 비용이 클 수 있습니다.

비용 절감 옵션 #

  • 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이 폭주 #

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의 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의 동반자를 한 번에 정리하겠습니다.

X