- Published on
EKS 비용 40%↓ - Karpenter+Graviton 전환 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
EKS 비용을 줄이는 가장 큰 레버는 결국 노드(EC2)입니다. 같은 트래픽을 처리하더라도 어떤 인스턴스 패밀리를 쓰고, 얼마나 촘촘히 스케줄링하며, 스팟을 어떻게 섞는지에 따라 월 비용이 크게 달라집니다.
이 글은 Karpenter로 노드 프로비저닝을 자동화하고, Graviton(ARM64)로 아키텍처를 전환해 총 40% 수준의 비용 절감을 목표로 하는 실전 가이드입니다. 단순히 “켜면 싸집니다”가 아니라, 전환 과정에서 실제로 부딪히는 이슈(이미지 아키텍처, 애드온 호환성, PDB, 드레이닝, 스팟 중단, 관측/검증)를 기준으로 설계와 운영 포인트를 정리합니다.
왜 Karpenter + Graviton 조합이 강력한가
Karpenter가 해결하는 문제
기존 Cluster Autoscaler는 노드 그룹(ASG) 단위로 증감하고, 인스턴스 타입 선택 폭이 제한되거나 반응 속도가 느린 편입니다. 반면 Karpenter는 파드의 요구사항(리소스/라벨/아키텍처/용량 타입)을 보고 그에 맞는 인스턴스를 즉시 선택해 띄웁니다.
즉, 아래를 동시에 달성하기 좋습니다.
- 빈 공간(낭비)을 줄이는
bin packing - 다양한 인스턴스 타입 믹스로
단가 최적화 - 스팟/온디맨드 혼합으로
비용 최적화 - 필요할 때만 노드를 띄우는
탄력 운영
Graviton(ARM64)이 주는 단가 이점
Graviton은 동일 스펙 대비 가격/성능이 좋은 경우가 많고(특히 범용/컴퓨트 계열), EKS에서 ARM64 워커 노드 운영이 성숙해지면서 전환 난이도가 크게 낮아졌습니다.
다만 “그냥 바꾸면 된다”는 아닙니다. 전환 성공의 핵심은 아래 두 가지입니다.
- 컨테이너 이미지가
linux/arm64를 제공하는가 - 워크로드(특히 네이티브 의존성)가 ARM64에서 정상 동작하는가
전환 전 체크리스트(이걸 먼저 해야 삽질이 줄어듭니다)
1) 워크로드 분류: ARM64 가능/불가/보류
다음 기준으로 파드를 분류합니다.
- 가능(우선 전환): Go, Java, Node.js, Python(순수 파이썬 위주), Nginx/Envoy 등 멀티아치 이미지가 잘 갖춰진 것
- 불가(당장 제외): x86 전용 바이너리, 특정 벤더 에이전트(보안/백업), 레거시 JNI/네이티브 모듈 고정
- 보류(검증 필요):
glibc/musl차이, 이미지가 멀티아치라도 런타임에서 네이티브 라이브러리를 다운받는 패턴, 머신러닝/특수 드라이버 의존
가장 빠른 방법은 현재 배포 이미지들의 아키텍처를 점검하는 것입니다.
# 로컬에서 이미지 매니페스트에 arm64가 있는지 확인
# (Docker Desktop 또는 buildx/manifest inspect 사용)
docker buildx imagetools inspect your-registry/your-image:tag
2) 애드온/데몬셋 호환성
CNI, CSI, kube-proxy, CoreDNS 같은 핵심 애드온은 대부분 ARM64 지원이 안정적이지만, 버전이 뒤처져 있으면 문제가 납니다. 특히 노드 부팅 직후 네트워크가 올라오지 않으면 NotReady가 길어지고 스케줄링이 꼬입니다.
운영 중 CNI plugin not initialized 류를 겪었다면, 전환 전에 원인과 복구 루틴을 정리해 두는 게 좋습니다.
3) IAM/IRSA와 노드 역할 설계
Karpenter 컨트롤러는 AWS API를 적극 호출합니다. IRSA 설정이 어긋나면 “파드는 떠 있는데 노드가 안 뜨는” 상황이 생깁니다. 또한 워크로드가 SSM Parameter Store, Secrets Manager 등을 쓰는 경우 IRSA 경계가 애매하면 403이 터질 수 있습니다.
목표 아키텍처: 노드풀 2개로 시작하기
처음부터 복잡하게 가지 말고, 아래 2개 노드풀로 출발하면 대부분의 팀에서 운영이 쉽습니다.
on-demand-arm64: 기본 안정성 풀(Graviton 온디맨드)spot-arm64: 비용 절감 풀(Graviton 스팟)
여기에 “x86 유지 풀”을 하나 더 두고, ARM64 전환이 어려운 워크로드를 임시로 수용합니다.
on-demand-amd64: 레거시/예외 처리용
핵심은 스케줄링 규칙을 명확히 해서 의도치 않게 스팟으로 중요한 파드가 가거나, ARM64로 가면 안 되는 파드가 가는 상황을 막는 것입니다.
Karpenter 설치(핵심 포인트만)
Karpenter는 Helm으로 설치하는 경우가 많습니다. 설치 자체는 공식 문서대로 진행하되, 실무에서는 아래를 꼭 확인합니다.
- EKS 버전과 Karpenter 버전 매트릭스
- 컨트롤러 파드 리소스(너무 작으면 AWS API 호출/큐 처리에서 병목)
- IRSA 권한 범위(최소 권한)
아래는 예시 수준의 Helm values 방향성입니다(환경에 맞게 조정).
# values.yaml (예시)
settings:
clusterName: your-eks
interruptionQueueName: karpenter-interruption
controller:
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
NodePool 설계: Graviton 온디맨드/스팟 분리
Karpenter의 핵심은 NodePool과 NodeClass(EC2NodeClass)입니다. 여기서는 “정책은 NodePool”, “AWS 세부는 NodeClass”로 분리해 관리하는 패턴이 운영에 유리합니다.
EC2NodeClass 예시
서브넷/보안그룹은 태그 셀렉터로 잡는 방식이 관리가 쉽습니다.
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: graviton
spec:
amiFamily: AL2023
role: KarpenterNodeRole-your-eks
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: your-eks
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: your-eks
tags:
Name: karpenter-graviton
온디맨드 NodePool 예시
requirements로 arm64와 인스턴스 패밀리를 제한합니다. 너무 많은 타입을 허용하면 예측이 어려워지고, 너무 좁히면 스케줄 실패가 늘어납니다. 보통 m7g, c7g, r7g 같은 1~2개 패밀리로 시작하는 것을 권합니다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: on-demand-arm64
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: graviton
requirements:
- key: kubernetes.io/arch
operator: In
values: [arm64]
- key: karpenter.sh/capacity-type
operator: In
values: [on-demand]
- key: node.kubernetes.io/instance-type
operator: In
values: [m7g.large, m7g.xlarge, c7g.large, c7g.xlarge]
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 60s
limits:
cpu: "500"
스팟 NodePool 예시(중요 파라미터)
스팟은 반드시 “중단을 감당할 수 있는 파드만” 타게 해야 합니다.
requirements에spot- 인스턴스 타입은 더 넓게 허용(스팟 가용성 확보)
disruption정책은 너무 공격적이면 재시작이 잦아지고, 너무 보수적이면 비용이 새어 나갑니다
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-arm64
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: graviton
requirements:
- key: kubernetes.io/arch
operator: In
values: [arm64]
- key: karpenter.sh/capacity-type
operator: In
values: [spot]
- key: node.kubernetes.io/instance-type
operator: In
values: [m7g.large, m7g.xlarge, c7g.large, c7g.xlarge, r7g.large, r7g.xlarge]
disruption:
consolidationPolicy: WhenUnderutilized
consolidateAfter: 120s
limits:
cpu: "800"
파드 스케줄링 전략: “기본은 ARM 온디맨드, 가능하면 스팟”
실무에서 가장 안전한 접근은 다음입니다.
- 기본은
arm64 + on-demand로 가도록nodeSelector또는nodeAffinity를 설정 - 비용 절감 대상(배치/워커/비동기 처리)은
spot으로 유도 - 스팟 중단 시에도 서비스가 유지되도록
PDB,HPA,multi-AZ를 함께 조정
Deployment에 아키텍처 고정(최소 안전장치)
spec:
template:
spec:
nodeSelector:
kubernetes.io/arch: arm64
스팟 선호(필수 아님, 선호로 시작)
스팟을 “필수”로 걸면 스팟 부족 시 스케줄이 멈춥니다. 초기에는 preferredDuringSchedulingIgnoredDuringExecution로 선호만 주는 것이 안전합니다.
spec:
template:
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: karpenter.sh/capacity-type
operator: In
values: [spot]
40% 절감이 나오는 지점: 낭비 제거 + 스팟 + Graviton
비용 절감은 보통 아래 3개가 합쳐져서 나옵니다.
- Graviton 단가/성능 이점: 온디맨드만 바꿔도 절감이 발생
- Karpenter의 bin packing: 애매하게 남는 리소스가 줄어듦
- 스팟 도입: 중단 허용 워크로드에서 큰 폭으로 절감
하지만 “무조건 스팟을 많이”는 사고로 이어지기 쉽습니다. 절감 목표를 잡을 때도 서비스 티어로 나누는 게 좋습니다.
- Tier 0(핵심 API): 온디맨드 100%
- Tier 1(중요하지만 복구 가능): 온디맨드 70% + 스팟 30%
- Tier 2(배치/큐 워커): 스팟 70~100%
운영에서 가장 많이 터지는 이슈와 방지책
1) 스팟 중단으로 인한 연쇄 장애
스팟 중단은 “언젠가 반드시” 옵니다. 따라서 중단 이벤트가 와도 정상 동작하도록 설계해야 합니다.
PDB로 최소 가용 파드 수 보장topologySpreadConstraints로 AZ/노드 분산- 워커류는 큐 기반(재처리 가능)으로 설계
PDB 예시는 아래처럼 시작합니다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: api
또한 드레이닝 중 readiness가 흔들리면 배포가 꼬일 수 있습니다. readinessProbe 실패로 CrashLoopBackOff처럼 보이는 상황도 있어, 전환 전에 프로브를 점검하세요.
2) ARM64에서만 발생하는 런타임 이슈
대표적으로는 다음 패턴이 많습니다.
node-gyp기반 네이티브 모듈 빌드 실패- Python 패키지의 휠 미제공으로 소스 빌드(시간 증가/실패)
- 이미지가 멀티아치라도 내부에서 추가 바이너리를 다운로드(amd64만 제공)
해결책은 결국 멀티아치 빌드 파이프라인을 갖추는 것입니다.
GitHub Actions로 멀티아치 이미지 빌드 예시
name: build-multiarch
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/your-org/your-app:${{ github.sha }} \
-t ghcr.io/your-org/your-app:latest \
--push .
마이그레이션 절차(롤백 가능한 순서)
1) Karpenter를 “추가”로 붙이고 기존 노드그룹 유지
처음부터 기존 노드그룹을 없애지 마세요. Karpenter가 만든 노드가 정상적으로 뜨고, 파드가 의도대로 스케줄링되는지 확인할 기간이 필요합니다.
2) 신규 워크로드부터 ARM64로
리스크가 낮은 워커/배치부터 arm64로 옮기고, 지표를 봅니다.
- 노드당 파드 밀도 증가 여부
- CPU throttling, 메모리 OOM
- p95/p99 레이턴시
3) 핵심 서비스는 카나리로
트래픽 일부만 ARM64 노드로 보내고, 문제가 없으면 확장합니다.
4) 스팟 비중은 “관측 가능한 만큼만”
스팟을 늘릴수록 비용은 줄지만 운영 난이도는 올라갑니다. 스팟 중단 알림, 드레이닝 시간, 재스케줄 지연을 모니터링하면서 점진적으로 올리세요.
5) 충분히 안정화되면 기존 노드그룹 축소
기존 노드그룹을 0으로 내리기 전에, 예외 워크로드(x86 고정)가 남아 있지 않은지 반드시 확인합니다.
비용 측정/검증: 무엇을 봐야 “40%↓”를 증명할 수 있나
비용 절감이 실제로 성과로 인정받으려면 “전/후 비교”가 명확해야 합니다.
- EC2 비용(온디맨드/스팟 분리)
- EBS 비용(노드 증가로 볼륨이 늘었는지)
- 데이터 전송 비용(AZ 분산으로 cross-AZ가 늘었는지)
- 파드 리소스 요청량 변화(요청 과대가 낭비를 만드는지)
실무에서는 Kubecost나 CUR 기반 대시보드를 많이 씁니다. 최소한 아래는 주간 리포트로 고정하세요.
- 노드 가동 시간 대비 평균 CPU/메모리 사용률
- 스팟 중단 횟수와 서비스 영향(에러율/지연)
- Karpenter consolidation으로 줄어든 노드 수
결론: 가장 안전하게 40% 절감에 접근하는 공식
- 1단계:
Graviton 온디맨드로 전환(이미지 멀티아치 확보)해 즉시 절감 - 2단계:
Karpenter로 bin packing과 자동 프로비저닝으로 낭비 제거 - 3단계: 중단 허용 워크로드에
스팟을 점진 도입해 절감 폭 확대
핵심은 기술 조합 자체가 아니라, 워크로드 티어링, 스케줄링 가드레일, 중단 내성(PDB/분산), 관측/검증을 함께 가져가는 것입니다. 이 네 가지가 갖춰지면 “비용 절감”이 “운영 리스크”로 바뀌지 않고, 안정적으로 숫자로 남습니다.