kubernetes

Karpenter — Spot & On-Demand

클라우드 환경에서 워크로드를 운영할 때 가장 중요한 고려 요소 중 하나는 비용 최적화와 안정성 확보입니다. AWS EC2는 다양한 구매 옵션을 제공하는데, 그중 가장 대표적인 것이 Spot 인스턴스와 On-Demand 인스턴스입니다. Karpenter는 이러한 인스턴스 타입을 유연하게 조합할 수 있는 기능을 제공하여, Kubernetes 클러스터 운영자가 비용 절감과 서비스 안정성을 동시에 추구할 수 있도록 돕습니다.


1. Spot vs On-Demand 차이

Spot과 On-Demand 인스턴스는 단순히 가격 차이뿐 아니라, 운영 전략에서의 접근 방식도 크게 다릅니다.

구분Spot 인스턴스On-Demand 인스턴스
비용저렴 (최대 90% 절감)표준 가격
안정성중간에 회수될 수 있음 (사전 알림 후 중단)안정적 (사용자가 종료하기 전까지 유지)
활용 사례배치, 비핵심 워크로드, 대규모 분산 연산핵심 서비스, 장기 실행, 고객-facing 워크로드
Karpenter 설정karpenter.sh/capacity-type=spotkarpenter.sh/capacity-type=on-demand

비핵심 워크로드는 Spot 인스턴스로 운영하여 비용을 줄이고, 고가용성이 필요한 핵심 서비스는 On-Demand 인스턴스로 운영하는 것이 일반적입니다. 또한 두 가지를 혼합해 사용하면, 비용과 안정성 사이에서 균형 잡힌 아키텍처를 설계할 수 있습니다.


2. 구성도

아래 그림은 Karpenter에서 Spot과 On-Demand를 분리하여 관리하는 기본적인 구조를 단순화해 나타낸 것입니다.

        ┌───────────────────────────────┐
        │   공통 NodeClass (default)     │
        └─────────────┬─────────────────┘
                      │
       ┌──────────────┴───────────────┐
       │                              │
 Spot NodePool                  On-Demand NodePool
(capacity-type=spot)        (capacity-type=on-demand)
  • NodeClass: 공통적인 AWS 네트워크 및 보안 그룹 설정을 담고 있는 리소스입니다. 모든 NodePool은 이를 참조합니다.
  • Spot NodePool: capacity-type=spot 라벨을 통해 생성되는 풀로, 비용 절감이 주 목적입니다.
  • On-Demand NodePool: capacity-type=on-demand 라벨을 가진 풀로, 안정적이고 예측 가능한 리소스를 제공합니다.

이렇게 분리된 구조를 기반으로, Pod 스케줄링 시 우선순위(affinity) 를 지정하거나 워크로드 특성별로 NodePool을 선택할 수 있습니다.


3. YAML 예시

Karpenter를 활용해 Spot / On-Demand 노드를 분리하려면, 먼저 공통 NodeClass를 정의하고 이를 기반으로 NodePool을 나눠 설정해야 합니다. NodeClass는 네트워크·보안 등 AWS 리소스 공통 속성을 정의하는 곳이고, NodePool은 노드 특성 및 스케줄링 정책을 지정하는 곳이라고 이해하면 쉽습니다.

3.1 공통 NodeClass

NodeClass는 공통 인프라 속성을 모아둔 설계도 역할을 합니다.

apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2023
  role: "KarpenterNodeRole-<YOUR-CLUSTER-NAME>"  # 변경 필요
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "<YOUR-CLUSTER-NAME>"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "<YOUR-CLUSTER-NAME>"
  • amiFamily: AL2023 → Amazon Linux 2023 기반 AMI를 사용하도록 지정.
  • role → Karpenter가 EC2 인스턴스를 생성할 때 필요한 IAM Role을 지정. (클러스터마다 맞게 변경 필요)
  • subnetSelectorTerms, securityGroupSelectorTerms → 클러스터와 연동된 서브넷/보안그룹을 자동 탐색할 수 있도록 karpenter.sh/discovery 태그 기반으로 선택.

3.2 Spot 전용 NodePool

Spot NodePool은 비용 최적화 중심의 풀로, 안정성보다는 가용성과 저비용을 강조합니다.

apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: spot-pool
spec:
  template:
    metadata:
      labels:
        capacity: spot
    spec:
      nodeClassRef:
        name: default
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["t", "m", "c"]
  disruption:
    consolidationPolicy: WhenUnderutilized
    consolidateAfter: 30s
  • metadata.labels.capacity: spot → 해당 풀에 속한 노드들이 Spot이라는 사실을 라벨로 구분.
  • requirements → 스케줄링 조건:
    • karpenter.sh/capacity-type=spot → Spot 인스턴스만 사용
    • kubernetes.io/arch=amd64 → 아키텍처는 amd64만 허용
    • karpenter.k8s.aws/instance-category=t,m,c → T/M/C 계열 인스턴스만 선택
  • disruption.consolidationPolicy: WhenUnderutilized → 노드 리소스가 비효율적으로 사용될 때 통합(Consolidation)
  • consolidateAfter: 30s → 30초 후 불필요한 노드 종료

3.3 On-Demand 전용 NodePool

On-Demand NodePool은 안정성 확보가 주 목적이며, 중요한 워크로드가 여기서 실행됩니다.

apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: ondemand-pool
spec:
  template:
    metadata:
      labels:
        capacity: ondemand
    spec:
      nodeClassRef:
        name: default
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["t", "m", "c"]
  disruption:
    consolidationPolicy: WhenUnderutilized
    consolidateAfter: 30s
  • metadata.labels.capacity: ondemand → On-Demand 전용 풀임을 구분.
  • requirements → Spot과 달리 karpenter.sh/capacity-type=on-demand 조건을 가짐.
  • consolidationPolicy → 동일하게 Underutilized 상태일 경우 통합하여 비용을 최소화.

4. Pod에서 Spot 우선 사용하기

실제 워크로드는 항상 특정 노드풀(NodePool)을 강제 배정하기보다는, 조건부 우선순위를 두는 방식이 더 유연합니다. 예를 들어 “Spot 인스턴스를 먼저 사용하고, 만약 가용하지 않다면 On-Demand로 넘어가기” 같은 전략이 대표적입니다. 예시 Pod 스펙에서는 nodeAffinity를 활용하여 Spot 노드 선호(preferred) 정책을 설정했습니다.

apiVersion: v1
kind: Pod
metadata:
  name: spot-priority-pod
spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          preference:
            matchExpressions:
              - key: capacity
                operator: In
                values: ["spot"]
  containers:
    - name: nginx
      image: nginx
  • preferredDuringSchedulingIgnoredDuringExecution: : 선호(preferred) 조건으로, 반드시 매칭되어야 하는 강제 조건은 아닙니다. Spot이 가능하면 Spot을, 불가능하면 다른 풀(=On-Demand)로 스케줄링 됩니다.
  • weight: 100: Spot 노드 선호도를 최대로 높임.
  • matchExpressions: capacity=spot 라벨을 가진 노드에 우선 배치하도록 지정.

이 방식은 서비스의 안정성을 해치지 않으면서도, 가능한 한 Spot 활용 → 비용 최적화라는 전략을 자연스럽게 반영할 수 있습니다.


5. 적용 방법

설정 파일을 작성했다면, kubectl apply 명령어를 통해 차례대로 리소스를 배포합니다.

kubectl apply -f ec2nodeclass.yaml        # 공통 NodeClass 생성
kubectl apply -f spot-nodepool.yaml       # Spot NodePool 생성
kubectl apply -f ondemand-nodepool.yaml   # On-Demand NodePool 생성

먼저 EC2NodeClass를 생성하여 공통 설정을 등록합니다. 이후 Spot / On-Demand NodePool을 각각 적용하면, 클러스터는 두 가지 용량 타입을 동시에 사용할 준비가 됩니다.


6. 동작 확인

Karpenter는 Pod가 배포될 때 필요한 노드가 없다면, 즉시 새로운 노드를 생성합니다. 이때 NodePool의 조건과 Pod의 affinity 설정에 따라 어떤 인스턴스가 뜰지가 결정됩니다. 이 과정을 검증하기 위해 세 가지 상황을 실험해 보면 이해가 확실해집니다.

1. Spot NodePool만 존재하는 경우

Karpenter는 Spot만 있으면 Spot을 쓰게 됩니다.

  1. 클러스터에 현재 노드가 전혀 없다고 가정합니다.
  2. Pod를 배포하면, Karpenter는 “이 Pod를 실행하려면 노드가 필요하다”는 사실을 인식합니다.
  3. NodePool을 확인했을 때, Spot 전용 NodePool만 있으므로 자동으로 Spot 인스턴스가 생성됩니다.
  4. 생성된 Spot 노드에 Pod이 스케줄링됩니다.

2. Spot NodePool을 삭제하고, On-Demand NodePool만 남긴 경우

Spot이 없어도 서비스는 중단되지 않고 On-Demand로 자동 대체(fallback) 됩니다.

  1. Spot NodePool 리소스를 삭제합니다. (이제 Spot 풀은 없는 상태)
  2. 같은 Pod를 다시 배포합니다.
  3. 이번에는 Spot 풀을 찾을 수 없으므로, Karpenter는 On-Demand NodePool을 사용합니다.
  4. 결과적으로 On-Demand 인스턴스가 새로 뜨고, Pod이 거기에 배치됩니다.

3. Spot과 On-Demand NodePool이 동시에 존재하는 경우

Spot → On-Demand로 자연스럽게 fallback 되는 메커니즘이 실제로 동작하게 됩니다.

  1. 두 NodePool이 모두 활성화된 상태에서 Pod를 배포합니다.
  2. Pod spec 안에 affinity가 “Spot을 더 선호한다”라고 적혀 있으므로, Karpenter는 먼저 Spot 풀을 확인합니다.
  3. Spot 인스턴스 용량이 충분하면 → Spot 인스턴스가 생성되고 Pod이 거기에 배치됩니다.
  4. 만약 Spot 인스턴스를 확보할 수 없는 상황(회수되거나 리소스 부족 등)이면 → 자동으로 On-Demand 인스턴스를 사용하여 Pod을 실행합니다.

정리

Karpenter는 Pod가 배포될 때 필요한 노드가 없으면, 미리 정의한 NodePool 정책에 따라 자동으로 노드를 생성합니다. Spot NodePool만 있으면 Spot을 사용하고, On-Demand만 있으면 On-Demand를 사용합니다. 두 NodePool이 모두 존재할 경우에는 Pod affinity 설정에 따라 Spot을 우선적으로 활용하고, 불가능할 때는 On-Demand로 자연스럽게 fallback 됩니다.

이 구조 덕분에 운영자는 인스턴스를 직접 관리하지 않아도 되고, 워크로드 특성에 따라 비용 절감(Spot)과 안정성 확보(On-Demand)를 동시에 달성할 수 있습니다.


참고자료