kubernetes
클라우드 환경에서 워크로드를 운영할 때 가장 중요한 고려 요소 중 하나는 비용 최적화와 안정성 확보입니다. AWS EC2는 다양한 구매 옵션을 제공하는데, 그중 가장 대표적인 것이 Spot 인스턴스와 On-Demand 인스턴스입니다. Karpenter는 이러한 인스턴스 타입을 유연하게 조합할 수 있는 기능을 제공하여, Kubernetes 클러스터 운영자가 비용 절감과 서비스 안정성을 동시에 추구할 수 있도록 돕습니다.
Spot과 On-Demand 인스턴스는 단순히 가격 차이뿐 아니라, 운영 전략에서의 접근 방식도 크게 다릅니다.
구분 | Spot 인스턴스 | On-Demand 인스턴스 |
---|---|---|
비용 | 저렴 (최대 90% 절감) | 표준 가격 |
안정성 | 중간에 회수될 수 있음 (사전 알림 후 중단) | 안정적 (사용자가 종료하기 전까지 유지) |
활용 사례 | 배치, 비핵심 워크로드, 대규모 분산 연산 | 핵심 서비스, 장기 실행, 고객-facing 워크로드 |
Karpenter 설정 | karpenter.sh/capacity-type=spot | karpenter.sh/capacity-type=on-demand |
비핵심 워크로드는 Spot 인스턴스로 운영하여 비용을 줄이고, 고가용성이 필요한 핵심 서비스는 On-Demand 인스턴스로 운영하는 것이 일반적입니다. 또한 두 가지를 혼합해 사용하면, 비용과 안정성 사이에서 균형 잡힌 아키텍처를 설계할 수 있습니다.
아래 그림은 Karpenter에서 Spot과 On-Demand를 분리하여 관리하는 기본적인 구조를 단순화해 나타낸 것입니다.
┌───────────────────────────────┐
│ 공통 NodeClass (default) │
└─────────────┬─────────────────┘
│
┌──────────────┴───────────────┐
│ │
Spot NodePool On-Demand NodePool
(capacity-type=spot) (capacity-type=on-demand)
이렇게 분리된 구조를 기반으로, Pod 스케줄링 시 우선순위(affinity) 를 지정하거나 워크로드 특성별로 NodePool을 선택할 수 있습니다.
Karpenter를 활용해 Spot / On-Demand 노드를 분리하려면, 먼저 공통 NodeClass를 정의하고 이를 기반으로 NodePool을 나눠 설정해야 합니다. NodeClass는 네트워크·보안 등 AWS 리소스 공통 속성을 정의하는 곳이고, NodePool은 노드 특성 및 스케줄링 정책을 지정하는 곳이라고 이해하면 쉽습니다.
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 태그 기반으로 선택.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이라는 사실을 라벨로 구분.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초 후 불필요한 노드 종료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
실제 워크로드는 항상 특정 노드풀(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 활용 → 비용 최적화
라는 전략을 자연스럽게 반영할 수 있습니다.
설정 파일을 작성했다면, 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을 각각 적용하면, 클러스터는 두 가지 용량 타입을 동시에 사용할 준비가 됩니다.
Karpenter는 Pod가 배포될 때 필요한 노드가 없다면, 즉시 새로운 노드를 생성합니다. 이때 NodePool의 조건과 Pod의 affinity 설정에 따라 어떤 인스턴스가 뜰지가 결정됩니다. 이 과정을 검증하기 위해 세 가지 상황을 실험해 보면 이해가 확실해집니다.
Karpenter는 Spot만 있으면 Spot을 쓰게 됩니다.
Spot이 없어도 서비스는 중단되지 않고 On-Demand로 자동 대체(fallback) 됩니다.
Spot → On-Demand로 자연스럽게 fallback 되는 메커니즘이 실제로 동작하게 됩니다.
Karpenter는 Pod가 배포될 때 필요한 노드가 없으면, 미리 정의한 NodePool 정책에 따라 자동으로 노드를 생성합니다. Spot NodePool만 있으면 Spot을 사용하고, On-Demand만 있으면 On-Demand를 사용합니다. 두 NodePool이 모두 존재할 경우에는 Pod affinity 설정에 따라 Spot을 우선적으로 활용하고, 불가능할 때는 On-Demand로 자연스럽게 fallback 됩니다.
이 구조 덕분에 운영자는 인스턴스를 직접 관리하지 않아도 되고, 워크로드 특성에 따라 비용 절감(Spot)과 안정성 확보(On-Demand)를 동시에 달성할 수 있습니다.