내일배움캠프

[내일배움캠프] AWS ECS

munsik22 2026. 6. 21. 18:33

🧩 ECS 핵심 개념 이해하기

왜 ECS가 필요할까?

스프링부트 앱을 클라우드에 배포할 때, 컨테이너를 여러 대/여러 환경에서 안정적으로 실행·관리하는 오케스트레이션이 필요해진다.

  • 컨테이너가 죽으면 누가 다시 띄워줄까?
  • 트래픽이 늘면 어떻게 자동으로 확장할까?
  • 새 버전 배포 시 다운타임 없이 어떻게 할까?
  • 수십 개의 컨테이너를 어디에 어떻게 배치할까?
  • 로그는 어떻게 모을까? (컨테이너는 재시작하면 사라짐)
오케스트레이션이란 여러 개의 컨테이너를 어디에 배치할지, 몇 개를 돌릴지, 죽으면 재시작할지 자동으로 관리하는 시스템이다.
(예: 쿠버네티스(K8s), ECS)

ECS가 해결하는 것

ECS는 단순한 컨테이너 실행 도구가 아니라 컨테이너의 생명주기 전체를 관리하는 운영 플랫폼이다.

문제 ECS가 제공하는 해결
컨테이너를 단일 명령으로 실행하고 관리하고 싶다 Task/Service 관리
서버 인프라 관리를 줄이고 싶다 Fargate(서버리스)
컨테이너 수평 확장이 필요하다 Auto Scaling & Load Balancing
변경된 버전 배포를 안전하게 하고 싶다 Rolling update, Blue/Green

ECS의 3대 핵심 요소

Task Definition

  • 정의: 컨테이너 실행을 위한 도면(설계도)
  • 왜 필요한가?
    • 사람이 매번 수동으로 설정하면 실수 발생
    • 자동 복구 시 동일한 환경 재현 필요
    • 배포 자동화의 기반
  • 포함 내용:
    • 어떤 Docker 이미지? ( myapp:latest )
    • CPU/메모리는? (512 CPU, 1GB RAM)
    • 환경 변수는? ( SPRING_PROFILE=prod )
    • 어떤 포트? (8080)
    • 로그는 어디로? (CloudWatch)
  • Spring Boot 예시
    • Task Definition은 불변이므로 수정하려면 새 버전을 만들어야 한다.
    • 도면(계획서)일 뿐이므로 실제로 실행되지 않는다.
{
  "family": "spring-boot-app",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:latest",
      "cpu": 256,
      "memory": 512,
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp"
        }
      ],
      "environment": [
        { "name": "SPRING_PROFILES_ACTIVE", "value": "prod" },
        { "name": "SERVER_PORT", "value": "8080" }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/spring-boot-app",
          "awslogs-region": "ap-northeast-2",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "networkMode": "awsvpc",
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789:role/ecsTaskRole"
}
  • Task: Task Definition을 기반으로 실행되는 컨테이너 그룹
    • 하나의 Task = 하나 이상의 컨테이너 묶음
    • 예: app + sidecar 형태도 가능

Service

  • 정의: Task가 항상 N개 실행되도록 유지하는 관리자
  • 왜 필요한가?
    • 컨테이너는 언제든 죽을 수 있음
    • 트래픽 변동에 따라 개수 조절 피룡
    • 무중단 배포 필요
  • Service가 하는 일
    • 지정된 개수의 Task를 항상 유지: Desired Count = 3이면 죽으면 자동으로 다시 띄움
    • Auto Scaling 연동: CPU 70% 넘으면 자동으로 Task 추가
    • Load Balancer와 연결: 새로 생성된 Task를 ALB에 자동 등록
    • 롤링 업데이트: 새 버전 배포 시 하나씩 교체
    • Health Check: 비정상 Task는 자동 제거 후 재생성
  • Service가 ECS의 자가 치유(self-healing) 기능을 수행한다.
    1. Service 설정: Desired Count = 3
    2. 현재 실행 중: Task A, Task B, Task C
    3. Task B가 갑자기 죽음 (메모리 부족)
    4. ECS Service: "어? 2개밖에 없네? Task D 생성!"
    5. 다시 3개 유지됨

Cluster

  • 정의: Task를 실행할 인프라 환경
  • 왜 필요한가?
    • 컨테이너를 어딘가에는 올려야 함
    • CPU/메모리 자원을 어디서 가져올지 결정
  • EC2 기반 Cluster
    • 장점: 비용 최적화 가능, 세밀한 제어 가능, 더 다양한 인스턴스 타입 선택
    • 단점: EC2 관리 필요, 용량 계획 필요, 운영 부담 추가
  • Fargate 기반 Cluster (추천!)
    • 장점: Serverless, 사용한 만큼만 과금, Auto Scaling 단순화, 보안 강화 (Host 격리)
    • 단점: EC2보다 약간 비쌈, CPU/Memory 제한, 네트워크 성능 제한
    • 추천하는 이유: 서버 관리 없이 컨테이너에만 집중할 수 있기 때문
    • 왜 CPU/Memory 조합이 제한되나: 서버리스 컨테이너 런타임이기 때문 (AWS 내부 인프라에 맞춰서 표준화된 조합으로 제한)

IAM Role 이해하기

Task Execution Role

  • 목적: ECS가 Task를 시작하기 위해 필요한 권한
  • 필요한 권한:
    • ECR에서 이미지 pull
    • CloudWatch Logs에 로그 전송
    • Secrets Manager에서 시크릿 읽기
  • 예시 정책
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:CreateLogGroup"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": "arn:aws:secretsmanager:*:*:secret:prod/*"
    }
  ]
}

 

Task Role

  • 목적: 컨테이너 내부의 애플리케이션이 AWS 서비스에 접근하기 위한 권한
    • 필요한 권한 예시
    • S3 버킷 읽기/쓰기
    • DynamoDB 테이블 접근
    • SQS 메시지 읽기
    • SNS 토픽 발행
  • 예시 정책 (S3 접근)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::my-app-bucket/*"
    }
  ]
}
💡 Task Execution Role은 ECS 엔진이 사용(인프라 레벨), Task Role은 애플리케이션 코드가 사용(애플리케이션 레벨)

Scheduler

  • 역할: 컨테이너를 어디에 배치할 지 자동으로 결정
  • Scheduler가 하는 판단
    • 어떤 서버에 여유 공간이 있는가?
    • 네트워크 접근이 가능한가?
    • 배치 제약 조건을 만족하는가?
    • 가용 영역(AZ)을 분산해야 하는가?
  • 배치 전략 (EC2 모드)
    1. binpack
      • CPU나 메모리를 최대한 활용
      • 인스턴스 수 최소화
      • 비용 최적화에 유리
    2. spread
      • 지정된 값(AZ, 인스턴스 ID)에 균등 분산
      • 가용성 향상
      • 한 AZ 장애 시 영향 최소화
    3. random
      • 무작위 배치
      • 특별한 요구사항 없을 때
  • Fargate는 spread 전략을 자동으로 사용한다.
Task 3개를 배치해야 함
→ Scheduler: "AZ-1에 1개, AZ-2에 1개, AZ-3에 1개 배치!"
→ 한 AZ 장애나도 서비스 계속 운영 가능
  • ECS 스케쥴러는 어떻게 Task를 배포하는가?
    1. Fargate의 경우
      • Task Definition의 CPU/Memory 스펙을 보고 그 스펙에 맞는 Fargate 실행 환경을 AWS가 자동 생성 (Fargate VM)
      • 특정 서버에 배치하는 과정 없기 떄문에 사실상 '자동 배치'로 이해하면 됨
    2. EC2 기반의 경우
      • ECS 스케줄러가 클러스터 내 EC2 인스턴스들을 살펴보고 Task가 필요로 하는 CPU/Memory가 남아 있는 인스턴스에 Task를 배치
      • 포트 충돌, AZ 분배, capacity provider까지 고려해서 배포

🧩 ECS 구조 깊이 파고들기

네트워크 모드: awsvpc

문제 상황

  • 옛날 방식: EC2 하나에 여러 컨테이너 → 포트 충돌
  • 보안 그룹을 EC2 단위로만 설정 → 세밀한 제어 불가

awsvpc의 해결책

  • 각 Task가 ENI(독립 네트워크 인터페이스)를 가짐
  • Task마다 Private IP 할당 보안 그룹을 Task 단위로 설정 가능
💡 ENI는 가상의 NIC로, 우리가 컴퓨터에서 사용하는 LAN 카드와 같은 역할을 한다. EC2나 Fargate Task가 네트워크에 연결되기 위해 반드시 필요한 가상 네트워 크 인터페이스라고 이해하면 편하다.

 

과거 vs 현재 비교

  • bridge 모드 (옛날 방식)
    • EC2 하나의 보안 그룹
    • Container1 (포트 8080)
    • Container2 (포트 8081) ← 포트 매핑 필요
    • Container3 (포트 8082)
    • 모두 같은 보안 정책
  • awsvpc 모드 (지금 방식)
    • Task1 (독립 ENI, 독립 보안그룹, 10.0.1.10:8080)
    • Task2 (독립 ENI, 독립 보안그룹, 10.0.1.20:8080)
    • Task3 (독립 ENI, 독립 보안그룹, 10.0.1.30:8080)
    • 포트 충돌 없음!

스프링부트에서의 의미

  • 각 앱 인스턴스가 독립적인 네트워크 엔티티
  • MSA 구조에 최적
  • 서비스별 보안 정책 적용 가능
  • 포트는 항상 8080 사용 가능 (포트 매핑 불필요)

awsvpc의 제약사항

  • ENI 개수 제한 (서브넷의 IP 개수 제한)
  • Task당 하나의 ENI 소모
  • 큰 클러스터는 충분한 IP 주소 확보 필요

Health Check 이해하기

Container Health Check (Docker 레벨)

  • 목적: 컨테이너 자체가 정상인지 확인
  • 동작
    • Task Definition에 정의
    • Docker가 주기적으로 실행
    • 실패 시 컨테이너 재시작
  • 예시 (Task Definition)
# wget을 사용하려면 이미지에 설치되어 있어야 함
{
  "healthCheck": {
    "command": [
      "CMD-SHELL",
      "wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1"
    ],
    "interval": 30,
    "timeout": 5,
    "retries": 3,
    "startPeriod": 60
  }
}

 

ALB Health Check (로드밸런서 레벨)

  • 목적: Task가 트래픽을 받을 준비가 되었는지 확인
  • 동작
    1. ALB Target Group에 정의
      • Target Group은 ALB가 “어디로 보낼지” 알려주는 주소 목록
      • Target Group은 Load Balancer가 전달한 요청을 실제로 처리할 대상(Target)들을 논리적으로 묶어 관리하는 단위(Resource Set)
      • ALB는 Listener 규칙에 따라 특정 Target Group으로 트래픽을 라우팅하며, Target Group은 등록된 대상의 상태를 지속적으로 모니터링하고 트래픽 분배 정 책을 관리함
    2. ALB가 주기적으로 HTTP 요청
    3. 실패 시 해당 Task로 트래픽 전송 중단
  • 예시 (Target Group)
lbv2 create-target-group \
 --name spring-boot-tg \
 --protocol HTTP \
 --port 8080 \
 --vpc-id vpc-xxxxx \
 --target-type ip \
 --health-check-enabled \
 --health-check-path /api/health \
 --health-check-interval-seconds 30 \
 --health-check-timeout-seconds 5 \
 --healthy-threshold-count 2 \
 --unhealthy-threshold-count 2
  • Health Check Grace Period
    • Service 설정
    • Task 시작 후 이 기간 동안은 ALB Health Check 실패를 무시
    • 애플리케이션 초기화 시간 고려
"healthCheckGracePeriodSeconds": 60
💡 Best Practice
- Container Health Check: 기본적인 생존 확인 (간단한 체크)
- ALB Health Check: 실제 트래픽 처리 준비 확인 (의존성 포함)

Load Balancer 통합

왜 ALB가 필요한가?

  • 컨테이너의 특성:
    1. IP가 계속 바뀜 (재시작, Auto Scaling)
      • ECS(Fargate/EC2) 컨테이너는 EIP(Elastic IP)를 직접 가질 수 없기 때문에 IP 고정이 불가능함
      • 그래서 ALB나 Service Discovery 같은 라우팅 계층이 반드시 필요하다.
      • EIP는 안정적인 Public Endpoint용인데, Task ENI는 너무 수명이 짧아서 부적합
      • (Task가 생성되면 새 ENI가 만들어지고, Task가 종료되면 그 ENI는 삭제됨)
    2. 여러 개가 동시 실행됨
    3. 동적으로 생성/삭제됨
  • ALB의 역할
    1. 동적 라우팅
      • Task 생성되면 자동으로 Target Group에 등록
      • Task 삭제되면 자동으로 제거
      • IP 기반 타게팅 (awsvpc 모드)
    2. Health Check
      • 비정상 Task는 트래픽 안 보냄
      • 정상화되면 다시 트래픽 전송
      • Draining: 종료 예정 Task는 새 연결 받지 않음
    3. 무중단 배포
      • 새 버전 Task 먼저 띄우기
      • Health Check 통과하면 트래픽 전환
      • 구 버전 Task 종료
    4. Connection Draining
      • Task 종료 시 기존 연결 유지
      • 새 연결만 차단
      • 기본 300초 대기
  • 실제 흐름: 사용자 요청 → ALB → Target Group → Health Check 확인 → 정상 Task들에만 분산
  • Target Group의 등록 해제 지연(Deregistration Delay)
    • 기본값: 300초
    • Task 종료 시 이 시간 동안 기존 연결 처리
    • 너무 길면 배포 느려짐 너무 짧으면 요청 실패

Auto Scaling

  • 왜 필요한가?
    • 평일 낮: 트래픽 많음 → 더 많은 Task 필요
    • 주말 새벽: 트래픽 적음 → 비용 낭비 방지
  • Target Tracking Scaling (권장)
    • 자동으로 지표를 추적하여 조정
    • 목표값 설정 및 CloudWatch 알람 자동 생성
    • Scale Out/In 자동 실행
  • Scaling 기준 예시
    1. CPU 기반
      • 평균 CPU 70% 유지
      • 대부분의 웹 애플리케이션에 적합 (가장 일반적)
    2. 메모리 기반
      • 평균 메모리 80% 유지
      • 메모리 집약적 앱에 적합
    3. ALB 기반
      • Target당 요청 수 1000개 유지
      • CPU보다 빠른 반응
      • 웹 서비스에 이상적
    4. 커스텀 메트릭 (SQS)
      • 큐 메시지 100개당 Task 1개
      • 비동기 워커에 적합
  • 설정 예시
    • Min Capacity: 2 (최소 2개는 항상 실행)
    • Max Capacity: 10 (최대 10개까지 확장)
    • Target Value: 70% (목표 CPU 사용률)
  • Cooldown 이해하기
    • Scale Out Cooldown: Scale Out 후 다음 Scale Out까지 대기 시간 (기본 60초)
    • Scale In Cooldown: Scale In 후 다음 Scale In까지 대기 시간 (기본 300초)
    • Scale In이 더 긴 이유: 안정성 우선 (너무 빨리 줄이면 트래픽 급증 시 문제)
트래픽 급증 → 5개로 증가 → 60초 대기 → 필요시 추가 증가
트래픽 감소 → 300초 대기 → 안정적이면 축소

[참고자료]

  • 내일배움캠프 AWS ECS 강의자료