CKA — 쿠버네티스 관리자 · 서비스와 네트워킹 · 이론
서비스는 프록시가 아니라 약속이다
한 줄 요약
Service 오브젝트는 트래픽을 나르지 않습니다. 셀렉터로 백엔드 목록(EndpointSlice)을 만들어 두는 선언일 뿐이고, 실제 패킷을 돌리는 것은 노드의 데이터플레인(kube-proxy 의 iptables/nftables 또는 CNI 의 eBPF)입니다. 이 분리를 알면 "Service 는 있는데 연결이 안 된다" 를 세 층으로 나눠 볼 수 있습니다.
왜 이게 필요했나
파드는 죽고 다시 뜨면서 IP 가 바뀝니다. 그래서 이름과 안정적인 가상 IP 가 필요했습니다. Service 는 이 두 가지를 줍니다.
문제는 누가 백엔드 목록을 유지하느냐입니다. Service 에 셀렉터를 적으면 엔드포인트 컨트롤러가 그 라벨에 맞고 Ready 인 파드를 찾아 EndpointSlice 를 채웁니다. 셀렉터가 한 글자라도 어긋나면 EndpointSlice 는 비어 있는 채로 정상 생성됩니다. 에러가 나지 않습니다. Service 도, Deployment 도, 파드도 전부 Ready 인데 연결만 안 됩니다.
그래서 서비스 장애 진단의 첫 질문은 언제나 같습니다. "엔드포인트에 IP 가 있는가?" 있으면 데이터플레인 문제이고, 없으면 셀렉터나 파드 Ready 문제입니다.
어떻게 동작하나
| 타입 | 하는 일 | 쓰는 곳 |
| --- | --- | --- |
| ClusterIP | 클러스터 내부 가상 IP 하나 | 기본값 |
| NodePort | 모든 노드의 30000~32767 포트를 연다 | 외부에서 임시 접근 |
| LoadBalancer | NodePort + 외부 LB 프로비저닝 | 클라우드 / MetalLB |
| ExternalName | IP 없이 CNAME 만 반환 | 클러스터 밖 시스템 가리기 |
| headless (clusterIP None) | VIP 없이 파드 IP 들을 직접 반환 | StatefulSet, 클라이언트 측 LB |
포트가 둘 이상이면 각 포트에 name 이 필수입니다. Ingress 나 다른 오브젝트가 포트를 이름으로 참조할 수 있게 하기 위해서입니다.
NetworkPolicy 에서 가장 많이 틀리는 곳은 from 배열의 구조입니다.
from항목이 두 개이고 각각 namespaceSelector, podSelector → OR. 둘 중 하나만 맞아도 허용.from항목이 하나인데 그 안에 둘 다 → AND. 그 네임스페이스의 그 라벨 파드만 허용.
YAML 에서 대시(-) 하나 차이인데 의미가 정반대입니다. 시험에서도 실무에서도 여기서 갈립니다.
현장에서 만나는 모습
사례 1 — kube-proxy 가 아예 없는 클러스터. 홈랩을 kubeadm 1.34 로 재구축하면서 --skip-phases=addon/kube-proxy 로 kube-proxy 를 처음부터 설치하지 않고, Cilium 1.20.1 을 kubeProxyReplacement=true 로 올렸습니다. 주장만으로는 부족해 실제로 세어 봤습니다.
kube-proxy 파드 수: 0노드의 iptables KUBE- 체인 개수: 0kube-proxy 를 쓰는 클러스터라면 KUBE-SERVICES, KUBE-SVC-*, KUBE-SEP-* 체인이 서비스 수에 비례해 수십에서 수백 개 존재합니다. 0개였습니다. 그러면 서비스는 어떻게 도나. eBPF 맵을 덤프하면 이렇게 나옵니다.
SERVICE ADDRESS BACKEND ADDRESS (REVNAT_ID) (SLOT)10.96.0.10:53/TCP (1) 10.244.0.150:53/TCP (4) (1)10.96.0.1:443/TCP (1) 10.0.0.120:6443/TCP (1) (1)커널 안의 해시맵에 프론트엔드와 백엔드가 직접 들어 있습니다. iptables 방식은 서비스가 늘수록 규칙 체인이 선형으로 길어지지만, eBPF 는 해시맵 조회라 서비스 수와 무관하게 일정합니다. Service 오브젝트는 그대로인데 데이터플레인만 갈아 끼운 것 — 이 분리가 실물로 확인되는 순간입니다.
여기에 부트스트랩 문제가 하나 딸려 옵니다. kubeProxyReplacement 를 켤 때 k8sServiceHost=10.0.0.120, k8sServicePort=6443 을 함께 줘야 합니다. kube-proxy 가 없으면 kubernetes 서비스의 ClusterIP(10.96.0.1)를 실제 apiserver 주소로 바꿔 줄 주체가 없는데, 그 역할을 할 Cilium 자신이 아직 뜨기 전이기 때문입니다. 닭과 달걀이라 실제 주소를 직접 알려 줘야 합니다.
사례 2 — L7 은 iptables 로 못 한다. 같은 클러스터에서 GET 은 200, POST 는 403 으로 가르는 정책을 걸어 봤습니다. 같은 IP, 같은 포트인데 HTTP 메서드로 갈립니다. iptables 는 L3/L4 라서 메서드가 보이지 않으므로 원리적으로 불가능합니다. 표준 NetworkPolicy 도 마찬가지로 L3/L4 까지입니다. CKA 범위의 NetworkPolicy 가 어디까지 할 수 있는지 선을 긋는 데 좋은 예입니다.
사례 3 — Endpoints 는 이제 옛말이다. v1.33 에서 Endpoints API 가 deprecated 되었습니다. 코드나 스크립트가 Endpoints 를 직접 읽고 있다면 kubernetes.io/service-name 라벨로 EndpointSlice 를 조회하도록 바꿔야 합니다. 업그레이드 전 점검 항목의 단골입니다.
다음 실습에서 할 것
첫 실습에서 ClusterIP, NodePort, headless, multi-port, ExternalName, Ingress 를 만들고, 셀렉터 오타로 엔드포인트가 비는 상황을 직접 만들어 고칩니다. 두 번째 실습에서는 NetworkPolicy 를 기본 거부부터 시작해 여섯 가지 형태로 좁혀 갑니다. 이 실습 환경에는 정책을 집행할 CNI 가 없어 실제 트래픽 차단은 확인할 수 없고 오브젝트 스펙만 채점합니다. 대신 AND 와 OR 의 차이 같은 스펙 상의 함정을 집중적으로 다룹니다.