IPが変わってもポリシーが揺れない理由
한국어 원문으로 표시합니다.
한 줄 요약
Cilium 은 파드의 라벨 집합에 숫자 하나(아이덴티티) 를 붙이고 그 숫자로 정책을 판정합니다. 파드가 재스케줄돼 IP 가 바뀌어도 라벨이 같으면 아이덴티티가 같으므로, 정책 맵은 손대지 않고 ipcache 한 곳만 갱신하면 됩니다.
왜 이게 필요했나
IP 기반 정책의 근본 문제는 쿠버네티스에서 IP 가 임시 값이라는 데 있습니다. 파드는 수시로 죽고 다시 뜨며 그때마다 새 주소를 받습니다. 정책을 IP 로 표현하면 파드가 하나 재시작할 때마다 모든 노드의 규칙을 다시 계산해야 하고, 그 사이의 짧은 창에서 트래픽이 잘못 허용되거나 잘못 차단됩니다.
Cilium 은 이 문제를 한 단계 위에서 풉니다. 정책은 "app=frontend 가 app=backend 의 8080 에 접근 가능"이라는 의미로 표현되고, 그 의미가 커널까지 숫자로 보존됩니다. iptables 시대에는 이 의미가 IP 규칙으로 번역되며 소실됐습니다.
어떻게 동작하나
파드가 뜨면 에이전트는 보안 관련 라벨만 추려 아이덴티티를 계산합니다. 여기서 어떤 라벨이 들어가고 어떤 라벨이 빠지는지가 중요합니다.
포함되는 라벨 (예) 제외되는 라벨 (예)
k8s:app=frontend k8s:pod-template-hash=7d4f9c
k8s:team=payments k8s:controller-revision-hash=...
k8s:io.kubernetes.pod. k8s:pod-template-generation=...
namespace=shop
pod-template-hash 류가 제외되는 이유는 명확합니다. 이 값은 디플로이먼트를 새로 배포할 때마다 바뀝니다. 아이덴티티에 포함시키면 롤아웃 한 번에 모든 파드가 새 아이덴티티를 받게 되고, 그러면 정책 맵을 전부 다시 계산해야 합니다. 라벨 기반 모델의 이점이 통째로 사라지는 셈입니다.
아이덴티티가 만들어지면 모든 노드의 cilium_ipcache 맵에 "IP → 아이덴티티" 매핑이 전파됩니다. 수신 측 엔드포인트의 정책 맵은 (소스 아이덴티티, 포트, 프로토콜, 방향) 을 키로 O(1) 판정합니다. 파드가 옮겨 다니면 ipcache 만 바뀌고 정책 맵은 그대로라는 것이 이 설계의 핵심 이득입니다.
kube-proxy 대체는 서비스 추상화를 맵 두 계층으로 구현한 것입니다. cilium_lb4_services_v2 에서 프론트엔드(IP, 포트)를 찾고, cilium_lb4_backends 에서 실제 파드 주소를 얻어 DNAT 합니다. 여기에 더해 소켓 로드밸런싱이라는 최적화가 있습니다. 파드 안 애플리케이션이 connect() 를 호출하는 그 순간, 커널의 소켓 훅에서 서비스 IP 를 백엔드 IP 로 바꿔 버립니다. 패킷이 아예 서비스 IP 로 나가지 않으므로 패킷 단위 NAT 도 conntrack 항목도 필요 없습니다.
kubeProxyReplacement 를 켤 때 k8sServiceHost 와 k8sServicePort 를 함께 줘야 하는 이유도 여기서 나옵니다. kube-proxy 가 없으면 kubernetes 서비스의 ClusterIP(보통 10.96.0.1)를 실제 API 서버 주소로 바꿔 줄 주체가 Cilium 자신인데, 부트스트랩 시점에는 그 Cilium 이 아직 뜨지 않았습니다. 닭과 달걀 문제를 피하려고 실제 주소를 직접 알려 주는 것입니다.
라우팅 모드 선택 기준은 단순합니다.
| 기준 | 터널(VXLAN/Geneve) | 네이티브 라우팅 |
|---|---|---|
| 네트워크 요구 | 노드 간 UDP 포트만 열리면 됨 | 언더레이가 PodCIDR 경로를 알아야 함 |
| 오버헤드 | 약 50바이트, MTU 축소 | 없음 |
| 적합 환경 | 언더레이를 통제할 수 없는 환경 | BGP 피어링 가능한 온프레미스, 클라우드 ENI |
부하 분산 알고리즘에서는 Maglev 일관 해싱을 알아 둬야 합니다. 백엔드가 추가되거나 빠질 때 기존 플로우 대부분이 같은 백엔드에 계속 매핑되도록 해시 테이블 재배치를 최소화합니다. 여러 노드가 ECMP 로 같은 VIP 를 받는 구성에서 노드마다 같은 선택을 하게 만드는 효과도 큽니다.
현장에서 만나는 모습
저자의 홈랩은 PodCIDR 10.244.0.0/16 위에 Cilium 1.20.1 을 올렸고, 데이터패스 상태는 이렇게 보고됩니다.
KubeProxyReplacement: True [enp2s0 10.0.0.117 (Direct Routing)]
Routing: Network: Tunnel [vxlan] Host: BPF
Masquerading: BPF [enp2s0] 10.244.2.0/24
Encryption: Wireguard [cilium_wg0 (Port: 51871, Peers: 2)]
Modules Health: Stopped(0) Degraded(0) OK(92)
읽는 법이 있습니다. Routing: Network Tunnel[vxlan] 은 노드 간에는 캡슐화를 쓴다는 뜻이고, Host: BPF 는 호스트 라우팅까지 eBPF 가 맡았다는 뜻입니다. Masquerading: BPF 는 SNAT 도 iptables 가 아니라 eBPF 로 처리된다는 뜻입니다. 즉 이 노드에는 kube-proxy 도, 마스커레이드용 iptables 규칙도 필요하지 않습니다. 실제로 KUBE- 체인은 0개였습니다.
k8sServiceHost 에는 컨트롤 플레인 노드의 실주소 10.0.0.120 이 들어갔습니다. 이 주소가 어떻게 정해졌는지도 교훈입니다. 원래 이 클러스터는 DHCP 임대로 .111 을 쓰다가 어느 날 .120 을 받아 죽었습니다. apiserver 인증서 SAN 에 .120 이 없었기 때문에 IP 만 되돌리는 것 외에는 복구가 어려웠고, 결국 static 으로 고정한 뒤 처음부터 다시 세웠습니다. 부트스트랩 주소는 나중에 바꾸기가 몹시 고통스러운 몇 안 되는 설정이라는 사실을 기억해 두면 좋습니다.
다음 실습에서 할 것
Cilium Helm values 파일을 직접 작성해 kube-proxy 대체와 데이터패스 옵션을 선언하고, 노드에 라벨을 붙여 워크로드 배치를 고정하고, 서비스와 디플로이먼트를 실제로 apply 해 백엔드가 잡히는지 확인한 뒤, kube-proxy 가 정말 없는지 검증하는 스크립트를 만듭니다.