TT Lab
시작하기
배우기 러닝패스 코스

CKA — 쿠버네티스 관리자

서비스는 프록시가 아니라 약속이다

TT Lab 에서 이어서 보기

한 줄 요약

Service 오브젝트는 트래픽을 나르지 않습니다. 셀렉터로 백엔드 목록(EndpointSlice)을 만들어 두는 선언일 뿐이고, 실제 패킷을 돌리는 것은 노드의 데이터플레인(kube-proxy 의 iptables/nftables 또는 CNI 의 eBPF)입니다. 이 분리를 알면 "Service 는 있는데 연결이 안 된다" 를 세 층으로 나눠 볼 수 있습니다.

왜 이게 필요했나

파드는 죽고 다시 뜨면서 IP 가 바뀝니다. 그래서 이름과 안정적인 가상 IP 가 필요했습니다. Service 는 이 두 가지를 줍니다.

문제는 누가 백엔드 목록을 유지하느냐입니다. Service 에 셀렉터를 적으면 엔드포인트 컨트롤러가 그 라벨에 맞고 Ready 인 파드를 찾아 EndpointSlice 를 채웁니다. 셀렉터가 한 글자라도 어긋나면 EndpointSlice 는 비어 있는 채로 정상 생성됩니다. 에러가 나지 않습니다. Service 도, Deployment 도, 파드도 전부 Ready 인데 연결만 안 됩니다.

그래서 서비스 장애 진단의 첫 질문은 언제나 같습니다. "엔드포인트에 IP 가 있는가?" 있으면 데이터플레인 문제이고, 없으면 셀렉터나 파드 Ready 문제입니다.

Service 에서 패킷까지 세 층

Service 를 만들었는데 연결이 안 될 때 어느 층에서 끊겼는지 가르기 위한 그림이다. 층마다 주인이 다르다.

  1. 셀렉터를 적은 선언Service 오브젝트는 이름과 가상 IP, 그리고 어떤 라벨의 파드를 원하는지만 적는다. 트래픽은 나르지 않는다.
  2. EndpointSlice 를 채우는 컨트롤러라벨이 맞고 Ready 인 파드의 IP 를 목록에 넣는다. 셀렉터가 한 글자라도 어긋나면 목록은 비어 있는 채로 정상 생성된다.
  3. 노드의 데이터플레인kube-proxy 의 iptables·nftables 규칙이나 CNI 의 eBPF 맵이 가상 IP 로 온 패킷을 목록의 파드로 넘긴다.

여기서 구분할 것 Service 도 파드도 전부 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 배열의 구조입니다.

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- 체인 개수: 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)

커널 안의 해시맵에 프론트엔드와 백엔드가 직접 들어 있습니다. 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 를 조회하도록 바꿔야 합니다. 업그레이드 전 점검 항목의 단골입니다.

대시 하나가 AND 와 OR 를 가른다

NetworkPolicy 의 from 에 namespaceSelector 와 podSelector 를 함께 적는다. 두 선택자를 한 항목에 묶느냐, 두 항목으로 나누느냐만 다르다.

  • from 항목 하나에 둘 다AND 다. 라벨이 맞는 네임스페이스 안의, 라벨이 맞는 파드만 허용한다.
  • from 항목 둘에 하나씩OR 다. 정책이 놓인 네임스페이스의 그 라벨 파드, 또는 라벨이 맞는 네임스페이스의 모든 파드를 허용한다.

여기서 구분할 것 YAML 에서 podSelector 앞에 대시 하나를 더 붙였을 뿐인데 허용 범위가 크게 넓어진다. 헷갈리면 kubectl describe 로 쿠버네티스가 어떻게 해석했는지 확인한다.

잠깐, 예측해 보세요 monitoring 네임스페이스의 prometheus 파드만 들이려 했는데 monitoring 의 모든 파드가 들어온다. 무엇을 의심하겠는가?

설명 확인 · 채점 없는 자가 점검

from 항목이 둘로 나뉘어 OR 가 된 것이다. namespaceSelector 와 podSelector 를 한 항목 안에 함께 두어야 두 조건이 AND 로 묶인다.

근거 문서

다음 실습에서 할 것

첫 실습에서 ClusterIP, NodePort, headless, multi-port, ExternalName, Ingress 를 만들고, 셀렉터 오타로 엔드포인트가 비는 상황을 직접 만들어 고칩니다. 두 번째 실습에서는 NetworkPolicy 를 기본 거부부터 시작해 여섯 가지 형태로 좁혀 갑니다. 이 실습 환경에는 정책을 집행할 CNI 가 없어 실제 트래픽 차단은 확인할 수 없고 오브젝트 스펙만 채점합니다. 대신 AND 와 OR 의 차이 같은 스펙 상의 함정을 집중적으로 다룹니다.