0. Contents

1. Overview

관리 셸kubectl apply -f …클러스터 cn4cn4-control-plane컨트롤 플레인kube-apiserver + etcd객체가 사는 곳Service webService web-nodeportService web-headlessService remote-dbStatefulSet identityEndpointSlice web-5kx9pPod identity-0 · identity-1Pod netdemo-… ×3파랑 — 사람이 적는다 주황 — 컨트롤러가 적는다1kube-controller-managerStatefulSet 컨트롤러→ Pod identity-0, 1 을 같은 이름으로 만든다EndpointSlice 컨트롤러→ selector 로 고른 Pod 의 주소·ready 를 Slice 에지켜본다써 넣는다2kube-proxy컨트롤 플레인 노드에도 있다③ 의 규칙을 여기에도 쓴다cn4-worker워커kube-proxy3API 의 Service · Slice 를 지켜보고 nat 규칙을 쓴다4커널 10.96.12.34:80 → 10.244.1.5:80 (DNAT)Podnetdemo10.244.1.5identity-010.244.1.6coredns10.244.1.35coredns 는 API 의 Service 를 보고 이름에 답한다cn4-worker2워커kube-proxy3API 의 Service · Slice 를 지켜보고 nat 규칙을 쓴다4커널 10.96.12.34:80 → 10.244.1.5:80 (DNAT)Podnetdemo10.244.2.9identity-110.244.2.8netdemo10.244.2.4지켜본다① 사람이 적은 객체는 API server 에만 있다 — ② 컨트롤 플레인의 컨트롤러가 다른 객체를 더 적고, ③ 모든 노드의 kube-proxy 가 규칙을 쓰고, ④ 커널이 나른다CoreDNS 는 컨트롤 플레인이 아니라 kube-system 의 Pod 다 — ⑤ API 의 Service 를 보고 이름에 답할 뿐 패킷에는 관여하지 않는다
Service 를 적는 사람은 ① 만 한다 — ②③④ 는 각각 다른 노드의 다른 프로그램이 맡고, 그 중 하나가 빠져도 ① 은 API server 에 그대로 남아 있다

04-x 에서 이어지는 것

  • 04-1 은 Pod 가 IP 를 얻어 노드를 넘는 길, 04-2 는 바뀌는 Pod 앞에 이름과 가상 IP 를 세우는 길이었다 — 문제 ③
  • 가상 IP 하나가 Pod IP 로 바뀌는 길은 노드 커널까지 04-2 3.2 절에 있다
  • 5장은 그 위에 놓인 추상화들을 한 줄로 세운다 — StatefulSet, EndpointSlice, Service 의 네 타입, Headless, ExternalName. 어느 것이 선언이고, 누가 그 선언을 실현하는지를 매번 가른다

5장이 묻는 것

  • Service 객체가 있다는 사실과 패킷이 Pod 에 닿는다는 사실 사이에 무엇이 있는가
  • StatefulSet 이 고정해 주는 것은 무엇이고, 고정해 주지 않는 것은 무엇인가
  • Service 의 백엔드 명단은 누가 쓰고, 명단의 한 줄은 어떤 상태를 거치는가
  • 같은 Service API 를 iptables · nftables · eBPF 가 나를 때, 지금 클러스터는 어느 것인지 어떻게 아는가

2. Three Layers — 선언 · 컨트롤러 · 데이터패스

2.1 요청서와 처리 주체

  • Service, Ingress, StatefulSet 같은 API 객체는 원하는 상태를 적은 요청서다 — API server 에 저장되는 데이터이고, 그 자체로는 아무 패킷도 움직이지 않는다
  • 요청서를 읽고 다른 객체를 쓰는 것이 컨트롤러다 — selector 를 보고 EndpointSlice 를 쓰고, 서수 이름으로 Pod 를 만들고, 외부 IP 를 배정해 status 에 적는다
  • 패킷을 실제로 나르는 것이 데이터패스다 — 노드마다 규칙을 쓰는 kube-proxy 와 그 규칙대로 바꾸는 커널, 또는 Cilium 의 eBPF 프로그램
  • 세 역할이 한 프로그램에 들어 있지 않다 — 그래서 어느 하나가 없어도 요청서는 그대로 남아 있고, 증상은 그 층의 모양으로 나타난다
관리 셸kubectl apply바깥 클라이언트203.0.113.10→ 203.0.113.40:80클러스터 cn4cn4-control-plane컨트롤 플레인kube-apiserver — Service web-lbtype: LoadBalancerspec.clusterIP: 10.96.30.5 ← 적는 즉시 생긴다. 안에서는 이걸로 통한다status.loadBalancer.ingress: [] → [203.0.113.40]② 가 써 넣기 전까지 EXTERNAL-IP <pending>1사람이 적는다cn4-workerkube-proxyspeaker3광고4203.0.113.40:80 → 10.244.1.5:80Podnetdemo10.244.1.55닿는다cn4-worker2kube-proxyspeaker같은 규칙이 여기에도 있다Podmetallb controller풀에서 203.0.113.40 을 골라 status 에 쓴다2status 에써 넣는다① 사람은 Service 만 적는다 — ② LB 컨트롤러 Pod 가 외부 IP 를 고르고, ③ speaker 가 그 IP 를 노드로 광고하고, ④ kube-proxy 규칙이 Pod 로 보낸다② 가 없으면 <pending> 이 남고 ClusterIP 로 안에서는 통한다. 클라우드에서는 ② 를 컨트롤 플레인의 cloud-controller-manager 가, ③ 을 클라우드 LB 가 맡는다
선언 하나에 구현이 둘 붙는다 — 주소를 주는 쪽(②③)과 패킷을 나르는 쪽(④)은 다른 Pod · 다른 프로그램이고, 둘 중 무엇이 빠졌는지는 status 와 노드 규칙을 따로 본다
  • type: LoadBalancer 를 적었다고 로드밸런서가 설치되는 것은 아니다 — 클라우드에서는 공급자 컨트롤러가, 직접 만든 클러스터에서는 MetalLB 나 Cilium 의 LB IPAM 이 ② 를 맡는다. 어느 것도 없으면 EXTERNAL-IP 는 <pending> 에 머문다
  • <pending> 은 Service API 의 고장이 아니다 — ClusterIP 는 그 순간에도 할당되어 안에서는 통한다. 외부 IP 를 배정할 컨트롤러와 주소 풀이 있는지를 본다
  • CNI 와 CSI 는 Service 나 PVC 같은 리소스가 아니라 플러그인을 끼우는 규격이다 — Cilium 과 Calico 가 CNI 를, 클라우드 디스크 드라이버가 CSI 를 구현한다. Cilium 은 CNI 에 더해 Service 데이터패스와 LoadBalancer IP 배정까지 맡을 수 있다

2.2 객체별 처리 주체

선언 (API 객체)사용자에게 주는 것실제로 동작시키는 주체
Pod · StatefulSet프로세스, 구성원의 안정된 이름과 서수kubelet 과 워크로드 컨트롤러
Service안정된 이름 · 가상 IP · 포트 · 전달 정책EndpointSlice 컨트롤러 + 노드의 Service 데이터패스
EndpointSlice선택된 백엔드의 주소 · 포트 · 준비 상태컨트롤 플레인, 또는 사용자 컨트롤러
NetworkPolicy허용 목록정책을 구현하는 CNI — 04-3 3.1 절
Ingress · Gateway APIHTTP(S) 호스트 · 경로 규칙별도의 Ingress Controller · Gateway Controller
PVC · StorageClass저장 공간 요청서기존 PV, 또는 동적 프로비저너(흔히 CSI 드라이버)
  • 둘째 열은 API 에 있는 것, 셋째 열은 설치돼 있어야 하는 것이다 — 셋째 열이 비어 있으면 둘째 열은 적혀 있기만 한다
  • ClusterIP 만 쓰는 클러스터 안 통신에는 셋째 열의 외부 구현이 필요하지 않다 — kube-proxy 나 그 대체물이면 된다

2.3 실습 환경과 전제

  • 클러스터는 04-2 에서 만든 kind 클러스터 cn4 를 그대로 쓴다 — 노드 셋, Cilium 을 평범한 CNI 로, kube-proxy 는 iptables 모드. 만드는 순서는 04-2 2.2 절
  • 명령은 모두 kubectl 전체 형태로 쓴다. 컨텍스트는 아래 첫 줄의 kubectl config use-context kind-cn4 로 한 번 고정하고, 이후 명령에는 --context 를 다시 적지 않는다
  • 네임스페이스는 5장 실습 전용으로 netlab 을 새로 만든다 — 04-3 의 cn4 네임스페이스와 섞이지 않는다
  • 외부 LoadBalancer 와 Ingress 는 설치하지 않는다 — 아래의 모든 절은 kube-proxy 와 CoreDNS 만으로 관찰할 수 있다
kubectl config use-context kind-cn4           # 새 터미널이면 다시 고정한다
kubectl config current-context                # kind-cn4 인지 확인한다
kubectl get nodes -o wide
kubectl -n kube-system get daemonset kube-proxy     # 있으면 kube-proxy 가 Service 를 나른다 (7.2)
kubectl get ingressclass                            # 없는 것이 정상이다
kubectl create namespace netlab
  • 컨텍스트를 매번 확인하는 이유는 운영 클러스터에 실습 리소스를 적용하는 실수를 막기 위해서다 — 새 터미널마다 use-context 로 kind-cn4 를 다시 고정한다
  • 노드 안의 규칙(iptables-save, nft)을 읽는 명령은 kind 노드 컨테이너에 docker exec 로 들어가 조회만 한다 — 운영 노드의 방화벽 규칙은 고치지 않는다

3. StatefulSet — 이름과 순서

3.1 Deployment 와의 차이

Deployment netdemo이름도 주소도 새것Pod10.244.1.5netdemo-7c9d-x8k2q✗교체Pod10.244.2.9netdemo-7c9d-q4w7z✓ReplicaSet 은 개수만 맞춘다 — 접미사는 무작위다클라이언트는 Pod 이름을 쓰지 않고 Service web 을 쓴다Service web10.96.12.34 — 뒤에 누가 서든 같은 접점StatefulSet identity이름은 같고 주소는 새것Pod10.244.1.6identity-0✗교체Pod10.244.2.11identity-0✓서수(0, 1, …)가 이름이다 — 교체돼도 같은 이름을 다시 쓴다PVC data-identity-0 도 같은 이름의 Pod 에 다시 붙는다identity-0.identity-headless.netlab.svc.cluster.local→ 10.244.2.11 (DNS 가 새 주소를 답한다)StatefulSet 이 고정하는 것은 이름(서수)과 그 이름에 붙는 PVC·DNS 다 — Pod IP 는 양쪽 다 바뀐다OrderedReady 기본값: 생성은 0 → 1 → 2, 앞 Pod 가 Ready 가 된 뒤 다음. 삭제·축소는 역순, 롤링 업데이트도 높은 서수부터
Deployment 의 안정된 접점은 Service 하나다. StatefulSet 은 구성원 하나하나에 이름을 준다 — 그래도 주소는 고정하지 않는다
비교 항목DeploymentStatefulSet
Pod 정체성교체 때 새 무작위 접미사이름-서수 를 유지
생성 · 삭제 순서복제본 사이 순서 보장 없음기본 OrderedReady 가 순서를 관리
영속 저장소PVC 를 직접 참조volumeClaimTemplates 로 복제본별 PVC
네트워크 이름Service 의 대표 이름Headless Service 로 복제본별 DNS
대표 용도무상태 웹 API · 워커DB 구성원, 합의 노드, 고정 식별자가 필요한 구성원
  • StatefulSet 을 썼다는 사실만으로 데이터 복제나 장애 조치가 생기지 않는다 — PostgreSQL Pod 둘을 만들어도 둘은 서로를 모른다. 복제 프로토콜, 리더 선출, 백업과 복구는 애플리케이션이나 Operator 의 일이다
  • StatefulSet 이 책임지는 범위는 구성원의 정체성과 생명주기다 — 이름, 순서, 그 이름에 붙는 PVC 와 DNS

3.2 생성 · 삭제 · 업데이트의 순서

spec:
  podManagementPolicy: OrderedReady   # 기본값
  updateStrategy:
    type: RollingUpdate               # 기본값
  # partition, minReadySeconds 는 필요할 때 따로 설계한다
  • OrderedReady 는 0 번부터 만들고, 앞 Pod 가 Running 이면서 Ready 가 된 뒤 다음을 만든다 — 삭제와 축소는 역순이다
  • RollingUpdate 는 높은 서수부터 낮은 서수로 갱신하며 매번 준비 상태를 확인한다
  • Parallel 은 생성 · 삭제의 순서 제약을 푼다 — 이름의 안정성은 그대로다
  • 잘못된 이미지로 업데이트해 새 Pod 가 Ready 가 되지 않으면 롤아웃이 그 서수에서 멈춘다 — 템플릿을 되돌리는 것만으로 끝나지 않는다. 공식 문서는 되돌린 뒤 잘못된 템플릿으로 생긴 Pod 를 직접 삭제하는 복구 경로를 안내한다. 삭제 전에 저장소와 애플리케이션의 일관성을 확인한다

3.3 Headless Service 와 구성원 DNS

  • spec.serviceName 은 구성원의 네트워크 정체성을 관할하는 Service 를 가리킨다 — 보통 clusterIP: None 인 Headless Service 다
  • 그 Service 를 통해 구성원 하나하나가 DNS 이름을 얻는다 — identity-0.identity-headless.netlab.svc.cluster.local
identity-0Pod 이름 = StatefulSet 이름-서수.identity-headlessspec.serviceName 의 Service.netlab네임스페이스.svc고정.cluster.local클러스터 도메인같은 Service 에 묻는 두 이름identity-headless.netlab.svc.cluster.localA 10.244.1.6 (identity-0)A 10.244.2.8 (identity-1)구성원 전체 — 목록 발견identity-0.identity-headless.netlab.svc.cluster.localA 10.244.1.6구성원 하나 — 정체성두 이름 모두 가상 IP 가 없다 — clusterIP: None 이라 CoreDNS 가 Pod IP 를 그대로 답하고, 커널의 DNAT 는 없다기본값은 Ready 인 Pod 만 답에 넣는다. publishNotReadyAddresses: true 면 준비 전 구성원도 답에 들어간다 (3.5)
이름의 앞 토막이 Pod 를, 둘째 토막이 Service 를 가리킨다 — Service 이름만 물으면 목록, Pod 이름까지 붙이면 하나
# 03-identity.yaml — 저장소 없이 이름과 DNS 만 본다
apiVersion: v1
kind: Service
metadata:
  name: identity-headless
  namespace: netlab
spec:
  clusterIP: None
  selector:
    app: identity-demo
  ports:
    - name: http
      port: 80
      targetPort: http
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: identity
  namespace: netlab
spec:
  serviceName: identity-headless
  replicas: 2
  selector:
    matchLabels:
      app: identity-demo
  template:
    metadata:
      labels:
        app: identity-demo
    spec:
      containers:
        - name: nginx
          image: nginx:stable-alpine
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet: {path: /, port: http}
            initialDelaySeconds: 1
            periodSeconds: 3
kubectl apply -f 03-identity.yaml
kubectl -n netlab rollout status statefulset/identity
kubectl -n netlab get pod -l app=identity-demo -o wide          # identity-0, identity-1 과 각자의 Pod IP
kubectl -n netlab get svc identity-headless                     # CLUSTER-IP 가 None
kubectl -n netlab get endpointslice -l kubernetes.io/service-name=identity-headless
kubectl -n netlab run dns-check --rm -i --restart=Never --image=busybox:1.36 -- \
  nslookup identity-0.identity-headless.netlab.svc.cluster.local      # A 레코드 하나
kubectl -n netlab run dns-check --rm -i --restart=Never --image=busybox:1.36 -- \
  nslookup identity-headless.netlab.svc.cluster.local                 # A 레코드 둘
  • nslookup 이 답한 주소를 get pod -o wide 의 Pod IP 와 맞춰 본다 — 같은 값이면 DNS 가 Pod IP 를 그대로 답한 것이다
  • Pod 를 하나 지우고 다시 묻는다 — kubectl -n netlab delete pod identity-0 뒤 같은 이름이 새 IP 로 돌아온다. 이름은 안정적이고 IP 는 아니다
  • 생성 직후 잠깐 응답이 없을 수 있다 — Ready 가 되기 전이거나 DNS 캐시 TTL 안이다
  • nginx:stable-alpine 은 실습용 이동 태그다 — 재현성이 필요한 배포는 digest 로 고정한다

3.4 저장소와 PVC

  • StatefulSet 이 영속 데이터를 가지려면 저장 공간을 요청하는 PVC 와 그것을 채울 PV 가 있어야 한다 — 미리 만든 PV 거나, StorageClass 에 연결된 프로비저너(흔히 CSI 드라이버)가 동적으로 만든 것이다. 둘 다 없으면 PVC 는 Pending 이다 — PV 위키
  • PVC 는 요청서, StorageClass 는 어떤 종류를 줄지의 설명, CSI 드라이버는 Ceph 나 EBS 같은 저장소와 통신해 볼륨을 만들고 붙이는 구현이다 — 데이터는 그 저장소에 있고, CSI 자체가 디스크는 아니다
  • 모든 볼륨에 CSI 가 필요하지는 않다 — emptyDir 는 kubelet 이 주고, 이미 마운트된 NFS 를 Pod 에 넘기는 설계도 있다
  • PVC 가 있다고 데이터가 복제되거나 여러 노드에서 동시에 안전하게 쓰이는 것도 아니다
# volumeClaimTemplates 의 모양 — 03-identity.yaml 에 그대로 붙이는 완성본은 아니다
spec:
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi
  template:
    spec:
      containers:
        - name: application
          volumeMounts:
            - name: data
              mountPath: /var/lib/application
  • 템플릿의 data 는 구성원마다 data-identity-0, data-identity-1 이라는 PVC 가 된다 — Pod 가 교체돼도 같은 서수의 PVC 가 다시 붙는다. 3.1 의 그림이 그 연결이다
  • 04-3 의 postgres StatefulSet 은 emptyDir 였다 — Pod 가 지워지면 데이터도 사라졌다. 실제 상태 저장 서비스는 StorageClass, 백업과 복구, 파일 시스템과 DB 의 일관성을 함께 설계한다

3.5 준비 상태와 DNS 공개 시점

  • Headless Service 가 고른 Pod 의 A/AAAA 레코드는 기본적으로 Ready 인 Pod 에 대해서만 공개된다 — probe 위키
  • publishNotReadyAddresses: true 를 켜면 아직 Ready 가 아닌 구성원도 DNS 에 들어간다 — 서로를 찾아야 비로소 준비가 끝나는 합의 노드 같은 멤버십 부트스트랩에 쓴다
  • 그 설정은 준비 전 구성원에게 일반 사용자 트래픽을 보내도 된다는 뜻이 아니다 — 멤버십 발견용 이름과 사용자 트래픽용 이름을 분리하는 이유다
  • DNS 응답을 오래 붙들고 있는 클라이언트는 교체된 구성원의 옛 IP 로 계속 간다 — 캐시와 재해석 정책을 함께 본다

4. EndpointSlice — 백엔드 명단

4.1 Endpoints 에서 EndpointSlice 로

  • Service 의 selector 가 어떤 Pod 가 백엔드 후보인지를 고른다 — 컨트롤 플레인이 그 Pod 들의 IP, 포트, 상태를 EndpointSlice 에 적는다
  • 클라이언트는 이름으로 가상 IP 를 얻고, 데이터패스는 준비된 endpoint 중 정책에 맞는 곳으로 보낸다 — 컨트롤러가 객체를 갱신하는 일과 TCP 연결이 처리되는 일은 다른 시점의 다른 일이다
옛 Endpoints — Service 당 객체 하나core/v1 · v1.33 부터 deprecatedEndpoints webaddresses: [300 개 …]notReadyAddresses: [...]Pod 하나가 바뀌면 →kube-proxycn4-control-planekube-proxycn4-workerkube-proxycn4-worker2셋 모두에게 300 개 전체를 다시 보낸다노드 수 × 변경 수 만큼 커진다현행 EndpointSlice — Service 당 Slice 여럿discovery.k8s.io/v1 · Stable v1.21web-5kx9p100 개ready/serving/terminatingnodeName·zoneweb-h2tqz100 개ready/serving/terminatingnodeName·zoneweb-r7mwd100 개ready/serving/terminatingnodeName·zone바뀐 Slicekubernetes.io/service-name: web 레이블로 셋이 한 Service 에 묶인다kube-proxycn4-control-planekube-proxycn4-workerkube-proxycn4-worker2셋 모두에게 바뀐 Slice 하나(100 개)만 보낸다기본 100 개씩, 최대 1,000명단을 쪼갠 이유는 전달 비용이다 — 받는 쪽은 모든 노드의 kube-proxy 라서, Pod 하나의 변경이 노드 수만큼 복사된다한 endpoint 가 전환 중 잠시 두 Slice 에 겹칠 수 있다 — 읽는 쪽은 Service 의 Slice 를 전부 모아 중복을 지운다
큰 클러스터에서 Pod 하나가 바뀔 때 전 노드가 받아야 하는 양을 줄이는 것이 Slice 의 목적이다 — 읽는 쪽이 여러 Slice 를 합쳐야 하는 비용은 그 대가다
구분옛 Endpoints현행 EndpointSlice
APIcore/v1 Endpointsdiscovery.k8s.io/v1
분할 단위Service 당 큰 객체 하나Service 당 여러 Slice
상태 정보addresses · notReadyAddressesconditions.ready · serving · terminating
변경 전달전체 객체를 모든 감시자에바뀐 Slice 하나만
새 구현의 선택호환성 확인용으로만표준 백엔드 발견과 모니터링 대상
상태v1.33 부터 deprecatedv1.21 부터 Stable
  • 책의 kind: Endpoints, kubectl get endpoints, addresses / notReadyAddresses 예제는 구조를 이해하는 역사적 자료다 — 새 연동 대상으로 Endpoints 를 고르지 않는다
  • 04-2 에서는 EndpointSlice 를 kube-proxy 가 읽는 입력으로만 봤다 — 04-2 3.2 절. 여기서는 그 명단 자체의 모양과 상태를 본다

4.2 레이블을 바꾸면 일어나는 일

  • 책은 Deployment 의 Pod 레이블을 app=app 에서 app=nope 로 바꿔, 그 Pod 가 Service 백엔드에서 빠지고 ReplicaSet 이 새 Pod 를 만드는 것을 보여 준다
  • 핵심은 Pod 가 종료돼서가 아니라 selector 와 맞지 않게 돼서라는 점이다 — Service 의 selector 와 ReplicaSet 의 selector 가 각자 그 Pod 를 놓친다. 옛 Pod 는 레이블만 바뀐 채 계속 돈다
kubectl -n netlab get svc web -o jsonpath='{.spec.selector}{"\n"}'
kubectl -n netlab get pods -l app=netdemo --show-labels
kubectl -n netlab get endpointslice -l kubernetes.io/service-name=web -o yaml   # endpoints 의 주소와 targetRef
kubectl -n netlab get pods -w                                                   # 다른 터미널에서 지켜본다
  • 운영 중인 서비스에서 디버깅 목적으로 Pod 레이블을 바꾸면 NetworkPolicy, PDB, 다른 Service 의 selector 까지 영향을 받는다 — 무해한 격리 방법으로 일반화하지 않는다
  • 보통은 readiness 를 떨어뜨리거나, 별도 진단 복제본을 쓰거나, 트래픽 배제 정책을 먼저 검토한다 — 레이블 변경은 실습 클러스터에서만 한다

4.3 EndpointSlice YAML 읽기

# 구조 설명용 — 적용하는 실습이 아니다. 10.0.0.10 은 설명용 주소다
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: example-manual-1
  namespace: netlab
  labels:
    kubernetes.io/service-name: selectorless-demo        # 어느 Service 의 명단인가
    endpointslice.kubernetes.io/managed-by: manual-lab   # 누가 관리하나
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses: ["10.0.0.10"]
    conditions:
      ready: true
      serving: true
      terminating: false
    nodeName: cn4-worker          # 위치 정보 — 분산을 보장하는 값은 아니다
    zone: zone-a
  • addressType 은 IPv4 와 IPv6 중 하나다 — 이중 스택 Service 는 계열마다 Slice 가 따로 생긴다. 04-2 6.2 절
  • targetRef 로 그 주소가 어느 Pod 인지 거슬러 갈 수 있다 — nodeName 과 zone 은 6.3 의 topology 선호에 쓰이는 입력이다
  • selector 가 있는 Service 에 사용자가 만든 Slice 를 섞으면 컨트롤러와 관리 범위가 충돌한다 — 수동 Slice 는 selector 없는 Service 에만 붙인다. 8.4 끝의 고정 IP 사례가 그 용도다
  • 책의 apiVersion: discovery.k8s.io/v1beta1 과 Topology: 출력은 옛 스키마다 — 지금 클러스터의 모양은 kubectl explain 으로 본다
kubectl api-resources | grep -i endpointslice
kubectl explain endpointslice.endpoints.conditions
kubectl -n netlab get endpointslices -l kubernetes.io/service-name=web -o wide

4.4 ready · serving · terminating

생성 직후Running · Ready 아님ready✗falseserving✗falseterminatingfalse규칙에서 제외Readyprobe 통과ready✓trueserving✓trueterminatingfalse규칙에 포함삭제 시작grace period 중 · 앱은 아직 답함ready✗falseserving✓trueterminatingtrue새 연결은 보통 제외종료Pod 사라짐조건 없음endpoint 삭제ready 는 'serving 이면서 terminating 이 아님' 과 보통 같다 — 삭제가 시작되면 ready 는 바로 false 가 되지만 주소는 남는다serving · terminating 은 v1.26 Stable. 모든 endpoint 가 종료 중이면 프록시가 serving 인 곳으로 보낼 수 있다
세 조건은 셋이 한 묶음이다 — ready 하나만 보면 '아직 답하는 종료 중 Pod' 와 '아직 준비 안 된 새 Pod' 가 같아 보인다
조건뜻읽을 때의 주의
ready지금 트래픽 대상으로 삼아도 좋은가publishNotReadyAddresses 가 켜진 Service 에서는 뜻이 달라진다
serving종료 여부와 별개로 요청에 답할 수 있는가드레이닝 처리가 참고하는 조건
terminating종료 과정에 들어갔는가준비 상태와 독립적으로 본다
nodeName · zone위치균등 분산을 보장하는 값이 아니다
  • 프록시는 보통 종료 중인 endpoint 를 새 연결에서 제외한다 — 모든 endpoint 가 종료 중이면 아직 serving 인 곳으로 보낼 수 있다. 롤링 업데이트 중 전부가 동시에 바뀌는 순간을 위한 규칙이다
  • ready: false 는 삭제가 아니다 — 주소는 명단에 남고 조건만 바뀐다. 04-2 에서 본 “주소는 남기고 ready 만 바꾼다” 가 이 조건들이다

5. Service — 네 타입과 포트

5.1 네 타입과 Headless 의 자리

  • Service 는 특정 Pod 대신 서는 안정된 접점이다 — selector 가 있으면 같은 네임스페이스에서 레이블이 맞는 Pod 를 골라 EndpointSlice 를 유지한다. selector 가 없으면 명단은 다른 컨트롤러나 사람이 쓴다 — Service 위키
  • spec.type 의 공식 값은 넷이다 — ClusterIP, NodePort, LoadBalancer, ExternalName. Headless 는 다섯 번째 type 이 아니라 clusterIP: None 이라는 ClusterIP 의 특수한 설정이다
spec.type진입점발견과 전달주로 쓰는 곳절
ClusterIP (기본)클러스터 안 가상 IP · DNS준비된 백엔드에 L4 전달내부 API, DB 앞5
NodePort모든 노드 IP 의 같은 포트ClusterIP 위에 노드 포트를 더함노드 직접 접근, 외부 LB 의 하위 수단6
LoadBalancer공급자가 준 외부 주소공급자별 L4 LB + Service 데이터패스TCP/UDP 서비스의 외부 공개—
ExternalName안의 이름 → 밖의 이름 CNAME프록시 없는 DNS 별칭외부 의존성의 이름 추상화8
Headless (clusterIP: None)개별 Pod 의 DNS가상 IP 와 프록시 없음StatefulSet, 클라이언트 쪽 선택3 · 8
  • 위 셋(ClusterIP · NodePort · LoadBalancer)은 쌓인다 — NodePort 는 ClusterIP 를 포함하고, LoadBalancer 는 NodePort 와 ClusterIP 를 포함한다. 바깥 문을 하나 더 열 때마다 안쪽 문은 그대로 남는다
  • 아래 둘(ExternalName · Headless)은 가상 IP 가 없다 — 8절에서 한 자리에 모아 본다

5.2 port · targetPort · containerPort

API server 의 객체 — 노드에는 없다Service web10.96.12.34ports: - port: 80 targetPort: httpPod netdemo 의 specports: - name: http containerPort: 80메모노드 A클라이언트 Pod10.244.0.9web:80 으로 연결1dst 10.96.12.34:80호스트 방 커널 — kube-proxy 가 쓴 규칙2dst 10.96.12.34:80 → 10.244.1.5:80targetPort http 는 Pod ports 의 name 으로 80 을 찾는다노드 Bnetdemo Pod10.244.1.5containerPort: 80 이라고 적혀 있다 — 이 줄이 포트를 열지는 않는다nginx LISTEN 0.0.0.0:804실제로 듣고 있는 프로세스가 연결을 받는다0.0.0.0 이라 Pod IP 로 온 연결을 받는다3CNI 경로연결이 닿는 기준은 ④ 앱이 실제로 듣는 포트다 — containerPort 는 포트를 열지 않고, 숫자 targetPort 가 containerPort 와 같아야 하는 것도 아니다② 는 보내는 쪽 노드의 커널에서 일어난다. 이름형 targetPort(http) 를 쓰면 Pod ports 의 name 과 맞아야 한다
세 칸 중 둘(port · targetPort)은 Service 의 것, 하나(containerPort)는 Pod 의 메모다 — 실제로 문을 여는 것은 셋 중 어느 칸도 아닌 앱이다
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: netlab
spec:
  # type 을 생략하면 ClusterIP
  selector:
    app: netdemo
  ports:
    - name: http
      port: 80            # 클라이언트가 Service 에 접속하는 포트
      targetPort: http    # Pod 가 듣는 포트 — 숫자이거나, Pod ports 의 name
      protocol: TCP
  • containerPort 는 Pod spec 의 메모다 — 선언했다고 프로세스가 그 포트를 열지 않고, 선언하지 않았다고 닫히지도 않는다. 04-1 3.3 절
  • 숫자형 targetPort 는 containerPort 와 일치해야 하는 것이 아니다 — 앱이 실제로 그 포트를 들으면 닿는다
  • 이름형 targetPort 는 Pod ports 의 name 과 맞아야 한다 — 이미지가 듣는 포트가 바뀌어도 Pod 쪽 숫자만 고치고 Service 는 그대로 둘 수 있는 것이 이름형의 장점이다

5.3 실습 웹 Deployment

# 06-web.yaml — 6절부터 공통으로 쓴다
apiVersion: apps/v1
kind: Deployment
metadata:
  name: netdemo
  namespace: netlab
spec:
  replicas: 3
  selector:
    matchLabels:
      app: netdemo
  template:
    metadata:
      labels:
        app: netdemo
    spec:
      containers:
        - name: web
          image: nginx:stable-alpine
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet: {path: /, port: http}
            initialDelaySeconds: 1
            periodSeconds: 3
---
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: netlab
spec:
  selector:
    app: netdemo
  ports:
    - name: http
      port: 80
      targetPort: http
kubectl apply -f 06-web.yaml
kubectl -n netlab rollout status deployment/netdemo
kubectl -n netlab get deployments,pods,services -o wide
kubectl -n netlab get endpointslices -l kubernetes.io/service-name=web      # 주소 셋, 전부 ready
  • 책의 Go 웹 서버 대신 nginx 를 쓴다 — 응답에 노드와 Pod 이름이 나오지 않는다. 어느 Pod 가 받았는지를 보려면 04-3 의 go-web 이미지와 8080 포트로 바꾼다
  • 관찰 대상은 연결 · 백엔드 집합 · DNS · 경로다 — 응답 내용은 보지 않는다

5.4 ClusterIP 와 Service CIDR

  • spec.type 을 생략하면 ClusterIP 다 — Service CIDR 안에서 가상 IP 하나가 할당되고, Service 를 지울 때까지 바뀌지 않는다
  • 그 주소는 어느 인터페이스에도 붙어 있지 않다 — 노드 커널이 목적지를 읽어 백엔드로 고쳐 쓰는 논리적 진입점이다. 그 과정은 04-2 3.3 절
  • Pod CIDR 과 Service CIDR 은 다른 자원에서 관리된다 — Pod CIDR 은 Pod 네트워크 구현의 주소 공간, Service CIDR 은 kube-apiserver 의 --service-cluster-ip-range 다. 둘은 겹치면 안 된다
  • spec.clusterIP 로 주소를 직접 지정할 수 있다 — 범위 안이어야 하고 중복이 없어야 한다. 특별한 이유가 없으면 자동 할당에 맡긴다
kubectl -n netlab get svc web -o jsonpath='{.spec.clusterIP}{"\n"}'
kubectl -n netlab run webcheck --rm -i --restart=Never --image=busybox:1.36 -- \
  wget -qO- http://web.netlab.svc.cluster.local/        # nginx 의 HTML 이 나오면 끝까지 닿은 것이다
  • 실패하면 순서대로 가른다 — 이름이 풀리는가, EndpointSlice 에 ready 주소가 있는가, Pod IP 로 직접 되는가. 04-2 7.1 절

5.5 Service DNS 이름

  • 이름은 서비스.네임스페이스.svc.클러스터도메인 이다 — cluster.local 은 흔한 기본값이지 고정값이 아니다
  • 같은 네임스페이스에서는 web 만 적어도 search 목록이 채워 준다 — 다른 네임스페이스에서는 web.netlab 처럼 보충하는 쪽이 안전하다
  • 짧은 이름이 search 목록과 ndots 때문에 여러 번 질의되는 일, resolver 가 이름을 채우고 CoreDNS 는 답만 한다는 것은 04-2 4.5 절에 있다
  • CoreDNS 는 이름을 주소로 바꿀 뿐이다 — 그 주소로 가는 TCP 연결을 백엔드로 옮기는 것은 노드의 데이터패스다
kubectl -n netlab run dns-check --rm -i --restart=Never --image=busybox:1.36 -- \
  nslookup web.netlab.svc.cluster.local
kubectl -n kube-system get svc kube-dns -o wide                 # CoreDNS 앞의 Service — 그 자체도 ClusterIP 다

5.6 L4 연결과 HTTP 요청

  • Service 의 전달은 연결 단위다 — 새 연결이 생길 때 백엔드 하나를 고르고, 그 연결의 뒤 패킷은 conntrack 이 같은 곳으로 보낸다. 04-2 3.6 절
  • Service 는 URL 경로나 Host 헤더를 보지 않는다 — /api 와 /metrics 를 다른 백엔드로 나누는 일은 앱이나 별도 L7 프록시(Ingress Controller, Gateway, 메시)의 몫이다
  • 책은 큰 클러스터에 iptables 대신 IPVS 를 권한다 — kube-proxy 의 IPVS 모드는 v1.35 부터 deprecated 다. 새 클러스터의 선택지는 nftables 모드나 요건을 충족하는 eBPF 데이터패스다. 이미 IPVS 인 클러스터를 당장 바꾸라는 뜻은 아니고, 기능 차이와 롤백을 확인한 전환 계획이 필요하다는 뜻이다

6. NodePort — 노드로 들어오는 문

6.1 노드 IP 와 공통 포트

  • NodePort Service 는 모든 노드의 IP 에 같은 포트 번호를 연다 — 어느 노드 IP 로 들어와도 그 Service 의 진입점이다
  • 그 노드에 백엔드 Pod 가 없어도 기본 정책에서는 다른 노드의 Pod 로 전달된다 — “NodePort 는 Pod 가 있는 노드에서만 된다” 는 생각이 틀리는 이유다
  • 기본 포트 범위는 30000–32767 이고, 바꾸는 플래그 --service-node-port-range 는 kube-apiserver 의 것이다 — 책에 적힌 kube-controller-manager 의 옵션이 아니다
  • nodePort 를 생략하면 빈 포트가 배정된다 — 직접 지정한 포트가 이미 쓰이거나 범위 밖이면 생성 요청이 실패한다
# 07-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-nodeport
  namespace: netlab
spec:
  type: NodePort
  selector:
    app: netdemo
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: http
      nodePort: 30080     # 실습값 — 충돌하면 이 줄을 지우고 자동 배정에 맡긴다
kubectl apply -f 07-nodeport.yaml
kubectl -n netlab get svc web-nodeport -o wide          # PORT(S) 가 80:30080/TCP, CLUSTER-IP 도 함께 있다
kubectl get nodes -o wide                                # INTERNAL-IP 열이 노드 주소다
  • kind 의 노드는 컨테이너라 노드의 INTERNAL-IP(172.18.0.x)가 호스트에서 바로 라우팅되지 않을 수 있다 — macOS 와 Windows 의 Docker Desktop 이 그렇다. localhost:30080 이 열렸다고 가정하지 않는다
  • 노드 안에서 시험하면 확실하다 — docker exec cn4-worker curl -s http://172.18.0.3:30080/ 처럼 노드 컨테이너 안에서 다른 노드의 IP 로 부른다. 노드 안에 curl 이 없으면 wget -qO- 를 쓴다

6.2 externalTrafficPolicy Cluster 와 Local

externalTrafficPolicy: Cluster (기본)바깥 클라이언트203.0.113.10cn4-worker2172.18.0.4netdemo Pod 없음:30080 nat 규칙cn4-worker172.18.0.3netdemo10.244.1.5:80:30080 nat 규칙1dst 172.18.0.4:300802다른 노드의 Pod 도 후보src 가 172.18.0.4 로 바뀜(SNAT)노드 한 홉 더externalTrafficPolicy: Local바깥 클라이언트203.0.113.10cn4-worker2172.18.0.4netdemo Pod 없음:30080 nat 규칙cn4-worker172.18.0.3netdemo10.244.1.5:80:30080 nat 규칙1dst 172.18.0.4:30080✗이 노드엔 Pod 가 없다 → 실패worker 로 들어온 것만 성공src 203.0.113.10 보존Cluster 는 어느 노드로 들어와도 닿지만 홉이 늘고 클라이언트 IP 가 노드 IP 로 바뀔 수 있다. Local 은 IP 를 지키고 홉을 줄이지만 Pod 없는 노드로 온 요청은 버려진다Local 을 쓰면 외부 LB 의 노드 헬스체크가 'Pod 있는 노드' 만 고르도록 함께 맞춘다. 범위 30000–32767 은 kube-apiserver 의 --service-node-port-range
같은 30080 이 모든 노드에 열린다 — 어느 노드로 들어왔을 때 어디까지 보내 주는가를 정책이 정한다
항목Cluster (기본)Local
후보 endpoint클러스터 전체들어온 노드의 로컬 endpoint 만
클라이언트 IP경로와 구현에 따라 노드 IP 로 바뀜(SNAT)보존에 유리 — 외부 LB 의 동작도 함께 확인
추가 홉생길 수 있음Service 전달에서 원격 노드 홉 없음
로컬 endpoint 가 없는 노드다른 노드로 보냄그 노드로 들어온 요청은 실패
주의계층별 NAT, 장기 연결의 편향노드별 트래픽 불균등, LB 헬스체크
spec:
  type: NodePort
  externalTrafficPolicy: Local
  • Local 을 고르는 이유는 둘이다 — 앱이 클라이언트 IP 를 봐야 하거나, 노드를 한 번 더 건너는 비용을 피하고 싶을 때
  • 그 대신 외부 LB 가 Pod 있는 노드만 고르게 헬스체크를 맞춰야 한다 — 그렇지 않으면 Pod 없는 노드로 간 요청이 조용히 버려진다
  • 전통적인 iptables 환경에서는 노드IP:NodePort → Service 규칙 → PodIP:targetPort 로 바뀌고, Pod 가 다른 노드면 CNI 의 노드 간 경로를 탄다 — nftables 와 Cilium eBPF 는 내부 처리가 다르지만 API 수준의 뜻은 같다

6.3 internalTrafficPolicy 와 trafficDistribution

필드값성격로컬에 없으면
externalTrafficPolicyCluster · Local바깥에서 들어온 트래픽의 후보Local: 실패
internalTrafficPolicyCluster · Local안에서 보낸 트래픽의 후보Local: 실패
trafficDistributionPreferSameZone · PreferSameNode가까운 곳을 선호다른 곳으로 간다
spec:
  internalTrafficPolicy: Cluster
  trafficDistribution: PreferSameZone
  • 두 정책(Policy)은 엄격하다 — Local 이면 그 노드에 endpoint 가 없을 때 연결이 실패한다
  • 분배(Distribution)는 선호다 — 같은 zone 이나 같은 노드에 endpoint 가 있으면 그쪽을 고르고, 없으면 다른 곳으로 간다. PreferClose 는 PreferSameZone 의 deprecated 별칭이다
  • 둘이 같이 있으면 엄격한 정책이 선호를 이긴다
  • 선호가 가능한 이유가 4.3 의 nodeName 과 zone 이다 — 명단에 위치가 적혀 있어 데이터패스가 가까운 것을 고를 수 있다

6.4 장기 연결과 sessionAffinity

  • Service 의 분산은 연결 단위다 — HTTP keep-alive 와 gRPC 의 HTTP/2 는 연결 하나에 많은 요청을 싣는다. Pod 셋이 있어도 요청은 한 Pod 에 몰릴 수 있다
  • sessionAffinity: ClientIP 는 같은 클라이언트 IP 의 연결을 일정 시간 같은 백엔드에 붙이는 설정이다 — 요청별 균등 분배가 아니라 그 반대 방향의 설정이다
  • 요청 단위로 나누려면 자리가 다르다 — gRPC 채널의 로드밸런싱 정책과 Headless DNS(8.3), 또는 서비스 메시의 요청 수준 분산이다. 어느 쪽도 Kubernetes 가 아니라 클라이언트 라이브러리나 프록시의 설정이다
  • 100 연결이 넷에 어떻게 나뉘는지의 셈과 conntrack 의 역할은 04-2 3.5 절에 있다

6.5 NodePort 의 자리

  • 노드 IP 가 바뀌거나 노드가 늘면 클라이언트나 외부 LB 가 노드 목록을 따라가야 한다 — 노드 주소는 Service 가 숨겨 주는 대상이 아니다
  • 방화벽에서 NodePort 범위를 열어 두는 범위도 함께 관리한다
  • Pod 사이 통신에 NodePort 는 필요하지 않다 — 안에서는 ClusterIP, 밖으로는 LoadBalancer 나 Gateway · Ingress 가 기본 선택이다
  • 노드 주소를 직접 쓰는 방식은 시험, 특수 환경, 외부 LB 의 하위 수단으로 이해한다

7. Datapath — 구현을 판별하는 법

7.1 같은 API 다른 구현

구현데이터패스규칙을 읽는 명령상태
kube-proxy iptablesnetfilter nat 규칙iptables-save -t nat, conntrack -Lv1.37 기본값
kube-proxy nftablesnftables 규칙nft list rulesetv1.33 Stable, 커널 5.13 이상
kube-proxy ipvsIPVS 가상 서버 표ipvsadm -Lnv1.35 deprecated
Cilium kube-proxy replacementeBPF 프로그램 · 서비스 맵cilium status, cilium service list, Hubble별도 전제 조건
  • kube-proxy 라는 이름 때문에 패킷이 그 프로세스를 지난다고 생각하기 쉽다 — Linux 의 kube-proxy 는 Service 와 EndpointSlice 를 지켜보며 커널 규칙을 설정하고, 패킷은 커널이 나른다. 네 모드의 차이는 04-2 3.4 절
  • 모드를 비워 두면 iptables 다 — 문서는 기본값이 nftables 로 바뀔 수 있다고 예고한다. 업그레이드 때 뜻하지 않게 모드가 바뀌지 않도록 설정에 적어 둔다
  • nftables 로 옮기기 전에 확인할 것은 NodePort 의 주소 바인딩, localhost 접근, 네트워크 장비와의 호환성이다

7.2 클러스터에서 판별하기

① kube-proxy DaemonSet 이 있나kubectl -n kube-system get dskubectl -n kube-system get cm kube-proxy -o yaml있다② ConfigMap 의 modemode: "" → iptables (v1.37 기본)mode: nftables | ipvsiptablesiptables-save -t natnftablesnft list rulesetipvsipvsadm -Ln · deprecated없다③ CNI 가 대신하나cilium status | grep KubeProxyReplacementkubectl -n kube-system get pods -l k8s-app=ciliumTrueeBPF 맵·프로그램KUBE-SVC-* 체인이 없는 것이 정상cilium service list · Hubble둘 다 없다Service 를 나르는 구현이 없다 — DNS 는 답해도 가상 IP 로는 아무도 못 간다API 는 같다어느 구현이든 Service ·EndpointSlice · 정책의 뜻은같다. 다른 것은 규칙을읽는 명령과 저장 위치다체인 이름(KUBE-SVC-…)은구현의 흔적이지 계약이 아니다kube-proxy DaemonSet 이 안 보여도 Service 가 고장난 것은 아니다 — 대체 데이터패스가 있는지 먼저 본다. 모드는 기본값에 맡기지 않고 적어 둔다nftables 모드는 v1.33 Stable, 커널 5.13 이상. 기본값이 nftables 로 바뀔 수 있다고 문서가 예고한다
규칙을 읽기 전에 누가 썼는지를 먼저 묻는다 — iptables-save 에 체인이 없다는 사실은 nftables 나 eBPF 환경에서는 아무 증거도 아니다
kubectl -n kube-system get daemonsets                            # kube-proxy 가 있나, cilium 이 있나
kubectl -n kube-system get configmap kube-proxy -o yaml | grep -A1 '^    mode'
kubectl -n kube-system get pods -l k8s-app=kube-proxy -o wide
kubectl -n kube-system get pods -l k8s-app=cilium -o wide
# Cilium CLI 가 있으면: cilium status | grep KubeProxyReplacement
# kind 노드 안의 규칙 — 조회만 한다
kind get nodes --name cn4
docker exec cn4-worker iptables-save -t nat | grep -c KUBE-SVC       # iptables 모드면 체인이 보인다
docker exec cn4-worker nft list ruleset | head                        # nftables 모드면 여기에 있다
  • kube-proxy DaemonSet 이 안 보여도 Service 가 멈췄다고 단정하지 않는다 — 대체 데이터패스가 설치돼 있을 수 있다
  • KUBE-SVC-*, KUBE-SEP-* 체인 이름은 iptables 구현을 이해하는 과거 예제다 — 해시 접미사와 체인 이름을 API 의 계약으로 다루지 않는다. nftables 나 Cilium 환경에서 이 체인이 없는 것은 장애가 아니다
  • 노드 이미지에 iptables, nft, ip 가 없으면 별도 진단 컨테이너나 호스트 도구가 필요하다

7.3 Pod 의 네트워크 네임스페이스와 veth

  • 책은 ip netns list, ip netns pids, ps 로 pause 컨테이너와 웹 서버 프로세스를 맞춰 본다 — 배우려는 것은 Pod 안의 인터페이스와 노드 쪽 veth, 네트워크 네임스페이스의 연결이다. 그 구조는 04-1 3.1 절과 5.3 절에 있다
  • ip netns list 에 모든 Pod 의 네임스페이스가 이름으로 나온다는 보장은 없다 — 런타임과 CNI 가 만든 네임스페이스가 /var/run/netns 에 바인드 마운트되지 않을 수 있다
  • pause 컨테이너는 같은 Pod 의 컨테이너들이 네트워크 네임스페이스를 공유하도록 그 네임스페이스를 붙들고 있는 기반이다 — 모든 애플리케이션 프로세스의 Linux 부모 프로세스라는 설명은 틀렸다. 부모 관계와 네임스페이스 공유는 다른 개념이다
POD=$(kubectl -n netlab get pod -l app=netdemo -o jsonpath='{.items[0].metadata.name}')
kubectl -n netlab exec "$POD" -- cat /proc/net/route           # nginx 이미지에는 iproute2 가 없어 ip addr 는 실패할 수 있다
# 노드 쪽에서는 crictl inspectp, nsenter 로 네임스페이스에 들어간다

7.4 conntrack · SNAT · NetworkPolicy

  • Service 전달의 기본은 목적지를 Pod IP 로 고쳐 쓰는 DNAT 다 — 응답 패킷은 conntrack 의 항목을 보고 반대 방향으로 되돌려 쓴다
  • 출발지를 보존할지 노드 IP 로 바꿀지(SNAT)는 6.2 의 정책과 데이터패스 구성이 정한다 — 패킷을 추적할 때는 클라이언트 → 진입 노드 → 데이터패스 → endpoint → 응답의 양방향을 본다
  • NetworkPolicy 는 Service 를 기준으로 자동 허용되는 규칙이 아니다 — 정책은 Pod 레이블 기준이고, CNI 가 구현해야 집행된다. 그래서 DNS 가 답하고 EndpointSlice 가 차 있어도 TCP 연결이 실패할 수 있다 — 04-3 3.5 절
  • 정책이 적용되는 위치가 NAT 의 앞인지 뒤인지, 메시의 L7 처리가 끼어 있는지는 구현마다 다르다

8. No Virtual IP — Headless 와 ExternalName

8.1 clusterIP: None 의 뜻

Service web — ClusterIP 10.96.12.34노드 A클라이언트10.244.0.9web ?호스트 방 커널dst 10.96.12.34 → 10.244.2.9 DNATCoreDNS PodA 레코드 하나 — 10.96.12.341커널이 고른다규칙의 확률로 셋 중 하나클라이언트는 가상 IP 만 안다노드 Bnetdemo10.244.1.5노드 Cnetdemo10.244.2.9netdemo10.244.2.42Service web-headless — clusterIP: None노드 A클라이언트10.244.0.9web-headless ?호스트 방 커널DNAT 없음 · 라우팅만CoreDNS PodA 레코드 셋 — Pod IP 그대로1클라이언트가 고른다셋 중 10.244.1.5 를 골라 직접규칙도 가상 IP 도 없다노드 Bnetdemo10.244.1.5노드 Cnetdemo10.244.2.9netdemo10.244.2.42같은 Pod 셋, 같은 노드 배치 — ClusterIP 는 노드 A 의 커널이 고르고(DNAT), Headless 는 클라이언트가 고른다. 고르는 자리만 다르다DNS 응답의 순서는 분산을 보장하지 않는다. 첫 주소만 쓰거나 오래 캐시하는 클라이언트는 한 Pod 에만 붙는다
Headless 는 다섯 번째 type 이 아니다 — clusterIP: None 한 줄이 '고르는 일' 을 커널에서 클라이언트로 옮긴다
# 11-headless.yaml — 5.3 의 Pod 셋을 다른 방식으로 부른다
apiVersion: v1
kind: Service
metadata:
  name: web-headless
  namespace: netlab
spec:
  clusterIP: None
  selector:
    app: netdemo
  ports:
    - name: http
      port: 80
      targetPort: http
kubectl apply -f 11-headless.yaml
kubectl -n netlab get svc web web-headless -o wide              # 하나는 CLUSTER-IP, 하나는 None
kubectl -n netlab run dns-check --rm -i --restart=Never --image=busybox:1.36 -- \
  nslookup web-headless.netlab.svc.cluster.local           # Pod IP 셋
kubectl -n netlab get endpointslices -l kubernetes.io/service-name=web-headless -o yaml
  • clusterIP: None 이면 가상 IP 가 할당되지 않고 kube-proxy 도 이 Service 의 규칙을 쓰지 않는다 — selector 가 있으면 DNS 가 준비된 endpoint 의 A/AAAA 레코드를 그대로 답한다
  • EndpointSlice 는 똑같이 생긴다 — 명단은 있고, 명단을 읽어 규칙으로 바꾸는 쪽만 없다
  • DNS 응답의 순서는 분산을 보장하지 않는다 — 첫 주소만 쓰는 클라이언트, 오래 캐시하는 클라이언트는 한 Pod 에만 붙는다. 클라이언트 쪽 분산을 원하면 주소를 다시 읽고 재접속하는 전략을 클라이언트에 설계한다

8.2 StatefulSet 의 Headless 와 Deployment 의 Headless

  • Headless Service 는 Deployment 에도 붙는다 — web-headless 가 답하는 것은 Pod IP 목록이다. 백엔드 목록의 발견이 목적이다
  • StatefulSet 의 identity-0.identity-headless… 는 개별 구성원을 이름으로 찾는다 — 안정된 정체성이 목적이다. 같은 clusterIP: None 이지만 쓰는 이름이 다르다
  • 구성원 DNS 가 있어도 누가 쓰기 리더인지는 정해지지 않는다 — primary 와 replica 를 구별해야 하는 DB 는 애플리케이션이나 Operator 가 리더 상태와 failover 를 관리하고, 클라이언트가 올바른 대상에 붙도록 구성한다

8.3 gRPC 와 클라이언트 선택

  • 책이 소개한 “클라이언트에서 백엔드 선택” 은 gRPC 에서 지금도 쓰이는 패턴이다 — Headless DNS 로 주소 목록을 받고 채널의 로드밸런싱 정책이 고른다
  • DNS 가 여러 A 레코드를 답한다고 요청이 균등해지지는 않는다 — gRPC 의 DNS resolver 가 여러 주소를 발견하는지, 채널에 어떤 정책(round_robin 등)을 붙였는지, 재해석과 실패 시 재시도를 어떻게 하는지에 따라 결과가 갈린다
  • gRPC 채널이 무조건 TCP 연결 하나라는 설명도 틀릴 수 있다 — 구현과 resolver 와 정책에 따라 채널 하나가 여러 연결(서브채널)을 관리한다
  • Service 의 L4 분배(연결 단위)와 gRPC 의 요청 단위 분배는 다른 층이다 — ClusterIP 로 gRPC 를 부르면 연결 하나가 한 Pod 에 붙고, 그 안의 모든 RPC 가 거기로 간다

8.4 ExternalName 과 CNAME

클러스터 cn4클러스터 밖클라이언트 Pod10.244.0.9remote-db ?CoreDNS PodCNAME 을 답한다1remote-db.netlab.svc.cluster.local → CNAME database.example.com2Service remote-dbtype: ExternalNameexternalName: database.example.com · ClusterIP 없음 · selector 없음 · Slice 없음상위 DNS밖의 이름을 푼다example.com ?A 198.51.100.20바깥 DB198.51.100.20CN: database.example.com인증서3경계를 넘어 198.51.100.20 으로 직접Service 도 kube-proxy 도 패킷에 관여하지 않는다ExternalName 은 DNS 별칭이다 — 이름 하나를 밖의 이름으로 바꿔 줄 뿐, 가상 IP 도 DNAT 도 없고 패킷은 클러스터 경계를 바로 넘는다HTTP Host 헤더와 TLS SNI 는 클라이언트가 부른 이름(remote-db…)으로 나간다 — 서버 인증서의 database.example.com 과 어긋날 수 있다
ExternalName 에 IP 문자열을 적는 것은 별칭이 아니라 오용이다 — 고정 IP 가 필요하면 selector 없는 Service 와 수동 EndpointSlice 를 쓴다
# 12-externalname.yaml
apiVersion: v1
kind: Service
metadata:
  name: remote-db
  namespace: netlab
spec:
  type: ExternalName
  externalName: database.example.com     # 설명용 예약 도메인 — 실제 DB 가 있다고 가정하지 않는다
kubectl apply -f 12-externalname.yaml
kubectl -n netlab get svc remote-db -o yaml                      # clusterIP 가 없다
kubectl -n netlab run dns-check --rm -i --restart=Never --image=busybox:1.36 -- \
  nslookup remote-db.netlab.svc.cluster.local              # canonical name = database.example.com
  • ExternalName 은 안의 Service 이름을 밖의 DNS 이름에 매핑한다 — selector 도 ClusterIP 도 없고, CoreDNS 가 CNAME 을 답한다. 패킷을 밖으로 프록시하거나 NAT 하지 않는다
  • 쓰는 자리는 기존 시스템을 옮길 때다 — 앱 설정에 외부 DNS 이름을 직접 넣는 대신 안의 이름을 쓰게 하고, 바깥 주소가 바뀌면 Service 한 줄만 고친다
  • 별칭이 풀렸다는 것과 앱이 연결됐다는 것은 다른 사실이다 — database.example.com 이 실제로 응답하는지는 DNS 가 보장하지 않는다
  • externalName 에 192.0.2.10 같은 IP 문자열을 적는 것은 IP 매핑 방법이 아니다 — 고정 IP 를 붙여야 하면 selector 없는 Service 와 수동 EndpointSlice(4.3) 를 쓴다

8.5 Host 헤더와 TLS 이름

  • HTTP 는 Host 헤더를, HTTPS 는 SNI 와 인증서의 호스트명 일치를 쓴다 — 클라이언트가 remote-db.netlab.svc.cluster.local 로 불렀다면 두 값이 그 이름으로 나간다
  • 외부 서버는 database.example.com 을 기대한다 — CNAME 은 이 차이를 고쳐 주지 않는다. 가상 호스팅 서버는 다른 사이트를 답하거나 거부하고, TLS 는 인증서 검증에서 실패한다
  • 그래서 ExternalName 을 모든 프로토콜에 투명한 프록시로 생각하지 않는다 — 클라이언트가 Host 와 SNI 를 바깥 이름으로 바꿔 보내도록 설정할 수 있을 때만 깔끔하게 쓸 수 있다
kubectl -n netlab delete -f 12-externalname.yaml -f 11-headless.yaml -f 07-nodeport.yaml
kubectl -n netlab delete -f 06-web.yaml -f 03-identity.yaml
kubectl delete namespace netlab                                  # 실습을 끝낼 때만

9. Discussion

9.1 어느 층이 비었나

선언1객체가 있나 · spec 이 맞나kubectl get svc,sts -o wide컨트롤러2Slice · status · DNS 답kubectl get endpointslicenslookup web데이터패스3노드 규칙 · 구현 판별iptables-save · nft · cilium앱4실제로 듣는 포트 · 응답LISTEN 포트 · wget -qO-객체가 있다 → 컨트롤러가 그 객체를 보고 다른 객체를 썼다 → 노드에 규칙이 있다 → 앱이 그 포트를 듣는다 — 끊어진 층에서 멈춘다
<pending> · 빈 Slice · 규칙 없음 · 연결 거부는 각각 다른 층의 증상이다 — 한 층의 증거로 다른 층을 판단하지 않는다
증상비어 있는 층먼저 볼 것
EXTERNAL-IP <pending>컨트롤러 — 외부 IP 를 줄 구현이 없다LB 컨트롤러 설치 여부, 주소 풀, Service 의 status
이름은 풀리는데 EndpointSlice 가 비었다컨트롤러 — selector 가 아무 Pod 도 못 골랐다Service 의 selector 와 Pod 의 레이블, 같은 네임스페이스인가
명단은 찼는데 전부 ready: false앱 — probe 가 통과하지 못했다Pod 의 conditions, readinessProbe 의 경로와 포트
명단은 ready 인데 가상 IP 로 연결이 안 된다데이터패스 — 규칙을 쓰는 쪽이 없거나 다른 것이다kube-proxy DaemonSet, 모드, Cilium 의 replacement
Pod IP 로는 되는데 Service 로는 안 된다데이터패스 또는 정책노드 규칙, NetworkPolicy 의 ingress
연결은 되는데 거부된다앱 — 그 포트를 듣는 프로세스가 없다targetPort 와 실제 LISTEN 포트
Headless 인데 한 Pod 에만 간다클라이언트 — 첫 주소만 쓰거나 캐시한다DNS 응답 전체, 클라이언트의 재해석 정책
  • 각 줄의 증거는 그 층에서만 유효하다 — iptables-save 에 체인이 없다는 사실은 nftables 환경에서는 아무것도 말하지 않고, EndpointSlice 가 찼다는 사실은 노드에 규칙이 있다는 뜻이 아니다
  • 같은 Service 를 두고 접속점은 넷이다 — 가상 IP, Pod IP, 노드 IP:NodePort, 외부 IP. 어느 주소에서 무엇을 시험했는지 적어야 결과를 비교할 수 있다
  • 선언에서 앱까지의 네 층은 LoadBalancer, Ingress, Gateway 에도 그대로 적용된다 — 거기서는 컨트롤러 층이 하나 더 두꺼워진다

핵심 메시지

객체는 요청서다
Service 가 있다는 것과 패킷이 간다는 것은 다른 사실이다. 사이에 컨트롤러와 노드의 규칙이 있고, 빠진 층의 모양으로 증상이 난다
이름은 고정, IP 는 아니다
StatefulSet 은 서수 이름과 그 이름에 붙는 PVC·DNS 를 지킨다. Pod IP 는 교체마다 바뀌고, 복제와 failover 는 앱의 몫이다
명단은 쪼개져 있다
EndpointSlice 는 100 개씩 나눠 전달 비용을 줄인다. 한 줄의 상태는 ready·serving·terminating 셋을 함께 읽어야 한다
문을 여는 것은 앱이다
port 는 Service 의 문, targetPort 는 Pod 의 문, containerPort 는 메모다. 연결이 닿는 기준은 프로세스가 실제로 듣는 포트다
Local 은 거래다
클라이언트 IP 와 홉 하나를 얻고, Pod 없는 노드로 온 요청을 잃는다. 선호(trafficDistribution)는 실패하지 않고, 정책(Policy)은 실패한다
누가 썼는지 먼저 묻는다
iptables·nftables·eBPF 는 같은 Service API 를 나른다. 체인 이름은 구현의 흔적이고, 없다는 사실은 다른 구현에서는 증거가 아니다
고르는 자리가 다르다
ClusterIP 는 커널이 고르고, Headless 는 클라이언트가 고른다. ExternalName 은 고르지 않고 이름만 바꿔 준다 — Host 와 SNI 는 따라가지 않는다

10. References

Service 의 네 type, Headless 가 clusterIP: None 이라는 것, port 와 targetPort 의 관계, sessionAffinity, ExternalName 이 CNAME 이라는 것과 그 주의, NodePort 범위와 --service-node-port-range 가 kube-apiserver 플래그라는 것은 Service 문서에 있다.
"For headless Services, a cluster IP is not allocated, kube-proxy does not handle these Services, and there is no load balancing or proxying done by the platform for them."
[해석] "Headless Service 에는 클러스터 IP 가 할당되지 않고, kube-proxy 가 다루지 않으며, 플랫폼이 해 주는 부하 분산이나 프록시가 없다." — 8.1 과 hl51 그림의 근거다. ExternalName 에 IP 를 적지 말라는 것, Host 헤더와 TLS 의 주의도 같은 문서의 ExternalName 절에 있다. 책의 "--service-node-port-range 는 kube-controller-manager 플래그" 는 kube-apiserver 레퍼런스와 어긋난다.
→ kubernetes.io — Service
→ kubernetes.io — kube-apiserver (--service-node-port-range · --service-cluster-ip-range)
EndpointSlice 의 세 조건, Slice 당 기본 100 개와 최대 1,000 개, 한 endpoint 가 잠시 두 Slice 에 겹칠 수 있다는 것, Endpoints API 가 v1.33 부터 deprecated 라는 것은 EndpointSlice 문서에 있다.
"The ready condition is a condition that maps to a Pod's Ready condition. ... serving is identical to the ready condition, except it does not account for terminating states."
[해석] "ready 는 Pod 의 Ready 조건에 대응한다. serving 은 종료 중인 상태를 따지지 않는다는 점만 빼면 ready 와 같다." — 4.4 와 cd51 그림의 근거다. 책의 apiVersion discovery.k8s.io/v1beta1 과 Topology 필드는 Stable 승격(v1.21) 뒤의 스키마와 다르다.
→ kubernetes.io — EndpointSlices
StatefulSet 의 서수 이름, OrderedReady 와 Parallel, RollingUpdate 가 높은 서수부터 간다는 것, serviceName 과 구성원 DNS 이름의 형식, 잘못된 업데이트 뒤의 수동 삭제 복구는 StatefulSet 문서에 있다.
"Each Pod in a StatefulSet derives its hostname from the name of the StatefulSet and the ordinal of the Pod. The pattern for the constructed hostname is $(statefulset name)-$(ordinal)."
[해석] "StatefulSet 의 각 Pod 는 StatefulSet 이름과 서수로 호스트명을 얻는다 — $(statefulset name)-$(ordinal)." — 3.1 과 hd51 그림의 첫 토막의 근거다. 구성원 DNS 가 Headless Service 를 거쳐 $(podname).$(service).$(namespace).svc.$(cluster domain) 이 된다는 것도 같은 문서에 있다.
→ kubernetes.io — StatefulSets
→ kubernetes.io — Persistent Volumes
kube-proxy 가 데이터 경로에 있지 않다는 것, 모드별 상태(iptables 기본, nftables Stable v1.33 과 커널 5.13 요구, IPVS deprecated v1.35), externalTrafficPolicy 와 internalTrafficPolicy 의 Local 이 실패할 수 있다는 것, trafficDistribution 의 값과 PreferClose 가 별칭이라는 것은 Virtual IPs 문서에 있다.
"If there are no local endpoints, packets sent to the node are dropped, so you can rely on the correct client IP in any packet processing rules you might apply a packet that make it through to the endpoint."
[해석] "로컬 endpoint 가 없으면 그 노드로 온 패킷은 버려진다 — 그래서 endpoint 까지 온 패킷의 클라이언트 IP 는 믿을 수 있다." — 6.2 와 np51 그림의 Local 쪽 근거다. 책의 "대규모에서는 IPVS" 는 deprecated 이후 적용하지 않는다.
→ kubernetes.io — Virtual IPs and Service Proxies
→ kubernetes.io — Service Internal Traffic Policy
→ docs.cilium.io — Kubernetes Without kube-proxy
Service 와 Pod 의 DNS 이름 형식, Headless Service 가 A/AAAA 레코드를 Pod IP 로 답한다는 것, publishNotReadyAddresses 가 준비 전 Pod 를 DNS 에 넣는다는 것은 DNS 문서에 있다. pause 가 네트워크 네임스페이스를 붙드는 샌드박스라는 것은 CRI 와 Pod 문서에 있다.
"For a headless Service, this resolves to the set of IPs of all of the Pods selected by the Service."
[해석] "Headless Service 의 이름은 그 Service 가 고른 모든 Pod 의 IP 집합으로 풀린다." — 3.3 과 8.1 의 근거다. 책의 "pause 가 모든 앱 프로세스의 부모 프로세스" 는 네임스페이스 공유와 프로세스 부모 관계를 섞은 서술이다.
→ kubernetes.io — DNS for Services and Pods
→ kubernetes.io — Pods