Karpenter vs Cluster Autoscaler, 우리 팀은 왜 Karpenter로 갔나

작년 이맘때만 해도 우리 팀은 Cluster Autoscaler(CAS)만 썼다. 노드 그룹 5개 정도로 쪼개놓고, 크기 별로 알아서 스케일링되게 두는 방식. 나쁘지 않았다. 진짜로. 근데 클러스터가 200노드를 넘어가고, 배치 잡이랑 온라인 워크로드가 같은 클러스터에서 뒤섞이기 시작하면서 이야기가 달라졌다.
이 글은 두 오토스케일러를 놓고 실무에서 어떤 게 다르고, 우리는 왜 결국 Karpenter로 갈아탔는지 정리해본 것이다. Karpenter 홍보글은 아니고, CAS가 여전히 낫다고 판단한 지점도 같이 적어둔다.
구조가 근본적으로 다르다
CAS는 "노드 그룹을 스케일한다". Karpenter는 "필요한 노드를 직접 만든다". 이거 한 줄이 사실상 전부다.
CAS는 EKS 노드 그룹이든 GKE 노드 풀이든, 미리 정의된 오토스케일링 그룹의 desired capacity를 조절한다. 그래서 새로운 인스턴스 타입을 추가하려면 노드 그룹을 새로 만들어야 한다. 우리 팀은 이걸 관리하려고 결국 Terraform 모듈로 노드 그룹 팩토리를 만들었었다. 인스턴스 타입 추가할 때마다 PR 하나 열고, plan 돌리고, 승인받고, apply. 아무리 자동화해도 하루 이틀은 걸린다.
Karpenter v1은 다르다. NodePool이라는 리소스에 "이런 조건의 노드를 원한다"고만 선언한다. 아키텍처는 amd64와 arm64, 인스턴스 카테고리는 c/m/r, 사이즈는 large 이상, 스팟이랑 온디맨드 둘 다 OK. 그러면 Karpenter가 unscheduled pod가 뜰 때마다 조건에 맞는 인스턴스 중 가장 싼 걸 골라서 EC2 RunInstances API로 직접 띄운다. 노드 그룹? 없다.
이게 왜 중요하냐면, 새로운 워크로드가 들어올 때 "이 파드를 위한 노드 그룹을 준비해두자"는 사전 작업이 사라진다는 뜻이다. 스팟 인스턴스 다양성도 자동으로 확보된다. 우리는 CAS 쓸 때 스팟 중단률 낮추려고 6개 인스턴스 타입을 노드 그룹에 나열해뒀는데, Karpenter는 지역별로 100개 넘는 후보 중에서 알아서 고른다.
프로비저닝 속도
이건 숫자로 보는 게 낫겠다. 우리 환경 기준으로 대충 이런 그림이었다.
CAS로 신규 노드가 올라와서 파드가 스케줄될 때까지: 대략 4-6분. 노드 그룹의 desired capacity 조정 → ASG가 EC2 launch → 노드가 kubelet 실행 → CNI 초기화 → Ready 상태. 각 단계 사이에 지연이 있고, 특히 ASG cool-down이 은근히 크다.
Karpenter는 45-90초 정도다. RunInstances API를 직접 때리니까 ASG 오버헤드가 없다. 우리 배치 잡의 코스트 리포트를 뽑았을 때, 노드 대기 시간 총합이 40% 이상 줄었다. 배치 잡의 wall clock 시간이 아니라, "노드가 없어서 파드가 대기하는 시간"만 따로 뽑은 수치다.
물론 45초라는 게 마법은 아니다. AMI 부팅, kubelet 초기화, CNI, 기타 데몬셋 모두 원래대로 시간을 먹는다. Karpenter가 빠른 건 딱 그 앞부분, 노드 프로비저닝 요청 자체다.
Consolidation, 이게 진짜 killer feature
CAS도 scale-down이 있다. 근데 노드 그룹 단위로만 동작하고, 노드 하나씩 evict하는 방식이라 큰 클러스터에선 잘 안 줄어든다.
Karpenter의 consolidation은 다르다. "지금 실행 중인 파드들을 다시 스케줄한다면 더 작은 노드로 옮길 수 있나?"를 주기적으로 계산해서, 가능하면 노드를 병합한다. 우리는 이걸 켠 첫 주에 EC2 비용이 22% 떨어졌다. 뭐 특별한 튜닝을 한 것도 아니고, 그냥 consolidationPolicy: WhenEmptyOrUnderutilized로 놔뒀을 뿐이다.
다만 이건 양날의 검이다. Consolidation은 파드를 옮긴다. PodDisruptionBudget이 제대로 안 잡혀 있으면 서비스가 흔들린다. 우리도 초기에 몇 번 알림이 왔다. 그래서 Disruption Budget 기능이 v1에 들어온 게 반갑다. Cron 문법으로 "평일 업무시간에는 최대 5%만 disrupt", "새벽 2-5시엔 40%까지 OK" 이런 식으로 정책을 짤 수 있다.
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
budgets:
- nodes: "5%"
schedule: "0 9 * * mon-fri"
duration: 9h
- nodes: "40%"
이 정도만 잡아둬도 낮에 급격한 재배치는 거의 없다.
그럼 CAS는 이제 끝인가
아니다. 두 가지 상황에선 여전히 CAS가 낫다.
첫째, 멀티클라우드 환경. Karpenter는 사실상 AWS에 최적화돼 있다. Azure는 AKS Node Auto-Provisioning으로 지원이 붙었지만 여전히 CAS만큼 성숙하진 않고, GCP는 kubernetes-sigs 조직 아래에 공식 provider가 아직 없다. 만약 EKS, GKE, AKS를 다 굴리는 팀이라면 각각 다른 오토스케일러를 배우고 운영하는 것보다 CAS 하나로 통일하는 게 팀의 인지 부하 측면에서 낫다.
둘째, 상대적으로 정적인 워크로드. 파드 수가 하루 종일 거의 변하지 않고, 노드 타입도 몇 개로 고정돼 있는 클러스터라면 Karpenter의 유연성이 오히려 오버킬이다. CAS가 훨씬 예측 가능하고 디버깅하기 쉽다. 새벽에 알림이 왔을 때 "왜 이 노드가 갑자기 뜨지?"를 CAS는 로그 몇 줄로 답할 수 있는데, Karpenter는 NodeClaim 리소스, EC2NodeClass, NodePool 조건까지 다 봐야 한다.
셋째로 하나 더 얹자면, CAS는 SIG-Autoscaling이 관리하는 커뮤니티 프로젝트다. 단일 벤더 종속 리스크가 걱정된다면 이것도 고려 요소다. Karpenter는 AWS 주도라 GA 후에도 방향성이 AWS 서비스에 강하게 끌린다.
우리가 내린 결론
우리 팀은 EKS만 쓴다. 그리고 트래픽 패턴이 하루에 3-4배 편차가 나는 이커머스 워크로드다. 이 조건에서 Karpenter의 이점이 CAS의 안정성을 압도했다. 특히 스팟 다양성과 consolidation 두 가지가 결정적이었다.
전환 자체는 예상보다 힘들었다. NodePool을 어떻게 나눌지 정하는 데 시간이 꽤 걸렸다. 처음에는 워크로드 종류별로 5개 NodePool을 만들었는데, 결국 3개로 줄였다. NodePool을 잘게 쪼갤수록 Karpenter의 장점인 인스턴스 유연성이 줄어드니까. taint랑 toleration으로 격리하고 NodePool은 최소한으로 두는 게 낫다.
혹시 지금 CAS를 쓰고 있고 EKS라면, 한 번쯤 진지하게 Karpenter를 검토해볼 만하다고 본다. 반대로 GKE/AKS 위주면 지금 자리를 지키는 게 아직은 안전하다.
혹시 다른 조합으로 잘 쓰고 계신 분 있으면 어떻게 하는지 궁금하다.