- Published on
EKS에서 Karpenter 노드 자동확장 튜닝 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
EKS에서 노드 자동확장을 “잘” 만든다는 건 단순히 노드가 늘고 줄어드는 수준이 아닙니다.
- 필요한 순간에 수십 초 내로 용량이 붙고
- 불필요한 노드는 안전하게 정리되며
- 워크로드 특성(아키텍처, 디스크, 네트워크, AZ, GPU 등)에 맞는 인스턴스가 정확히 선택되고
- 비용(온디맨드/스팟), 안정성(PDB/중단 예산), 운영 편의성(드리프트/업그레이드)까지 균형을 잡아야 합니다.
Karpenter는 EKS에서 이런 목표를 달성하기 위한 강력한 구성요소입니다. 하지만 기본 예제만 적용하면 다음 문제가 자주 발생합니다.
- 스케줄링은 되는데 특정 워크로드가 계속
Pending - 스팟 중단/회수 이후 복구가 느림
- 노드가 너무 자주 교체되거나(드리프트/통합) 반대로 정리가 안 됨
- 인스턴스 타입 선택이 비효율적이라 비용이 튐
- 노드 부팅은 됐는데 네트워크/권한 문제로 파드가 못 뜸
이 글은 위 문제를 줄이기 위한 Karpenter 튜닝 체크리스트를 설정 예제와 함께 정리합니다.
관련 운영 이슈를 함께 보면 원인 파악이 더 빨라집니다.
Karpenter 튜닝의 핵심: “제약을 명확히, 선택지는 넓게”
Karpenter는 크게 두 단계를 반복합니다.
- 스케줄러가 파드를 배치하지 못하면(리소스/제약 충돌)
Pending발생 - Karpenter가
Pending파드를 관찰하고, 요구사항을 만족하는 노드를 프로비저닝
여기서 튜닝 포인트는 두 가지입니다.
- 요구사항(requirements)을 과하게 좁히지 말기: 인스턴스 타입을 1~2개로 고정하면 가용성과 가격 경쟁이 떨어지고, 특정 AZ에서 용량 부족 시 확장이 멈춥니다.
- 반대로 운영상 반드시 필요한 제약은 명확히: 아키텍처, AZ, 디스크, 네트워크, 스팟/온디맨드 혼합 정책 같은 필수 조건은 모호하면 장애가 납니다.
사전 점검: EKS 네트워크와 권한이 “노드 부팅 후”를 보장하는가
Karpenter는 노드를 빠르게 띄우는 데 강하지만, 노드가 떠도 다음이 깨지면 파드는 계속 실패합니다.
- 노드 IAM 역할(인스턴스 프로파일) 권한 부족
- CNI 설정/보안그룹/서브넷 라우팅 문제
- VPC 엔드포인트 정책 문제로 ECR/S3 접근 실패
특히 ECR 이미지 풀, S3 접근, CloudWatch 로그 전송은 노드가 뜬 직후 바로 필요해집니다. ImagePullBackOff 나 AccessDenied 가 보이면 Karpenter 문제가 아니라 권한/엔드포인트/라우팅을 먼저 확인하세요.
NodePool/EC2NodeClass 설계: 워크로드 클래스로 분리하기
운영에서 가장 효과가 큰 튜닝은 “하나의 풀로 모든 걸 해결”하려는 시도를 버리고, 워크로드 성격에 따라 풀을 나누는 것입니다.
- 기본 서비스용(온디맨드 위주, 안정성)
- 배치/비동기 작업용(스팟 위주, 비용)
- 메모리 집약형(메모리 최적화 인스턴스)
- 네트워크 집약형(ENA 성능 좋은 계열)
아래 예시는 Karpenter v1 계열 리소스(일반적으로 NodePool + EC2NodeClass) 형태로 작성했습니다. 클러스터 버전에 따라 CRD 이름이 다를 수 있으니 설치한 Karpenter 버전에 맞춰 확인하세요.
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default-ec2
spec:
amiFamily: AL2
role: eks-karpenter-node-role
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-eks
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-eks
tags:
karpenter.sh/discovery: my-eks
owner: platform
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: on-demand-general
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default-ec2
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"]
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
taints:
- key: dedicated
value: general
effect: NoSchedule
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
expireAfter: 720h
limits:
cpu: "2000"
튜닝 포인트 1: 인스턴스 선택을 “패밀리”로 열어두기
instance-type 를 고정하기보다 instance-category, generation, arch 같은 조건으로 범위를 열어두면:
- 특정 타입 용량 부족 시 대체 타입으로 즉시 우회
- 스팟 시장에서 더 싼 타입을 자동으로 선택
반대로, 워크로드가 특정 타입에 강하게 의존(예: 로컬 NVMe, 특정 네트워크 대역폭)한다면 그때만 좁히는 게 좋습니다.
튜닝 포인트 2: 풀별 taint 로 “의도치 않은 혼재” 방지
기본 풀에 taint를 걸고, 워크로드가 tolerations 로 들어오게 하면 다음이 좋아집니다.
- 배치 파드가 서비스 노드를 잠식하는 상황 방지
- 스팟 노드에 올라가면 안 되는 파드(상태ful, 중요한 API)를 보호
예시 워크로드:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
tolerations:
- key: dedicated
operator: Equal
value: general
effect: NoSchedule
containers:
- name: api
image: public.ecr.aws/nginx/nginx:stable
resources:
requests:
cpu: "500m"
memory: "512Mi"
스팟 튜닝: 중단 내성 설계가 먼저, 그 다음이 비용
스팟을 쓰면 비용은 내려가지만, “중단 이벤트를 정상 시나리오로 받아들이는 설계”가 필요합니다.
1) 스팟 전용 NodePool을 따로 만들기
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-batch
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default-ec2
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"]
taints:
- key: dedicated
value: batch
effect: NoSchedule
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
expireAfter: 168h
배치 워크로드는 tolerations 로만 스팟 풀에 들어오게 하고, 중요한 서비스는 온디맨드 풀에 남겨두는 것이 기본입니다.
2) PDB와 종료 유예시간으로 “중단 시 품질” 지키기
스팟 회수나 통합(consolidation)으로 노드가 종료될 때, 파드가 한꺼번에 내려가면 장애로 이어집니다.
PodDisruptionBudget로 동시에 내려갈 수 있는 파드 수를 제한terminationGracePeriodSeconds를 충분히 부여- 프리스탑 훅으로 커넥션 드레인
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: api
이 설정은 Karpenter가 노드를 정리할 때도 영향을 줍니다. 즉, 통합 정책을 공격적으로 잡아도 PDB가 안전장치가 됩니다.
통합(Consolidation)과 만료(Expire): 비용 최적화의 스위치
Karpenter 비용 최적화의 핵심은 통합입니다.
WhenEmptyOrUnderutilized: 비거나 덜 찬 노드를 더 적은 노드로 합치기expireAfter: 노드 수명을 제한해 장기 드리프트/누적 문제를 정리
하지만 통합을 켜면 “노드 교체가 잦다”는 체감이 생길 수 있습니다. 이때는 아래 순서로 접근하세요.
- 먼저 PDB로 서비스 품질을 방어
- 그 다음 통합을 켜서 비용을 줄임
- 마지막으로
expireAfter로 정기 교체 주기를 부여
운영 팁:
- Stateful 워크로드가 많으면 통합이 기대만큼 안 될 수 있습니다(볼륨/어피니티 제약).
- 통합이 안 되는 이유의 대부분은
requests과도 설정, 과한nodeAffinity, PDB 과보호 중 하나입니다.
스케줄링 튜닝: requests 가 곧 “용량 계획”이다
Karpenter는 파드의 resources.requests 를 기준으로 노드를 고릅니다. 즉, requests 가 부정확하면:
- 과대 요청: 필요 이상 큰 인스턴스가 뜨고 비용 증가
- 과소 요청: 노드는 작게 떠서 실제 사용량이 치솟고 OOM/CPU 스로틀링
특히 JVM/Node.js 같은 런타임은 메모리 피크가 튀기 쉬워서, 요청을 너무 낮게 잡으면 노드가 자주 불안정해집니다.
실무적으로는 다음을 권장합니다.
- 서비스:
requests는 “평균 사용량 + 여유”,limits는 “폭주 상한” - 배치:
requests를 높게 잡아도 되지만, 병렬도를 조절해 노드 낭비를 줄이기
멀티 AZ와 서브넷 선택: 확장 실패의 숨은 원인
Karpenter가 노드를 만들 때 서브넷을 고르는 로직은 subnetSelectorTerms 에 달려 있습니다.
- 특정 AZ에만 서브넷이 선택되면, 그 AZ 용량 부족 시 확장이 멈춥니다.
- 반대로 모든 서브넷을 열어두되, 워크로드가 AZ 편향(예: 특정 데이터 저장소)이라면 cross-AZ 트래픽 비용이 늘 수 있습니다.
권장 패턴:
- 기본은 2~3개 AZ 모두 열어두기
- 데이터 로컬리티가 필요한 워크로드는
topology.kubernetes.io/zone어피니티를 “필요한 만큼만” 추가
주의: 어피니티를 너무 강하게 걸면 Pending 이 늘고, 그걸 해결하려고 인스턴스 타입을 좁히면 악순환이 됩니다.
네트워크 병목 튜닝: 노드가 늘어도 연결이 끊기면 의미가 없다
노드 자동확장을 해도, 노드 단위 커널/네트워크 한계로 장애가 나면 “확장할수록 더 망가지는” 상황이 됩니다. 대표적으로 conntrack 포화가 그렇습니다.
- 고QPS API, 프록시, NAT, egress가 많은 워크로드는 conntrack 엔트리가 급증
- 노드가 늘어나면 총 연결 수가 늘어 포화가 더 빨리 오기도 함
이 경우는 Karpenter 튜닝보다 노드 커널 파라미터, CNI, 서비스 메시/프록시 구성 점검이 먼저입니다. 자세한 원인/대응은 다음 글이 도움이 됩니다.
관측과 디버깅: “왜 Pending인가”를 1분 안에 답하기
Karpenter 튜닝은 관측 없이는 불가능합니다. 최소한 아래는 상시 확인 가능해야 합니다.
- 특정 파드가
Pending인 이유(스케줄러 이벤트) - Karpenter가 어떤 요구사항으로 노드를 만들려 했는지
- 실제 생성된 노드의 인스턴스 타입, AZ, 용량 타입(스팟/온디맨드)
필수 커맨드
kubectl describe pod -n myns mypod
Events 에서 0/.. nodes are available 뒤에 붙는 사유(taint 미허용, affinity 불일치, 리소스 부족)를 먼저 봅니다.
kubectl get events -A --sort-by=.lastTimestamp | tail -n 50
kubectl -n karpenter logs deploy/karpenter -f
Karpenter 로그에서 “어떤 제약 때문에 어떤 인스턴스 후보가 탈락했는지”가 드러납니다. 여기서 자주 보이는 안티패턴은:
nodeSelector로 특정 라벨을 강제했는데 해당 라벨을 가진 노드는 Karpenter가 만들 수 없음instance-type를 너무 좁혀서 후보가 0개- 스팟만 허용했는데 해당 시점에 스팟 용량이 없음
자주 겪는 튜닝 시나리오 5가지
1) 노드는 뜨는데 파드가 계속 ContainerCreating
- CNI 문제, 보안그룹, 서브넷 라우팅, ECR 접근 문제 가능성이 큼
- VPC 엔드포인트 정책이 S3/ECR을 막는 경우도 흔함
네트워크/엔드포인트 이슈는 아래 글의 케이스와 유사하게 나타납니다.
2) 특정 워크로드만 Pending
tolerations누락(taint 풀에 못 들어감)- 강한
nodeAffinity또는topologySpreadConstraints충돌 requests가 커서 어떤 인스턴스에도 안 들어감
해결 순서:
kubectl describe pod이벤트 확인- 요구사항을 “필수/선호”로 재분류(가능하면 선호로)
- 인스턴스 후보군 확대
3) 비용이 기대보다 안 내려감
- 통합이 꺼져 있거나, PDB/어피니티 때문에 통합이 불가
requests과대 설정으로 노드가 비효율적으로 큼- 스팟 풀에 들어가야 할 배치가 온디맨드에 올라감(taint/toleration 설계 미흡)
4) 노드가 너무 자주 갈아끼워짐
- 통합이 너무 공격적이거나
expireAfter가 너무 짧음 - 워크로드가 자주 스케일 인/아웃하며 underutilized 판정이 잦음
대응:
- 서비스 계열은
expireAfter를 길게(예: 30일) - 배치 계열은 짧게(예: 7일)
- PDB로 안전장치를 두고 통합 강도를 조절
5) 스팟 중단 후 복구가 느림
- 스팟 후보군이 좁음(타입/세대/AZ)
- 이미지 풀 시간이 길고, 워밍업이 없음
대응:
- 인스턴스 후보군 확대
- 컨테이너 이미지 최적화, 레이어 정리, 프리풀 전략 검토
운영용 권장 템플릿: “온디맨드 기본 + 스팟 배치”
실무에서 가장 무난한 조합은:
- 온디맨드 NodePool: API/웹/핵심 서비스
- 스팟 NodePool: 배치/큐 컨슈머/비동기
- 공통 EC2NodeClass: 서브넷/보안그룹/태그 표준화
taint로 워크로드 분리- PDB로 중단 안전성 확보
- 통합은 켜되,
expireAfter는 워크로드별로 다르게
이 조합을 기준으로, 워크로드가 커지면 메모리 최적화 풀, GPU 풀 등을 추가하는 방식이 확장성도 좋습니다.
마무리: 튜닝 체크리스트
- 인스턴스 타입을 고정하지 말고 카테고리/세대/아키텍처로 범위를 열어둘 것
- 워크로드 클래스로 NodePool을 분리하고
taint로 의도치 않은 혼재를 막을 것 - 스팟은 “중단 내성(PDB/드레인)”이 설계된 워크로드에만 적용할 것
- 통합은 비용 최적화의 핵심이지만, PDB/어피니티/requests가 발목을 잡는지 먼저 점검할 것
Pending디버깅은kubectl describe pod이벤트에서 시작하고, Karpenter 로그로 후보 탈락 이유를 확인할 것- 노드가 늘어도 네트워크가 버티는지(conntrack, 엔드포인트, 라우팅) 함께 볼 것
위 원칙대로 구성하면 Karpenter는 “그냥 오토스케일러”가 아니라, EKS 운영의 비용과 안정성을 동시에 끌어올리는 핵심 레이어가 됩니다.