Karpenter 완벽 가이드 — EKS 노드 오토스케일링, 비용 절감, 스팟 전략

Karpenter 완벽 가이드 — EKS 노드 오토스케일링, 비용 절감, 스팟 전략

이 글의 핵심

Karpenter는 AWS가 개발한 Kubernetes 노드 오토스케일러로, 기존 Cluster Autoscaler가 노드 그룹(ASG) 단위에서만 작동한다는 한계를 극복합니다. Pending Pod의 요구사항을 직접 분석해 EC2 인스턴스를 맞춤 프로비저닝하고, 스팟+온디맨드 혼합·빈 노드 자동 청소로 비용을 30-70% 줄이는 것이 흔합니다. 이 글은 설치·NodePool/NodeClass·Disruption·프로덕션 운영까지 실전으로 정리합니다.

이 글의 핵심

Karpenter는 AWS가 2021년 공개하고 2023년 v1.0 GA, 2026년 현재 v1.1/1.2 기준으로 EKS의 사실상 표준 노드 오토스케일러입니다. Kubernetes SIG의 Cluster Autoscaler가 가진 다음 한계를 해결합니다.

  • ASG 단위로만 스케일 → 인스턴스 타입 고정
  • Pending Pod의 실제 요구사항을 세밀히 반영하지 못함
  • 프로비저닝 지연 1-3분
  • 스팟+온디맨드 혼합 전략이 번거로움
  • 빈 노드 정리가 느림

Karpenter는:

  • ASG 없이 EC2 Fleet API 직접 호출
  • Pending Pod의 CPU·RAM·GPU·아키텍처 요구를 정확히 반영해 인스턴스 선택
  • 40초 이내 프로비저닝
  • Consolidation으로 빈·저활용 노드를 자동 청소해 비용 절감
  • 스팟 + 온디맨드 + CPU 아키텍처(amd64/arm64) 혼합을 NodePool 레벨에서 표현

이 글은 설치·설정·운영·비용 최적화를 실전 순서대로 다룹니다.

아키텍처

┌──────────────────────────────────────────────────────┐
│ Kubernetes API Server                                │
│                                                       │
│   Pending Pod ── watch ──► Karpenter Controller      │
│                                      │                │
│                                      ▼                │
│                          NodePool / EC2NodeClass     │
│                                      │                │
└──────────────────────────────────────┼────────────────┘


                            AWS EC2 Fleet API


                   ┌─────────────────────────────────┐
                   │ EC2 인스턴스 (스팟/온디맨드)    │
                   │ kubelet → Kubernetes 자동 가입  │
                   └─────────────────────────────────┘

설치

전제 조건

  • EKS 클러스터 (관리 노드 그룹 최소 1개)
  • IAM OIDC provider 연결
  • aws-load-balancer-controller·cert-manager·metrics-server 등 표준 애드온

Helm 설치 (v1.x)

export CLUSTER_NAME=prod-eks
export AWS_REGION=ap-northeast-2
export KARPENTER_NAMESPACE=kube-system
export KARPENTER_VERSION=1.1.0

# IAM 역할·SQS 큐·EventBridge 규칙 생성 (공식 CloudFormation)
curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml > karpenter.yaml

aws cloudformation deploy \
  --stack-name Karpenter-${CLUSTER_NAME} \
  --template-file karpenter.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"

# Karpenter 컨트롤러 설치
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
  --version ${KARPENTER_VERSION} \
  --namespace ${KARPENTER_NAMESPACE} --create-namespace \
  --set "settings.clusterName=${CLUSTER_NAME}" \
  --set "settings.interruptionQueue=${CLUSTER_NAME}" \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --wait

Karpenter 컨트롤러는 2 replicas로 HA 구성을 권장합니다.

NodeClass와 NodePool

v1부터 두 리소스로 분리되었습니다.

  • EC2NodeClass: “어떤 AWS 리소스로 노드를 만들까” — AMI·서브넷·보안그룹·IAM·디스크·UserData
  • NodePool: “어떤 Pod를 어떤 요구사항의 노드에 배치할까” — taint·requirements·limit·Disruption

EC2NodeClass

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023
  amiSelectorTerms:
    - alias: al2023@latest

  role: "KarpenterNodeRole-prod-eks"

  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "prod-eks"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "prod-eks"

  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 100Gi
        volumeType: gp3
        iops: 3000
        encrypted: true
        deleteOnTermination: true

  tags:
    Environment: production
    ManagedBy: karpenter

NodePool (기본 워크로드)

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default

      requirements:
        - key: "karpenter.sh/capacity-type"
          operator: In
          values: ["spot", "on-demand"]
        - key: "kubernetes.io/arch"
          operator: In
          values: ["amd64", "arm64"]
        - key: "karpenter.k8s.aws/instance-category"
          operator: In
          values: ["c", "m", "r"]
        - key: "karpenter.k8s.aws/instance-generation"
          operator: Gt
          values: ["5"]

      expireAfter: 720h   # 30일마다 롤링 교체 (보안·AMI 갱신)

  limits:
    cpu: "2000"
    memory: "4000Gi"

  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 30s

  weight: 10
  • capacity-type: 스팟 먼저 시도, 실패 시 온디맨드 fallback
  • instance-category: c(compute), m(general), r(memory) 세대 이상
  • consolidationPolicy: 빈·저활용 노드 자동 통합
  • weight: 여러 NodePool 간 선호도

상태 저장 워크로드용 NodePool

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: stateful
spec:
  template:
    metadata:
      labels:
        workload: stateful
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default

      taints:
        - key: workload
          value: stateful
          effect: NoSchedule

      requirements:
        - key: "karpenter.sh/capacity-type"
          operator: In
          values: ["on-demand"]    # 온디맨드만
        - key: "node.kubernetes.io/instance-type"
          operator: In
          values: ["m6i.xlarge", "m6i.2xlarge", "m6i.4xlarge"]

  limits:
    cpu: "200"

  disruption:
    consolidationPolicy: WhenEmpty   # 저활용으로는 통합 안 함 (state 이동 위험)
    consolidateAfter: 1h

  weight: 20

Postgres·Redis 같은 stateful 워크로드는 taints + toleration으로 분리하고 온디맨드만 사용해 안정성을 확보합니다.

Pod에서 활용

# 무상태 웹 (스팟 허용)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 10
  template:
    spec:
      containers:
        - name: app
          resources:
            requests: {cpu: 500m, memory: 512Mi}
            limits:   {cpu: 1,    memory: 1Gi}
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: {app: web}

PodDisruptionBudget 필수:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: web}
spec:
  minAvailable: 80%
  selector:
    matchLabels: {app: web}

Disruption: 비용 절감의 핵심

Karpenter의 Consolidation은 “같은 워크로드를 더 적은/저렴한 노드로 재배치할 수 있다면 그렇게 한다”는 원리로 동작합니다.

disruption:
  consolidationPolicy: WhenEmptyOrUnderutilized
  consolidateAfter: 30s
  budgets:
    - nodes: "20%"        # 동시에 최대 20%까지 Disrupt
    - nodes: "0"          # 업무 시간엔 Disrupt 금지
      schedule: "0 9 * * mon-fri"
      duration: 8h

Consolidation 유형:

  1. Empty: 비어있는 노드 제거
  2. Replace: 여러 노드의 Pod를 더 큰 단일 노드로 통합
  3. Delete: 크기가 과한 노드를 더 작은 노드로 교체

드리프트

NodeClass나 NodePool 스펙이 변경되면 기존 노드는 “drift”로 표시되고 점진적으로 새 스펙에 맞게 교체됩니다.

스팟 운영 베스트 프랙티스

  1. 다양한 인스턴스 타입 허용: 5종 이상 지정해 interruption 위험 분산
  2. 여러 AZ 허용: 하나의 AZ 스팟 풀 고갈 대비
  3. PDB 필수: 동시에 많은 노드가 사라지는 것 방지
  4. graceful termination: Pod terminationGracePeriodSeconds: 30-120
  5. stateful/critical 은 분리 NodePool: 온디맨드만 사용
  6. interruption handler: Karpenter 자체가 처리하지만 애플리케이션도 SIGTERM 대응
containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 20"]  # drain 동안 연결 처리

비용 최적화 체크리스트

  • NodePool에 다양한 인스턴스 타입(c/m/r + 여러 세대) 허용
  • arm64(Graviton) 지원하는 워크로드는 활성화해 30% 가격 이점
  • consolidationPolicy: WhenEmptyOrUnderutilized + consolidateAfter: 30s
  • 주말·야간에 더 공격적인 consolidation 스케줄
  • 무상태 워크로드는 스팟 우선
  • Reserved Instance / Savings Plans와 혼합하여 “기본 부하는 RI, 스케일업은 스팟”
  • expireAfter로 주기적 롤링 (AMI 갱신, 보안 패치, drift 청소)
  • limits로 예산 상한 설정 — 장애/버그로 Pod가 폭증해도 예산 초과 방지
  • CloudWatch 비용 경보 + Karpenter 노드 태그로 비용 대시보드

관측: 반드시 확인할 메트릭

Karpenter는 Prometheus 메트릭을 :8000/metrics로 노출합니다.

메트릭의미
karpenter_nodes_created분당 생성된 노드 수
karpenter_nodes_terminated분당 종료된 노드 수
karpenter_pods_startup_time_secondsPending→Running 시간 (p95 목표 60초 이하)
karpenter_disruption_actions_performed_totalDisruption 실행 횟수·종류
karpenter_cloudprovider_instance_type_price_estimate현재 인스턴스 타입별 예상 가격

Grafana 공식 대시보드가 제공되어 바로 import 가능합니다.

보안

  • Karpenter IAM 역할은 EC2 생성·삭제·태깅 권한만 최소화
  • iam:PassRole 대상 역할은 노드가 실제로 써야 하는 IAM instance profile
  • 노드 OS 이미지는 AL2023 최신 alias 사용 → 최신 보안 패치 자동 적용
  • 민감 워크로드는 전용 NodePool + taint로 네트워크·호스트 분리
  • securityGroupSelectorTerms로 워크로드별 SG 분리
  • Pod Security Standard restricted 적용

트러블슈팅

Pending Pod가 노드를 못 받음

kubectl describe pod <pod>           # Event 확인
kubectl logs -n kube-system deploy/karpenter -c controller --tail 200

일반 원인:

  • NodePool requirements에 Pod nodeSelector가 맞지 않음
  • limits(cpu/memory) 도달
  • 서브넷/SG selector가 리소스를 못 찾음
  • Pod의 nodeAffinity가 너무 엄격

새 노드가 Ready 상태로 못 넘어감

  • IAM 권한 (특히 ec2:RunInstances, ec2:CreateTags)
  • aws-auth ConfigMap에 Karpenter 노드 역할 매핑 필수
  • Bottlerocket/AL2023에서 UserData 충돌

스팟 interruption이 자주 발생

  • 인스턴스 타입을 더 다양화 (5종 이상)
  • 여러 AZ 허용
  • AWS Spot Advisor로 interruption 빈도가 낮은 타입 우선 선택

Consolidation이 발생하지 않음

  • 모든 Pod에 PDB가 있고 maxUnavailable: 0으로 설정되면 차단됨
  • do-not-disrupt 어노테이션이 붙은 Pod는 보호됨 (고의/실수 확인)

실전 시나리오

시나리오 1: CI 빌드 버스트

  • 아침 9시 일제히 빌드 Pending → Karpenter가 40초 내 20+ 스팟 노드 생성
  • 빌드 완료 후 30초 뒤 consolidation으로 모두 종료
  • 월 CI 비용 50% 이상 절감 사례 흔함

시나리오 2: 프로모션 트래픽

  • HPA가 replicas를 10→200으로 증가
  • Karpenter가 필요 용량 계산해 m6i.4xlarge 5대를 한 번에 fleet으로 생성
  • 기존 Cluster Autoscaler 대비 프로비저닝 시간 3분 → 40초

시나리오 3: ML 추론

  • GPU Pod Pending → nvidia.com/gpu: 1 요청 감지 → g5.xlarge 자동 선택
  • NVIDIA Device Plugin이 자동으로 GPU 리소스 등록

v1 vs v0.x 주요 차이

  • ProvisionerNodePool
  • AWSNodeTemplateEC2NodeClass
  • ttlSecondsAfterEmptydisruption.consolidateAfter
  • ttlSecondsUntilExpireddisruption.expireAfterspec.template.spec.expireAfter

v0.x 사용 중이라면 공식 마이그레이션 가이드를 따라 v1.x로 업그레이드하세요.

마무리

Karpenter는 EKS의 비용·속도·유연성을 동시에 개선하는 도구로, 도입 후 첫 달 안에 노드 비용 30% 이상 절감이 흔한 결과입니다. Cluster Autoscaler의 ASG 종속성에서 벗어나 “Pod 요구사항에 가장 잘 맞는 인스턴스를 필요한 만큼만 띄운다”는 원칙이 쿠버네티스 스케일링의 미래 방향을 보여줍니다. 프로덕션 도입 전 스테이징에서 NodePool·스팟 정책·PDB를 검증하고, 단계적으로 CA를 대체해 나가세요.

관련 글

  • Kubernetes 완벽 가이드
  • AWS EKS 완벽 가이드
  • AWS 스팟 인스턴스 운영 가이드
  • Kubernetes HPA/VPA 오토스케일링 가이드