Service, DNS, Ingress, NetworkPolicy
저번 포스팅에서는, Pod 와 Deployment · StatefulSet 같은 워크로드 리소스를 공부하였습니다. 이번에는 네트워킹입니다. Pod 는 죽고 살아나며 IP 가 계속 바뀌는데 그럼에도 서로를 찾아 통신할 수 있어야 합니다. 그 문제를 쿠버네티스가 어떻게 푸는지 정리해보려 합니다.
개인적으로 쿠버네티스에서 가장 어렵다고 느낀 영역이고, 면접에서도 깊이가 가장 잘 드러나는 부분이었습니다.
1. 네트워크 모델
쿠버네티스는 네트워크 구현을 직접 제공하지 않습니다. 대신 지켜야 할 규칙을 정해두고 그것을 만족하는 구현체를 끼워 쓰게 합니다.
규칙은 이렇습니다.
- 모든 Pod 는 고유한 IP 를 갖습니다.
- 모든 Pod 는 NAT 없이 서로 통신할 수 있습니다.
- 노드의 에이전트는 그 노드의 Pod 와 NAT 없이 통신할 수 있습니다.
두 번째 규칙이 핵심입니다. NAT 이 없다는 것은, A 가 B 에게 보낸 패킷이 B 에게 도착했을 때 출발지 주소가 여전히 A 라는 뜻입니다. 도커의 기본 브리지 네트워크에서는 포트 매핑과 NAT 이 끼어들어 이것이 깨지는데 쿠버네티스는 이를 금지했습니다.
"모든 Pod 가 하나의 평평한 네트워크에 있는 것처럼 보인다" 가 이 모델의 요약입니다. 덕분에 애플리케이션이 컨테이너인지 VM 인지 신경 쓰지 않고 평소처럼 IP 와 포트로 통신할 수 있습니다.
1.1. CNI
이 규칙을 실제로 구현하는 것이 CNI(Container Network Interface) 플러그인입니다. kubelet 이 Pod 를 띄울 때 CNI 플러그인을 호출해 네트워크 인터페이스를 만들고 IP 를 할당합니다. 주요 구현체들의 성격이 조금씩 다릅니다.
- Flannel : 가장 단순합니다. VXLAN 으로 노드 간 오버레이 네트워크를 만듭니다. NetworkPolicy 를 지원하지 않습니다.
- Calico : 오버레이 없이 BGP 로 라우팅할 수 있어 성능이 좋습니다. NetworkPolicy 를 잘 지원합니다.
- Cilium : eBPF 기반입니다. iptables 를 거치지 않고 커널에서 직접 처리해 성능이 좋고 L7 수준의 정책도 걸 수 있습니다.
여기서 오버레이와 라우팅의 차이를 알아두면 좋습니다. 오버레이는 패킷을 한 번 더 감싸(캡슐화) 노드 간에 전달하므로 기존 네트워크를 건드리지 않아도 되지만 오버헤드가 있습니다. 라우팅 방식은 물리 네트워크가 Pod 대역을 알아야 하지만 캡슐화 비용이 없습니다.
2. Service
2.1. 왜 필요한가
Pod 의 IP 는 믿을 수 없습니다. 재시작하면 바뀌고 스케일하면 개수가 달라집니다. 그러니 바뀌는 Pod 들 앞에 고정된 주소가 필요합니다. 그것이 Service 입니다. Service 는 두 가지를 제공합니다.
- 고정된 가상 IP(ClusterIP)와 DNS 이름
- 여러 Pod 로의 부하 분산
2.2. 엔드포인트
Service 는 셀렉터로 Pod 를 고릅니다. 그런데 Service 오브젝트 자체가 Pod 목록을 들고 있지는 않습니다. EndpointSlice 라는 별도의 오브젝트가 그 역할을 합니다. 동작은 이렇습니다.
- 엔드포인트 컨트롤러가 Service 의 셀렉터에 맞는 Ready 상태의 Pod 를 찾습니다.
- 그 IP 목록을 EndpointSlice 에 기록합니다.
- 각 노드의 kube-proxy 가 그것을 보고 라우팅 규칙을 만듭니다.
Ready 인 Pod 만 들어간다는 점이 중요합니다. 3편에서 본 readinessProbe 가 여기로 이어집니다. 예전에는 Endpoints 라는 오브젝트 하나에 모든 IP 를 넣었는데 Pod 가 수천 개가 되면 오브젝트 하나가 거대해지고 Pod 하나만 바뀌어도 전체가 다시 전송되는 문제가 있었습니다. 그래서 여러 조각으로 나누는 EndpointSlice 로 바뀌었습니다.
2.3. Service 의 종류
| 타입 | 하는 일 |
|---|---|
| ClusterIP | 클러스터 내부에서만 접근 가능한 가상 IP. 기본값 |
| NodePort | 모든 노드의 특정 포트를 열어 외부에서 접근 |
| LoadBalancer | 클라우드의 로드밸런서를 붙여 외부에 노출 |
| ExternalName | 외부 도메인으로 가는 CNAME 만 제공 |
NodePort 는 ClusterIP 위에 얹히고, LoadBalancer 는 NodePort 위에 얹힙니다. LoadBalancer 를 만들면 NodePort 와 ClusterIP 도 함께 생깁니다.
2.4. 헤드리스 서비스
clusterIP: None 으로 두면 가상 IP 를 만들지 않습니다. 대신 DNS 를 조회하면 Pod 들의 IP 가 그대로 전부 돌아옵니다. 부하 분산을 쿠버네티스에 맡기지 않고 클라이언트가 직접 어느 Pod 로 갈지 정해야 할 때 씁니다. 3편에서 본 StatefulSet 이 대표적입니다. 데이터베이스 클러스터에서는 "리더에게 쓰고 팔로워에서 읽는" 식의 판단이 필요하기 때문에, 아무 Pod 로나 분산되면 곤란합니다.
3. kube-proxy 는 실제로 무엇을 하는가
ClusterIP 는 어느 네트워크 인터페이스에도 붙어 있지 않은 가상의 주소입니다. ping 을 해도 응답하지 않습니다. 그런데도 그 주소로 보낸 패킷은 Pod 에 도착합니다. 이 마법을 kube-proxy 가 만듭니다.
3.1. iptables 모드
오랫동안 기본이었던 방식입니다. kube-proxy 가 iptables 규칙을 노드에 설치합니다. 패킷이 ClusterIP 로 향하면, 커널의 NAT 테이블에서 규칙에 걸려 목적지 주소가 실제 Pod IP 로 바뀐다(DNAT). 여러 Pod 중 하나를 고르는 것은 확률 기반 규칙으로 처리합니다.
주의할 점은 kube-proxy 가 패킷 경로에 없다는 것입니다. 규칙만 넣어두고 빠지며 실제 처리는 전부 커널이 합니다. 그래서 kube-proxy 가 잠시 죽어도 이미 설치된 규칙은 그대로 동작합니다. 다만 그동안의 변경은 반영되지 않습니다.
한계는 규칙이 선형 리스트라는 점입니다. Service 가 수천 개가 되면 규칙이 수만 줄이 되고 규칙 갱신에 걸리는 시간이 길어집니다.
3.2. IPVS 모드
리눅스 커널의 IPVS(IP Virtual Server) 를 쓰는 방식입니다. 원래 L4 로드밸런싱을 위해 만들어진 기능이라 이 용도에 더 맞습니다.
- 해시 테이블 기반이라 Service 가 많아져도 조회 시간이 거의 일정합니다.
- 라운드로빈, 최소 연결, 해시 등 분산 알고리즘을 고를 수 있습니다.
규모가 큰 클러스터에서 iptables 대신 쓰는 이유가 이것입니다.
3.3. nftables 모드
최근에 들어온 방식입니다. iptables 를 대체하기 위해 만들어진 nftables 를 쓰며 iptables 모드의 성능 문제를 해결하면서도 IPVS 만큼 별도의 커널 모듈에 의존하지 않는다는 점이 특징입니다. 1.33 에서 정식(GA)이 되었습니다.
3.4. eBPF 로 kube-proxy 를 대체하기
Cilium 같은 CNI 는 kube-proxy 자체를 걷어내고 eBPF 프로그램으로 같은 일을 합니다. 커널의 네트워크 처리 경로에 직접 코드를 붙이는 방식이라, iptables 체인을 타지 않아 지연이 줄어듭니다.
3.5. 부하 분산의 한계
여기서 꼭 짚어야 할 것이 있습니다. kube-proxy 의 부하 분산은 연결(connection) 단위입니다. TCP 연결이 한 번 맺어지면 그 연결의 모든 요청은 같은 Pod 로 갑니다. HTTP/1.1 처럼 요청마다 연결을 새로 맺거나 재사용을 짧게 하는 경우에는 잘 분산되지만 gRPC 나 HTTP/2 처럼 하나의 연결을 오래 유지하며 여러 요청을 보내는 경우에는 분산이 되지 않습니다.
클라이언트 Pod 가 서버 Pod 하나에 연결을 맺고 계속 쓰면, 서버를 아무리 늘려도 트래픽이 한쪽에 몰립니다. 실무에서 자주 만나는 문제이고 해결하려면 클라이언트 쪽 로드밸런싱을 하거나 서비스 메시를 두어 L7 에서 분산해야 합니다.
4. DNS
4.1. 이름 규칙
클러스터 안에는 CoreDNS 가 떠 있고, 모든 Service 에 DNS 이름이 붙습니다.
<서비스이름>.<네임스페이스>.svc.cluster.local
같은 네임스페이스 안에서는 web 만으로도 찾아집니다. Pod 의 /etc/resolv.conf 에 검색 도메인이 들어 있어서, 짧은 이름을 순서대로 붙여보며 조회하기 때문입니다. 헤드리스 서비스와 StatefulSet 을 함께 쓰면 Pod 마다 개별 이름도 생깁니다.
web-0.web.default.svc.cluster.local
4.2. 운영에서 자주 만나는 문제
DNS 는 쿠버네티스에서 장애 원인으로 꽤 자주 지목됩니다.
- ndots 문제 :
기본
ndots:5설정 때문에, 점이 5개 미만인 이름은 검색 도메인을 먼저 다 붙여보고 나서야 그대로 조회합니다.github.com을 찾는 데 조회가 여러 번 발생합니다. 외부 도메인을 자주 부르는 워크로드에서는dnsConfig로 조정하거나 끝에 점을 붙입니다. - CoreDNS 자원 부족 : 모든 Pod 의 모든 조회가 CoreDNS 로 몰립니다. 복제본과 자원을 충분히 두어야 하고 노드마다 캐시를 두는 NodeLocal DNSCache 를 쓰기도 합니다.
5. 외부에서 들어오는 트래픽
5.1. Service 로 노출할 때의 한계
NodePort 는 포트 번호가 보기 좋지 않고(기본 30000-32767), 노드 주소를 알아야 합니다. LoadBalancer 는 깔끔하지만 Service 하나마다 로드밸런서가 하나씩 생겨 비용이 큽니다.
그리고 둘 다 L4 입니다. 경로에 따라 다른 서비스로 보내거나, 호스트 이름으로 가르거나, TLS 를 종료하는 일을 할 수 없습니다.
5.2. Ingress
HTTP/HTTPS 트래픽을 규칙에 따라 라우팅하는 오브젝트입니다. 여기서 중요한 것은 Ingress 오브젝트 자체는 아무 일도 하지 않는다는 점입니다. 그저 규칙을 적어둔 문서일 뿐이고 실제로 트래픽을 처리하는 것은 Ingress 컨트롤러입니다. NGINX Ingress Controller, Traefik, HAProxy 등이 있습니다.
컨트롤러를 설치하지 않고 Ingress 만 만들면 아무 일도 일어나지 않습니다. 처음에 자주 겪는 혼란입니다. 이 구조의 이득은 로드밸런서 하나 뒤에 여러 서비스를 붙일 수 있다는 것입니다. 비용도 줄고 TLS 인증서도 한곳에서 관리합니다.
5.3. Gateway API
Ingress 에는 한계가 있었습니다. 스펙이 HTTP 위주로 단순해서, 가중치 기반 분배나 헤더 조작 같은 것을 하려면 컨트롤러마다 다른 어노테이션을 써야 했습니다. 그러다 보니 컨트롤러를 바꾸면 설정을 다시 써야 했습니다.
Gateway API 는 이를 해결하려고 나온 후속 표준입니다. 역할을 세 가지로 나눈 것이 특징입니다.
- GatewayClass : 어떤 구현체를 쓸 것인가 : 인프라 관리자의 몫
- Gateway : 실제 진입점과 리스너 : 클러스터 운영자의 몫
- HTTPRoute : 라우팅 규칙 : 애플리케이션 개발자의 몫
누가 무엇을 책임지는지가 오브젝트로 갈려 있어서, 개발자에게 라우팅 권한만 주고 진입점 설정은 막는 식의 구성이 가능합니다. 트래픽 분할 같은 기능도 표준에 들어 있어 카나리 배포를 어노테이션 없이 할 수 있습니다.
5.4. externalTrafficPolicy
외부에서 들어온 트래픽에서 자주 걸리는 설정입니다.
- Cluster (기본) : 어느 노드로 들어오든 클러스터 전체의 Pod 로 분산합니다. 고르게 퍼지지만 다른 노드로 넘길 때 SNAT 이 일어나 클라이언트의 원래 IP 가 사라집니다.
- Local : 들어온 노드에 있는 Pod 로만 보냅니다. 클라이언트 IP 가 보존되고 홉이 하나 줄지만 그 노드에 해당 Pod 가 없으면 트래픽이 버려집니다.
접근 로그에 클라이언트 IP 가 전부 노드 IP 로 찍히는 문제의 원인이 대개 이것입니다.
6. NetworkPolicy
1편에서 언급한 대로, 기본 상태에서 모든 Pod 는 서로 통신할 수 있습니다. 네임스페이스가 달라도 마찬가지입니다.
NetworkPolicy 는 이것을 제한하는 오브젝트입니다. 성질이 몇 가지 있습니다.
- 화이트리스트 방식이다 : 어떤 Pod 에 정책이 하나라도 걸리면, 그 Pod 는 명시적으로 허용된 것만 통신할 수 있게 됩니다. "거부" 규칙을 적는 것이 아니라 "허용" 만 적습니다.
- Ingress 와 Egress 를 따로 정한다 : 들어오는 것과 나가는 것을 나눠 제어합니다.
- CNI 가 지원해야 동작한다 : 이것이 가장 중요합니다. NetworkPolicy 오브젝트를 만들어도 CNI 플러그인이 구현하지 않으면 아무 일도 일어나지 않습니다. 오류도 나지 않고 조용히 무시됩니다. Flannel 은 지원하지 않고 Calico 나 Cilium 은 지원합니다.
- L3/L4 수준이다 : IP 와 포트 기준입니다. HTTP 메서드나 경로 기준으로 막으려면 서비스 메시나 Cilium 의 확장 기능이 필요합니다.
실무에서는 네임스페이스마다 기본 거부(default deny) 정책을 깔아두고 필요한 통신만 열어주는 방식을 씁니다. 이때 DNS 조회(53 포트)를 열어주는 것을 잊으면 모든 통신이 이름 조회 단계에서 막힙니다. 자주 하는 실수입니다.
7. 서비스 메시
Pod 가 많아지면 통신에 얽힌 요구가 늘어납니다. 재시도, 타임아웃, 회로 차단기, 트래픽 분할, 서비스 간 상호 TLS, 호출 추적 같은 것들입니다.
이것을 애플리케이션마다 구현하면 언어별로 중복이 생깁니다. 서비스 메시는 이 기능을 사이드카 프록시로 빼내, 애플리케이션 코드를 건드리지 않고 제공합니다. Istio 와 Linkerd 가 대표적입니다.
3.5 에서 본 gRPC 부하 분산 문제도 여기서 풀립니다. 프록시가 L7 을 이해하므로 하나의 연결 안에서도 요청 단위로 분산할 수 있습니다. 대신 Pod 마다 프록시가 하나씩 더 뜨므로 자원과 지연이 늘어납니다. 최근에는 사이드카 없이 노드 수준에서 처리하는 방식(Istio 의 앰비언트 모드 등)도 나오고 있습니다.
8. 조금 더 들어가보기 : 패킷이 지나는 길
여기까지는 쿠버네티스의 오브젝트 관점에서 봤습니다. 이번에는 한 단계 내려가, 패킷 하나가 Pod 를 떠나 다른 Pod 에 닿기까지 커널에서 무슨 일이 벌어지는지를 따라가겠습니다. 네트워크 장애를 진단할 때 결국 이 층에서 답을 찾게 됩니다.
8.1. Pod 의 네트워크 인터페이스는 어떻게 만들어지는가
Pod 는 자기만의 네트워크 네임스페이스를 갖습니다. 그런데 네임스페이스는 격리된 공간일 뿐이라, 바깥과 이어줄 통로가 따로 필요합니다. 그 통로가 veth 쌍(veth pair) 입니다. 양쪽 끝이 이어진 가상 랜선이라고 보면 됩니다. 한쪽에 넣은 패킷이 반대쪽으로 그대로 나옵니다.
- 한쪽 끝은 Pod 의 네임스페이스 안에 들어가
eth0이라는 이름을 갖습니다. - 다른 쪽 끝은 노드의 기본 네임스페이스에 남아
vethXXXX같은 이름으로 보입니다.
노드에서 ip link 를 쳐보면 Pod 개수만큼 veth 인터페이스가 보이는 것이 이 때문입니다. 그리고 CNI 플러그인이 하는 일이 바로 이것입니다. veth 쌍을 만들고, 한쪽을 Pod 안에 넣고, IP 를 할당하고, 라우팅을 설정하는 것입니다.
8.2. 같은 노드 안에서의 통신
가장 단순한 경우입니다. 노드 쪽 veth 끝들이 모두 같은 브리지에 연결되어 있으면, 브리지가 L2 스위치처럼 동작해 패킷을 상대 Pod 쪽 veth 로 넘깁니다. 노드 바깥으로 나가지 않으므로 물리 네트워크와 무관하고 가장 빠릅니다.
8.3. 다른 노드로 갈 때
여기서 구현 방식이 갈립니다. 오버레이 방식(VXLAN 등) 은 원래 패킷을 통째로 한 번 더 감쌉니다. Pod 의 IP 패킷을 UDP 패킷 안에 넣어 노드 IP 로 보내고 받는 노드가 껍질을 벗겨 원래 패킷을 꺼냅니다. 물리 네트워크는 Pod IP 대역을 전혀 몰라도 됩니다.
라우팅 방식(BGP 등) 은 감싸지 않습니다. 대신 물리 네트워크의 라우터가 "이 Pod 대역은 저 노드로" 를 알고 있어야 합니다. 캡슐화 비용이 없어 빠르지만 네트워크 쪽 협조가 필요합니다.
8.4. 오버레이에서 반드시 만나는 MTU 문제
앞의 두 방식 중 오버레이를 쓸 때 실무에서 가장 자주 겪는 장애가 여기서 나옵니다. VXLAN 은 원래 패킷 앞에 헤더를 덧붙입니다. 그만큼 안쪽 패킷이 쓸 수 있는 크기가 줄어듭니다. 물리 네트워크의 MTU 가 1500 이라면, Pod 인터페이스의 MTU 는 그보다 작게 잡아야 합니다.
이것을 맞추지 않으면 아주 특이한 증상이 나타납니다.
- ping 은 됩니다. 작은 패킷이기 때문입니다.
- TCP 연결도 맺어집니다. 핸드셰이크 패킷도 작습니다.
- 그런데 큰 응답을 받는 순간 멈춥니다. 데이터가 꽉 찬 패킷이 크기를 넘어 버려지기 때문입니다.
"작은 요청은 잘 되는데 큰 응답에서 멈춘다" 는 증상을 만나면 가장 먼저 MTU 를 의심해야 합니다. 원래는 경로 MTU 탐색(Path MTU Discovery)이 이것을 자동으로 맞춰주는데 그 과정에 쓰이는 ICMP 를 방화벽이 막아두는 경우가 많아 조용히 멈추게 됩니다.
8.5. ClusterIP 는 어디에서 바뀌는가
앞에서 ClusterIP 가 실체 없는 주소라고 했는데 그 변환이 일어나는 자리가 netfilter 입니다. 리눅스 커널의 네트워크 처리 경로에는 규칙을 끼워 넣을 수 있는 지점이 정해져 있습니다.
- PREROUTING : 패킷이 막 들어와 라우팅 결정을 하기 전
- INPUT : 이 호스트가 받을 패킷
- FORWARD : 다른 곳으로 넘길 패킷
- OUTPUT : 이 호스트가 내보내는 패킷
- POSTROUTING : 최종적으로 나가기 직전
kube-proxy 가 넣는 것은 nat 테이블의 DNAT 규칙입니다. Pod 가 ClusterIP 로 패킷을 보내면 OUTPUT 지점에서 규칙에 걸리고 목적지 주소가 실제 Pod IP 로 바뀐 뒤 라우팅이 이어집니다.
그래서 ClusterIP 는 어디에도 붙어 있지 않아도 됩니다. 그 주소로 보낸 패킷이 목적지에 닿기 전에 다른 주소로 바뀌기 때문입니다. ping 이 되지 않는 이유도 같습니다. ICMP 에 대한 DNAT 규칙이 없으니 바뀌지 않고 아무도 없는 주소로 가서 사라집니다.
8.6. conntrack, 그리고 연결 단위 분산의 정체
여기서 앞서 말한 "부하 분산이 연결 단위" 라는 성질의 진짜 이유가 나옵니다. 목적지를 바꿨으면 돌아오는 패킷도 되돌려 놓아야 합니다. 응답 패킷의 출발지가 Pod IP 인 채로 오면, 클라이언트는 자기가 보낸 적 없는 주소에서 온 패킷이라 버립니다.
그래서 커널은 연결 추적(conntrack) 테이블에 "이 연결은 이렇게 바꿨다" 를 기록해둡니다. 이후 같은 연결의 패킷은 규칙을 다시 평가하지 않고 기록된 대로 처리됩니다. 즉 어느 Pod 로 갈지는 연결이 처음 맺어질 때 한 번만 정해집니다. gRPC 나 HTTP/2 처럼 연결 하나를 오래 쓰면 그 연결의 모든 요청이 같은 Pod 로 가는 이유가 이것입니다. 정책의 문제가 아니라 커널이 연결 단위로 기억하기 때문입니다.
conntrack 테이블은 크기가 정해져 있습니다. 짧은 연결을 아주 많이 만드는 워크로드에서는 이 테이블이 가득 차고 그러면 새 연결이 조용히 버려집니다. 노드의 커널 로그에 nf_conntrack: table full, dropping packet 이 찍히면 이 상황입니다. 간헐적인 연결 실패의 원인으로 꽤 자주 지목됩니다.
8.7. SNAT 과 클라이언트 IP
앞에서 externalTrafficPolicy: Cluster 를 쓰면 클라이언트 IP 가 사라진다고 했는데 그 이유도 같은 층에 있습니다. 노드 A 로 들어온 패킷을 노드 B 의 Pod 로 넘긴다고 하겠습니다. 이때 출발지를 그대로 두면, Pod 의 응답이 노드 B 에서 클라이언트로 직접 나가버립니다. 그런데 클라이언트는 노드 A 에게 보냈으므로 노드 B 에서 오는 응답을 모르는 연결로 보고 버립니다.
그래서 노드 A 는 출발지 주소를 자기 것으로 바꿔서(SNAT) 보냅니다. 응답이 반드시 자기를 거쳐 돌아오게 만드는 것입니다. 그 대가로 Pod 는 원래 클라이언트 IP 를 볼 수 없습니다. Local 정책이 클라이언트 IP 를 보존할 수 있는 이유도 분명해집니다. 같은 노드 안에서 끝나니 SNAT 이 필요 없습니다.
8.8. eBPF 는 무엇을 바꾸는가
iptables 방식은 규칙이 늘어날수록 패킷마다 훑어야 할 체인이 길어집니다. Service 가 수천 개면 규칙이 수만 줄이 됩니다. eBPF 는 커널의 네트워크 처리 경로에 작은 프로그램을 직접 붙입니다. 패킷이 네트워크 드라이버를 지나는 아주 이른 지점에서 목적지를 바꿔버릴 수 있어, netfilter 체인 전체를 건너뜁니다.
여기에 더해 Cilium 같은 구현은 연결 추적도 자체적으로 관리해서, 앞서 본 conntrack 테이블의 제약에서도 자유롭습니다. 대규모 클러스터에서 eBPF 기반 CNI 가 선택되는 이유입니다.
결론
네트워킹을 정리하면서 가장 크게 느낀 것은, 쿠버네티스가 네트워크를 직접 구현하지 않았다는 점이었습니다. "Pod 는 고유 IP 를 갖고 NAT 없이 통신한다" 는 규칙만 정해두고 구현은 CNI 에 맡겼습니다. 그래서 오버레이든 BGP 든 eBPF 든 상황에 맞게 고를 수 있는 것이고 동시에 NetworkPolicy 가 CNI 에 따라 동작하지 않을 수도 있다는 함정도 여기서 나옵니다. 표준을 정하고 구현을 위임하는 설계의 양면인 셈입니다.
두 번째로 인상 깊었던 것은 ClusterIP 가 실체가 없는 주소라는 사실이었습니다. 어디에도 붙어 있지 않은 IP 인데 커널의 패킷 처리 규칙 덕분에 동작하고 kube-proxy 는 규칙을 넣어둘 뿐 패킷 경로에는 없습니다. 그래서 kube-proxy 가 죽어도 기존 통신이 끊기지 않습니다. 판단하는 쪽과 처리하는 쪽을 갈라놓는 방식이 여기서도 반복되고 있었습니다.
세 번째는 연결 단위 부하 분산의 한계입니다. 쿠버네티스가 알아서 잘 나눠준다고 막연히 생각했는데 gRPC 를 쓰면 그렇지 않다는 것을 알고 나서 왜 서비스 메시가 필요한지 비로소 이해가 되었습니다. 어느 계층에서 분산이 일어나는지를 알아야 문제를 진단할 수 있다는 것을 배웠습니다.
공부 참고 자료
📑 1. Kubernetes 공식 문서 - Cluster Networking
📑 2. Kubernetes 공식 문서 - Service
📑 3. Kubernetes 공식 문서 - EndpointSlices
📑 4. Kubernetes 공식 문서 - DNS for Services and Pods
📑 5. Kubernetes 공식 문서 - Ingress
📑 6. Kubernetes 공식 문서 - Network Policies