ClusterIP (기본)
클러스터 내부만. 가상 IP 하나를 준다.
변하는 Pod 앞의 고정된 주소
Service는 고정된 이름과 IP를 주고, 라벨 셀렉터에 맞는 Pod들에게 부하를 나눠 보낸다.
그런데 Service는 프로세스가 아니다. 어디서도 돌고 있지 않다. 이 점이 처음에 가장 헷갈린다 — 바로 아래에서 정체를 본다.
답부터 말하면 노드마다 도는 kube-proxy가 커널에 심어 둔 규칙이다 (아키텍처의 노드 컴포넌트). 시작하기 전에의 축이 여기에도 그대로 적용된다 — Service·EndpointSlice라는 선언을 kube-proxy가 watch하고 있다가, 커널의 NAT 규칙이라는 실제 상태를 선언에 맞게 계속 고쳐 놓는 루프다.
10.96.0.10:80이면 → 10.244.1.5:8080 또는 10.244.2.7:8080 으로
DNAT(Destination NAT — 목적지 주소를 바꿔치기)”# 노드에서 직접 확인 (iptables 모드)sudo iptables -t nat -L KUBE-SERVICES -n | headsudo ipvsadm -Ln # ipvs 모드일 때위 시퀀스는 목적지 주소가 바뀌는 데까지만 보여 준다. DNAT은 패킷의 목적지를 Pod IP로 바꿔치기할 뿐, 바뀐 주소까지 실어 나르지는 않는다. 그 배달은 CNI(Container Network Interface) 플러그인 — Calico·Cilium·Flannel — 이 미리 깔아 둔 Pod 네트워크의 몫이다. Service 하나가 동작하려면 서로 다른 두 층이 맞물려야 한다.
| 층 | 대역 | 실현하는 주체 | 하는 일 |
|---|---|---|---|
| Service 층 | Service CIDR — apiserver의 --service-cluster-ip-range | kube-proxy가 커널 규칙으로 | 가상 IP를 Pod IP로 번역(DNAT) |
| Pod 층 | Pod CIDR — controller-manager의 --cluster-cidr 또는 CNI 자체 IPAM | CNI 플러그인이 인터페이스·라우팅으로 | Pod IP 할당과 노드 사이 배달 |
패킷 하나의 여정으로 보면 —
10.96.0.10:80(ClusterIP)으로 보낸다. Service CIDR의 가상 주소다10.244.1.5:8080으로 바꾼다.
Service 층의 일은 여기서 끝난다두 대역이 겹치면 안 되는 이유가 여기 있다 — 커널 입장에서 “번역해야 할 가상 주소”와 “그대로 배달할 진짜 주소”가 대역으로 구분되어야 하기 때문이다. CNI가 IP를 어떻게 주고 누가 실행하는지는 확장 CNI 절에서 본다. Cilium처럼 kube-proxy의 역할까지 eBPF로 흡수해 kube-proxy 없이 도는 CNI도 있지만, 시험 환경은 표준 구성(kube-proxy DaemonSet + CNI)이다.
ClusterIP (기본)
클러스터 내부만. 가상 IP 하나를 준다.
NodePort
클러스터 외부에서. 모든 노드의 특정 포트를 연다.
LoadBalancer
클러스터 외부에서. 클라우드 LB를 프로비저닝한다.
ExternalName
프록시하지 않는다. DNS CNAME만 반환한다.
포함 관계다. NodePort는 ClusterIP를 포함하고, LoadBalancer는 NodePort를 포함한다. LoadBalancer를 만들면 ClusterIP도, NodePort도 함께 생긴다.
apiVersion: v1kind: Servicemetadata: name: web-svcspec: type: ClusterIP selector: app: web # 이 라벨을 가진 Pod들에게 보낸다 ports: - name: http protocol: TCP port: 80 # Service가 여는 포트 targetPort: 8080 # Pod의 포트kubectl expose deploy web --port=80 --target-port=8080 --name=web-svckubectl create svc clusterip web-svc --tcp=80:8080시험 중 YAML을 직접 써야 하면 공식 Service 문서의 예시를 복사해 이름·셀렉터·포트만 바꾸는 게 가장 빠르다.
port = Service의 포트 · targetPort = Pod의 포트 · nodePort = 노드의 포트
# Podcontainers: - name: app ports: - name: http-api # 이름을 붙인다 containerPort: 8080# Serviceports: - port: 80 targetPort: http-api # 숫자 대신 이름Service의 spec에는 셀렉터라는 조건만 있지, 실제 Pod IP는 한 줄도 없다. 그 조건을 “지금 이 순간의 IP 목록”으로 계속 번역해 주는 무언가가 없으면 kube-proxy는 규칙을 만들 수 없다 — 그 번역 결과를 담는 오브젝트가 엔드포인트다.
kubectl get endpoints web-svc# NAME ENDPOINTS AGE# web-svc 10.244.1.5:8080,10.244.2.7:8080 5m
kubectl get endpointslices -l kubernetes.io/service-name=web-svckubectl describe svc web-svc # Endpoints 줄을 본다ENDPOINTS가 비어 있으면 Service는 아무 데도 못 보낸다Endpoints는 오브젝트 하나에 모든 IP를 담았다 → Pod이 수천 개면 갱신 비용이 폭발EndpointSlice는 100개씩 쪼개서 담는다. 지금은 이쪽이 실제 데이터 소스다 —
시험 커리큘럼에도 명시된 항목이니 두 이름의 관계는 알아둘 가치가 있다kubectl get endpoints는 여전히 동작한다 (호환용으로 유지)kubectl get endpointslices# NAME ADDRESSTYPE PORTS ENDPOINTS AGE# web-svc-x7k2p IPv4 8080 10.244.1.5,10.244.2.7 5m
kubectl get endpointslice web-svc-x7k2p -o yaml# endpoints:# - addresses: ["10.244.1.5"]# conditions: { ready: true, serving: true, terminating: false }# nodeName: node01conditions.ready가 readinessProbe의 결과다. 이 한 줄이 트래픽을 켜고 끈다.
나머지 둘은 종료 처리용이다 — terminating은 Pod이 삭제 중이라는 표시,
serving은 종료 중이어도 아직 응답은 가능하다는 표시다. 종료 중인 Pod에도
트래픽을 흘리는 특수한 경우에만 쓰이니 이름만 알아두면 된다.
spec: type: NodePort selector: app: web ports: - port: 80 targetPort: 8080 nodePort: 30080 # 생략하면 30000-32767 중에서 자동 할당모든 노드가 그 포트를 연다. Pod이 없는 노드로 가도 전달된다.
--service-node-port-range로 변경)kubectl create svc nodeport web-svc --tcp=80:8080 --node-port=30080curl http://<노드IP>:30080# LoadBalancerspec: type: LoadBalancer selector: app: web ports: - port: 80 targetPort: 8080status.loadBalancer.ingress에 주소를 채운다EXTERNAL-IP가 영원히 <pending>
(MetalLB 같은 것을 설치해야 한다 — 온프레미스에서 LB 프로비저닝을 대신해 주는
오픈소스다. 이름만 알아두면 된다)# ExternalName — 프록시하지 않는다. DNS CNAME만 준다spec: type: ExternalName externalName: db.example.com클러스터 안에서 my-db.default.svc.cluster.local 을 조회하면
db.example.com 으로 CNAME(Canonical Name — “이 이름은 저 이름의 별칭”이라는
DNS 레코드)이 돌아온다. 셀렉터도 엔드포인트도 없다.
실무에서도 드물게 쓰는 타입이다 — 깊게 팔 필요는 없고, “프록시 없이 DNS 별칭만 준다”는 한 줄이면 충분하다.
apiVersion: v1kind: Servicemetadata: name: external-dbspec: ports: - port: 5432 targetPort: 5432# selector 없음 → 엔드포인트를 직접 만든다---apiVersion: discovery.k8s.io/v1kind: EndpointSlicemetadata: name: external-db-1 labels: kubernetes.io/service-name: external-db # ← 이 라벨로 연결된다addressType: IPv4ports: - port: 5432endpoints: - addresses: ["192.168.10.50"]external-db라는 이름으로 부를 수 있다시험에 자주 나오는 패턴은 아니다 — “셀렉터 대신 수동 EndpointSlice로도 Service를 채울 수 있다”는 것만 기억해 두면 된다.
지금까지는 “여러 Pod을 IP 하나 뒤에 감춘다”가 장점이었다. 그런데 데이터베이스처럼 어느 인스턴스인지가 중요한 앱은 그 감춤이 오히려 방해가 된다 — 리더에게만 쓰기를 보내야 하는데 커널이 아무 Pod이나 고르기 때문이다. 그래서 가상 IP 없이 Pod IP 목록을 그대로 돌려주는 모드가 따로 있다.
DNS가 ClusterIP 하나를 준다. 어느 Pod으로 갈지는 커널이 정한다.
spec: clusterIP: None selector: app: db ports: - port: 5432쓰는 곳
headless에 셀렉터까지 없는 조합이면 — 앞 절의 수동 EndpointSlice와 합쳐진 경우 — DNS는 수동으로 등록한 IP 목록을 그대로 반환한다.
| 모드 | 방식 | 특징 |
|---|---|---|
| iptables | NAT 규칙 체인 | 기본값. 서비스가 많으면 규칙이 선형적으로 늘어난다 |
| ipvs | 커널 L4 로드밸런서 | 대규모에 유리. 여러 분배 알고리즘(rr 라운드 로빈 · lc 최소 연결 · sh 소스 해싱…) |
| nftables | iptables의 후속 | 신규 클러스터용. 성능 개선 |
kubectl get ds kube-proxy -n kube-systemkubectl get cm kube-proxy -n kube-system -o yaml | grep -i modekubectl logs -n kube-system -l k8s-app=kube-proxy --tail=20어느 모드든 동작은 같다. 트래픽 경로가 아니라 규칙을 심는 방식이 다를 뿐이다.
시험에서 모드별 내부 구현까지 파고들지는 않는다 — 위 명령으로 지금 클러스터가 어느 모드인지 확인할 수 있으면 충분하다.
sessionAffinity — 기본값은 매 요청을 아무 Pod에나 분배하는 것이라,
로그인 세션처럼 Pod 안에 상태가 남는 앱은 요청이 다른 Pod으로 가면 깨진다.
같은 클라이언트를 같은 Pod에 고정하고 싶을 때 ClientIP를 준다.
시험에서 무게가 실리는 쪽은 아래 externalTrafficPolicy다 —
sessionAffinity는 이런 필드가 있다는 것만 알아두면 된다.
spec: sessionAffinity: ClientIP # None(기본) | ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800
externalTrafficPolicy: Local # Cluster(기본) | LocalexternalTrafficPolicy (NodePort/LoadBalancer에만 해당)
어느 노드로 와도 다른 노드의 Pod으로도 보낸다. 부하는 고르지만 소스 IP가 SNAT(Source NAT — 출발지 주소를 바꿔치기)로 가려진다.
그 노드의 Pod에만 보낸다. 소스 IP가 보존되지만 Pod 없는 노드는 응답하지 않는다.
spec: selector: app: web ports: - name: http # 포트가 2개 이상이면 name이 필수다 port: 80 targetPort: 8080 - name: https port: 443 targetPort: 8443 - name: metrics port: 9090 targetPort: 9090 protocol: TCPname을 반드시 줘야 한다 (하나면 생략 가능)port.name에서 참조된다. SRV 쪽은 이름만 알아두면 된다 —
실제로 쓸 일은 Ingress·NetworkPolicy에서 포트를 이름으로 가리킬 때다kubectl create svc clusterip web-svc --tcp=80:8080 --tcp=443:8443Service가 존재하고 셀렉터가 맞는가
kubectl get svc web-svckubectl describe svc web-svc # Selector / Endpoints 확인엔드포인트가 채워졌는가 — ★ 여기서 대부분 갈린다
kubectl get endpoints web-svcPod 라벨이 셀렉터와 일치하는가
kubectl get pods -l app=web --show-labelsPod이 Ready인가 — readinessProbe 실패면 엔드포인트에서 빠진다
kubectl get pods -l app=webPod에 직접 붙어보기 — Service를 건너뛴다
kubectl run tmp --image=busybox:1.36 --rm -it --restart=Never -- \ wget -qO- http://10.244.1.5:8080Service를 통해 붙어보기
kubectl run tmp --image=busybox:1.36 --rm -it --restart=Never -- \ wget -qO- http://web-svc.default.svc.cluster.local| 결과 | 원인 |
|---|---|
| Pod IP 직접도 안 된다 | 앱 문제 — 포트를 안 열었거나 죽었다 |
| Pod IP는 되는데 ClusterIP가 안 된다 | 엔드포인트 비어 있음 또는 kube-proxy 문제 |
| ClusterIP는 되는데 이름이 안 된다 | DNS 문제 — CoreDNS를 본다 (DNS) |
| 다른 네임스페이스에서만 안 된다 | FQDN(Fully Qualified Domain Name — 네임스페이스까지 붙인 완전한 이름, DNS) 필요 또는 NetworkPolicy (NetworkPolicy) |
| 외부에서만 안 된다 | NodePort/LB 설정 또는 방화벽 |
port / targetPort / nodePort 세 포트를 구분하라. targetPort 누락이 단골 실수kubectl get endpoints가 진단의 1번이다. 비었으면 라벨 또는 Ready 문제conditions.ready가 readinessProbe 결과다clusterIP: None) 는 Pod IP를 직접 준다 — StatefulSet의 짝externalTrafficPolicy: Local