세션 데모 3개를 실제 클러스터에서 구축하고 검증한 기록입니다. 모든 수치는 2026-10-15에 실측한 값이고, 데모 2는 시나리오별 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 호스트가 있을 때만
| 구성 요소 | 고정한 버전 | 비고 |
|---|---|---|
| Kubernetes | 1.36 | EKS 지원 목록에서 확인 |
| Cilium (AWS 빌드) | v1.18.13-1 | VTEP 최소 1.18.8-1 충족. vtep.enabled=true, l7Proxy=false 필수 |
| Hybrid Nodes gateway chart | 1.0.2 | namespace eks-hybrid-nodes-gateway |
| Karpenter | 1.15.0 | chart values에 podDeletionCostManagement 키 확인 |
| Kyverno chart | 3.9.1 | EKS Workshop 정책을 기반으로 함 |
| nodeadm | v1.0.20 | 호스트에서 sha256 검증 후 설치 |
| NVIDIA device plugin | v0.17.1 | AWS 블로그와 같은 버전 |
| vLLM, 모델 | v0.19.1, Qwen2.5-0.5B-Instruct | Apache-2.0 모델이라 토큰이 필요 없음 |
보안 기준은 구축 중 계속 확인했습니다. 보안 그룹의 인터넷 개방 ingress가 없고, 인스턴스에 공인 IP가 없고, 로드 밸런서를 만들지 않았습니다. 시크릿(SSM 활성화 코드)은 git에서 제외한 파일에만 저장했습니다.
모든 단계는 plan으로 바뀔 내용을 먼저 출력하고, 확인한 뒤 apply를 실행합니다.
| 단계 | 하는 일 | 걸린 시간 |
|---|---|---|
| 01-network | CloudFormation으로 VPC 2개, TGW, NAT, EICE, 호스트 2대 | 약 6분 |
| 01-cluster | eksctl로 EKS와 remoteNetworkConfig(노드와 Pod CIDR, SSM), SSM 활성화, 호스트에 nodeadm install과 init | 클러스터 약 15분, 조인 약 3분 |
| 02-cilium | hybrid 노드에만 Cilium 설치, 노드가 NotReady에서 Ready로 | 약 2분 |
| demo1-gateway | gateway 노드 그룹, Pod Identity 권한, SNAT 제외 설정, chart 설치 | 약 5분 |
| 03-karpenter, kyverno | Karpenter와 burst NodePool, Kyverno와 정책 | 약 5분 |
aws-node에 AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.17.0.0/16을 설정하지 않으면 클라우드 Pod에서 hybrid Pod로 가는 ClusterIP 트래픽이 실패합니다. Pod IP로 직접 호출하는 것은 이 설정 없이도 동작합니다.
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가 그대로 동작했습니다.
같은 워크로드를 세 가지 방식으로 줄여서 어떤 Pod가 먼저 사라지는지 숫자로 비교합니다. hybrid 노드 2대에 Pod 8개가 들어가도록 Pod당 CPU 요청을 930m로 계산해서 정했고, 방식은 다음과 같습니다.
controller.kubernetes.io/pod-deletion-cost: "1"을 붙임podDeletionCostManagement를 켬각 시나리오는 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 1 | 123초 |
| B Kyverno | 8 / 2 / 4대 | hybrid 3, cloud 0 | 118초 |
| C Kyverno와 Karpenter | 8 / 2 / 3대 | hybrid 3, cloud 0 | 87초 |
Pod/binding을 매치해서 노드의 eks.amazonaws.com/compute-type이 hybrid일 때만 값 1을 씁니다. 클라우드 Pod에는 쓰지 않습니다.Pod/binding을 처리 대상에서 빼므로 config.resourceFiltersExclude로 다시 켜야 합니다.nodes get 권한이 없으면 에러 없이 annotation이 붙지 않습니다. 이번 구축에서 시나리오 B가 A와 똑같이 나온 원인이었습니다.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가 처리합니다).
karpenter_pod_deletion_cost_pod_annotation_writes_total은 첫 쓰기 전에는 존재하지 않습니다.
발표 포인트: pod-deletion-cost는 best-effort여서 노드 장애나 축출에는 적용되지 않습니다. 슬라이드 블록 4의 "스케일-인 순서는 선호일 뿐"과 이어집니다.
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 plugin | hybrid GPU 노드에만 DaemonSet. 그 뒤 allocatable의 nvidia.com/gpu가 1 |
| 추론 Pod | hybrid와 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여야 합니다.
| 상황 | 원인과 처리 |
|---|---|
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 인스턴스 타입별 처리량입니다.
환경이 켜져 있는 동안 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 출력과 문법 검사까지만 했고 실제 삭제는 아직 실행하지 않았습니다.