배경 기존에는 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에서 환경별로 설정한다. Overlay - 환경별 차이만 패치 overlay에서는 환경별로 달라지는 부분만 정의한다. kustomization.yaml에서 base를 참조하고, 패치 파일로 차이점을 덮어쓴다. nameSuffix: -prod가 모든 리소스 이름에 -prod를 붙여준다. my-backend → my-backend-prod가 되어서 같은 클러스터 안에서 환경이 겹치지 않는다. prod의 deployment 패치는 이렇다: prod에서만 replicas를 3으로 올리고, 전용 nodepool에 스케줄링하고, 리소스 제한을 걸고, Prometheus 스크래핑을 활성화한다. dev overlay는 replicas 1에 리소스도 작게 잡는다. 환경별 비교 | | dev | stage | prod | ||||| | Replicas | 1 | 1~2 | 3 (백엔드), 6 (웹, HPA 6~24) | | CPU/Memory | 200m / 128Mi | 500m / 512Mi | 1500m / 4Gi | | Nodepool | dev-nodepool | stage-nodepool | prod-backend-nodepool | | 이미지 태그 | dev (mutable) | stage (mutable) | prod-YYMMDD-HHMM (immutable) | | PDB | 없음 | 없음 | minAvailable: 1 | | HPA | 없음 | 없음 | CPU 30% 기준 | 적용은 한 줄이면 된다: 같은 base를 공유하면서도 환경별로 완전히 다른 설정이 적용된다. ECS에서 Task Definition을 환경별로 복사-수정하던 것과 비교하면 훨씬 깔끔해졌다. K8s Gateway API 왜 Ingress가 아닌 Gateway API인가 EKS에서 외부 트래픽을 받는 방법은 여러 가지다. NGINX Ingress Controller를 쓸 수도 있고, AWS ALB Ingress Controller를 쓸 수도 있다. 실제로 다른 클러스터에서는 NGINX Ingress의 VirtualServer CRD를 사용하고 있었다. 이번에는 K8s Gateway API를 선택했다. 이유: K8s 공식 표준 - Ingress API는 v1 이후 새로운 기능 추가 없이 사실상 유지보수 모드에 들어갔다. Gateway API가 공식 후속 사양으로, K8s 커뮤니티에서 적극적으로 개발하고 있다. 새로 구축하는데 Ingress를 선택할 이유가 없었다 역할 분리가 명확하다 - GatewayClass(인프라) → Gateway(클러스터 운영) → HTTPRoute(서비스 개발) 세 계층으로 나뉘어서, 인프라 팀과 서비스 팀이 각자 관리할 영역이 명확하다 weight 기반 트래픽 분할이 네이티브 - Blue/Green 배포에 필수인 트래픽 가중치가 HTTPRoute 스펙에 내장되어 있다 AWS Load Balancer Controller + Gateway API Gateway API는 스펙일 뿐이고, 실제로 ALB를 생성하고 관리하는 건 AWS Load Balancer Controller다. v2.16부터 Gateway API를 네이티브로 지원한다. GatewayClass, Gateway, HTTPRoute 리소스를 감시하다가, 변경이 생기면 ALB를 자동으로 생성/수정한다. 설치는 Helm으로 한다: 컨트롤러가 ALB를 관리하려면 EC2, ELBv2, ACM, WAF 등 광범위한 AWS 권한이 필요하다. IRSA로 전용 ServiceAccount에 IAM Role을 바인딩해서 최소 권한을 부여한다. 핵심은 Gateway API 매니페스트만 관리하면 ALB가 알아서 따라온다는 것이다. ECS에서는 Terraform으로 ALB를 따로 만들고, Target Group을 등록하고, Listener Rule을 설정하고, WAF를 연결하는 게 전부 별도 작업이었다. 이제는 YAML 파일 몇 개로 끝난다. GatewayClass - ALB 타입 선언 GatewayClass는 "어떤 종류의 로드밸런서를 쓸 것인가"를 선언