Notes
Kubernetes Service, NodePort, kube-proxy, NAT 흐름 정리
Pod IP와 ClusterIP의 차이, NodePort 트래픽이 iptables 기반 DNAT으로 실제 Pod까지 전달되는 과정을 정리한다.
- Published
- Updated
- Area
- Cloud Infrastructure
- Type
- concept
- Category
- Notes
핵심 요약
Kubernetes에서 Service는 여러 Pod를 안정적으로 접근하기 위한 L4 추상화다. Pod IP는 실제 Pod에 붙는 주소이고, ClusterIP는 Service에 붙는 가상 IP다.
kube-proxy는 트래픽을 직접 받아서 전달하는 프록시라기보다, Service와 EndpointSlice 정보를 기반으로 각 Node의 iptables 또는 IPVS rule을 관리하는 agent에 가깝다. 실제 패킷 rewrite와 forwarding은 Linux kernel networking stack이 수행한다.
kube-proxy = Service/EndpointSlice 상태를 보고 iptables/IPVS rule을 관리하는 agent
iptables/netfilter = 실제 패킷의 IP/Port를 rewrite하는 data plane
Service = Pod 집합에 대한 안정적인 L4 접근 지점
Pod IP와 ClusterIP의 차이
| 구분 | 붙는 대상 | 성격 | 예시 |
|---|---|---|---|
Pod IP | Pod | 실제 endpoint 주소 | 10.42.1.7 |
ClusterIP | Service | Service용 virtual IP | 10.43.0.15 |
NodePort | Node의 특정 port | 외부에서 Service로 들어오는 entrypoint | NodeIP:30080 |
Pod IP는 실제 Pod에 붙는 주소다. Pod가 재생성되면 바뀔 수 있다.
Pod A -> 10.42.1.7
Pod B -> 10.42.2.3
반면 ClusterIP는 Service에 붙는 virtual IP다.
Service web-svc
ClusterIP = 10.43.0.15
주의할 점은 ClusterIP가 특정 Pod에 직접 붙는 IP가 아니라는 것이다. ClusterIP는 iptables 또는 IPVS가 가로채기 위한 Service용 virtual destination에 가깝다.
Service는 ReplicaSet을 직접 묶는가?
정확히는 Service가 ReplicaSet 자체를 묶는 것이 아니라, selector로 특정 label을 가진 Pod들을 선택한다.
apiVersion: v1
kind: Service
metadata:
name: web-svc
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 8080
위 Service는 app=nginx label을 가진 Pod들을 backend endpoint로 사용한다.
보통 Deployment 또는 ReplicaSet이 같은 label을 가진 Pod replica들을 만들기 때문에, 결과적으로는 Service가 ReplicaSet의 여러 Pod를 묶는 것처럼 보인다.
Deployment/ReplicaSet
-> app=nginx Pod 여러 개 생성
Service selector
-> app=nginx Pod들을 backend로 선택
kube-proxy의 역할
kube-proxy는 각 Node에서 동작하면서 Kubernetes API Server를 watch한다.
주로 관찰하는 정보는 다음과 같다.
| 대상 | 의미 |
|---|---|
Service | 어떤 virtual IP/port가 존재하는지 |
EndpointSlice | 해당 Service 뒤에 어떤 Pod IP들이 있는지 |
| Pod readiness | backend로 보낼 수 있는 Ready Pod인지 |
kube-proxy는 이 정보를 바탕으로 iptables rule을 생성, 수정, 삭제한다.
Service/EndpointSlice 상태 변경
↓
kube-proxy가 감지
↓
각 Node의 iptables/IPVS rule 업데이트
↓
Linux kernel이 실제 패킷 처리
따라서 다음 표현은 약간 부정확하다.
kube-proxy가 트래픽을 Pod replica 중 하나로 전달한다.
더 정확한 표현은 다음과 같다.
kube-proxy가 Service/EndpointSlice 정보를 기반으로 iptables rule을 관리하고,
실제 패킷은 Linux kernel의 netfilter/iptables rule에 의해 Pod IP로 DNAT된다.
NAT 개념
NAT는 Network Address Translation의 약자다. 중간 장비나 kernel network stack이 패킷의 주소 정보를 rewrite하는 기술이다.
NAT는 보통 L3의 IP 주소와 L4의 port 정보를 함께 다룬다.
src IP
src Port
dst IP
dst Port
대표적으로 다음 두 종류를 구분한다.
| 종류 | 바꾸는 대상 | 대표 용도 |
|---|---|---|
SNAT | Source IP/Port | 사설망에서 외부로 나갈 때 출발지 주소 변경 |
DNAT | Destination IP/Port | 외부 요청을 내부 서버나 Pod로 전달 |
DNAT
DNAT는 Destination NAT이다. 패킷의 목적지 주소를 바꾼다.
예를 들어 외부 클라이언트가 다음 주소로 접속했다고 하자.
NodeIP:30080
실제 애플리케이션 Pod는 다음 주소에서 실행 중일 수 있다.
10.42.1.7:8080
이때 iptables rule은 패킷의 destination을 다음처럼 변경한다.
before:
dst = NodeIP:30080
after:
dst = 10.42.1.7:8080
이것이 DNAT이다.
Kubernetes Service 관점에서는 다음과 같이 볼 수 있다.
ClusterIP:ServicePort
↓ DNAT
PodIP:TargetPort
NodePort도 같은 원리를 사용한다.
NodeIP:NodePort
↓ DNAT
PodIP:TargetPort
NodePort 트래픽 흐름
NodePort는 외부에서 NodeIP:NodePort로 들어온 트래픽을 Service backend Pod 중 하나로 전달하기 위한 Service type이다.
예시 상황은 다음과 같다.
Service: web-svc
ClusterIP: 10.43.0.15
Service Port: 80
NodePort: 30080
TargetPort: 8080
Ready Pods:
10.42.1.7:8080
10.42.2.3:8080
외부 클라이언트가 다음으로 요청한다.
NodeIP:30080
Node의 kernel network stack은 iptables rule에 따라 Service chain으로 이동한 뒤, backend endpoint 중 하나를 선택하고 destination을 rewrite한다.
Client
-> NodeIP:30080
-> iptables PREROUTING
-> KUBE-NODEPORTS
-> KUBE-SVC-xxxxx
-> KUBE-SEP-yyyyy
-> DNAT to 10.42.1.7:8080
-> Pod
개념적으로는 다음과 같다.
외부 클라이언트
↓
NodeIP:NodePort
↓
iptables rule
↓
Service backend 중 하나 선택
↓
DNAT
↓
PodIP:TargetPort
ClusterIP 트래픽 흐름
Cluster 내부의 다른 Pod가 Service의 ClusterIP로 접근하는 경우도 유사하다.
Client Pod
-> 10.43.0.15:80
-> iptables OUTPUT 또는 PREROUTING
-> KUBE-SVC-xxxxx
-> KUBE-SEP-yyyyy
-> DNAT to 10.42.2.3:8080
-> Backend Pod
즉 ClusterIP는 실제 Pod IP가 아니라, Service backend 중 하나로 DNAT되기 위한 virtual destination이다.
iptables mode에서의 mental model
iptables mode의 kube-proxy는 다음 mental model로 이해하면 좋다.
1. kube-proxy가 Service와 EndpointSlice를 watch한다.
2. Ready Pod 목록을 기반으로 iptables rule을 만든다.
3. 패킷이 Node에 들어오거나 로컬에서 생성된다.
4. netfilter hook에서 iptables rule이 적용된다.
5. destination IP/Port가 PodIP:TargetPort로 DNAT된다.
6. Linux kernel이 routing table을 보고 실제 Pod로 forwarding한다.
kube-proxy는 rule을 관리하고, packet path에서 실제 rewrite는 kernel이 수행한다.
netfilter hook과 NAT 위치
Linux kernel의 netfilter에는 패킷 흐름 중간에 hook point가 있다.
| Hook | 대표 역할 |
|---|---|
PREROUTING | 외부에서 들어온 패킷의 routing 결정 전 처리 |
OUTPUT | 로컬 프로세스가 만든 패킷 처리 |
POSTROUTING | 패킷이 나가기 직전 처리 |
Kubernetes Service의 DNAT는 주로 PREROUTING 또는 OUTPUT 경로에서 적용된다.
외부에서 들어온 NodePort traffic
-> PREROUTING
-> DNAT
-> routing
-> Pod
Node 내부에서 ClusterIP로 접근
-> OUTPUT
-> DNAT
-> routing
-> Pod
SNAT과 MASQUERADE
Kubernetes에서는 DNAT뿐 아니라 SNAT 또는 MASQUERADE도 사용될 수 있다.
SNAT은 source address를 바꾸는 NAT다.
before:
src = 10.42.1.7
after:
src = NodeIP
대표적인 사용 맥락은 다음과 같다.
| 상황 | 목적 |
|---|---|
| Pod가 cluster 외부로 나갈 때 | 외부 대상이 Pod CIDR을 모르는 경우 응답 경로 보장 |
| NodePort에서 특정 traffic policy 사용 시 | return path를 맞추기 위해 source address 변경 |
| CNI 구성에 따라 필요한 경우 | Pod network routing 조건에 맞춤 |
주의할 점은 SNAT이 적용되면 backend Pod 입장에서는 원래 client IP가 보존되지 않을 수 있다는 것이다.
확인 필요 항목은 다음과 같다.
externalTrafficPolicy설정- CNI plugin 동작 방식
- Pod CIDR routing 가능 여부
- NodePort 접근 경로
MASQUERADErule 적용 여부
자칫 실수하기 쉬운 표현
| 표현 | 판단 | 더 정확한 표현 |
|---|---|---|
| kube-proxy가 직접 트래픽을 전달한다 | 부정확 | kube-proxy가 iptables/IPVS rule을 관리하고 kernel이 전달한다 |
| ClusterIP는 Pod 여러 개를 묶은 IP다 | 부분적으로만 맞음 | ClusterIP는 Service에 붙는 virtual IP이고, selector가 Pod 집합을 backend로 연결한다 |
| Service는 ReplicaSet을 바라본다 | 부정확 | Service는 label selector로 Pod를 선택한다 |
| NodePort는 특정 Node의 Pod로만 보낸다 | 부정확 | NodePort로 들어온 Node에서 iptables rule에 따라 Service backend Pod 중 하나로 DNAT될 수 있다 |
| NAT는 IP만 바꾼다 | 부정확 | 보통 IP와 port를 함께 바꾼다 |
구분 기준
Kubernetes Service networking을 볼 때는 다음 기준으로 구분하면 명확하다.
| 질문 | 확인할 대상 |
|---|---|
| 이 IP는 누구에게 붙는가? | Pod IP인지 ClusterIP인지 |
| Service가 어떤 Pod를 선택하는가? | spec.selector, Pod label |
| 실제 backend 목록은 무엇인가? | EndpointSlice |
| 트래픽 rewrite는 누가 하는가? | Linux kernel netfilter |
| rewrite rule은 누가 관리하는가? | kube-proxy |
| 목적지를 바꾸는가? | DNAT |
| 출발지를 바꾸는가? | SNAT 또는 MASQUERADE |
검증 포인트
실제 cluster에서 확인할 때는 다음 항목을 점검하면 된다.
kubectl get svc
kubectl get endpointslice
kubectl get pod -o wide
Service와 Pod IP를 비교한다.
kubectl get svc web-svc
kubectl get endpointslice -l kubernetes.io/service-name=web-svc
kubectl get pod -l app=nginx -o wide
Node의 iptables rule을 확인할 수 있는 환경이라면 다음을 확인한다.
iptables-save | grep KUBE-SVC
iptables-save | grep KUBE-SEP
iptables-save | grep KUBE-NODEPORTS
확인할 핵심은 다음과 같다.
KUBE-NODEPORTS
-> NodePort traffic 처리
KUBE-SVC-xxxxx
-> Service 단위 load balancing chain
KUBE-SEP-yyyyy
-> 특정 endpoint Pod로 DNAT
정리
Kubernetes Service의 핵심은 Pod IP를 직접 노출하지 않고, 안정적인 virtual endpoint를 제공하는 것이다.
ClusterIP = Service에 붙는 virtual IP
Pod IP = 실제 Pod에 붙는 endpoint IP
NodePort = Node의 특정 port를 통해 Service로 들어오는 외부 entrypoint
iptables mode에서 kube-proxy는 packet forwarding 자체를 수행하지 않는다. 대신 Service와 EndpointSlice 상태를 보고 iptables rule을 관리한다.
실제 트래픽은 Linux kernel의 netfilter rule에 의해 처리되며, Service 또는 NodePort로 들어온 패킷은 backend Pod 중 하나의 PodIP:TargetPort로 DNAT된다.
Service abstraction
-> kube-proxy가 iptables rule로 구현
-> Linux kernel이 DNAT 수행
-> Ready Pod 중 하나로 도달