ClusterIP (기본)
클러스터 내부만. 가상 IP 하나를 준다.
변하는 Pod 앞의 고정된 주소
Service는 고정된 이름과 IP를 주고, 라벨 셀렉터에 맞는 Pod들에게 부하를 나눠 보낸다.
그런데 Service는 프로세스가 아니다. 어디서도 돌고 있지 않다. 이 점이 처음에 가장 헷갈린다 — 바로 아래에서 정체를 본다.
sequenceDiagram
participant API as kube-apiserver
participant KP as kube-proxy · 노드마다
participant K as 노드 커널 (iptables/ipvs)
participant C as 클라이언트 Pod
API->>KP: Service · EndpointSlice watch 이벤트
KP->>K: DNAT 규칙 갱신<br/>10.96.0.10:80 → 10.244.1.5:8080 …
Note over KP: 규칙만 심고 빠진다
C->>K: 10.96.0.10:80 으로 패킷
K->>K: 커널이 직접 DNAT
K-->>C: Pod 이 응답
Note over C,K: 트래픽은 kube-proxy 를 통과하지 않는다
10.96.0.10:80이면 → 10.244.1.5:8080 또는 10.244.2.7:8080 으로 DNAT”# 노드에서 직접 확인 (iptables 모드)sudo iptables -t nat -L KUBE-SERVICES -n | headsudo ipvsadm -Ln # ipvs 모드일 때ClusterIP (기본)
클러스터 내부만. 가상 IP 하나를 준다.
NodePort
클러스터 외부에서. 모든 노드의 특정 포트를 연다.
LoadBalancer
클러스터 외부에서. 클라우드 LB를 프로비저닝한다.
ExternalName
프록시하지 않는다. DNS CNAME만 반환한다.
포함 관계다. NodePort는 ClusterIP를 포함하고, LoadBalancer는 NodePort를 포함한다. LoadBalancer를 만들면 ClusterIP도, NodePort도 함께 생긴다.
flowchart LR
E["외부 클라이언트"] --> LB["LoadBalancer<br/>클라우드 LB"]
LB --> NP["NodePort<br/>노드IP:30080"]
NP --> CI["ClusterIP<br/>10.96.0.10:80"]
CI --> P1["Pod :8080"]
CI --> P2["Pod :8080"]
IN["내부 Pod"] --> CI
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef ext fill:#fef3c7,stroke:#d97706,color:#78350f
classDef pod fill:#dcfce7,stroke:#16a34a,color:#14532d
class CI key
class LB,NP ext
class P1,P2 pod
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:8080flowchart LR
EXT["외부 클라이언트"] -->|"nodePort<br/>30080"| N["노드"]
N -->|"port<br/>80"| S["Service<br/>10.96.0.10"]
INT["내부 Pod"] -->|"port<br/>80"| S
S -->|"targetPort<br/>8080"| P["Pod<br/>10.244.1.5"]
classDef np fill:#fef3c7,stroke:#d97706,color:#78350f
classDef pt fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef tp fill:#dcfce7,stroke:#16a34a,color:#14532d
class N np
class S pt
class P tp
port = Service의 포트 · targetPort = Pod의 포트 · nodePort = 노드의 포트
# Podcontainers: - name: app ports: - name: http-api # 이름을 붙인다 containerPort: 8080# Serviceports: - port: 80 targetPort: http-api # 숫자 대신 이름flowchart LR
SVC["Service<br/>selector: app=web"] --> EC["엔드포인트 컨트롤러"]
EC -->|"라벨이 맞고<br/>Ready 인 Pod 만"| ES["EndpointSlice<br/>10.244.1.5:8080<br/>10.244.2.7:8080"]
P1["Pod app=web · Ready"] --> EC
P2["Pod app=web · NotReady"] -.->|"제외 ❌"| EC
P3["Pod app=api"] -.->|"라벨 불일치 · 제외 ❌"| EC
ES --> KP["kube-proxy 가 규칙으로 반영"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
class P1 ok
class P2,P3 bad
class ES key
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의 결과다. 이 한 줄이 트래픽을 켜고 끈다.
flowchart LR
RP["readinessProbe"] -->|성공| R1["conditions.ready: true"] --> T1["트래픽 받는다 ✅"]
RP -->|실패| R2["conditions.ready: false"] --> T2["엔드포인트에서 빠진다 ❌"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class R1,T1 ok
class R2,T2 bad
class RP mute
spec: type: NodePort selector: app: web ports: - port: 80 targetPort: 8080 nodePort: 30080 # 생략하면 30000-32767 중에서 자동 할당모든 노드가 그 포트를 연다. Pod이 없는 노드로 가도 전달된다.
flowchart LR
C1["클라이언트"] -->|":30080"| N1["node01<br/>Pod 있음"]
C2["클라이언트"] -->|":30080"| N2["node02<br/>Pod 없음"]
N1 --> P["Pod :8080"]
N2 -->|"다른 노드로 전달"| P
classDef pod fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef node fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class P pod
class N1,N2 node
class C1,C2 mute
--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 같은 것을 설치해야 한다)flowchart LR
S["type: LoadBalancer 생성"] --> Q{"클라우드 컨트롤러가<br/>있는가"}
Q -->|"있다 · EKS/GKE 등"| A["실제 LB 프로비저닝<br/>EXTERNAL-IP 채워짐 ✅"]
Q -->|"없다 · 온프레미스"| B["EXTERNAL-IP 가 영원히 pending ❌<br/>→ MetalLB 등을 설치"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class A ok
class B bad
class S,Q mute
# ExternalName — 프록시하지 않는다. DNS CNAME만 준다spec: type: ExternalName externalName: db.example.com클러스터 안에서 my-db.default.svc.cluster.local 을 조회하면
db.example.com 으로 CNAME이 돌아온다. 셀렉터도 엔드포인트도 없다.
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"]flowchart LR
APP["앱<br/>external-db:5432 로 접속"] --> SVC["Service external-db<br/>셀렉터 없음"]
SVC -->|"kubernetes.io/service-name 라벨로 연결"| ES["수동 EndpointSlice<br/>192.168.10.50"]
ES --> DB[("클러스터 밖 DB")]
LATER["나중에 DB 를 클러스터 안으로"] -.->|"셀렉터를 붙이면 끝<br/>앱은 그대로 ✅"| SVC
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SVC key
class LATER ok
class APP,ES,DB mute
external-db라는 이름으로 부를 수 있다flowchart LR
C["클라이언트"] -->|"DNS: web-svc"| VIP["가상 IP 하나<br/>10.96.0.10"]
VIP -->|"커널이 하나를 골라 DNAT"| P1["Pod A"]
VIP -.-> P2["Pod B"]
VIP -.-> P3["Pod C"]
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class VIP key
class C,P1,P2,P3 muteDNS가 ClusterIP 하나를 준다. 어느 Pod으로 갈지는 커널이 정한다.
spec: clusterIP: None selector: app: db ports: - port: 5432flowchart LR
C["클라이언트"] -->|"DNS: db-headless"| DNS["A 레코드 여러 개<br/>10.244.1.5 · 10.244.2.7 · 10.244.3.9"]
DNS --> CH["클라이언트가 직접 고른다"]
CH --> P1["Pod db-0"]
CH --> P2["Pod db-1"]
CH --> P3["Pod db-2"]
classDef key fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class DNS key
class C,CH,P1,P2,P3 mute쓰는 곳
셀렉터도 없고 clusterIP: None이면 DNS는 ExternalName이나
수동 엔드포인트를 따라간다.
| 모드 | 방식 | 특징 |
|---|---|---|
| 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어느 모드든 동작은 같다. 트래픽 경로가 아니라 규칙을 심는 방식이 다를 뿐이다.
spec: sessionAffinity: ClientIP # None(기본) | ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800
externalTrafficPolicy: Local # Cluster(기본) | LocalexternalTrafficPolicy (NodePort/LoadBalancer에만 해당)
flowchart LR
C["클라이언트<br/>1.2.3.4"] --> N2["node02<br/>Pod 없음"]
N2 -->|"SNAT 후 전달"| P["Pod on node01"]
P -.->|"소스 IP 가 node02 로 보인다 ❌"| SEE["앱이 보는 IP: 10.244.0.1"]
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SEE bad
class P ok
class C,N2 mute어느 노드로 와도 다른 노드의 Pod으로도 보낸다. 부하는 고르지만 소스 IP가 SNAT로 가려진다.
flowchart LR
C["클라이언트<br/>1.2.3.4"] --> N1["node01<br/>Pod 있음"]
N1 -->|"SNAT 없이 전달"| P["Pod on node01"]
P -->|"소스 IP 보존 ✅"| SEE["앱이 보는 IP: 1.2.3.4"]
C2["클라이언트"] -.->|"응답 없음 ❌"| N3["node03<br/>Pod 없음"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SEE,P ok
class N3 bad
class C,C2,N1 mute그 노드의 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에서 참조된다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.localflowchart TD
Q1{"Pod IP 로 직접<br/>되는가"}
Q1 -->|안 된다| A1["앱 문제<br/>포트를 안 열었거나 죽었다"]
Q1 -->|된다| Q2{"ClusterIP 로<br/>되는가"}
Q2 -->|안 된다| A2["엔드포인트가 비었거나<br/>kube-proxy 문제"]
Q2 -->|된다| Q3{"이름으로<br/>되는가"}
Q3 -->|안 된다| A3["DNS 문제 · CoreDNS · 10장"]
Q3 -->|된다| Q4{"다른 네임스페이스<br/>· 외부에서는"}
Q4 -->|"다른 ns 만 안 된다"| A4["FQDN 필요 또는 NetworkPolicy · 12장"]
Q4 -->|"외부만 안 된다"| A5["NodePort · LB 설정 또는 방화벽"]
Q4 -->|"전부 된다"| OK["정상 ✅"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class OK ok
class A1,A2,A3,A4,A5 bad
class Q1,Q2,Q3,Q4 mute
| 결과 | 원인 |
|---|---|
| Pod IP 직접도 안 된다 | 앱 문제 — 포트를 안 열었거나 죽었다 |
| Pod IP는 되는데 ClusterIP가 안 된다 | 엔드포인트 비어 있음 또는 kube-proxy 문제 |
| ClusterIP는 되는데 이름이 안 된다 | DNS 문제 — CoreDNS를 본다 (10장) |
| 다른 네임스페이스에서만 안 된다 | FQDN 필요 또는 NetworkPolicy (12장) |
| 외부에서만 안 된다 | NodePort/LB 설정 또는 방화벽 |
port / targetPort / nodePort 세 포트를 구분하라. targetPort 누락이 단골 실수kubectl get endpoints가 진단의 1번이다. 비었으면 라벨 또는 Ready 문제conditions.ready가 readinessProbe 결과다clusterIP: None) 는 Pod IP를 직접 준다 — StatefulSet의 짝externalTrafficPolicy: Local