기준: DVA-C02 시험 가이드오류 제보
컨테이너는 "내 노트북에서는 되는데 서버에서는 안 된다"는 오래된 고통을 끝내려고 등장했다. 애플리케이션과 그것이 의존하는 라이브러리, 런타임, 환경 변수를 하나의 이미지로 봉인해서, 어디서 실행하든 똑같이 동작하게 만든다. 그런데 컨테이너 하나를 만드는 것과 수백 개의 컨테이너를 여러 서버에 걸쳐 띄우고, 죽으면 다시 살리고, 트래픽에 맞춰 늘렸다 줄이는 것은 완전히 다른 문제다. 이 "여러 컨테이너를 조율하는 일"이 컨테이너 오케스트레이션(container orchestration)이고, Amazon ECS는 AWS가 내놓은 답이다. Fargate는 거기서 한 걸음 더 나아가 "그 컨테이너를 올릴 서버조차 네가 관리하지 마라"는 서버리스 컨테이너 모델이다.
DVA-C02에서 ECS/Fargate/ECR은 배포(Deployment) 도메인의 핵심이다. 단순 암기(awsvpc 모드, executionRole vs taskRole)도 다루지만, 동작 원리를 묻는 시나리오도 있다. 예를 들면 "왜 Fargate가 awsvpc만 강제하는가", "executionRole과 taskRole을 헷갈리면 어떤 에러가 나는가", "ECR pull이 실패하는 근본 원인"이다.
이번 글은 네 가지를 다룬다.
컨테이너는 마법이 아니라 리눅스 커널 두 기능의 조합이다. 네임스페이스(namespace) 는 프로세스에게 "너만의 세상"을 보여준다.
cgroups(control groups) 는 그 프로세스가 쓸 수 있는 자원(CPU, 메모리, I/O)에 천장을 씌운다. 즉 네임스페이스가 "보이는 것을 격리"하고 cgroups가 "쓸 수 있는 것을 제한"한다. 이 둘이 합쳐져 "한 커널 위에서 여러 격리된 실행 환경"이라는 컨테이너가 만들어진다.
이게 가상 머신(VM)과 결정적으로 다른 점이다. VM은 하이퍼바이저 위에 게스트 OS 커널을 통째로 올리지만, 컨테이너는 호스트 커널을 공유하고 그 위에서 격리만 한다. 그래서 컨테이너는 가볍고 빠르게 뜨지만(수백 ms), 커널을 공유하기 때문에 격리 강도는 VM보다 약하다. 이 "커널 공유로 인한 격리 약점"이 나중에 Fargate가 왜 특별한 격리 기술을 쓰는지의 출발점이 된다.
컨테이너 이미지는 유니온 파일시스템(union filesystem) 기반의 레이어 구조다. 베이스 이미지(예: python:3.12-slim) 위에 의존성 설치 레이어, 애플리케이션 코드 레이어가 차곡차곡 쌓이고, 각 레이어는 콘텐츠 해시로 식별된다. 이게 두 가지 이득을 준다. 첫째, 캐싱: 코드만 바뀌면 코드 레이어만 다시 빌드·푸시하고 나머지는 재사용한다. 둘째, 공유: 같은 베이스 레이어를 쓰는 여러 이미지가 디스크에서 그 레이어를 한 번만 저장한다. ECR이 "이미지 푸시 시 변경된 레이어만 업로드"하고, Docker가 "이미 받은 레이어는 다시 안 받는" 것이 모두 이 구조 덕분이다. Dockerfile에서 자주 바뀌는 명령(COPY . .)을 맨 아래 두라는 조언도 캐시 무효화를 줄이려는 것이다.
ECS를 이해하는 열쇠는 "스케줄링을 결정하는 두뇌"와 "실제 컨테이너가 도는 몸통"이 분리돼 있다는 것이다. 이건 분산 오케스트레이터의 보편적 구조다.
핵심 구성 요소는 다음과 같이 계층을 이룬다.
my-app:2)이 생긴다.{
"family": "my-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::...:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::...:role/ecsTaskRole",
"containerDefinitions": [
{
"name": "my-app",
"image": "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest",
"portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:...:secret:prod/myapp/db"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-app",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}ECS 서비스가 "원하는 개수와 실제 개수를 끊임없이 맞추는" 동작은 쿠버네티스에서 유명해진 조정 루프(reconciliation loop) 패턴 그 자체다. "원하는 상태(desired state)"를 선언하면, 컨트롤러가 "현재 상태(current state)"를 관찰해 둘의 차이를 줄이는 행동을 반복한다. 태스크가 죽으면 실제 개수가 3에서 2로 줄고, 컨트롤러가 이 차이를 감지해 새 태스크를 띄워 다시 3으로 맞춘다. 이 "선언적(declarative) 모델"은 명령적(imperative) 모델("태스크를 띄워라"는 일회성 명령)보다 자가 치유에 강하다. CloudFormation의 스택 상태 관리, DynamoDB Auto Scaling, Auto Scaling Group이 모두 같은 철학이다.
⚠️ 함정: 태스크 정의는 불변이고 버전이 매겨진다는 점이 시험 함정이다. "환경 변수를 바꾸려면?"의 답은 "기존 태스크 정의를 수정"이 아니라 "새 리비전을 등록하고 서비스를 그 리비전으로 업데이트"다. 또 서비스를
:latest처럼 특정 리비전 없이 업데이트하면 의도치 않은 배포가 일어날 수 있어, 보통 명시적 리비전 번호로 배포한다.
EC2 시작 유형과 Fargate의 표면적 차이는 "EC2를 내가 관리하느냐"지만, 그 밑에는 흥미로운 격리 기술이 있다. Fargate에서 사용자는 EC2 인스턴스를 보지도, 관리하지도 않는다. 그렇다고 AWS가 모든 고객의 컨테이너를 한 거대한 공유 커널 위에 그냥 올리면 격리가 위험하다 — 앞서 봤듯 컨테이너는 커널을 공유하므로 멀티테넌트 환경에서 격리 약점이 보안 문제가 된다.
AWS의 해법이 Firecracker다. Firecracker는 AWS가 만들어 오픈소스로 공개한 초경량 가상화 기술(microVM)로, "VM의 강한 격리"와 "컨테이너의 빠른 시작"을 동시에 잡으려는 것이다. 일반 VM이 수백 MB의 메모리 오버헤드와 수십 초의 부팅을 가지는 데 비해, Firecracker microVM은 5MB 미만의 오버헤드로 125ms 안에 뜬다. Fargate는 각 태스크(또는 작은 묶음)를 별도의 microVM에 넣어, 고객 간에 커널을 공유하지 않는 강한 격리를 제공하면서도 컨테이너처럼 빠르게 시작한다. "서버를 관리하지 않아도 되는" 마법의 정체는 이 microVM을 AWS가 대신 운영해주는 것이다.
| 특성 | EC2 시작 유형 | Fargate |
|---|---|---|
| 데이터 평면 | 내가 EC2 직접 운영 | AWS가 microVM 운영 |
| 격리 | 호스트 커널 공유 | 태스크별 microVM(Firecracker) |
| 과금 | 인스턴스 시간(놀아도 과금) | 태스크의 vCPU·메모리·실행 시간 |
| 패치·AMI | 내 책임 | AWS 책임 |
| 네트워크 모드 | bridge/host/awsvpc/none 선택 | awsvpc 강제 |
| 적합 | 비용 최적화, GPU·특수 하드웨어, 데몬 | 간편·빠른 확장, 운영 부담 최소 |
Firecracker는 Rust로 작성됐고, KVM(리눅스 커널 가상 머신) 위에서 동작한다. 핵심 설계는 "꼭 필요한 것만 남긴 최소 디바이스 모델"이다 — 일반 QEMU 기반 VM이 온갖 레거시 하드웨어를 에뮬레이트하는 것과 달리, Firecracker는 네트워크·블록·직렬 콘솔 등 극소수 디바이스만 제공한다. 공격 표면이 작아 보안이 강하고, 부팅할 게 적어 빠르다. Lambda도 같은 Firecracker를 쓴다 — 그래서 Lambda와 Fargate는 "서버리스 컴퓨팅"이라는 같은 뿌리를 공유한다. Firecracker는 "서버리스의 격리를 어떻게 안전하면서도 빠르게 만들 것인가"라는 질문에 대한 AWS의 답이다.
네트워크 모드는 컨테이너가 네트워크를 어떻게 보느냐를 결정한다. EC2 시작 유형에서는 네 가지 중 고를 수 있지만, Fargate는 awsvpc만 쓴다. 이게 단순 제약이 아니라 Fargate 격리 모델의 필연적 결과다.
| 모드 | 동작 | 한계 |
|---|---|---|
| bridge | Docker 기본. 호스트의 가상 브리지에 NAT로 연결 | 포트 충돌, 성능 오버헤드 |
| host | 컨테이너가 호스트 네트워크를 직접 사용 | 격리 약함, 포트 공유 불가 |
| awsvpc | 태스크마다 자체 ENI(탄력적 네트워크 인터페이스) 부여 | ENI 개수 한도 |
| none | 네트워크 없음 | 외부 통신 불가 |
awsvpc 모드에서 각 태스크는 VPC 안의 진짜 네트워크 인터페이스(ENI)를 자기 것으로 받는다. 그래서 태스크는 자기만의 사설 IP를 가지고, 보안 그룹을 태스크 단위로 붙일 수 있으며, VPC 흐름 로그에 개별적으로 잡힌다. 마치 태스크 하나하나가 작은 EC2 인스턴스처럼 네트워크에 존재한다. Fargate는 사용자에게 호스트를 노출하지 않으므로 host/bridge처럼 "호스트 네트워크"라는 개념을 줄 수가 없다 — 호스트가 보이지 않는데 그 호스트의 네트워크를 공유할 방법이 없다. 그래서 "태스크에 독립 ENI를 준다"는 awsvpc가 유일하게 일관된 선택이 된다.
awsvpc는 태스크마다 ENI를 쓰므로, 한 서브넷의 가용 IP가 부족하거나 EC2 인스턴스의 ENI 한도에 걸리면 "태스크를 띄울 수 없다"는 사고가 난다(EC2 시작 유형에서 특히). 시험에서 "Fargate 태스크가 시작 안 됨 + 서브넷 IP 고갈"이 보이면 답은 "더 큰 CIDR의 서브넷 추가" 계열이다. 또 awsvpc 태스크가 인터넷에서 ECR/Secrets Manager에 접근하려면 NAT 게이트웨이나 VPC 엔드포인트가 필요하다 — 프라이빗 서브넷에 두고 NAT 없이 두면 "이미지 pull 실패"가 난다.
이 둘은 ECS 시험에서 헷갈리기 쉬운 짝이다. 핵심은 "누가, 언제, 무엇을 위해 권한을 쓰느냐"가 시점상 완전히 다르다는 것이다.
시점으로 기억하면 명확하다. executionRole = 시작 전 인프라가 쓰는 권한, taskRole = 실행 중 앱이 쓰는 권한. 이미지가 ECR에서 안 받아지면 executionRole 문제고, 앱이 DynamoDB에서 AccessDenied를 받으면 taskRole 문제다.
이 분리는 최소 권한 원칙(Principle of Least Privilege) 의 깔끔한 적용이다. 만약 한 역할에 ECR pull, 로그 쓰기, DynamoDB 접근을 다 몰아넣으면, 그 컨테이너가 탈취됐을 때 공격자가 ECR과 로그 인프라 권한까지 다 가져간다. 둘을 나누면 "컨테이너 안에서 노출되는 자격 증명"은 taskRole뿐이고(앱이 IMDS 같은 경로로 받는 건 taskRole), executionRole은 컨테이너 바깥의 ECS 에이전트가 쥐고 있어 앱 코드가 접근하지 못한다. 권한의 "폭발 반경(blast radius)"을 줄이는 설계다.
대표적인 오류 두 가지. (1) Secrets Manager에서 DB 비밀번호를 환경 변수로 주입하도록 태스크 정의에 secrets를 넣었는데 executionRole에 secretsmanager:GetSecretValue 권한이 없어 태스크가 시작조차 안 되는 경우 — 이건 taskRole이 아니라 executionRole 문제다(주입은 시작 시점, 인프라가 한다). (2) 앱이 잘 떴는데 S3 업로드에서 AccessDenied — 이건 taskRole 문제다. 시험에서 "비밀 주입 실패"는 executionRole, "런타임 앱의 AWS 호출 실패"는 taskRole로 가르면 구분할 수 있다.
EC2 시작 유형에서는 "여러 인스턴스 중 어디에 태스크를 둘지"를 결정해야 한다(Fargate는 AWS가 알아서 하므로 해당 없음). 이게 태스크 배치 전략(task placement strategy) 이다.
| 전략 | 동작 | 의도 |
|---|---|---|
| binpack | CPU·메모리가 가장 적게 남은 인스턴스부터 채움 | 인스턴스 수 최소화 → 비용 절감 |
| spread | 인스턴스/AZ/호스트에 균등 분산 | 한 곳이 죽어도 영향 최소 → 고가용성 |
| random | 무작위 | 단순·테스트용 |
binpack과 spread는 정반대의 목표를 가진다. binpack은 "적은 인스턴스에 꽉꽉 눌러 담아" 놀고 있는 인스턴스를 없애 비용을 줄이고, spread는 "AZ에 골고루 흩뿌려" 한 AZ가 통째로 죽어도 서비스가 살아남게 한다. 실무에선 "AZ로는 spread, AZ 안에서는 binpack" 식으로 조합한다.
용량 공급자(Capacity Provider) 는 여기서 한 단계 위 추상화다. Fargate, Fargate Spot, EC2 Auto Scaling Group을 섞어 "기본 N개는 안정적인 Fargate, 나머지는 70% 싼 Fargate Spot"처럼 비용·안정성을 배합한다. Spot은 2분 전 회수 통보가 오므로 배치 작업·내결함성 있는 워크로드에 쓴다.
ECS의 binpack은 완벽한 최적이 아니라 "가장 적게 남은 인스턴스부터 채우는" 그리디 휴리스틱이다.
Amazon ECR(Elastic Container Registry) 은 컨테이너 이미지를 저장하는 완전 관리형 레지스트리다. 단순 저장소처럼 보이지만 시험에 나오는 디테일이 꽤 있다.
# ECR 인증: 12시간 유효한 토큰을 받아 docker login
aws ecr get-login-password --region ap-northeast-2 | \
docker login --username AWS --password-stdin \
123456789.dkr.ecr.ap-northeast-2.amazonaws.com
docker build -t my-app .
docker tag my-app:latest 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest
docker push 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest| 항목 | 내용 |
|---|---|
| 인증 | IAM 기반. get-login-password가 주는 토큰은 12시간 유효 |
| 취약점 스캔 | Basic(무료, CVE 기반, 푸시 시 스캔) / Enhanced(Amazon Inspector 통합, 유료, 지속 스캔) |
| 수명 주기 정책 | 태그·개수·일수 기준으로 오래된 이미지 자동 삭제 |
| 복제 | 리전 간·계정 간 자동 복제 |
| 풀 스루 캐시 | Docker Hub/ECR Public 이미지를 ECR에 캐싱 → 속도·rate limit 회피 |
| 불변 태그 | latest 같은 태그를 덮어쓰지 못하게 막아 배포 추적성 확보 |
"Fargate 태스크가 ECR 이미지를 pull하지 못한다"의 원인은 대개 셋 중 하나다. (1) executionRole에 ECR 권한 부족, (2) 프라이빗 서브넷에 NAT/VPC 엔드포인트가 없어 ECR에 도달 못 함, (3) get-login-password 토큰 만료(12시간) — CI 파이프라인이 오래 돌면 토큰이 만료돼 push가 실패한다. "인증 토큰이 12시간"이라는 숫자도 기억해 둔다.
Docker Hub가 2020년 익명/무료 사용자에게 이미지 pull rate limit(6시간당 100~200회)을 도입했을 때, Docker Hub 베이스 이미지를 직접 받아 빌드하던 수많은 CI 파이프라인이 "Too Many Requests"로 깨졌다. AWS의 권장 해법이 ECR 풀 스루 캐시(pull-through cache) 다 — Docker Hub 이미지를 처음 한 번만 받아 ECR에 캐싱해두고, 이후로는 ECR에서 받아 rate limit과 외부 의존을 동시에 없앤다. 외부 레지스트리 의존이 만든 장애를 캐싱으로 푸는, 클라우드에서 반복되는 패턴이다.
ECS를 관통하는 한 문장은 "선언적 조정 루프로 컨테이너의 원하는 상태를 끊임없이 유지하되, 데이터 평면을 EC2로 직접 쥘지 Fargate에 맡길지를 고른다"이다.
다음 글에서는 이 모든 인프라를 손으로 클릭하지 않고 코드로 선언하는 CloudFormation의 세계로 넘어간다.
선택지를 클릭하면 정답·해설이 펼쳐집니다.
문제 1
ECS 태스크 정의에 Secrets Manager 비밀을 환경 변수로 주입하도록 설정했는데, 태스크가 시작조차 되지 않고 비밀을 가져오지 못한다. 원인은?
문제 2
Fargate 태스크가 사용할 수 있는 네트워크 모드와 그 이유로 옳은 것은?
문제 3
비용을 최소화하기 위해 EC2 시작 유형에서 가능한 한 적은 수의 인스턴스에 태스크를 몰아넣고 싶다. 적절한 배치 전략은?
문제 4
CI 파이프라인이 길게 실행되던 중 ECR push가 갑자기 "authentication" 오류로 실패하기 시작했다. 가장 가능성 높은 원인은?
문제 5
안정적인 기본 용량은 정규 Fargate로, 추가 부하는 비용이 싼 옵션으로 처리하되 일부 중단을 감내하려 한다. 적절한 구성은?
문제 6
한 태스크 안에 애플리케이션 컨테이너와 로그 수집용 보조 컨테이너를 함께 실행하려 한다. 이를 가리키는 패턴과 ECS에서의 단위는?
문제 7
Fargate가 멀티테넌트 환경에서 강한 격리를 빠른 시작 시간과 함께 제공하는 기술적 토대는?