ECS에서 EKS로 - kustomize + Gateway API 기반 운영 환경 구축기

AWS ECS에서 EKS로 마이그레이션하면서 kustomize base/overlay 패턴, K8s Gateway API, Karpenter 노드 오토스케일링, Blue/Green 배포 전략까지 구축한 경험을 공유합니다.

배경

기존에는 AWS ECS 위에서 서비스를 운영하고 있었다. GitHub Actions에서 Docker 이미지를 빌드하고, aws ecs update-service --force-new-deployment로 배포하는 단순한 구조였다.

이 구조가 동작은 했지만, 서비스가 커지면서 불편한 점이 쌓여갔다.

ECS에서 느꼈던 한계 환경별 설정 관리가 귀찮았다 - dev, stage, prod마다 별도의 Task Definition을 관리해야 했고, 환경 변수 하나 바꾸려면 AWS 콘솔을 들여다보거나 별도 스크립트를 돌려야 했다 인프라 상태를 코드로 추적하기 어려웠다 - Task Definition 리비전이 쌓이긴 하지만, "현재 어떤 설정으로 돌아가고 있는지"를 Git에서 확인할 수 없었다 로그 확인이 비싸고 불편했다 - CloudWatch Logs 자체 비용도 만만치 않았다. 서비스 수가 늘면서 로그 그룹도 늘어나고, 특히 dev나 stage 같은 개발 환경에서까지 CloudWatch 비용을 내면서 로그를 보는 건 비효율적이었다. Datadog을 붙이면 편해지지만 비용이 더 올라가고, CloudWatch만으로는 여러 서비스의 로그를 크로스로 추적하거나 트레이스와 연동하기가 한계가 있었다 Blue/Green 배포가 복잡했다 - 가장 큰 고통이었다. 아래에서 자세히 설명한다

ECS Blue/Green - 왜 복잡했나

ECS에서 Blue/Green 배포를 하려면 ECS 서비스를 두 개(Blue, Green) 띄우고, ALB Target Group을 각각 연결한 뒤, 배포할 때마다 ALB 라우팅 룰을 조작해야 했다. 이걸 GitHub Actions 워크플로우에서 셸 스크립트로 관리하고 있었다.

실제 deploy-blue.yml의 핵심 로직이다:

이 방식의 문제점: 배포 워크플로우가 두 개 - deploy-blue.yml과 deploy-green.yml을 따로 관리해야 했다. 빌드 로직이 바뀌면 두 파일을 동시에 수정해야 했다 ALB 상태 조회에 의존 - 배포 시점에 ELB API를 호출해서 현재 라우팅 상태를 판단한다. API 응답이 느리거나 실패하면 배포가 막힌다 트래픽 전환이 별도 작업 - 이미지를 배포하는 것과 트래픽을 전환하는 것이 분리되어 있어서, 배포 후 ALB 라우팅 룰을 수동으로 바꿔야 했다 상태 추적 불가 - "지금 Blue가 활성이야, Green이 활성이야?"를 ALB API를 호출하지 않으면 알 수 없었다. Git에는 이 상태가 기록되지 않는다

왜 EKS인가

EKS를 선택한 건 결국 선언적 인프라 관리가 핵심이었다.

| | ECS | EKS | |||| | 설정 관리 | Task Definition (AWS 콘솔/CLI) | YAML 매니페스트 (Git) | | 환경 분리 | 서비스별 Task Definition | Kustomize overlay | | 배포 전략 | ALB 라우팅 룰 직접 조작 | HTTPRoute weight 한 줄 변경 | | 로그/모니터링 | CloudWatch or Datadog (비용 부담) | OTel 사이드카 + LGTM 스택 (오픈소스) | | 사이드카 | Task Definition에 컨테이너 추가 | Pod spec에 컨테이너 추가 | | 상태 추적 | AWS 콘솔 | Git + ArgoCD |

인프라 설정 전체를 Git으로 관리하고, ArgoCD로 클러스터와 동기화하면 "현재 운영 상태 = Git 저장소 상태"가 된다. 이게 GitOps의 핵심이고, EKS 마이그레이션의 가장 큰 동기였다.

전체 아키텍처

마이그레이션 후 구성된 전체 아키텍처다.

핵심 구성 요소: my-cluster 저장소 - 모든 K8s 매니페스트를 관리하는 GitOps 저장소 Kustomize - base/overlay 패턴으로 환경별 설정 분리 K8s Gateway API - ALB Controller 기반 트래픽 라우팅 Karpenter - 워크로드 기반 노드 오토스케일링 ArgoCD - Git ↔ 클러스터 상태 동기화

클러스터 구성

eksctl로 EKS 클러스터 생성

클러스터는 eksctl로 생성했다. 선언적 YAML 설정 파일 하나로 VPC, 노드그룹, 애드온까지 한 번에 구성할 수 있어서 편하다.

4개 AZ에 private/public 서브넷을 배치하고, OIDC Provider를 활성화해서 IRSA(IAM Roles for Service Accounts)를 쓸 수 있게 했다. 시스템 노드그룹은 CoreDNS, Karpenter 같은 클러스터 컴포넌트용이고, 실제 워크로드는 Karpenter가 관리하는 노드에서 돌아간다.

IRSA - Pod별 최소 권한

ECS에서는 Task Role로 권한을 관리했는데, EKS에서는 IRSA로 ServiceAccount에 IAM Role을 바인딩한다. Pod가 AWS API를 호출할 때 해당 ServiceAccount에 연결된 IAM Role의 권한만 사용하게 된다.

서비스마다 전용 ServiceAccount를 만들어서, my-backend는 S3/SQS/Secrets Manager 접근 권한만, my-web은 필요한 최소 권한만 갖도록 분리했다. ECS Task Role과 개념은 같지만, K8s 네이티브하게 관리할 수 있어서 더 깔끔하다.

Kustomize - 환경별 설정 관리

마이그레이션의 가장 큰 고민 중 하나가 "환경별 설정을 어떻게 관리할 것인가"였다.

왜 Kustomize인가

Helm도 고려했지만 Kustomize를 선택했다. 이유는 단순하다 - 우리 서비스에 범용 차트가 필요 없었다. Helm은 범용 패키지 배포에 강점이 있지만, 자체 서비스는 환경별 "차이점"만 관리하면 되기 때문에 Kustomize의 base/overlay 패턴이 더 직관적이었다.

디렉토리 구조

Base - 공통 리소스 정의

base에는 모든 환경에서 공유하는 리소스 템플릿을 둔다.

base에서 주목할 점은 OTel Collector 사이드카가 기본으로 포함된다는 것이다. 모든 환경에서 로그·트레이스·메트릭을 수집하되, 전송 대상(Loki, Tempo 등)은 overlay에서 환경별로 설정한다.