EKS Hybrid Nodes 데모 구현 가이드

세션 데모 3개를 실제 클러스터에서 구축하고 검증한 기록입니다. 모든 수치는 2026-10-15에 실측한 값이고, 데모 2는 시나리오별 1회 측정입니다.

시뮬레이션 환경입니다 온프렘 서버와 VPN이 없어서, 두 번째 VPC를 Transit Gateway로 연결하고 그 안의 EC2를 온프렘 호스트로 썼습니다. EC2를 hybrid node로 쓰는 구성은 프로덕션에서 지원되지 않고, 노드가 join하면 hybrid node 요금이 부과됩니다. 발표에서는 "온프렘을 시뮬레이션한 환경"이라고 먼저 말씀하세요.

1. 어디서 확인하나

대상위치
데모 코드와 런북~/eks-hybrid-demo (GitHub Atom-oh/eks-hybrid-demo, private 저장소라 로그인이 필요합니다). 시작은 RUNBOOK.md
문서와 다르게 동작한 점같은 저장소의 KNOWN-ISSUES.md
데모 2 측정 결과 원본같은 저장소의 results/compare-*.md
살아 있는 클러스터클러스터 hybrid-demo, 리전 ap-northeast-2. aws eks update-kubeconfig --name hybrid-demo 후 kubectl get nodes
이 설명이 페이지(eks-hybrid-nodes/demo-guide.html). 슬라이드 발표자 노트의 데모 링크는 블록 2의 13번, 블록 4의 4번과 5번에 있습니다
# 사전 점검 (읽기 전용), 그다음 각 데모
./00-prereq/check.sh
./demo1-gateway/verify.sh && ./demo1-gateway/failover.sh
./demo2-burst/run-compare.sh && ./demo2-burst/verify.sh
./demo3-gpu/verify.sh      # GPU 호스트가 있을 때만

2. 환경과 버전

클라우드 VPC 10.0.0.0/16

  • EKS 1.36 컨트롤 플레인, system 노드 2대
  • gateway 노드 2대 (c6in.xlarge, 2개 AZ, source/dest check 해제)
  • Karpenter burst 노드 (m6i/m7i large, xlarge)
  • NAT 게이트웨이 1개, 인스턴스에 공인 IP 없음
Transit Gateway

시뮬레이션 온프렘 VPC 172.18.0.0/16

  • hybrid 노드 호스트 2대 (m7i.xlarge, Ubuntu 24.04)
  • EC2 Instance Connect Endpoint로만 SSH 접근
  • Pod CIDR 172.17.0.0/16 (Cilium cluster-pool)
  • 인터넷 egress는 TGW를 거쳐 클라우드 VPC NAT 사용
구성 요소고정한 버전비고
Kubernetes1.36EKS 지원 목록에서 확인
Cilium (AWS 빌드)v1.18.13-1VTEP 최소 1.18.8-1 충족. vtep.enabled=true, l7Proxy=false 필수
Hybrid Nodes gateway chart1.0.2namespace eks-hybrid-nodes-gateway
Karpenter1.15.0chart values에 podDeletionCostManagement 키 확인
Kyverno chart3.9.1EKS Workshop 정책을 기반으로 함
nodeadmv1.0.20호스트에서 sha256 검증 후 설치
NVIDIA device pluginv0.17.1AWS 블로그와 같은 버전
vLLM, 모델v0.19.1, Qwen2.5-0.5B-InstructApache-2.0 모델이라 토큰이 필요 없음

보안 기준은 구축 중 계속 확인했습니다. 보안 그룹의 인터넷 개방 ingress가 없고, 인스턴스에 공인 IP가 없고, 로드 밸런서를 만들지 않았습니다. 시크릿(SSM 활성화 코드)은 git에서 제외한 파일에만 저장했습니다.

3. 구축 순서

모든 단계는 plan으로 바뀔 내용을 먼저 출력하고, 확인한 뒤 apply를 실행합니다.

단계하는 일걸린 시간
01-networkCloudFormation으로 VPC 2개, TGW, NAT, EICE, 호스트 2대약 6분
01-clustereksctl로 EKS와 remoteNetworkConfig(노드와 Pod CIDR, SSM), SSM 활성화, 호스트에 nodeadm install과 init클러스터 약 15분, 조인 약 3분
02-ciliumhybrid 노드에만 Cilium 설치, 노드가 NotReady에서 Ready로약 2분
demo1-gatewaygateway 노드 그룹, Pod Identity 권한, SNAT 제외 설정, chart 설치약 5분
03-karpenter, kyvernoKarpenter와 burst NodePool, Kyverno와 정책약 5분
SNAT 제외 설정을 빠뜨리지 마세요 aws-node에 AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.17.0.0/16을 설정하지 않으면 클라우드 Pod에서 hybrid Pod로 가는 ClusterIP 트래픽이 실패합니다. Pod IP로 직접 호출하는 것은 이 설정 없이도 동작합니다.

4. 데모 1: gateway와 리더 전환

verify.sh가 확인하는 것: gateway Pod 2개 Running, Lease HOLDER, 리더의 :8088/healthz 200, VPC 라우트 테이블에서 172.17.0.0/16의 대상이 리더 노드 ENI인지, CiliumVTEPConfig 존재, 클라우드와 hybrid Pod의 양방향 통신과 ClusterIP 경로입니다.

failover.sh는 리더 Pod를 삭제하고 1초 간격 요청의 끊김 구간을 잽니다.

실행Lease 전환실패한 요청 (1초 간격)라우트
1회차3초60개 중 1개새 리더 ENI로 이동
2회차5초60개 중 1개새 리더 ENI로 이동
3회차 (최소 권한 IAM)5초45개 중 2개새 리더 ENI로 이동

Lease HOLDER 값은 Pod 이름이 아니라 노드 이름_uuid 형식입니다. 발표에서 kubectl get lease 출력을 보여 줄 때 알려 주세요. gateway의 IAM은 문서 예시(Resource: *)보다 좁혀서, 라우트 쓰기 권한(CreateRoute, ReplaceRoute)을 gateway의 route table로만 제한했고 failover가 그대로 동작했습니다.

5. 데모 2: bursting과 pod-deletion-cost

같은 워크로드를 세 가지 방식으로 줄여서 어떤 Pod가 먼저 사라지는지 숫자로 비교합니다. hybrid 노드 2대에 Pod 8개가 들어가도록 Pod당 CPU 요청을 930m로 계산해서 정했고, 방식은 다음과 같습니다.

각 시나리오는 3개에서 15개로 늘려 hybrid를 채우고 넘치는 7개를 burst 노드가 받게 한 뒤, 12개, 10개, 3개 순서로 줄입니다. 15개로 늘렸을 때 NodeClaim은 1~2초 안에 생성됐고, 15개가 모두 Ready가 되기까지 37~38초가 걸렸습니다.

시나리오10개일 때 (hybrid / cloud / EC2)최종 3개빈 burst 노드 정리
A 정책 없음6 / 4 / 3대hybrid 2, cloud 1123초
B Kyverno8 / 2 / 4대hybrid 3, cloud 0118초
C Kyverno와 Karpenter8 / 2 / 3대hybrid 3, cloud 087초
읽는 법 A는 기본 규칙이 Pod 위치를 보지 않아서 온프렘 Pod를 먼저 지웠습니다. B와 C는 온프렘 Pod를 모두 지켰습니다. C는 Karpenter가 burst 노드의 Pod에 음수 값(-2, -1)을 써서 EC2가 4대에서 3대로, 정리 시간이 118초에서 87초로 줄었지만, 시나리오당 1회 측정이라 차이는 작습니다. 숫자를 인용하기 전에 반복 측정하세요.

정책 구성 (Kyverno)

Karpenter 기능 켜기

helm upgrade karpenter oci://public.ecr.aws/karpenter/karpenter --version 1.15.0 \
  -n kube-system --reuse-values --set settings.featureGates.podDeletionCostManagement=true

이 값을 Helm으로 켤 때만 pods patch 권한이 함께 부여됩니다. 기능을 켜면 Karpenter가 이 annotation을 덮어쓰고 consolidation은 더 이상 읽지 않습니다. 끌 때는 기능을 끈 뒤 Karpenter가 쓴 음수 값을 지워야 합니다(karpenter-gate.sh off가 처리합니다).

시나리오 C에는 warm-up 단계가 필요합니다 Karpenter는 정리 가능한(덜 쓰이는) 노드에만 순위를 매기고, 갱신에 1~5분이 걸립니다. 노드가 꽉 찬 15개 상태에서 바로 줄이면 아직 순위가 없어서 C가 B와 똑같아집니다. 그래서 12개로 먼저 줄이고 순위가 쓰이길 기다린 뒤 10개, 3개로 줄였습니다. 지표 karpenter_pod_deletion_cost_pod_annotation_writes_total은 첫 쓰기 전에는 존재하지 않습니다.

발표 포인트: pod-deletion-cost는 best-effort여서 노드 장애나 축출에는 적용되지 않습니다. 슬라이드 블록 4의 "스케일-인 순서는 선호일 뿐"과 이어집니다.

6. 데모 3: hybrid GPU 추론

GPU 서버가 없어서 시뮬레이션 온프렘 VPC에 g6.xlarge(NVIDIA L4, 23GB) 한 대를 hybrid 노드로 붙여 한 번 검증하고 바로 삭제했습니다. 실제 GPU 서버에서는 드라이버와 container toolkit이 이미지에 들어 있다고 가정하는 부분을, 여기서는 스크립트(prepare-host.sh)가 설치했습니다.

단계내용
호스트 준비nvidia-driver-580-server-open과 nvidia-container-toolkit 설치, 재부팅 후 nvidia-smi로 L4 확인
조인kubelet 플래그로 taint nvidia.com/gpu=Exists:NoSchedule와 label node-role/gpu=true, nvidia-ctk runtime configure --runtime=containerd --set-as-default
device pluginhybrid GPU 노드에만 DaemonSet. 그 뒤 allocatable의 nvidia.com/gpu가 1
추론 Podhybrid와 GPU nodeSelector, GPU toleration, limits nvidia.com/gpu: 1, sidecar.istio.io/inject: "false", hostNetwork 사용 안 함, 모델 캐시는 hostPath
호출클라우드 노드의 Job이 inference.gpu-demo.svc를 호출하고, 응답 시간 약 1초(모델 로드 후)

verify.sh는 GPU allocatable, 추론 Pod에 istio-proxy가 없는지, hostNetwork가 꺼져 있는지, 클라우드 Pod의 호출 성공과 응답 시간을 확인하고 통과했습니다. 첫 시작은 느립니다. vLLM 이미지(약 10GB)를 받는 데 4분 28초가 걸렸으니 발표 전에 미리 받아 두세요. GPU가 1개라 Deployment는 strategy: Recreate여야 합니다.

7. 문서와 다르게 동작한 점

상황원인과 처리
nodeadm init이 403으로 실패eksctl이 만든 role에 eks:ListAccessEntries가 없습니다. 문서가 안내하는 --skip cluster-access-validation을 썼습니다.
Kyverno 정책이 적용되지 않음백그라운드 컨트롤러의 nodes get 권한 부족입니다. 값 파일에 추가했습니다.
Kyverno 정책 생성이 거부됨3.9.1에서는 spec.failurePolicy가 금지됩니다. spec.webhookConfiguration.failurePolicy를 씁니다. ClusterPolicy 자체는 deprecated입니다.
8개가 들어가야 할 hybrid 노드에 6개만 배치됨Pod의 유효 요청은 컨테이너 합과 가장 큰 init container 중 큰 값입니다. Cilium agent의 100m가 init container에 있어서 계산에서 빠졌습니다.
vLLM이 빈 모델 이름으로 종료env 예제의 빈 MODEL_ID=가 고정값을 덮어썼습니다. 빈 줄을 제거했습니다.
옵션 데모(Cilium + Istio L7)를 이 클러스터에서 재현할 수 없음gateway가 Cilium l7Proxy: false를 요구해서 L7 정책이 동작하지 않습니다. gateway 없는 별도 클러스터가 필요하고, 코드만 작성했습니다.

아직 측정하지 못한 항목은 consolidation이 hybrid 노드의 빈자리를 재배치 대상으로 보는지(Karpenter는 hybrid 노드를 관리하지 않으므로 보지 않아야 합니다), hostNetwork Pod의 gateway 경로, gateway 인스턴스 타입별 처리량입니다.

8. 비용과 정리

환경이 켜져 있는 동안 EKS 컨트롤 플레인, NAT, Transit Gateway attachment, EC2, hybrid node vCPU 요금이 계속 발생합니다. 리허설이 끝나면 다음 순서로 삭제합니다.

./teardown.sh plan
./teardown.sh apply all   # demo, karpenter, gateway, hybrid, cluster, network 순서

Karpenter 노드를 먼저 정리해서 고아 EC2가 남지 않게 하고, gateway가 만든 VPC 라우트(helm uninstall이 지우지 않습니다)와 SSM 등록을 직접 지우며, CloudFormation 스택이 실제로 사라질 때까지 확인합니다. 이 스크립트는 plan 출력과 문법 검사까지만 했고 실제 삭제는 아직 실행하지 않았습니다.