DevOps

Karpenter + Spot 조합을 알아보자 (1)

YoonJong 2026. 5. 13. 21:28
728x90
반응형

요약

- DEV 환경이니 월1~2회 지연이 발생해도 개발/테스트에는 큰 지장이 없다(협의는 꼭 필요), 그 대신 매달 $200~ 이상의 비용 절감 가능

- Karpenter의 빠른 프로비저닝과 자동 통합(consolidation), 그리고 Spot 인터럽트 대응을 활용하면 개발 환경 비용을 크게 줄이면서도 안정성을 유지가능하다.

 

 

현재 새롭게 DEV 인프라를 구성하고 있는 프로젝트는 총 5개의 도메인(member, order, point 등등)은 MSA로 이루어진 플랫폼이다.

수십개의 서비스가 동시에 떠있는 DEV 환경에서 항상 켜져 있어야 하는 인프라 비용은 꽤 부담(낭비)가 된다.

 

이러한 문제를 해결하기 위해 Karpenter + Spot 인스턴스 조합을 선택했고, 어떻게 설계했는지 정리해본다.

--

 

Spot 인스턴스는 AWS의 유휴 컴퓨팅 자원을 경매 방식으로 제공하는 인스턴스 유형으로 비용 절감에 최적화되어 있다.

On-Demand에 대비해 최대 90% 저렴하다. 

하지만 안정성은 AWS가 회수해갈 수 있기 때문에 불안정하며, 다행히(?)도 회수 2분전에 인터럽트 신호를 준다.

* 핵심은 2분전 통보인데, 이 신호를 받으면 pod를 안전하게 다른 노드로 이동시키면서 서비스 중단 없이 비용을 절감 할 수 있다.

--

 

Karpenter와 CA(Cluster Autosaler)의 큰 차이점은 시간과 인스턴스 선택이 있다.

특징 Cluster Autoscaler Karpenter
스케일링 단위 노드그룹 개별 노드
인스턴스 선택 사전 고정 동적 선택
프로비저닝 시간 3~5분 ~60초
Spot 다변화 제한적 여러 타입 동시 시도
빈 노드 정리 느림 consolidateAfter 설정에 따라

--

 

NodePoll 은 어떻게 설계했나 

  dev-cluster
  ├── infra-ng (On-Demand, m6g.large × 2)   ← EKS 관리형 노드그룹
  │   └── Karpenter, ESO, CoreDNS 등 인프라 컴포넌트
  │
  ├── service-spot NodePool                  ← Karpenter 관리
  │   └── 00개 서비스 Pod (Spot, arm64, 4코어)
  │
  ├── batch-spot NodePool                    ← Karpenter 관리
  │   └── Jenkins agent, Argo Workflows 배치 잡
  │
  └── neo4j-ondemand NodePool               ← Karpenter 관리
      └── Neo4j DB (On-Demand, m6g.xlarge 고정)

 

위 설계를 보면 On-Demand로 구분한 것도 있다.

이유는 Karpenter 자체가 Spot에서 실행되다가 회수되면 다른 Pod들을 이동시킬 주체가 없다.

Neo4j(DB)는 Spot 회수 시 데이터 정합성 문제가 생길 수 있다. 

--

# dev 환경 기준 - 노드 1개에 여러 Pod 수용
resources:
  requests:
    cpu: 250m
    memory: 512Mi
  limits:
    cpu: "1"
    memory: 2Gi
--
# eks.tf
  resource "kubernetes_manifest" "karpenter_nodepool_spot" {
    manifest = {
      spec = {
        weight = 100  # 최우선 사용
        template.spec = {
          requirements = [
            { key = "karpenter.sh/capacity-type", values = ["spot"] },
            { key = "kubernetes.io/arch",         values = ["arm64"] },  # Graviton
            { key = "karpenter.k8s.aws/instance-cpu", values = ["4"] },  # 4코어 고정
          ]
          expireAfter = "720h"  # 30일 후 노드 교체 (보안 패치)
        }
        disruption = {
          consolidationPolicy = "WhenEmptyOrUnderutilized"
          consolidateAfter    = "1m"  # 유휴 시 1분 후 삭제
        }
        limits.cpu = "20"  # 00서비스 × HPA 최대 0개 = 최대 ~00코어
      }
    }
  }

서비스 pod1개의 CPU Request가 250m 이므로, 4코어 노드에 약 15개의 pod가 고밀도로 패킹된다.

(4코어 노드 실제 할당(3.8코어) , 3.8 / 0.25 = 약 15개 pod 수용 가능)

 

이외 8,16코어로 하면 빈 자리가 많아져 낭비가 생긴다.

--

 

Spot 인스턴스가 회수 될때의 흐름은 어떻게 될까?

* 2분안에 처리 가능한 이유는 Karpenter의 노드 프로비저닝이 약 60초안에 완료되기 때문이다.

신호 수신 즉시 새 노드를 생성하므로 2분의 여유 시간 안에 pod 이동까지 완료할 수 있다.

 

1. AWS -> Spot 회수 2분전 인터럽트 신호 전송

2.Karpenter SQS Queue에서 신호 수신

3. 해당 노드의 pod들을 Drain(새 node로 이동 시작)

4. 새 Spot 인스턴스 프로비저닝 (~60초)

5. Pod 재스케줄링 완료

--

 

Spot 인스턴스여도 불안하다. pod를 최소 2개를 띄어놓을텐데, 이걸 다른 인스턴스에 놓을 수 없을까?

먼저 pod는 spot 인스턴스에 배치를 할것이다.

  # base-chart/values.yaml - 모든 서비스의 기본값
  nodeSelector:
    nodegroup: service  # ← service-spot NodePool 노드와 매칭

Karpenter NodePool의 노드는 생성 시, nodegroup: service 라벨이 붙어있고, 이 라벨로 pod와 노드가 연결된다.

또한 Pod Anti-Affinity를 통해 같은 서비스의 pod 2개가 서로 다른 노드에 분산 배치 되도록 한다.

  # base-chart/templates/rollout.yaml
  affinity:
    podAntiAffinity:
      # (권장)설정이므로, 노드가 부족한 경우 같은 노드에 배치 가능
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            topologyKey: kubernetes.io/hostname  # 서로 다른 노드로 분산

 

이를 통해서 Spot 노드 1개가 회수되더라도 같은 서비스의 나머지 Pod는 살아있어 무중단이 유지할 수 있다.

--

 

비용 효과로는 대략 아래와 같다.

개발 환경 기준 서비스 노드 100% Spot 인스턴스 전환 시

  • On-Demand m6g.xlarge (4코어): 약 $0.154/h
  • Spot m6g.xlarge 평균: 약 $0.046/h → 약 70% 절감

 

 

728x90
반응형