LabHub

KCNA — 쿠버네티스·클라우드 네이티브 입문 · 서비스·네트워킹·스토리지 · 이론

파드 IP 는 못 믿는다 — Service 가 푸는 문제

LabHub 에서 이어서 보기

한 줄 요약

파드는 언제든 죽고 새 IP 로 태어납니다. Service 는 죽지 않는 이름과 주소를 앞에 세워 주고,
뒤의 파드 목록은 라벨 셀렉터로 자동 갱신합니다. 스토리지도 같은 사고방식입니다 —
"어떤 디스크"가 아니라 "어떤 성질의 저장소가 얼마나 필요한가"를 선언합니다.

왜 이게 필요했나

앞 모듈에서 확인했듯, 파드를 지우면 다른 이름과 다른 IP 의 파드가 옵니다. 그러면 그 파드에
접속하던 쪽은 어떻게 해야 할까요. 매번 목록을 다시 조회하고 커넥션을 다시 맺어야 한다면
모든 애플리케이션이 쿠버네티스 API 클라이언트가 되어야 합니다.

Service 는 이 문제를 간접 계층으로 풉니다. 안정된 가상 IP(ClusterIP)와 DNS 이름을 하나 만들어 두고,
그 뒤에 붙는 파드 목록은 셀렉터로 계속 갱신합니다. 클라이언트는 이름만 알면 됩니다.

어떻게 동작하나

쿠버네티스 네트워크 모델의 세 가지 약속

1. 모든 파드는 NAT 없이 서로 통신할 수 있다
2. 노드의 에이전트는 그 노드의 모든 파드와 통신할 수 있다
3. 파드가 보는 자기 IP 와 남이 보는 그 파드의 IP 가 같다

CNI 플러그인은 이 약속을 지키는 방식만 다를 뿐, 약속 자체는 같습니다.

Service 와 엔드포인트

Service 를 만들면 엔드포인트 컨트롤러가 셀렉터에 맞는 준비된(Ready) 파드의 IP 를 모아
EndpointSlice(예전에는 Endpoints)에 적습니다. 여기서 두 가지가 중요합니다.

참고로 쿠버네티스 v1.33 부터 Endpoints API 는 deprecated 이고 EndpointSlice 로 옮기라고 안내합니다.
EndpointSlice 는 대규모 클러스터에서 목록을 여러 조각으로 쪼갤 수 있고 듀얼스택도 지원합니다.

Service 타입

| 타입 | 노출 범위 | 쓰는 곳 |
| --- | --- | --- |
| ClusterIP (기본) | 클러스터 내부 | 서비스 간 통신 |
| NodePort | 모든 노드의 30000-32767 포트 | 개발·간단한 외부 노출 |
| LoadBalancer | 외부 로드밸런서 | 클라우드/온프렘 LB 연동 |
| ExternalName | DNS CNAME | 클러스터 밖 서비스에 이름 붙이기 |

헤드리스 서비스(clusterIP: None)는 예외입니다. VIP 를 만들지 않고 DNS 조회에 파드 IP 를
그대로 돌려줍니다. StatefulSet 처럼 개별 파드를 직접 지목해야 할 때 씁니다.

DNS 이름 규칙

<서비스>.<네임스페이스>.svc.cluster.local

같은 네임스페이스 안에서는 <서비스> 만으로도 됩니다. 이 규칙 하나로 "환경마다 주소가 다르다"는
문제가 사라집니다 — 네임스페이스만 갈아도 같은 매니페스트가 돕니다.

설정과 비밀

ConfigMap 과 Secret 은 이미지와 환경을 분리하는 장치입니다. 주입 방법은 둘입니다 —
환경변수로 넣거나, 볼륨으로 마운트하거나. 볼륨으로 마운트하면 값이 바뀔 때 파일이 갱신되지만,
환경변수는 프로세스 시작 시점에 박히므로 갱신되지 않습니다.

Secret 에 대해 KCNA 수준에서 반드시 알아야 할 사실 하나 — base64 는 인코딩이지 암호화가 아닙니다.
기본 설정에서 Secret 은 etcd 에 사실상 평문으로 저장되고, kubectl get secret ... -o jsonpath
base64 -d 한 번이면 원문이 나옵니다. 진짜 암호화는 EncryptionConfiguration 을 따로 켜야 합니다.
(자세한 건 KCSA 에서 다룹니다.)

스토리지

파드는 PV 를 직접 가리키지 않고 PVC 를 가리킵니다. 이 간접 계층 덕분에 백엔드가 NFS 든
블록 스토리지든 파드 매니페스트는 그대로입니다.

접근 모드도 시험에 자주 나옵니다 — ReadWriteOnce(한 노드에서 읽기/쓰기),
ReadOnlyMany(여러 노드 읽기), ReadWriteMany(여러 노드 읽기/쓰기). RWX 를 지원하는 백엔드는
제한적이라 NFS 같은 파일 스토리지를 쓰게 됩니다.

현장에서 만나는 모습

저자의 홈랩은 kube-proxy 를 아예 설치하지 않고 Cilium 의 eBPF 로 Service 로드밸런싱을 대신합니다.
그리고 그게 실제로 도는지 확인했습니다 — 노드의 iptables KUBE- 체인 0 개, 대신 eBPF 맵에
10.96.0.10:53/TCP → 10.244.0.150:53/TCP, 10.96.0.1:443/TCP → 10.0.0.120:6443/TCP 같은
프론트엔드-백엔드 매핑이 직접 들어 있었습니다.

여기서 재미있는 부트스트랩 문제가 나옵니다. kube-proxy 가 없으면 Cilium 자신이 API 서버에 붙을 때
kubernetes 서비스의 ClusterIP(10.96.0.1)를 쓸 수 없습니다 — **그 VIP 를 만들어 줄 주체가
Cilium 자신**이기 때문입니다. 그래서 설치할 때 k8sServiceHost=10.0.0.120, k8sServicePort=6443
으로 실제 주소를 직접 알려줘야 합니다. Service 가 '간접 계층'이라는 말의 의미가 여기서 선명해집니다.

같은 클러스터의 스토리지는 csi-driver-nfs 로 NAS 를 붙여 RWX 를 씁니다.
L4 로드밸런서는 MetalLB 이고 주소 풀은 10.0.0.200-215 입니다. Gitea 가 .200, ArgoCD 가 .201,
Harbor 가 .202, Grafana 가 .203 을 받아 갔습니다. LoadBalancer 타입 Service 가 실제로
"풀에서 IP 를 하나 떼어 붙이는" 동작이라는 걸 눈으로 보기 좋은 구성입니다.

다음 실습에서 할 것

다음 실습에서 백엔드 디플로이먼트를 세우고 ClusterIP Service 를 붙여 엔드포인트가 잡히는지 확인합니다.
그다음 일부러 셀렉터에 오타가 있는 Service 를 만들어 엔드포인트가 0 건이 되는 걸 재현하고,
NodePort·ConfigMap/Secret 주입·PVC 요청·DNS 이름 규칙까지 손으로 확인합니다.