왜 문제가 되는가

스팟 인스턴스는 비용을 크게 줄여주지만 클라우드 사업자가 언제든 회수할 수 있다. AWS는 회수 2분 전, GCP는 약 30초 전에 중단 신호를 준다. 이 짧은 시간 동안 파드를 안전하게 옮기지 못하면, 처리 중이던 요청이 끊기고 5xx가 사용자에게 그대로 노출된다. 특히 상태를 가진 워크로드나 긴 배치 작업은 중단 지점에서 데이터가 유실될 수 있다.

문제의 핵심은 두 가지다. 첫째, 노드가 사라지는 것을 미리 감지해야 한다. 둘째, 감지한 뒤 남은 2분 안에 트래픽을 빼고, 새 파드가 뜰 시간을 확보하며, 진행 중인 작업을 마무리해야 한다.

중단 신호를 감지하는 방법

각 인스턴스 내부의 메타데이터 엔드포인트를 폴링하면 중단 예정을 알 수 있다. 이 값이 나타나는 순간이 카운트다운의 시작이다.

#!/bin/bash
# AWS EC2 IMDSv2 기준 스팟 중단 감지
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 60")

while true; do
  CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/spot/instance-action)
  if [ "$CODE" = "200" ]; then
    echo "중단 예정 감지, 노드 드레인 시작"
    kubectl drain "$NODE_NAME" --ignore-daemonsets \
      --delete-emptydir-data --grace-period=90 --force
    break
  fi
  sleep 5
done

직접 스크립트를 돌리기보다 AWS Node Termination Handler나 Karpenter 같은 컨트롤러를 쓰면 이 폴링과 드레인을 대신 처리해준다. 실무에서는 데몬셋으로 배포된 핸들러를 기본으로 두는 편이 안정적이다.

파드가 우아하게 종료되도록 만들기

드레인만으로는 부족하다. 파드가 SIGTERM을 받고도 즉시 죽으면 인플라이트 요청이 끊긴다. preStop 훅으로 잠깐 대기해 엔드포인트에서 빠질 시간을 벌고, terminationGracePeriodSeconds로 실제 종료 마감을 넉넉히 준다.

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 90
      containers:
        - name: api
          lifecycle:
            preStop:
              exec:
                # 엔드포인트 제거가 전파될 때까지 대기 후 정리
                command: ["/bin/sh", "-c", "sleep 15"]
          readinessProbe:
            httpGet: { path: /healthz, port: 8080 }
            periodSeconds: 3

순서가 중요하다. SIGTERM과 엔드포인트 제거는 거의 동시에 일어나므로, 애플리케이션은 SIGTERM을 받자마자 /healthz를 실패로 바꿔 새 트래픽을 막고, 이미 받은 요청만 마무리한 뒤 종료해야 한다.

새 파드를 미리 띄워 빈틈 없애기

기존 파드가 빠지는 동안 대체 파드가 준비되지 않으면 순간적으로 가용 용량이 부족해진다. PodDisruptionBudget으로 동시 중단 수를 제한하고, 여러 노드·가용영역에 파드를 분산해 한 노드 회수가 전체에 영향을 주지 않게 한다.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 80%
  selector:
    matchLabels:
      app: api

온디맨드 혼합과 폴백 전략

스팟만으로 구성하면 대규모 회수 시 전체가 흔들린다. 온디맨드를 일정 비율 섞어 최소 용량을 보장하는 편이 안전하다.

구성비용회수 리스크적합 워크로드
스팟 100%가장 낮음높음재시도 가능한 배치
스팟+온디맨드 혼합중간중간일반 API 서비스
온디맨드 100%높음낮음결제·상태 저장 코어

노드 그룹을 여러 인스턴스 타입으로 다양화하면 특정 타입 회수가 몰려도 대체 확보가 쉬워진다.

주의점

2분은 생각보다 짧다. 이미지 풀에 1분이 걸리면 대체 파드가 뜨기도 전에 원본이 사라진다. 이미지를 미리 노드에 캐싱하고, 앱 부팅 시간을 줄여야 한다. 또 stateful 워크로드는 스팟에 올리기 전에 체크포인트나 큐 기반 재처리를 반드시 마련해야 한다. 마지막으로, 로컬 스크립트든 핸들러든 실제 회수 이벤트를 흘려보내는 카오스 테스트로 검증해야 한다. 감지·드레인·재기동 경로 중 하나라도 검증되지 않으면 실제 회수 때 그대로 장애로 이어진다.