Сетевая модель Kubernetes: требования и CNI
В отличие от классической виртуализации, где сеть — это network team и кучу VLAN-ов, Kubernetes начинается с очень простой, почти радикальной модели: каждый Pod получает уникальный routable IP внутри кластера, и любой Pod может пойти на любой другой Pod по этому IP, без NAT, без port-mapping, без shenanigans.
Это звучит просто, но скрывает огромную работу. На каждом узле живёт CNI plugin (Calico, Cilium, Flannel, Weave), который и реализует эту модель — настраивает veth-пары, bridge-интерфейсы, маршруты, VXLAN-туннели, BGP-сессии или eBPF-программы. Понимать сетевую модель критично — потому что Services, Ingress и NetworkPolicies все строятся поверх неё.
Subnetting и CIDR: как делить IP-пространство
Четыре требования network model
Kubernetes не реализует сеть сам — он формулирует требования к сетевому решению. Любой CNI plugin, претендующий на conformance, обязан выполнять:
- Все Pods могут общаться со всеми Pods без NAT — независимо от node, на которой они работают.
- Все узлы могут общаться со всеми Pods без NAT — node-level процессы (kubelet, kube-proxy) видят Pods напрямую.
- IP, который Pod видит у себя — это IP, по которому к нему обращаются другие. Никакого “внутреннего” и “внешнего” адреса.
- Services получают отдельный virtual IP (ClusterIP), который реализуется уже kube-proxy (об этом — в следующих уроках).
Эти требования формализованы в документе “Cluster Networking” в kubernetes.io. Они намеренно высокоуровневые: Kubernetes не диктует как делать (overlay, underlay, BGP) — только что должно работать. Это позволяет cloud-провайдерам и vendor-ам делать совершенно разные имплементации.
В классическом Docker эти требования не выполняются — там по умолчанию NAT через docker0 bridge и port mapping наружу. Именно поэтому Kubernetes не использует Docker networking — он подменяет его CNI-плагином.
CNI: Container Network Interface
CNI — стандарт CNCF, описывающий как именно “сетевое решение” подключается к runtime. Спецификация простая: набор binary-плагинов в /opt/cni/bin/ и JSON-конфиг в /etc/cni/net.d/. Runtime (containerd, CRI-O) вызывает плагины с действиями ADD (создать сеть для контейнера), DEL (убрать), CHECK.
{
"cniVersion": "1.0.0",
"name": "k8s-pod-network",
"type": "calico",
"ipam": {
"type": "calico-ipam"
},
"policy": {
"type": "k8s"
}
}
CNI plugin — это исполняемый файл, который получает на stdin JSON-описание сети, на stdout возвращает результат (выданный IP, маршруты).
Кто что делает в цепочке создания Pod
PodIP выдаётся ОДНОКРАТНО при создании Pod sandbox. При рестарте Pod-а (новый объект с тем же именем — например, через Deployment rolling update) выдаётся новый IP. Это критично: PodIP — ephemeral, не используйте его напрямую как target — вот зачем нужны Services со stable virtual IP.
Популярные CNI plugins
Все они реализуют одну спецификацию, но способы — разные.
Flannel
Простой, “по умолчанию minikube”. Использует overlay-сеть VXLAN: пакет от Pod A заворачивается в UDP-пакет (с VXLAN-заголовком, VNI), отправляется на node, где живёт Pod B, разворачивается и доставляется. Каждой ноде выделен sub-CIDR из общего Pod CIDR (например, node 1: 10.244.1.0/24, node 2: 10.244.2.0/24).
Pod A (10.244.1.5) → пакет → veth → flannel.1 (VXLAN encap) →
UDP 8472 → eth0 ноды → сеть нод → eth0 другой ноды →
flannel.1 (decap) → bridge → veth → Pod B (10.244.2.7)
Плюс: работает на любой сети между нодами (всё через UDP). Минус: overhead VXLAN, нет NetworkPolicies.
Calico
Без overlay по умолчанию: использует BGP для рассылки маршрутов между нодами. Каждая нода становится BGP-роутером, анонсирует свой sub-CIDR, остальные узнают и прописывают маршруты в kernel routing table напрямую. Encap не нужен.
Калико умеет fallback на IPIP или VXLAN, если ноды в разных AS/подсетях, где нельзя BGP. Также поддерживает NetworkPolicies через iptables правила.
Cilium
eBPF-based. Не использует iptables вообще: все Pod-to-Pod, Service load balancing, NetworkPolicy реализованы как eBPF-программы, прикреплённые к network interfaces в kernel. Это быстрее iptables/IPVS, особенно на больших кластерах с тысячами Services. Поддерживает L7 NetworkPolicies (фильтрация по HTTP path, Kafka topics).
В современных кластерах Cilium часто полностью заменяет kube-proxy (kubeProxyReplacement: true) — реализует ClusterIP через eBPF без вообще каких-либо netfilter правил.
Weave Net
Устаревший, рекомендации к продакшну сегодня нет. Использует собственный overlay с шифрованием.
На CKAD не требуют знания конкретного CNI и его конфигурации — это CKA. Но нужно понимать, что есть выбор, и что CNI определяет: тип сети (overlay vs routing), поддержка NetworkPolicy, производительность. На собеседовании “почему выбрали Calico/Cilium” — частый вопрос.
Pod-to-Pod внутри ноды: veth и bridge
Внутри одной ноды Pod-ы соединяются через veth-пары: пара виртуальных интерфейсов, как кабель между двумя network namespaces. Один конец — eth0 внутри Pod, второй — cali... / vethXXX на ноде. Все эти “ножные” концы либо подключены к bridge (Linux bridge), либо обрабатываются роутингом (Calico без bridge).
# На ноде посмотреть veth-пары
ip link | grep cali
# Routing table
ip route
# 10.244.1.5 dev cali123 scope link
# 10.244.1.6 dev cali456 scope link
# 10.244.2.0/24 via 192.168.1.20 dev eth0 ← маршрут на другую ноду
Pod-to-Pod внутри ноды — это просто bridging/routing в kernel, никакого encapsulation. Latency микросекундная.
Pod-to-Pod между нодами: overlay vs underlay
Когда Pod на node 1 пишет Pod-у на node 2 — пакет должен пройти через физическую сеть между нодами. Здесь два подхода:
Overlay (VXLAN, Geneve)
Пакет упаковывается в UDP (VXLAN: UDP 8472, Geneve: UDP 6081). Внешний IP — IP ноды-источника, внутренний — Pod-у-получателю. Преимущество: не нужно конфигурировать underlying сеть, работает где угодно. Недостаток: MTU overhead (50 байт на VXLAN), CPU на encap/decap.
Underlay routing (BGP, direct routes)
Маршруты до Pod CIDR-ов нод рассылаются по BGP, либо прописываются вручную/cloud route table. Пакет от Pod A идёт прямо как src=10.244.1.5, dst=10.244.2.7 без encap — сеть знает как маршрутизировать. Преимущество: без overhead, нативная производительность. Недостаток: требует контроля над сетью между нодами.
eBPF (Cilium)
Использует eBPF-программы на TC (traffic control) hooks для туннелирования напрямую через VXLAN или native routing, плюс полностью свой стек для NodePort, ClusterIP, NetworkPolicy. Скоростной плюс и observability.
Killer момент: Pod не знает о Service ClusterIP
Это важный концептуальный поворот, который проваливают многие.
Когда Pod отправляет HTTP-запрос на http://my-service:80 — он резолвит DNS → получает ClusterIP (например, 10.96.0.10) → отправляет TCP SYN на 10.96.0.10:80. Pod НЕ ЗНАЕТ, что это виртуальный IP. С его точки зрения он шлёт пакет на конкретный адрес.
Что происходит дальше — это работа kube-proxy (или его замены через eBPF в Cilium). На локальной ноде Pod-а netfilter (через iptables/IPVS/nftables правила, сгенерированные kube-proxy) перехватывает пакет:
- Видит dst
10.96.0.10:80— это ClusterIP Servicemy-service. - Выбирает один из endpoints (Pod IP) — например,
10.244.2.7:8080. - Делает DNAT (destination NAT): меняет dst в IP-заголовке на
10.244.2.7:8080. - Пакет уходит дальше по обычным CNI-маршрутам до этого Pod-а.
Никакой Pod в кластере не отвечает на ARP по адресу 10.96.0.10. Этот IP не привязан ни к какому интерфейсу. Если запустить ping 10.96.0.10 — пакет уйдёт, но никто на ARP-запрос не ответит. Service работает только для TCP/UDP, потому что только эти протоколы обрабатываются netfilter-правилами kube-proxy для DNAT.
# Это НЕ будет работать на ClusterIP:
kubectl exec -it pod -- ping 10.96.0.10
# А это будет:
kubectl exec -it pod -- curl http://10.96.0.10:80
CoreDNS: in-cluster DNS
Без DNS все Pod-ы должны были бы знать ClusterIP-ы Service-ов через env vars или конфиги. Но ClusterIP может меняться (если пересоздать Service). Поэтому используют DNS: <service>.<namespace>.svc.cluster.local.
В кластере есть Deployment coredns в namespace kube-system, expose-нутый как Service kube-dns (имя историческое — от старого kube-dns server-а до v1.11). kubelet прописывает каждому Pod-у в /etc/resolv.conf IP этого Service-а в качестве nameserver:
# Внутри Pod:
$ cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
При обращении к my-service — резолвер по search-domains пробует my-service.default.svc.cluster.local → отправляет DNS query на 10.96.0.10:53 → CoreDNS возвращает ClusterIP. Дальше — DNAT через kube-proxy на endpoint.
CoreDNS — это обычные Pod-ы. Они отвечают на DNS-запросы, общаются с apiserver через watch на Services/Endpoints/Pods и кэшируют записи. Подробнее DNS — в уроке 4.
Полная картина: путь HTTP-запроса
Соберём всё вместе. Pod frontend в namespace default пишет curl http://api:8080/users:
- DNS resolve:
apiчерез/etc/resolv.conf→api.default.svc.cluster.local→ DNS query на10.96.0.10:53(CoreDNS). - Pod-to-Pod на ClusterIP CoreDNS: пакет DNS-query летит на ClusterIP
10.96.0.10→ netfilter DNAT на один из CoreDNS Pod-ов (например,10.244.3.4). Ответ обратно через conntrack. - CoreDNS отвечает: ClusterIP service
api—10.96.0.42. - TCP connect на api ClusterIP: пакет на
10.96.0.42:8080→ netfilter DNAT на один из endpoints api-сервиса (например,10.244.2.7:8080). - CNI доставляет пакет: внутри ноды через veth/bridge, между нодами — overlay/BGP. Pod
apiполучает пакет отfrontendIP.
Всё это происходит на каждом TCP-handshake. Понимание этой цепочки — основа диагностики проблем connectivity.