콘텐츠로 이동

ECS vs EC2

분류: Layer 3 - AWS 인프라 & 보안

EC2는 AWS에서 빌려 쓰는 가상 서버이고, ECS는 Docker 컨테이너를 자동으로 배포/관리해주는 컨테이너 오케스트레이션 서비스이다.

“우리 서비스가 어디서 돌아가는지”를 이해하는 핵심 질문이다. EC2 위에서 직접 서버를 관리하는 것과, ECS로 컨테이너를 관리하는 것은 운영 방식이 근본적으로 다르다.

AWS에서 빌려 쓰는 가상 서버(VM). 리눅스/윈도우 OS가 깔려 있고, SSH로 접속해서 직접 세팅한다.

Docker 컨테이너를 실행/관리해주는 서비스. “이 이미지를, 몇 개, 어떤 리소스로 실행해줘”라고 정의하면 ECS가 알아서 배포하고 관리한다.

비유로 이해하면: “EC2 vs ECS를 선택한다”는 것은 “핸들 vs 자동차를 선택한다”와 같다. EC2는 서버(자동차)이고, ECS는 그 서버를 운전하는 방식(핸들)이다. ECS + Fargate 조합에서는 자동차 자체도 AWS가 제공한다.

📖 더 보기: Amazon ECS vs Amazon EC2: Complete Comparison Guide — EC2와 ECS의 역할 차이, 비용 모델, 선택 기준을 실무 관점에서 정리

  1. Task Definition 등록: “어떤 이미지를 어떤 설정으로 실행할지” JSON으로 정의
  2. Service 생성: “이 Task를 N개 유지해줘”라고 ECS에 지시
  3. Scheduler 동작: ECS 스케줄러가 어떤 인프라(Fargate or EC2)에서 실행할지 결정
  4. Container Agent 실행: Fargate는 AWS가, EC2 모드는 EC2 인스턴스 위의 ECS Agent가 컨테이너를 시작
  5. Health Check 모니터링: ALB Health Check 또는 컨테이너 Health Check 결과에 따라 비정상 Task를 자동으로 교체

Fargate 격리 모델 — 보안과 성능

섹션 제목: “Fargate 격리 모델 — 보안과 성능”

Fargate는 각 Task가 고유한 마이크로VM(Firecracker 기술 기반) 위에서 실행된다. 즉, Task끼리 커널, CPU, 메모리를 공유하지 않는다. 이는 EC2 launch type(여러 컨테이너가 같은 EC2 인스턴스 커널을 공유)과 근본적으로 다른 격리 수준이다.

실무에서 의미하는 것:

  • Fargate는 다른 테넌트의 컨테이너로부터 격리가 강함 → 금융/의료 데이터 등 규정 준수 환경에 유리
  • 각 Task가 별도 ENI를 가짐 → Security Group을 Task 단위로 적용 가능
  • 단, 격리 오버헤드로 인해 동일 메모리 기준 EC2 launch type보다 약간 느릴 수 있음
  • EC2 launch type: EC2 인스턴스 위에 컨테이너를 올림. 서버를 직접 관리해야 하지만 비용 효율적.
  • Fargate launch type: 서버를 아예 신경 안 써도 됨. “컨테이너만 정의하면 AWS가 알아서 서버 관리.”

Fargate는 awsvpc 모드만 지원한다. 이 모드에서는 각 Task가 고유한 ENI(Elastic Network Interface)와 Private IP를 받는다.

  • 장점: Security Group을 Task 단위로 적용 가능
  • 장점: 포트 충돌 없음 (Task마다 독립된 네트워크 인터페이스)
  • 주의: Task 수가 많으면 VPC의 IP 주소 소진 가능 → Subnet CIDR 범위를 여유 있게 설계
  • Task Definition: 컨테이너 실행 스펙 (어떤 이미지, CPU/메모리, 포트, 환경변수 등)
  • Task: Task Definition을 실행한 인스턴스 (= 실행 중인 컨테이너 묶음)
  • Service: Task를 원하는 개수만큼 유지해주는 단위. 1개가 죽으면 자동으로 새로 띄움.
  • Cluster: Service와 Task를 묶는 논리적 그룹.
{
"family": "my-nest-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"containerDefinitions": [
{
"name": "my-nest-app",
"image": "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/my-nest-app:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [{ "name": "NODE_ENV", "value": "production" }],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-nest-app",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}

📖 더 보기: AWS 공식 - ECS Fargate 시작하기 — Task Definition 생성부터 Service 배포까지 공식 튜토리얼

인터넷 → ALB(로드밸런서) → Target Group → ECS Service → Task(컨테이너)

ALB 없이는 외부에서 ECS 컨테이너에 직접 접근할 수 없다.

ECS Service 생성 시 ALB와 Target Group을 연결하는 설정이 필요하다.

배포 흐름 — Rolling Update 내부 동작

섹션 제목: “배포 흐름 — Rolling Update 내부 동작”

ECS Service의 Rolling Update 방식 배포 과정:

  1. 새 Task Definition 등록 (이미지 태그 변경 등)
  2. ECS Service가 새 Task Definition으로 신규 Task 시작
  3. 신규 Task가 ALB Target Group에 등록되고 Health Check 통과 대기
  4. 신규 Task 정상 확인 → ALB가 신규 Task로 트래픽 전환
  5. 구버전 Task를 ALB에서 제거(Deregister) → Drain 후 종료

이 과정에서 minimumHealthyPercent(최소 유지 Task 비율)와 maximumPercent(최대 Task 비율) 설정으로 무중단 배포를 보장한다.

ECS Auto Scaling — 트래픽에 따른 자동 Task 수 조절

섹션 제목: “ECS Auto Scaling — 트래픽에 따른 자동 Task 수 조절”

ECS Service는 Application Auto Scaling을 통해 Task 수를 자동으로 늘리거나 줄인다. 실무에서 주로 사용하는 두 가지 방식이 있다.

Target Tracking Scaling (권장): 특정 메트릭의 목표값을 유지하도록 Task 수를 자동 조절한다. 예를 들어 “CPU 사용률 60%를 유지”로 설정하면, 사용률이 60%를 넘으면 Task를 늘리고 낮아지면 줄인다. 설정이 간단하고 AWS가 스케일링 계산을 대신 해주는 것이 장점이다.

// Target Tracking 정책 예시: CPU 60% 목표
{
"TargetValue": 60.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}
// ScaleOutCooldown: 60초 — 급격한 트래픽 증가에 빠르게 반응
// ScaleInCooldown: 300초 — Scale In은 천천히 (플래핑 방지)

Step Scaling: 메트릭 값의 구간(Step)별로 Task 수 변화량을 직접 지정한다. 예를 들어 “CPU 70~85%이면 Task +2, 85% 이상이면 Task +4”처럼 세밀한 제어가 필요할 때 사용한다. 예측 가능한 트래픽 패턴이 있는 서비스에 적합하다.

실무 기준값: CPU 60%, Memory 70%를 Scale Out 기준으로 설정하는 것이 일반적이다. 최소 Task 수는 Multi-AZ를 위해 2개 이상, 최대 Task 수는 VPC Subnet의 가용 IP 수와 RDS max_connections를 고려해 설정한다.

Terminal window
# Auto Scaling 설정 등록 (ECS Service에 Scalable Target 연결)
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/my-cluster/my-service \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 \
--max-capacity 20
# Target Tracking 정책 생성
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/my-cluster/my-service \
--scalable-dimension ecs:service:DesiredCount \
--policy-name cpu-target-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration file://scaling-policy.json

Fargate 비용은 vCPU/시간 + Memory GB/시간으로 청구된다. 잘못 설정된 Task 스펙은 가장 큰 낭비 원인이다.

  1. Right-Sizing (태스크 스펙 최적화)

    AWS Compute Optimizer가 실제 사용량을 분석해 CPU/메모리 권고안을 제시한다. 적절히 조정하면 30~70% 비용 절감이 가능하다.

    Terminal window
    # CloudWatch에서 실제 CPU/메모리 사용량 확인
    aws cloudwatch get-metric-statistics \
    --namespace AWS/ECS \
    --metric-name CPUUtilization \
    --dimensions Name=ClusterName,Value=my-cluster Name=ServiceName,Value=my-service \
    --start-time 2024-01-01T00:00:00Z \
    --end-time 2024-01-15T00:00:00Z \
    --period 3600 \
    --statistics Average
    # → 평균 CPU 사용률이 20% 미만이면 Task CPU를 절반으로 줄일 수 있음
  2. Fargate Spot 활용

    상태 비저장(Stateless), 중단 가능한 배치 작업이나 개발 환경은 Fargate Spot을 사용하면 최대 70% 절감된다.

    // capacityProviderStrategy에서 Spot 비중 설정
    {
    "capacityProviders": [
    { "capacityProvider": "FARGATE", "weight": 1, "base": 1 },
    { "capacityProvider": "FARGATE_SPOT", "weight": 3 }
    ]
    }
    // → 4개 Task 중 1개는 On-Demand(안정), 3개는 Spot(70% 저렴)
  3. Compute Savings Plans

    1년 또는 3년 약정으로 Fargate 비용을 최대 52% 절감. EC2, Lambda와 통합 적용된다. 예측 가능한 프로덕션 워크로드에 적합하다.

  4. Public IP 비용 제거

    ECS Task에 Public IP를 할당하면 IP당 시간 과금이 발생한다 (2024년부터 AWS 유료화). Private Subnet + ALB 구조로 전환하면 비용 절감과 보안 강화를 동시에 달성한다.

  • 웹 서비스 배포 (ECS Service + ALB로 컨테이너 운영)
  • 배치 작업 실행 (ECS Task로 1회성 실행)
  • 개발/스테이징/프로덕션 환경 분리 (Cluster 단위)
  • Auto Scaling (트래픽에 따라 컨테이너 수 자동 조절)
  • 팀 서비스가 ECS로 배포되고 있다면, 배포 구조를 이해하는 데 필수
  • 배포 실패 시 Task 상태, 로그, Service 이벤트를 확인해야 함
  • “서버 리소스가 부족하다”는 이슈 대응 시 Task Definition의 CPU/메모리 확인
  • 새 서비스 배포 시 ECS 설정을 이해하고 있어야 함

⚠️ Fargate에서는 docker exec이 안 된다

컨테이너에 직접 접속하려면 AWS ECS Exec을 써야 한다. 자주 환경변수를 확인하거나 내부 상태를 디버깅할 때 필요하다.

Terminal window
aws ecs execute-command \
--cluster my-cluster \
--task <task-id> \
--container my-container \
--interactive --command "/bin/sh"

5.5 컨테이너 오케스트레이션 원리의 전이

섹션 제목: “5.5 컨테이너 오케스트레이션 원리의 전이”

ECS를 이해하면 Kubernetes(K8s)나 Nomad 같은 다른 오케스트레이터로 전이하기 쉽다. 핵심 개념이 대응되기 때문이다.

ECS ↔ Kubernetes 핵심 개념 대응 표

섹션 제목: “ECS ↔ Kubernetes 핵심 개념 대응 표”
ECS 개념Kubernetes 대응역할
Task DefinitionPod Spec (manifest의 spec 섹션)컨테이너 이미지, CPU/메모리, 포트, 환경변수 등 실행 스펙 정의
TaskPod실제 실행 중인 컨테이너 인스턴스
ServiceDeployment + ServiceDesired State 유지(Deployment) + 네트워크 엔드포인트 노출(Service)
ClusterNamespace + Node 그룹워크로드를 묶는 논리적 단위
Task RoleServiceAccount + RBAC앱 코드가 AWS/Cloud 서비스에 접근할 때 사용하는 권한 ID
Execution Rolekubelet 자격증명컨테이너 런타임이 이미지 Pull, 로그 전송 등 플랫폼 작업 수행 시 사용
Service DiscoveryCoreDNS + Service ClusterIP서비스 이름으로 내부 통신 (ECS: <svc>.<namespace>.svc.cluster.local)
Health Check (ALB/컨테이너)Liveness/Readiness Probe비정상 컨테이너 자동 교체 트리거
FargateKnative / Fargate for EKS서버 프로비저닝 없이 컨테이너만 정의하는 서버리스 레이어

📖 참고: ECS Terminology for Kubernetes Users (Medium) — K8s 배경 엔지니어가 ECS로 전환할 때 참고

Desired State Reconciliation — 공통 원리

섹션 제목: “Desired State Reconciliation — 공통 원리”

ECS와 Kubernetes 모두 Desired State Reconciliation 방식으로 동작한다. 운영자가 “Service를 3개 유지”라고 선언(Declare)하면, 컨트롤 플레인이 실제 상태(Actual State)를 주기적으로 체크해 선언된 상태(Desired State)로 맞춘다.

[ECS 흐름]
Service desiredCount=3 선언
→ ECS Scheduler가 실행 중 Task 수 확인
→ Task 1개 죽음 → Scheduler가 새 Task 1개 시작 → desiredCount=3 복원
[K8s 흐름]
Deployment replicas=3 선언
→ ReplicaSet Controller가 실행 중 Pod 수 확인
→ Pod 1개 죽음 → Controller가 새 Pod 1개 스케줄 → replicas=3 복원

이 원리를 이해하면 ECS뿐 아니라 K8s, Nomad 어디서도 같은 사고방식을 적용할 수 있다.

ECS와 EKS는 모두 컨테이너 오케스트레이터이지만 선택 기준이 명확히 다르다.

📖 출처: AWS 공식 - Choosing an AWS container service

선택 기준ECS 유리EKS 유리
팀 규모 / DevOps 전문성소~중규모, 전담 SRE 없음중~대규모, Kubernetes 전문 SRE 보유
운영 복잡도 감내 수준낮은 운영 부담 선호복잡도를 감내하고 세밀한 제어 필요
멀티클라우드 요구AWS 단일 환경GCP/Azure 혼용 또는 온프레미스 병행 (K8s 표준 활용)
오픈소스 생태계 필요불필요 (AWS 관리형으로 충분)Istio, Argo CD, OPA, KEDA 등 CNCF 생태계 활용 필요
클러스터 업그레이드 부담낮음 (AWS가 컨트롤 플레인 관리)높음 (K8s 연 3회 릴리스, 버전 만료 관리 필요)
멀티테넌시 / Namespace 격리제한적 (Cluster 단위)강력한 Namespace 기반 격리 지원
AWS 서비스 통합 간편성매우 높음 (IAM, ALB, ECR 네이티브 통합)높음 (추가 설정 필요할 수 있음)
추가 비용ECS 자체 무료 (Fargate/EC2 과금)EKS 클러스터당 $0.10/시간 (~$73/월) 추가 과금

실무 판단 기준: 팀이 Kubernetes 전문가 없이 AWS 워크로드를 빠르게 운영해야 한다면 ECS가 답이다. 멀티클라우드 이식성이나 CNCF 생태계 도구(Istio, Argo CD 등)가 필요하다면 EKS를 선택한다. AWS도 공식적으로 “대부분의 고객은 두 서비스를 혼용한다”고 밝히고 있으며, 선택은 단방향 결정이 아니다.

5.9 Fargate vs EC2 launch type 비용 비교

섹션 제목: “5.9 Fargate vs EC2 launch type 비용 비교”

Fargate는 서버 관리 비용을 없애는 대신 컨테이너 리소스에 프리미엄이 붙는다. 실제 비용을 파악하지 않으면 예상보다 훨씬 높은 청구서를 받을 수 있다.

항목FargateEC2 launch type
과금 단위vCPU/시간 + GB/시간 (초 단위 과금)EC2 인스턴스 시간 + EBS 볼륨
최소 과금1분인스턴스 타입에 따라 다름
서버 관리불필요 (AWS 관리)OS 패치, 에이전트 업데이트 직접 관리
인스턴스 공유불가 (Task별 격리)여러 Task가 인스턴스 공유 가능

비용 예시: 단일 Task 기준 (ap-northeast-2, Linux/x86)

섹션 제목: “비용 예시: 단일 Task 기준 (ap-northeast-2, Linux/x86)”

AWS Fargate 공식 요금 (참고: AWS Fargate Pricing)

  • vCPU: 약 $0.04048/시간 (서울 리전 기준, 환율/리전에 따라 변동)
  • 메모리: 약 $0.004445/GB/시간

Task 스펙: 0.25 vCPU / 0.5 GB — 24시간 × 30일 운영

항목계산월 비용(USD)
Fargate vCPU0.25 × $0.04048 × 720h~$7.29
Fargate 메모리0.5 × $0.004445 × 720h~$1.60
Fargate 합계~$8.89
t3.small On-Demand (2 vCPU, 2 GB)~$0.0208/시간 × 720h~$14.98
  • Fargate 0.25vCPU/0.5GB는 최소 스펙이므로, 인스턴스 비용만 보면 t3.small보다 저렴하다.
  • 그러나 실무에서는 Task 여러 개를 EC2 1개에 집약(bin packing)하므로, Task 수가 많아질수록 EC2 launch type이 유리해진다.

Task 10개 운영 시 비교 (0.5 vCPU / 1 GB per Task)

방식월 비용 추정
Fargate × 10 Task(0.5 × $0.04048 + 1 × $0.004445) × 720h × 10 ≈ $177
t3.large × 2대 (EC2)$0.0832/h × 720h × 2 ≈ $120 (서버 관리 비용 별도)

💡 실무 결론: Task 수가 적고 팀이 작다면 Fargate의 운영 편의성이 비용 차이를 상쇄한다. Task 수가 20개 이상이고 워크로드가 예측 가능하다면 EC2 launch type + Reserved Instance를 검토할 만하다.

📖 참고: AWS Blog - Theoretical cost optimization by Amazon ECS launch type: Fargate vs EC2

개념 A개념 B차이점
EC2ECSEC2는 서버 자체, ECS는 컨테이너 관리 서비스
ECS + EC2ECS + FargateEC2 모드는 서버 직접 관리, Fargate는 서버리스
TaskServiceTask는 1회 실행 단위, Service는 Task를 원하는 수만큼 유지
ECSEKSECS는 AWS 전용 오케스트레이션, EKS는 Kubernetes 기반
Task RoleExecution RoleTask Role은 앱이 AWS 서비스 접근할 때, Execution Role은 ECS가 태스크 시작할 때

🔧 Task가 계속 STOPPED 상태로 반복 재시작되는 경우

섹션 제목: “🔧 Task가 계속 STOPPED 상태로 반복 재시작되는 경우”

증상: ECS Service 이벤트 탭에서 Task stopped (Exit Code: 1) 또는 Task stopped (Essential container exited) 메시지가 반복됨

원인: 컨테이너 애플리케이션이 시작 직후 오류로 종료되고 있음. 환경변수 누락, DB 연결 실패, 포트 바인딩 실패 등이 원인

해결:

  1. ECS 콘솔 → 해당 Service → Tasks 탭 → Stopped 상태의 Task 클릭
  2. Stopped Reason 필드 확인 (예: "Essential container exited")
  3. CloudWatch Logs에서 컨테이너 로그 확인 (/ecs/<task-definition-family> 로그 그룹)
  4. 로그에서 실제 오류 메시지 확인 후 애플리케이션 수정
경로: CloudWatch → Log Groups → /ecs/my-nest-app → 최신 로그 스트림
예상 출력 (NestJS 환경변수 누락 시):
Error: Config validation error: "DATABASE_URL" is required
at Object.<anonymous> (/app/dist/main.js:1:...)

🔧 “CannotPullContainerError” — 이미지 Pull 실패

섹션 제목: “🔧 “CannotPullContainerError” — 이미지 Pull 실패”

증상: Task가 시작되지 않고 CannotPullContainerError: failed to resolve... access denied 에러로 멈춤

원인 1: Task Definition에 설정된 Execution Role에 ECR Pull 권한(ecr:GetAuthorizationToken, ecr:BatchGetImage 등)이 없음

원인 2: Fargate Task가 Private Subnet에 있는데 NAT Gateway 또는 ECR VPC Endpoint가 없어서 ECR에 접근 불가

해결:

  1. Task Definition의 taskRoleArn vs executionRoleArn 구분 확인 (ECR Pull은 executionRoleArn)
  2. AmazonECSTaskExecutionRolePolicy 관리형 정책이 Execution Role에 붙어있는지 확인
  3. Fargate가 Private Subnet이면 NAT Gateway 또는 ECR/CloudWatch VPC Endpoint 설정

🔧 “OOMKilled” — 메모리 부족으로 컨테이너 종료

섹션 제목: “🔧 “OOMKilled” — 메모리 부족으로 컨테이너 종료”

증상: Task가 exit code 137 또는 OOMKilled로 종료됨. 트래픽이 몰릴 때 또는 서비스 시작 직후 발생

원인: Task Definition에 설정된 메모리 한도(Memory)보다 컨테이너가 더 많은 메모리를 사용하려 할 때 Linux 커널이 프로세스를 강제 종료

해결:

  1. CloudWatch → ECS 메트릭 → MemoryUtilization으로 최대 사용량 확인
  2. Task Definition의 memory 값을 현재 최대 사용량보다 20~30% 여유 있게 증가
  3. NestJS의 경우 --max-old-space-size Node.js 옵션 조정 검토

🔧 Fargate Task가 PROVISIONING 상태에서 멈추는 경우

섹션 제목: “🔧 Fargate Task가 PROVISIONING 상태에서 멈추는 경우”

증상: ECS Task가 PROVISIONING 상태에서 수 분이 지나도 RUNNING으로 넘어가지 않음

원인: Private Subnet에 배치된 Fargate Task가 ECR 이미지를 Pull하거나 Secrets Manager에 접근하기 위한 인터넷 경로(NAT Gateway 또는 VPC Endpoint)가 없음

해결:

확인 경로:
1. VPC → Route Tables → Private Subnet 연결된 Route Table
→ 0.0.0.0/0 → NAT Gateway 경로 존재 여부 확인
2. 또는 VPC Endpoint 확인:
→ VPC → Endpoints → com.amazonaws.ap-northeast-2.ecr.dkr
→ com.amazonaws.ap-northeast-2.ecr.api
→ com.amazonaws.ap-northeast-2.logs (CloudWatch)
위 세 Endpoint가 없으면 NAT Gateway 없이는 이미지 Pull 불가

🔧 ECS Service의 배포가 진행되지 않고 멈추는 경우

섹션 제목: “🔧 ECS Service의 배포가 진행되지 않고 멈추는 경우”

증상: 새 Task Definition으로 배포를 시작했는데 서비스가 오랫동안 in progress 상태. 구버전 Task는 계속 실행 중이고 신버전 Task가 교체되지 않음

원인 1: 신규 Task의 ALB Health Check가 통과되지 않아 ECS가 구버전을 제거하지 못함

원인 2: minimumHealthyPercent가 100%로 설정되어 새 Task가 올라올 여유 공간이 없음

원인 3: 새 Task 자체가 시작 직후 실패하고 있어 ECS가 계속 새 Task를 시도하다 포기하지 못함

해결:

Terminal window
# 1. Service 이벤트 탭에서 배포 상태 확인
# ECS → Clusters → <클러스터> → Services → <서비스> → Events 탭
# 예상 출력:
# service my-service has started 1 tasks: task abc123.
# service my-service (port 3000) is unhealthy in target-group ...due to (reason Health checks failed)
# 2. ALB Target Group 상태 확인
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:ap-northeast-2:123456789:targetgroup/my-tg/xxx
# → "State": "unhealthy", "Description": "Health checks failed" 나오면 Health Check 경로/포트 확인
# 3. Health Check 설정 점검
# ALB → Target Groups → <그룹> → Health checks 탭
# 확인: Health check path가 NestJS에서 실제 200을 반환하는 경로인지 (예: /health)

🔧 ALB 502 Bad Gateway 에러 — 프로덕션에서 간헐적으로 발생

섹션 제목: “🔧 ALB 502 Bad Gateway 에러 — 프로덕션에서 간헐적으로 발생”

증상: 하루에 수십 건 ALB에서 502 에러 반환. CloudWatch에서 HTTPCode_ELB_5XX_Count 지표가 산발적으로 올라감. 트래픽이 많지 않을 때도 발생

원인: ECS Task가 종료(스케일 다운 또는 배포)될 때 ALB가 아직 해당 Target을 De-register하기 전에 요청이 들어오는 경우. 또는 NestJS 앱이 응답 전에 연결을 닫는 경우

해결:

Terminal window
# 1. ALB Access Log 활성화 → 502 발생 시간과 Target IP 확인
# ALB → Attributes → Access logs → S3 버킷 지정
# 2. Target Group의 Deregistration delay 확인 (기본 300초)
aws elbv2 describe-target-group-attributes \
--target-group-arn arn:aws:elasticloadbalancing:ap-northeast-2:123456789:targetgroup/my-tg/xxx
# → "deregistration_delay.timeout_seconds": "300"
# 너무 길면 배포가 느려지고, 너무 짧으면 502 발생 가능
# 3. NestJS Graceful Shutdown 구현 (핵심 해결책)
# main.ts에 추가:
// main.ts — NestJS Graceful Shutdown
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// ALB가 De-register 신호 보낸 후 실제 연결이 드레인될 시간 확보
app.enableShutdownHooks();
await app.listen(3000);
}
// app.module.ts 또는 별도 모듈
@Injectable()
export class AppService implements OnApplicationShutdown {
async onApplicationShutdown(signal: string) {
console.log(`Shutting down on signal: ${signal}`);
// 진행 중인 DB 트랜잭션, SQS 처리 등을 완료할 시간 확보
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
Terminal window
# 4. ECS Task Definition에 Stop Timeout 설정 (Fargate 기본 30초)
# Task Definition → stopTimeout: 60 (최대 120초)
# → SIGTERM 수신 후 60초 동안 Graceful Shutdown 시간 부여

AWS 권고: ALB Deregistration delay(300초)와 ECS stopTimeout(120초 이하), NestJS Graceful Shutdown 3가지를 모두 맞춰야 502가 사라진다.

🔧 HealthCheckGracePeriod 미설정으로 Task 반복 재시작

섹션 제목: “🔧 HealthCheckGracePeriod 미설정으로 Task 반복 재시작”

증상: 새로 배포한 NestJS Task가 시작 직후 ALB Health Check에서 실패 → ECS가 Task를 교체 → 새 Task도 실패를 반복. 로그에는 정상 부팅 메시지가 찍히는데 Task가 계속 교체됨

원인: NestJS 앱이 완전히 부팅되기 전(DB 연결 초기화, 모듈 로딩 등)에 ALB Health Check가 먼저 실패 판정. ECS가 unhealthy로 판단해 Task를 교체

해결:

Terminal window
# ECS Service에 HealthCheckGracePeriod 설정 (초 단위)
# 앱이 완전히 부팅될 때까지 Health Check 실패를 무시하는 시간
aws ecs update-service \
--cluster my-cluster \
--service my-service \
--health-check-grace-period-seconds 60
# → NestJS가 DB 연결 포함 완전 부팅에 30~45초 걸리면 60초 이상 설정
# 확인: Service 설정에서 healthCheckGracePeriodSeconds 값
aws ecs describe-services \
--cluster my-cluster \
--services my-service \
--query 'services[0].healthCheckGracePeriodSeconds'
# 예상 출력: 60

ALB Health Check 자체 설정도 함께 조정:

ALB → Target Groups → Health checks
- Healthy threshold: 2 (2회 연속 성공 시 healthy)
- Unhealthy threshold: 3 (3회 연속 실패 시 unhealthy)
- Interval: 30초
- Timeout: 5초
→ unhealthy 판정까지 최소 90초 소요 → NestJS 부팅 시간 커버 가능
  • EC2와 ECS의 차이를 한 문장으로 설명할 수 있다
  • Fargate와 EC2 launch type의 차이를 설명할 수 있다
  • Task Definition, Task, Service, Cluster의 관계를 설명할 수 있다
  • 인터넷 → ALB → ECS로 트래픽이 흐르는 구조를 설명할 수 있다
  • 팀 서비스의 배포 방식이 EC2인지 ECS인지 말할 수 있다
  • Task가 STOPPED되었을 때 로그를 어디서 확인하는지 안다

EKS(Kubernetes), Fargate Spot, Auto Scaling, ALB(Application Load Balancer), Target Group, Blue-Green 배포, Rolling 배포

  • AWS 콘솔에서 ECS Cluster 목록 확인

    경로: AWS 콘솔 → ECS → Clusters
    확인: 클러스터 이름, 실행 중인 서비스/태스크 수
  • 팀 서비스의 Task Definition 열어보기 (이미지, CPU/메모리, 환경변수 확인)

    경로: ECS → Task Definitions → <이름> → 최신 버전 클릭
    확인: CPU/Memory 할당, 컨테이너 이미지 주소, 환경변수, 로그 설정
  • ECS Service의 이벤트 탭에서 배포 이력 확인

    경로: ECS → Clusters → <클러스터> → Services → <서비스> → Events 탭
    예상 출력:
    2024-01-15 10:30:01 service my-service has reached a steady state.
    2024-01-15 10:29:45 service my-service has started 1 tasks: task abc123def.
    2024-01-15 10:28:30 service my-service deregistered 1 targets in target group arn:aws:...
  • 배포 시 ECS에서 무슨 일이 일어나는지 흐름 파악

  1. EC2는 가상 서버(인프라), ECS는 Docker 컨테이너 오케스트레이션(관리 방식)이다 — 둘은 비교 대상이 아니라 함께 쓰이는 계층이다
  2. ECS는 Fargate(서버리스, 비용 효율), EC2 launch type(고성능·특수 요구), Managed Instances(새 옵션, 특수 워크로드) 세 가지 실행 모드가 있다
  3. Task Definition(스펙) → Task(실행 인스턴스) → Service(개수 유지·배포) → Cluster(논리 그룹) 4계층 구조이다
  4. 외부 트래픽은 ALB → Target Group → ECS Service → Task 순으로 흐르며, 배포는 신규 Task 헬스체크 통과 후 구버전 교체 순서로 진행된다
  5. 배포 실패 시 확인 순서: ECS Events 탭 → CloudWatch 로그(컨테이너 오류) → ALB Target Group 헬스체크 → Task 메모리/CPU 설정

Docker Compose를 써봤다면 ECS Task Definition과 비교하기

로컬 개발에서 docker-compose.yml을 사용해봤다면 ECS Task Definition이 매우 친숙하게 느껴질 것이다:

# docker-compose.yml (로컬 개발)
version: "3"
services:
api:
image: my-nest-app:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://...
depends_on:
- postgres
postgres:
image: postgres:14
environment:
POSTGRES_DB: mydb
// ECS Task Definition (프로덕션)
{
"family": "my-nest-app",
"cpu": "256",
"memory": "512",
"containerDefinitions": [{
"name": "api",
"image": "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/my-nest-app:latest",
"portMappings": [{"containerPort": 3000}],
"secrets": [
{"name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:...:prod/db-url"}
],
"logConfiguration": {"logDriver": "awslogs", ...}
}]
}

차이점 요약:

docker-composeECS Task Definition
image:"image": (ECR URI)
ports:"portMappings":
environment:"environment": + "secrets": (Secrets Manager 참조)
depends_on:ECS Service + RDS (별도 리소스)
로컬 볼륨S3, EFS (ECS에서 관리)
로그: 터미널 출력CloudWatch Logs (awslogs 드라이버)

핵심 차이: 환경변수 값을 직접 넣지 않고 Secrets Manager ARN으로 참조한다. 그리고 RDS는 compose의 postgres 서비스가 아니라 별도 AWS 리소스로 분리된다.

프론트에서 배포 결과를 확인하는 흐름

CI/CD로 코드를 push했을 때 서비스에 반영되는 과정:

git push → GitHub Actions → Docker build → ECR push
→ ECS Service Update Deployment 시작
→ 새 Task 시작 (PROVISIONING → RUNNING)
→ ALB Health Check 통과 대기
→ 신규 Task에 트래픽 전환
→ 구버전 Task STOPPED
배포 확인 방법:
ECS → Clusters → Services → <서비스> → Events 탭
"service reached a steady state" 메시지 = 배포 완료
로그 확인:
CloudWatch → Log Groups → /ecs/<task-family> → 최신 로그 스트림

실무 아키텍처 패턴 — ECS 프로덕션 구성

섹션 제목: “실무 아키텍처 패턴 — ECS 프로덕션 구성”
[일반적인 프로덕션 ECS 아키텍처]
인터넷
└─ ALB (Public Subnet, HTTPS 443)
└─ Target Group (Health Check: /health)
└─ ECS Service (Private Subnet)
├─ Task (Fargate, 2개 이상 Multi-AZ)
│ └─ NestJS 컨테이너 (Port 3000)
└─ Auto Scaling Policy (CPU 60% 이상 시 Scale Out)
[주요 연결 서비스]
Task → RDS (Private Subnet, SG 허용)
Task → ElastiCache (Private Subnet, SG 허용)
Task → S3 (VPC Endpoint 경유, NAT 없이)
Task → CloudWatch (VPC Endpoint 경유)
Task → Secrets Manager (VPC Endpoint 경유, 환경변수 주입)

2025년 신기능 — ECS Managed Instances: re:Invent 2025에서 발표된 ECS의 새 컴퓨팅 옵션이다. Fargate처럼 서버 관리를 AWS에 맡기면서도, Fargate에서 지원하지 않는 GPU 워크로드, eBPF 기반 보안 에이전트, 특권 컨테이너(privileged container), 120GB 이상 메모리 작업에 사용할 수 있다. 2025년 신기능 — Fargate 태스크 종료 시 Stop Signal 지원: Fargate가 이제 컨테이너 이미지에 정의된 STOPSIGNAL 지시를 그대로 전달한다. 기존에는 SIGTERM만 보냈지만, 이제 SIGQUIT, SIGINT 등 컨테이너별 커스텀 종료 신호를 보내 Graceful Shutdown을 더 정확하게 구현할 수 있다.