[인턴] EC2에서 ECS on EC2로: 프로덕션 백엔드 무중단 이전 계획

들어가며

이전 글에서는 카나리 배포 중 DDL Migration 실행 시점을 DDL 유형별로 정리했습니다.
이번 글은 그 배경이 되는 플랫폼 이전 계획 자체를 기록합니다.

현재 백엔드는 단일 EC2 위에서 PM2 + Nginx + Docker Compose로 운영되고 있는데, 이를 ECS on EC2로 옮기기로 했습니다.
한 번에 스위칭하지 않고, ALB Weighted Target Group을 이용해 트래픽을 점진적으로 넘기는 병렬 운영 + 카나리 전략을 택했고, 그 과정을 5개 Stage로 나눠 설계한 내용을 정리합니다.


현재 구조와 한계

현재 백엔드는 단일 EC2 인스턴스 위에서 다음과 같이 구성되어 있습니다.

                    ┌─ EC2 Instance ──────────────────────────┐
                    │                                         │
Internet ── ALB ──▶ │  Nginx (ip_hash)                        │
                    │   ├─ backend-blue  (PM2 multi-fork)     │
                    │   ├─ backend-green (대기)                │
                    │   ├─ backend-cron  (다수의 배치 잡)       │
                    │   └─ datadog-agent                      │
                    └────────────────┬────────────────────────┘
                                     ▼
                                  RDS (관리형 DB)
  • 배포: SSH 접속 후 스크립트 실행 기반의 수동 Blue/Green
  • 스케일링: PM2 fork 수 수동 조정
  • 롤백: git revert 후 동일한 수동 배포 재실행
  • 관측: Docker Compose로 Datadog Agent 동거

동작은 하지만 문제는 명확합니다. 단일 인스턴스이기 때문에 AZ 장애에 무방비이고, 배포·롤백이 사람 손을 타며, 오토스케일링이 전혀 작동하지 않습니다.

ECS on EC2를 선택한 이유는 세 가지입니다.

  1. 비용 예측 가능성 — Fargate 대비 컴퓨팅 비용을 예측·튜닝하기 쉽다.
  2. 기존 EC2 운영 경험 재활용 — 호스트 레벨 디버깅 경로가 그대로 유지된다.
  3. awsvpc 네트워크 모드 — Task별 독립 ENI로 보안 그룹을 깔끔하게 분리할 수 있다.

가장 먼저 결정한 것: 네트워크 전략

ECS로 옮기기로 결심한 뒤 첫 번째 난관은 “Task를 어디에 놓을 것인가”였습니다. 크게 세 가지 옵션을 저울질했습니다.

옵션 설명 장점 단점
A. 기존 VPC Public Subnet assignPublicIp: ENABLED로 IGW를 통해 외부 통신 추가 인프라 비용 없음, 기존 EC2와 네트워크 경로 동일 → 메트릭 비교가 깨끗 보안 모범(Private Subnet)에서는 벗어남
B. Private Subnet + NAT Gateway 같은 VPC 안에 Private Subnet 신설 보안 모범에 부합 NAT Gateway 월 고정 비용, 네트워크 재설계가 카나리 이전과 섞임
C. 신규 VPC VPC·ALB·DNS 전면 재구성 가장 깨끗한 이관 이번 범위를 크게 초과

결론은 옵션 A입니다. 핵심 원칙은 “리스크 분리” 입니다.
카나리 이전(런타임 플랫폼 교체)과 네트워크 재설계는 별개 프로젝트로 쪼개야, 문제 발생 시 원인을 명확히 특정할 수 있습니다. Private Subnet + NAT Gateway 전환은 EC2를 완전히 퇴역시킨 뒤 별도 이니셔티브로 진행할 계획입니다.


왜 병렬 운영인가

“한번에 스위칭” vs “병렬 운영 후 점진 이전” — 이 결정이 전체 마이그레이션의 성패를 좌우합니다.

한번에 스위칭하면 D-day에 모든 걸 걸어야 합니다. 롤백 경로가 불명확하고, DB 커넥션·크론·WebSocket을 동시에 검증해야 합니다.

병렬 운영은 EC2를 그대로 둔 채 ECS를 옆에 띄우고, ALB Weighted Target Group으로 트래픽을 조금씩 넘깁니다. 문제가 생기면 weight를 원복하면 끝입니다. 이 단순한 롤백 경로 덕분에 각 단계에서 과감하게 실험할 수 있습니다.


5단계 마이그레이션 플레이북

Stage 0            Stage 1            Stage 3              Stage 4
인프라 준비         ECS 기동 검증       카나리 전환             EC2 퇴역
(코드/IaC)         (트래픽 0%)        (점진 가중치 전환)       (정리)
    │                 │                  │                     │
    ▼                 ▼                  ▼                     ▼
────●─────────────────●──────────────────●─────────────────────●──▶

Stage 0: 인프라 준비 — 트래픽 영향 제로

프로덕션에 손대기 전에 모든 인프라를 먼저 세팅합니다. IaC(Pulumi)로 ECS 클러스터, ASG(멀티 AZ 분산), 보안 그룹, Target Group을 생성하고, GitHub Actions 배포 워크플로우를 구성합니다. 이 단계에서 ECS Service의 desired count는 0입니다. 아무것도 뜨지 않지만, 배포 파이프라인이 ECR push부터 Task Definition 등록까지 성공하는지 확인합니다.

특히 신경 쓰고 있는 포인트:

  • 보안 그룹 분리: 기존에는 ALB와 EC2가 하나의 SG를 공유하는 안티패턴이었습니다. ALB 전용, Task ENI 전용, ECS 호스트 전용 세 개로 분리할 예정입니다. awsvpc 모드에서는 Task가 독립 ENI를 가지므로 “ALB SG → Task SG” 단일 규칙으로 축약됩니다.
  • 시크릿 하드코딩 제거: Docker Compose에 평문으로 들어가 있던 외부 서비스 API Key를 Secrets Manager로 이전하고, 기존 키는 즉시 revoke할 계획입니다.
  • IAM 권한 이관: EC2 instance profile의 모든 정책을 dump해 ECS Task Role에 1:1 이관합니다. 하나라도 누락되면 런타임에 S3/SES 등에서 실패하므로, 정책 diff가 0건임을 반드시 확인합니다.

Stage 1: ECS 기동 검증 — 트래픽 0%

Task를 실제로 띄우되, ALB weight는 여전히 0입니다. 외부 트래픽 없이 내부적으로만 검증하는 단계입니다.

확인 항목은 명확합니다.

  • ECS Task가 정상 RUNNING 상태인가
  • RDS 커넥션 합산이 안전 범위 내인가
  • GraphQL 쿼리/뮤테이션/서브스크립션이 정상 응답하는가
  • Datadog APM 트레이스가 수집되는가
  • awsvpc + assignPublicIp: ENABLED 조합이 실제로 외부 통신까지 뚫리는가

24시간 이상 무장애 기동을 확인한 뒤 다음 단계로 넘어갑니다.

Stage 2는 건너뜁니다 — Shadow 트래픽 포기

Shadow 트래픽(프로덕션 트래픽을 복제해 ECS에 흘리는 방식)을 고려했지만 포기했습니다. 이유는 두 가지입니다.

  1. GraphQL 특성: POST body 안에 Query와 Mutation이 혼재되어 읽기/쓰기 분리가 불가능합니다.
  2. DB 공유: 양쪽이 같은 RDS를 공유하므로, 쓰기 미러링은 데이터 중복 위험이 있습니다.

카나리 초반의 낮은 가중치 구간이 실질적인 Shadow 역할을 대신할 것으로 보고, 별도 Shadow 단계는 스킵합니다.

Stage 3: 카나리 전환 — 본게임

ALB Weighted Target Group의 weight를 단계적으로 올립니다.

단계     EC2 Weight    ECS Weight    유지 시간
─────────────────────────────────────────────
Step 1      95            5           몇 시간
Step 2      90           10           반나절
Step 3      75           25           약 하루
Step 4      50           50           1~2일
Step 5      25           75           약 하루
Step 6       0          100           수일간 관찰

각 단계마다 지켜볼 지표는 정해져 있습니다.

  • 5xx 에러율, p99 레이턴시
  • DB 커넥션 합산(EC2 + ECS)
  • 큐 backlog(BullMQ)
  • WebSocket 연결 안정성

롤백 트리거도 사전에 정의했습니다. 아래 중 하나라도 해당하면 즉시 EC2 100%로 원복합니다.

  • 5xx가 baseline 대비 2배 이상
  • p99가 baseline 대비 50% 이상 증가
  • DB 커넥션이 max_connections의 80% 초과
  • OOM Kill 반복 발생

원복은 aws elbv2 modify-listener 한 줄로 끝나도록 해 두었습니다.

⚠️ 크론 전환은 특히 주의가 필요합니다. 배치 잡이 양쪽에서 동시에 돌면 데이터 정합성이 파괴됩니다. 반드시 EC2 크론을 먼저 중지한 뒤 ECS 크론을 기동하는 순서를 지킬 것입니다.

Stage 4: EC2 퇴역

ECS 100% 상태에서 피크 타임을 2회 이상 무사히 넘기고, 최소 2일간 관찰한 뒤에 퇴역을 시작합니다.

이때 인스턴스를 바로 Terminate하지 않고 Stop 상태로 1~2주 유지합니다. 문제가 뒤늦게 발견되더라도 기존 디스크/설정을 그대로 살려서 다시 띄울 수 있도록 하기 위함입니다. 관찰 기간이 지나고 이상이 없으면 그때 Terminate합니다.


DB 커넥션 풀: 숫자로 확인하는 안전 마진

병렬 운영 중 가장 긴장되는 부분은 DB 커넥션입니다. EC2와 ECS가 동일 RDS를 바라보기 때문입니다.

숫자를 구체 값 대신 상대적 비율로 정리하면 다음과 같습니다.

  • 현재 EC2(PM2 멀티 fork + 배치 잡)의 평상시 커넥션 사용량: A
  • ECS Auto Scaling이 max까지 확장됐을 때의 최대 사용량: B
  • 카나리 중 최악의 경우 합산: A + B
  • 이 합산은 max_connections 대비 절반에 크게 못 미치는 수준

“아마 괜찮겠지”가 아니라 숫자로 안전 마진을 확인한 뒤 진행한다는 원칙이 중요하다고 느꼈습니다. 결과적으로 병렬 운영 구간에서도 커넥션 한도에는 충분한 여유가 있다는 걸 확인했습니다.


Datadog 사이드카 패턴

EC2 시절에는 Docker 브릿지 네트워크로 Datadog Agent 한 개를 여러 프로세스가 공유했습니다.
ECS awsvpc 모드에서는 Task 내부 컨테이너들이 같은 네트워크 네임스페이스를 공유하므로, backend 컨테이너가 DD_AGENT_HOST=127.0.0.1로 사이드카에 접근할 수 있습니다.

Task
 ┌──────────────────────────────────────┐
 │  backend container                   │
 │  └─ DD_AGENT_HOST=127.0.0.1          │
 │                                      │
 │  datadog-agent container (sidecar)   │
 └──────────────────────────────────────┘

Task Definition에 backend와 datadog-agent를 함께 정의하면, Auto Scaling으로 Task가 늘어날 때 Agent도 자연스럽게 함께 늘어납니다.
Agent 이미지는 :latest 대신 고정 태그를 사용해 예기치 않은 자동 업그레이드로 인한 장애를 방지할 계획입니다.


이전이 완료되면 달라지는 것들

구분 Before (EC2) After (ECS on EC2)
프로세스 관리 PM2 ECS(재시작·헬스체크)
프론트 라우팅 Nginx ALB → Task ENI 직결
배포 방식 SSH + 수동 스크립트 GitHub Actions → ECR → TaskDef
인프라 가용성 단일 인스턴스 ASG, 멀티 AZ 분산
스케일링 PM2 fork 수 수동 조정 Application Auto Scaling

비용 관점

솔직히 컴퓨팅 비용은 늘어납니다. 단일 인스턴스에서 멀티 AZ 분산으로 가면서 인스턴스 대수가 증가하기 때문입니다. 세대 차이로 단가는 조금 내려가지만 대수가 늘어 순증입니다. Datadog Infrastructure host 집계도 함께 늘어납니다.

다만 운영 비용은 큰 폭으로 줄어들 것으로 기대합니다. SSH 접속·수동 배포·수동 복구 같은 사람 시간이 자동화로 대체되기 때문입니다. 병렬 운영 기간의 이중 비용은 Stage 4에서 EC2를 빠르게 퇴역시켜 최소화할 예정입니다.


아직 남은 숙제들

모든 게 정해진 건 아닙니다. 현재 미결정 상태인 항목들이 있고, 각각이 해결되기 전까지 해당 Stage 진입을 차단하는 게이트로 관리하고 있습니다.

  • 크론 전환 정책: 배치 잡의 중복/gap 방지를 위한 전환 순서 — Stage 3 진입 전 확정 필요
  • 외부 API 고정 IP 감사: assignPublicIp: ENABLED 환경에서는 Task 재기동마다 Public IP가 바뀌므로, 고정 IP를 요구하는 외부 서비스가 있는지 전수 조사 필요. 있다면 옵션 B(Private + NAT) 전환 검토
  • 관측 비용 재산정: ECS on EC2 구조에서 Infrastructure/APM host 집계 방식 재확인

마치며

이번 이전에서 가장 중요하게 생각하는 원칙은 세 가지입니다.

  1. 리스크를 분리할 것.
    카나리 이전과 네트워크 재설계를 동시에 하면 실패 원인을 특정할 수 없습니다. 하나의 변경에 하나의 관심사만 담습니다.

  2. 숫자로 검증할 것.
    “아마 괜찮겠지”가 아니라, DB 커넥션 합산이 한도의 몇 %인지, ENI 한도가 충분한지를 직접 계산하고 실측합니다.

  3. 롤백 경로를 먼저 설계할 것.
    전진보다 후퇴가 더 중요합니다. ALB weight 원복 한 줄로 즉시 롤백할 수 있다는 확신이 있어야, 5%를 10%로 올릴 용기가 생깁니다.

아직 진행 중인 프로젝트이고, 실제 카나리 전환 과정에서 예상치 못한 이슈가 나올 수 있습니다. 이후 단계의 실전 기록도 별도 포스트로 남길 예정입니다.

비슷한 전환을 고민 중이라면, “병렬 운영 + 카나리” 가 가장 안전한 경로라는 점을 강조하고 싶습니다. 한번에 스위칭하는 것은 자신감의 문제가 아니라 롤백 경로의 문제입니다.

Categories:

Updated:

Leave a comment