LabHub

CKAD — 쿠버네티스 애플리케이션 개발자 · 서비스와 네트워킹 · 이론

Service 는 IP 가 아니라 '이름'을 파는 물건이다

LabHub 에서 이어서 보기

한 줄 요약

파드 IP 는 재시작마다 바뀌는 휘발성 주소다. Service 는 그 위에 바뀌지 않는 이름과 가상 IP 를 얹고, 라벨 셀렉터로 뒤에 붙을 파드를 계속 갱신한다. 그래서 Service 의 본질은 로드밸런싱이 아니라 간접 주소(indirection) 다.

왜 이게 필요했나

파드는 언제든 죽고 다시 뜬다. 뜰 때마다 IP 가 달라진다. 프런트엔드가 백엔드 파드 IP 를 직접 알고 있으면 백엔드가 한 번 재시작할 때마다 프런트엔드를 고쳐야 한다. 스케일 아웃하면 IP 가 여러 개가 되고, 그 목록을 누가 관리할지도 문제다.

Service 는 이 문제를 "이름 한 겹"으로 푼다. 클라이언트는 backend 라는 이름만 알고, 그 이름이 실제로 어느 IP 들로 풀리는지는 쿠버네티스가 관리한다. 이름은 DNS 로 풀린다 — 같은 네임스페이스면 backend, 다른 네임스페이스면 backend.other-ns, 완전한 형태는 backend.other-ns.svc.cluster.local 이다.

간접 주소의 가치는 그것이 없을 때 가장 아프게 드러난다. 홈랩 클러스터를 3노드에서 7노드로 확장하면서 컨트롤 플레인을 3대로 만들었는데, kubeadm-config 를 열어 보니 이랬다.

controlPlaneEndpoint: 10.0.0.120:6443     # ← cp-1 의 물리 IP

VIP 도 DNS 도 아닌 첫 노드의 실제 IP 가 박혀 있었다. 그래서 cp-1 이 죽으면 etcd 쿼럼은 2/3 로 멀쩡하고 나머지 apiserver 프로세스도 살아 있는데, 워커 7대의 kubelet 과 모든 kubeconfig 가 그 주소만 알고 있어서 아무도 문을 못 찾는 상태가 됐다. 게다가 apiserver 인증서 SAN 에 다른 컨트롤 플레인 IP 가 없어 직접 붙어도 TLS 검증이 실패했다. Service 가 파드에게 해 주는 일이 정확히 이것이다 — 뒤에서 무엇이 바뀌든 클라이언트가 아는 이름은 그대로 두는 것.

어떻게 동작하나

Service 스펙에서 가장 많이 틀리는 것이 포트 세 개다.

| 필드 | 누가 보는가 | 값의 의미 |
| --- | --- | --- |
| port | 클라이언트 | Service 의 ClusterIP 에서 여는 포트 |
| targetPort | 파드 | 실제 컨테이너가 듣는 포트 (숫자 또는 포트 이름) |
| nodePort | 클러스터 밖 | 모든 노드에서 여는 포트 (30000~32767) |

targetPort 에 컨테이너 포트의 이름을 쓰면(targetPort: api), 컨테이너마다 실제 포트 번호가 달라도 Service 를 고치지 않아도 된다. 이게 named port 의 요점이다.

Service 타입은 위로 쌓인다. ClusterIP(클러스터 내부만) → NodePort(ClusterIP + 노드 포트) → LoadBalancer(NodePort + 외부 LB). 여기에 셀렉터 없는 특수형이 둘 있다. ExternalName 은 DNS CNAME 만 만들고, 헤드리스(clusterIP: None)는 가상 IP 를 만들지 않고 DNS 가 파드 IP 들을 그대로 돌려준다.

헤드리스는 StatefulSet 과 짝이다. StatefulSet 의 serviceName 에 헤드리스 Service 를 지정하면 각 파드가 고유한 DNS 이름을 갖는다.

<파드이름>.<서비스이름>.<네임스페이스>.svc.cluster.localcache-0.cache-hs.ckad-net.svc.cluster.local

DB 복제처럼 "0번이 primary" 같은 역할이 있는 워크로드는 로드밸런싱이 아니라 개별 주소가 필요하므로 이 조합을 쓴다.

Ingress 는 L7 이다. 하나의 진입점에서 호스트경로로 갈라 여러 Service 로 보낸다. pathType 이 중요한데 Prefix 는 경로 세그먼트 단위 접두어 일치(/api/api/users 도 잡음), Exact 는 완전 일치다. Ingress 오브젝트만으로는 아무 일도 일어나지 않고 Ingress 컨트롤러가 있어야 실제 라우팅이 된다.

NetworkPolicy 는 기본이 허용이다. 정책이 하나도 없으면 모든 파드가 서로 통신한다. 그런데 어떤 파드에 정책이 하나라도 걸리는 순간 그 파드는 화이트리스트 모드가 되어 명시된 것만 허용된다. policyTypesIngress 만 적으면 나가는 트래픽은 통제되지 않는다. 그리고 정책은 더해지기만 한다 — 여러 정책이 걸리면 합집합이 허용된다.

현장에서 만나는 모습

홈랩 클러스터를 kubeadm 1.34 + Cilium 1.20 으로 다시 세우면서 kube-proxy 를 아예 설치하지 않았다(--skip-phases=addon/kube-proxy). 그러고도 Service 가 동작하는지 실측으로 확인했다.

kube-proxy 파드 수: 0노드의 iptables KUBE- 체인 개수: 0

kube-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)

여기서 배울 것은 Service 라는 추상은 그대로인데 구현은 완전히 갈아 끼울 수 있다는 점이다. 매니페스트는 한 글자도 안 바뀌었고, 파드에서 서비스 이름으로 붙어 HTTP 200 을 받는 검증도 그대로 통과했다.

NetworkPolicy 쪽에서도 실측이 있었다. Cilium 의 L7 정책으로 같은 IP·같은 포트인데 HTTP 메서드로 트래픽을 갈랐다.

정책 적용 전:  GET / → 200,  POST / → 200정책 적용 후:  GET / → 200,  POST / → 403

표준 NetworkPolicy 는 L3/L4(파드 셀렉터, 포트)까지만 표현할 수 있고, 메서드나 경로 단위는 CNI 확장(CiliumNetworkPolicy)이나 서비스 메시의 영역이다. CKAD 범위는 표준 NetworkPolicy 지만, 어디까지가 표준이고 어디부터 확장인지는 알아 두는 편이 좋다.

마지막으로 셀렉터 사고 하나. Service 의 셀렉터가 파드 라벨과 한 글자라도 다르면 엔드포인트가 빈 채로 만들어진다. 오류도 나지 않고 이벤트도 없다. kubectl get endpoints <이름><none> 이면 십중팔구 셀렉터 오타다.

다음 실습에서 할 것

ckad-net 네임스페이스에서 ClusterIP·NodePort 를 만들고 port/targetPort/nodePort 를 구분해 지정한다. named port 로 targetPort 를 연결하고, 헤드리스 Service + StatefulSet 으로 파드별 DNS 이름 규칙을 확인하고, Ingress 로 호스트·경로 라우팅을 작성하고, NetworkPolicy 로 프런트엔드에서 백엔드로만 허용하는 화이트리스트를 만든다. 이 환경에는 실제 CNI 데이터플레인이 없으므로 정책이 진짜로 패킷을 막는지는 확인할 수 없다. 오브젝트를 정확히 쓰는 것이 목표이고, 동작 원리는 퀴즈에서 확인한다.