VPC CNI 동작 원리: 데이터패스·IPAM·NetworkPolicy
개요
Amazon VPC CNI(amazon-vpc-cni-k8s)는 EKS의 기본 네트워크 플러그인입니다. Calico VXLAN이나 Cilium 오버레이 모드와 달리 캡슐화 없이 Pod에 VPC의 실제 IP 주소를 직접 할당하고, 노드 내부에서는 L3 라우팅만으로 트래픽을 전달합니다. 이 문서는 VPC CNI의 내부 동작을 세 축으로 나누어 설명합니다.
- 데이터패스 — Pod의 패킷이 veth pair와 라우팅 규칙을 거쳐 ENI로 나가는 경로
- IPAM — ipamd 데몬이 ENI와 IP 주소 풀(warm pool)을 관리하는 알고리즘
- NetworkPolicy — 컨트롤러와 노드 에이전트(eBPF)로 분리된 정책 적용 구조
트러블슈팅 절차(kubectl 명령 중심)는 EKS 네트워킹 디버깅에서 다루며, 이 문서는 그 절차가 왜 그렇게 구성되는지에 해당하는 동작 원리에 집중합니다.
배경: 두 개의 프로세스, 하나의 플러그인
VPC CNI는 단일 바이너리가 아니라 역할이 다른 두 컴포넌트로 구성됩니다.
| 컴포넌트 | 실행 형태 | 역할 |
|---|---|---|
CNI 플러그인 바이너리 (aws-cni) | kubelet이 Pod 생성/삭제 시마다 호출 | veth pair 생성, 라우팅 규칙 설정 등 네트워크 배선 |
ipamd (aws-node DaemonSet) | 노드당 상주 데몬 | ENI attach/detach, 보조 IP 풀 관 리, EC2 API 호출 |
CNI 바이너리는 Pod가 뜰 때 로컬 ipamd에 gRPC로 IP 할당을 요청하고, ipamd는 미리 확보해 둔 warm pool에서 즉시 IP를 반환합니다. EC2 API 호출(ENI 생성·IP 할당)은 Pod 생성 경로에서 분리되어 백그라운드에서 비동기로 수행됩니다. Pod 기동 지연이 EC2 API 지연에 좌우되지 않는 이유가 이 분리 구조입니다.
노드가 수용 가능한 Pod 수는 인스턴스 타입의 ENI 수와 ENI당 보조 IP 수로 결정됩니다. 예를 들어 ENI 4개 × ENI당 IP 15개인 인스턴스는 기본 모드에서 최대 4 × (15 - 1) + 2 = 58개의 Pod IP를 제공합니다(각 ENI의 첫 IP는 노드 자신이 사용).
아키텍처: L3 Routed Mode 데이터패스
VPC CNI는 노드 내부에 L2 브리지를 만들지 않습니다. Pod마다 veth pair를 만들고 정적 라우팅과 정책 라우팅(ip rule)만으로 패킷을 전달하는 L3 routed mode를 사용합니다.
Pod 내부: 더미 게이트웨이와 정적 ARP
Pod 네트워크 네임스페이스의 라우팅 테이블에는 링크로컬 주소 169.254.1.1을 기본 게이트웨이로 하는 경로가 설정됩니다.
# Pod 내부에서 확인한 라우팅 테이블
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0
# 정적 ARP 엔트리 (PERM 플래그)
? (169.254.1.1) at 2a:09:74:cd:c4:62 [ether] PERM on eth0
169.254.1.1은 실재하는 게이트웨이가 아닙니다. CNI 플러그인이 host 쪽 veth의 MAC 주소를 가리키는 정적 ARP 엔트 리를 미리 심어 두므로, Pod는 ARP 질의 없이 모든 아웃바운드 패킷을 veth pair 너머 호스트로 밀어냅니다. 이 설계의 결과로 다음이 성립합니다.
- Pod 간 통신에서 ARP 브로드캐스트가 발생하지 않음 — 모든 전달 결정은 호스트의 L3 라우팅에서 수행
- 같은 노드의 Pod 간 트래픽도 항상 호스트 라우팅 테이블을 경유
- L2 도메인이 없으므로 브리지 기반 CNI에서 발생하는 MAC 학습·플러딩 문제가 원천적으로 없음
호스트 쪽: veth 이름 규칙과 이중 라우팅
호스트 쪽 veth 인터페이스 이름은 eni 접두사(기본값, AWS_VPC_K8S_CNI_VETHPREFIX로 변경 가능) 뒤에 네트워크 이름·Pod 식별자·인터페이스 이름을 SHA-1 해시한 값의 앞 11자를 붙여 결정적으로 생성됩니다(networkutils.GeneratePodHostVethName). 즉 eni3a52ce78d95 같은 이름에서 Pod를 역추적하려면 해시 입력을 재계산하거나 ip addr 라우팅 엔트리와 대조합니다.
트래픽 방향에 따라 서로 다른 라우팅 테이블이 사용됩니다.
| 방향 | 사용 테이블 | 동작 |
|---|---|---|
| VPC → Pod (ingress) | main 테이블 | Pod IP/32 → host veth 호스트 라우트로 전달 |
| Pod → VPC (egress) | ENI별 테이블 | ip rule이 Pod IP를 소스 기준으로 매칭해 해당 IP가 속한 ENI의 라우팅 테이블로 보내고, 그 테이블의 기본 경로가 서브넷 게이트웨이를 가리킴 |
egress에 ENI별 테이블이 필요한 이유는 보조 ENI에 할당된 IP의 응답 패킷이 반드시 같은 ENI로 나가야 하기 때문입니다. VPC는 소스 IP와 ENI의 매핑을 검증하므로, primary ENI의 기본 경로로 내보내면 스푸핑으로 간주되어 폐기됩니다.
Deep Dive: IPAM — ipamd의 풀 관리 알고리즘
Warm Pool: 3개의 타깃 변수
ipamd는 Pod 생성 요청에 즉시 응답하기 위해 여유 IP를 미리 확보(warm pool)합니다. 풀 크기는 세 개의 절대치 타깃 변수 조합으로 결정됩니다.
| 변수 | 기본값 | 의미 |
|---|---|---|
WARM_ENI_TARGET | 1 | ENI 1개 분량의 전체 IP를 여유분으로 유지. WARM_IP_TARGET 설정 시 무시됨 |
WARM_IP_TARGET | 없음 | 여유 IP 개수를 직접 지정. WARM_ENI_TARGET을 override |
MINIMUM_IP_TARGET | 없음 | 노드가 항상 보유할 IP의 하한(floor). 기동 직후 다수 Pod 스케줄링 대비 pre-scaling 용도 |
WARM_ENI_TARGET=1(기본값)은 여유가 커 보이지만 의도된 설계입니다. ENI attach에는 최대 10초가 걸리므로, Pod 급증 시 ENI를 새로 붙이는 경로에 들어가면 그 노드의 Pod 기동이 일괄 지연됩니다. 반대로 WARM_IP_TARGET을 너무 작게 잡으면 Pod 생성·삭제(churn)마다 개별 IP를 EC2 API로 attach/detach하게 되어 API 호출이 급증하고, 스로틀링이 발생하면 해당 노드가 아니라 클러스터 전체의 ENI/IP 할당이 막힙니다. 공개 문서(eni-and-ip-target.md)가 대규모 클러스터·high churn 환경에서 WARM_IP_TARGET 사용을 자제하라고 명시하는 이유입니다.
MINIMUM_IP_TARGET은 WARM_IP_TARGET과 함께 쓰는 것이 안전합니다. MINIMUM_IP_TARGET만 설정하면 WARM_IP_TARGET이 0으로 간주되어, 하한을 채운 뒤 여유분이 전혀 확보되지 않는 상태가 될 수 있습니다.
Prefix Delegation: /28 단위 할당
ENABLE_PREFIX_DELEGATION=true(v1.9.0+)를 설정하면 ipamd는 개별 보조 IP 대신 /28 프리픽스(연속 IP 16개) 단위로 ENI에 주소를 할당합니다(IPv6는 /80). 도입 효과는 두 가지입니다.
- Pod 밀도 향상 — ENI당 슬롯 하나가 IP 1개가 아니라 16개로 확장됩니다. 예: c5.xlarge는 기본 모드 58 Pod → Prefix 모드에서 노드 최대치(110 Pod)까지 수용
- EC2 API 호출 감소 — IP 16개를 API 호출 1번으로 확보하므로 스케일링 시 API 부하가 크게 줄어듦
전제 조건이 있습니 다. /28은 연속된 16개 주소이므로 서브넷 단편화(fragmentation)가 심하면 프리픽스 확보에 실패할 수 있고, 이때 개별 IP 모드로 폴백하지 않고 에러가 됩니다. 신규 전용 서브넷 또는 CIDR 예약(subnet CIDR reservation)과 함께 사용하는 것이 안전합니다. Prefix 모드에서는 warm 타깃 계산도 프리픽스 단위로 바뀌며 WARM_PREFIX_TARGET(기본 1)이 추가로 관여합니다.
IP 쿨다운: 삭제된 Pod의 IP는 30초간 재사용 금지
Pod가 삭제되면 그 IP는 즉시 할당 가능 상태로 돌아가지 않고 쿨다운 상태를 거칩니다. 기본 쿨다운은 30초이며 IP_COOLDOWN_PERIOD(v1.15.0+)로 조정합니다.
쿨다운이 필요한 이유는 Kubernetes의 비동기성입니다. Pod 삭제 후에도 kube-proxy가 각 노드의 iptables/IPVS 규칙에서 해당 IP를 제거하기까지 시간이 걸립니다. 쿨다운 없이 IP를 새 Pod에 즉시 재할당하면, 아직 갱신되지 않은 규칙을 통해 이전 Service의 트래픽이 새 Pod로 유입될 수 있습니다. 값을 0으로 설정하는 것은 지원되지만 공식 문서가 강하게 비권장하며, 반대로 지나치게 크게 잡으면 가용 IP가 쿨다운에 묶여 EC2 API 호출이 늘어납니다. Pod churn이 큰 워크로드에서는 초당 Pod 삭제율 × 쿨다운 기간만큼의 IP가 상시 쿨다운 상태에 있다는 점을 warm pool 사이징에 반영해야 합니다.