- Published on
EKS Karpenter 스팟 중단 2분 전 무중단 운영
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
스팟 인스턴스는 비용 효율이 뛰어나지만, 예고된 중단(보통 2분)이라는 제약이 있습니다. EKS에서 Karpenter로 스팟 노드를 적극 활용하려면 **중단 예고 2분을 “자동 드레이닝 + 빠른 대체 노드 프로비저닝 + 애플리케이션의 그레이스풀 종료”**로 연결해, 사용자 관점에서 무중단에 가깝게 만드는 것이 핵심입니다.
이 글은 다음을 목표로 합니다.
- 스팟 중단 알림을 감지해 해당 노드를 cordon + drain
- 트래픽을 안전하게 빼기 위해 PDB, readiness, preStop, terminationGracePeriodSeconds를 정교화
- Karpenter가 대체 노드를 즉시 띄우고, 워크로드가 빠르게 재스케줄되도록 설계
- “2분” 안에 끝내기 위해 병목(이미지 풀, 디스크, 노드 부팅)을 줄이는 운영 팁까지 포함
관련해서 노드 디스크 이슈가 드레이닝을 더 어렵게 만드는 경우가 많습니다. 스팟 교체가 잦은 클러스터라면 특히 DiskPressure로 인한 Evicted 폭주를 먼저 잡아두는 것이 안정성에 크게 기여합니다: EKS DiskPressure로 Pod Evicted 폭주 해결 10가지
1) “2분 무중단”의 현실적인 정의
스팟 중단은 완전 무중단(0 drop)을 보장하기 어렵습니다. 하지만 아래 조건을 충족하면 대부분의 HTTP 트래픽에서 체감 무중단에 가깝게 만들 수 있습니다.
- 워크로드가 최소
replicas: 2이상이고, AZ 분산이 되어 있음 - Pod가 readiness 기반으로 LB/Service 엔드포인트에서 빠르게 제외됨
- 종료 시 graceful shutdown(연결 종료, 큐 flush, in-flight 요청 처리)이 동작
- PDB로 “동시에 너무 많이 내려가는” 상황을 제한
- 드레이닝이 시작되자마자 Karpenter가 대체 노드를 띄우고, 스케줄러가 빠르게 재배치
즉, “2분”은 드레이닝과 재스케줄을 마치는 제한 시간이며, 애플리케이션이 그 시간 안에 정상 종료할 수 있어야 합니다.
2) 구성요소 개요: Karpenter + NTH(또는 Karpenter 연동) + PDB
스팟 중단 대응은 크게 두 층으로 나뉩니다.
- 노드 레벨: 스팟 중단 알림 감지 → 해당 노드를 cordon/drain
- 파드/앱 레벨: readiness 제거 → preStop 실행 → SIGTERM 처리 → 종료
EKS에서 많이 쓰는 패턴은 다음 중 하나입니다.
- **AWS Node Termination Handler(NTH)**를 DaemonSet으로 설치해 중단 이벤트를 감지하고 drain 수행
- Karpenter 버전/구성에 따라 중단 이벤트를 기반으로 노드 교체를 자동화하되, 실무에서는 NTH를 함께 쓰는 경우가 여전히 많습니다(운영 편의성).
이 글에서는 “중단 2분 전 drain”을 확실하게 만들기 위해 NTH를 예시로 들고, Karpenter는 빠른 대체 노드 프로비저닝을 담당하는 구조로 설명합니다.
3) Karpenter: 스팟 우선 + 온디맨드 폴백 전략
스팟이 부족하거나 중단이 잦을 때도 서비스가 유지되려면 온디맨드 폴백이 필요합니다. 가장 흔한 전략은 “기본은 스팟, 부족하면 온디맨드”입니다.
아래는 Karpenter NodePool 예시(개념 예시)입니다. 실제 필드/버전은 사용 중인 Karpenter API에 맞춰 조정하세요.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-general
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: topology.kubernetes.io/zone
operator: In
values: ["ap-northeast-2a", "ap-northeast-2c"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: general
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 60s
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: on-demand-fallback
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: general
weight: 10
운영 포인트:
- 스팟
NodePool을 기본으로 두고, 온디맨드를weight(또는 우선순위 개념)로 낮게 두어 폴백되게 합니다. - AZ 분산을
requirements로 강제하면 “한 AZ에서만 스팟이 빠지는” 상황에도 전체 장애를 줄일 수 있습니다.
4) 중단 2분 전 감지 및 자동 드레이닝: Node Termination Handler
스팟 중단은 EC2 메타데이터/이벤트로 감지할 수 있고, NTH가 이를 받아 노드를 드레이닝합니다.
NTH에서 중요한 것은 다음입니다.
- 이벤트 감지 즉시 cordon
drain시--grace-period,--timeout을 2분 창에 맞게 설정ignore-daemonsets,delete-emptydir-data등 옵션을 워크로드 성격에 맞게 조정
Helm을 쓴다면 values에서 주요 옵션을 조절합니다(예시).
# values.yaml (예시)
enableSpotInterruptionDraining: true
enableRebalanceMonitoring: true
enableScheduledEventDraining: true
# 드레이닝 타임아웃을 2분 내로 관리
nodeTerminationHandler:
drainTimeout: 110s
podTerminationGracePeriod: 60
cordonOnly: false
taintNode: true
주의:
drainTimeout을 너무 짧게 잡으면 PDB나 느린 종료 때문에 파드가 남아 노드가 강제 종료될 수 있습니다.- 반대로 너무 길면 2분 창을 넘기기 쉽습니다. 보통
110s내외로 두고, 앱의 종료 시간을terminationGracePeriodSeconds로 관리하는 방식이 많이 쓰입니다.
5) 무중단의 핵심: PDB로 “동시 축출”을 제어
스팟 중단이 여러 노드에서 연달아 발생하거나, Karpenter가 통합(consolidation)을 수행하는 타이밍이 겹치면 파드가 한꺼번에 내려갈 수 있습니다. 이때 PDB가 안전장치가 됩니다.
예시: replicas: 4인 API 디플로이먼트에서 최소 3개는 항상 유지.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 3
selector:
matchLabels:
app: api
운영 팁:
minAvailable과maxUnavailable중 하나만 사용하세요.- PDB는 “자발적(eviction 기반) 중단”에 주로 적용됩니다. 즉, 드레이닝이 eviction으로 진행될 때 효과가 큽니다.
- 스팟 강제 종료처럼 비자발적 종료는 PDB로 막을 수 없으므로, 드레이닝이 2분 내에 끝나도록 앱 종료를 빠르게 만들어야 합니다.
6) readiness + preStop + SIGTERM: 트래픽을 먼저 빼고 종료하기
무중단 체감의 대부분은 “종료되는 파드가 트래픽을 더 이상 받지 않게” 만드는 데서 결정됩니다.
6.1 readinessProbe는 필수
LB/Service 엔드포인트에서 빠지려면 readiness가 정확해야 합니다.
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
timeoutSeconds: 1
failureThreshold: 2
- readiness 엔드포인트는 의존성(DB 등)이 죽었을 때도 “트래픽을 받으면 안 되는지”를 반영해야 합니다.
- 너무 느린 readiness는 드레이닝 시간을 잡아먹습니다.
6.2 preStop으로 “엔드포인트 제거 대기” 시간 확보
파드가 SIGTERM을 받으면 곧바로 종료될 수 있습니다. 하지만 LB가 엔드포인트를 제거하는 데 약간의 시간이 필요합니다. preStop으로 짧게 대기하거나, 애플리케이션에 “드레이닝 모드”를 넣을 수 있습니다.
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 60
sleep 10은 단순하지만 효과적입니다.- 더 좋은 방식은 앱이 SIGTERM 시 readiness를 즉시
false로 만들고, in-flight 요청을 처리한 뒤 종료하는 것입니다.
6.3 (예시) Node.js/Express 그레이스풀 종료
const http = require('http');
const express = require('express');
const app = express();
let shuttingDown = false;
app.get('/health/ready', (req, res) => {
if (shuttingDown) return res.status(503).send('draining');
res.status(200).send('ok');
});
app.get('/', (req, res) => {
setTimeout(() => res.send('hello'), 50);
});
const server = http.createServer(app);
server.listen(8080);
process.on('SIGTERM', () => {
shuttingDown = true; // readiness를 503으로 전환
// 새 연결은 거부, 기존 연결은 일정 시간 처리
server.close(() => process.exit(0));
// 안전장치: 너무 오래 끌면 강제 종료
setTimeout(() => process.exit(1), 25000).unref();
});
핵심은 SIGTERM에서 바로 process.exit 하지 않고, readiness를 먼저 내리고(server.close) 종료하는 것입니다.
7) 스팟 중단 2분을 지키는 성능 포인트
드레이닝이 늦어지는 주된 원인은 대부분 아래 중 하나입니다.
7.1 이미지 풀 시간
새 노드가 떠도 이미지 풀이 오래 걸리면 재스케줄이 늦습니다.
- 이미지 사이즈 줄이기(멀티스테이지 빌드)
- ECR 같은 리전 로컬 레지스트리 사용
- 자주 쓰는 베이스 이미지 레이어 캐시 전략
7.2 노드 부팅/조인 지연
- AMI 부팅 최적화(불필요한 부트스트랩 제거)
- CNI 초기화/ENI 할당 병목 점검
7.3 디스크/임시 스토리지 문제
스팟 교체가 잦으면 로그/캐시로 노드 디스크가 쉽게 차고, DiskPressure로 파드가 축출되면서 드레이닝이 꼬일 수 있습니다. 이 경우는 먼저 노드 디스크 운영을 안정화하세요.
- 임시 파일/로그 경로를
emptyDir크기 제한과 함께 관리 - 로그는 stdout로 내고 수집기로 넘기기
- 이미지 가비지 컬렉션/컨테이너 런타임 설정 점검
이 주제는 별도 글로 정리한 항목들이 많습니다: EKS DiskPressure로 Pod Evicted 폭주 해결 10가지
8) 스케줄링 안정성: topology spread + anti-affinity
스팟 중단이 “한 노드”에서만 일어나면 대개 무난하지만, 같은 AZ/같은 인스턴스 타입 풀에서 연쇄적으로 발생하면 위험합니다. 따라서 파드를 노드/존에 고르게 분산시키는 것이 중요합니다.
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: api
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api
podAntiAffinity로 동일 노드 집중을 완화topologySpreadConstraints로 AZ 분산을 유도
9) 관측과 테스트: “정말 2분 안에 되나” 확인하는 방법
운영에서 중요한 건 설정 자체보다 검증 루프입니다.
9.1 로그/이벤트 확인
- NTH 로그에서 interruption 감지 시각과 drain 완료 시각
- Kubernetes 이벤트에서 eviction 흐름
kubectl get events -A --sort-by=.lastTimestamp | tail -n 50
kubectl describe node `NODE_NAME`
kubectl logs -n kube-system ds/aws-node-termination-handler
9.2 카오스 테스트(권장)
- 특정 노드를 cordon/drain 해보고, 서비스 에러율과 p99 지연을 관측
terminationGracePeriodSeconds를 늘렸다 줄였다 하며 “2분 창”에 맞추기
kubectl cordon `NODE_NAME`
kubectl drain `NODE_NAME` --ignore-daemonsets --delete-emptydir-data --grace-period=60 --timeout=110s
이 테스트로 “readiness가 제대로 빠지는지”, “preStop이 과도하게 길지 않은지”, “PDB가 의도대로 동작하는지”를 한 번에 확인할 수 있습니다.
10) 자주 터지는 함정 6가지
replicas가 1인 워크로드
- 스팟 중단 시 무조건 순간 다운이 납니다. 최소 2 이상 + 분산이 필요합니다.
readiness가 “살아있음”만 체크
- 종료 중에도
200을 반환하면 트래픽이 계속 들어와 에러가 증가합니다.
- 종료 중에도
terminationGracePeriodSeconds가 너무 짧음- in-flight 요청이 끊기고, 큐/스트림 처리가 유실됩니다.
PDB를 너무 빡빡하게 설정
- 드레이닝이 막혀서 2분 내 퇴거가 실패할 수 있습니다. 워크로드별로 타협점을 찾으세요.
이미지가 너무 커서 재스케줄이 늦음
- 새 노드는 떴는데 파드가 “이미지 풀”에서 시간을 다 씁니다.
노드에 상태를 저장
- 로컬 디스크/메모리에 상태를 두면 스팟 교체 시 데이터 유실 또는 복구 지연이 커집니다.
마무리: “2분”을 설계 변수로 만들기
EKS에서 Karpenter로 스팟 비용 절감을 하면서도 무중단에 가깝게 운영하려면, 스팟 중단 2분을 단순한 알림이 아니라 명시적인 설계 변수로 다뤄야 합니다.
- NTH(또는 동등한 메커니즘)로 이벤트 감지 즉시 드레이닝
- PDB로 동시 축출을 통제
- readiness/preStop/SIGTERM 처리로 트래픽을 먼저 빼고 종료
- Karpenter의 스팟 우선 + 온디맨드 폴백으로 용량 공백 최소화
- 이미지/부팅/디스크 병목을 줄여 재스케줄 시간을 단축
이 조합을 갖추면, 스팟 중단이 일상인 환경에서도 장애가 아니라 “자동 복구되는 이벤트”로 바뀝니다.