Под не достучался до базы. Сервис отвечает то 200, то таймаут. DNS внутри пода резолвит внешний домен по полсекунды, а у соседнего пода - мгновенно. Половина команд в такой ситуации идет в логи приложения, хотя проблема живет на уровень ниже - в сети кластера. Понять, как именно kubernetes сеть устроена под капотом, это не академический интерес, а способ за пять минут локализовать инцидент вместо двух часов гадания.
В этом уроке разберем три кита: модель сети Kubernetes (почему у каждого пода свой IP и нет NAT), плагины kubernetes cni (Flannel, Calico, Cilium), service discovery через CoreDNS с его знаменитыми ndots-граблями, и изоляцию через NetworkPolicy. Везде - не определения из документации, а механика: как пакет реально идет от пода к поду и от пода к сервису, и где это ломается.

Сетевая модель: плоская сеть и под-к-поду без NAT
Kubernetes навязывает жесткие правила, которые обязан выполнить любой CNI. Их всего четыре, и они звучат обманчиво просто:
- каждый под получает собственный уникальный IP в пределах кластера;
- под-к-поду общается напрямую, без NAT - под видит реальный IP собеседника, а не подмененный;
- под и нода, на которой он живет, тоже видят друг друга без трансляции адресов;
- IP, который под видит у себя (через ip addr), совпадает с тем, каким его видят другие.
Почему "без NAT" так важно. Если бы под-к-поду шел через трансляцию, приложение видело бы чужой адрес шлюза вместо реального источника. Сломались бы все механизмы, завязанные на source IP: аудит, rate limiting по клиенту, mTLS-валидация, да и сами NetworkPolicy (они фильтруют по реальным IP подов). Поэтому модель и требует прозрачности.
Важно различать три плоскости адресов. Pod CIDR - диапазон, из которого раздаются IP подам (например 10.244.0.0/16), его режет CNI. Service CIDR - виртуальные адреса сервисов (ClusterIP), их не существует ни на одном интерфейсе, это правила в iptables/IPVS/eBPF. Node IP - реальные адреса нод. Когда новичок путает ClusterIP с подовым IP и пытается пингануть сервис - пинг не идет, потому что ClusterIP не отвечает на ICMP, он живет только как DNAT-правило.
CNI-плагины: кто и как реализует kubernetes cni
Сам Kubernetes сеть не строит. Kubelet через стандарт CNI (Container Network Interface) дергает бинарь плагина в момент создания пода: "дай этому сетевому namespace интерфейс и IP". Плагин создает veth-пару (один конец в поде как eth0, другой на ноде), назначает адрес из Pod CIDR и прописывает маршруты. Дальше начинается специфика конкретного плагина - и именно от выбора CNI зависит, заработают ли у тебя сетевые политики.
Flannel - самый простой. Дает плоскую сеть через оверлей VXLAN: пакет пода заворачивается в UDP и едет между нодами. Просто, надежно, но не поддерживает NetworkPolicy вообще. Если ты ставишь Flannel и применяешь политику - она создастся в etcd и молча не сработает. Это грабли номер один в сети кластера. Flannel хорош для учебных стендов и k3s "из коробки".
Calico - рабочая лошадка продакшена. Умеет два режима: оверлей (IP-in-IP/VXLAN) и нативный роутинг через BGP, когда ноды анонсируют свои Pod CIDR соседям как настоящие маршрутизаторы - тогда оверлея нет вовсе, пакеты идут чистым L3 с минимальным оверхедом. Calico полноценно реализует NetworkPolicy плюс свой расширенный GlobalNetworkPolicy. Связка calico cilium - типичный предмет выбора при проектировании.
Cilium - тренд 2026 и де-факто новый стандарт. Построен на eBPF: вместо тысяч правил iptables политики и балансировка компилируются в программы, которые исполняются прямо в ядре Linux на хуках сетевого стека. Это дает три вещи. Первая - скорость: на больших кластерах iptables линейно деградирует, eBPF держит O(1) по хешам. Вторая - политики L7: можно разрешить не просто "порт 8080", а "только GET /api/v1/orders", фильтруя HTTP, gRPC, Kafka. Третья - наблюдаемость через Hubble: видно поток каждого пакета и причину дропа. Cilium полностью совместим со стандартным networking.k8s.io/v1 NetworkPolicy, а для L7 добавляет свой CRD cilium.io/v2 CiliumNetworkPolicy - его берешь точечно, только когда нужен слой 7. Именно поэтому в спорах calico cilium для новых кластеров чаша все чаще склоняется к Cilium.
CoreDNS и service discovery: как coredns kubernetes находит сервисы
ClusterIP сервиса - случайное число из Service CIDR, оно меняется при пересоздании. Хардкодить его нельзя, поэтому поды ходят по именам. За резолв имен в кластере отвечает coredns kubernetes - это поды (обычно в kube-system), которые сами выступают сервисом kube-dns с фиксированным ClusterIP, и этот адрес прописан в /etc/resolv.conf каждого пода.
Формат FQDN сервиса: <service>.<namespace>.svc.cluster.local. Из того же namespace достаточно короткого имени, из другого - service.namespace. Посмотрим реальный resolv.conf внутри пода:
Код: Выделить всё
$ kubectl exec -it web-7c9f -- cat /etc/resolv.conf
nameserver 10.96.0.10
search myapp.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
Грабли ndots на пальцах. ndots:5 означает: если в имени меньше 5 точек, считай его неполным и сначала пройдись по всем search-доменам, и только потом пробуй как есть. Теперь смотри, что происходит при запросе внешнего api.github.com (две точки, меньше пяти):
Код: Выделить всё
api.github.com.myapp.svc.cluster.local -> NXDOMAIN
api.github.com.svc.cluster.local -> NXDOMAIN
api.github.com.cluster.local -> NXDOMAIN
api.github.com. -> A 140.82.x.x (наконец-то)
Код: Выделить всё
spec:
dnsConfig:
options:
- name: ndots
value: "2"
NetworkPolicy: default-deny плюс явные allow
По умолчанию сеть полностью открыта - любой под достучится до любого. Для продакшена это недопустимо: скомпрометированный фронтенд не должен лезть напрямую в базу платежей. NetworkPolicy - это файрвол на уровне подов, отбирающий цели по меткам, namespace и портам. Критично помнить: политика работает только если ее поддерживает CNI. На Flannel она бесполезна (нужен Calico, Cilium или Calico-режим у managed-провайдера).
Логика построена на белых списках. Как только хотя бы одна политика выбрала под через podSelector для данного направления (Ingress или Egress), под переключается в режим "запрещено все, кроме явно разрешенного" по этому направлению. Стандартный паттерн - сначала глобальный default-deny на весь namespace, потом точечные allow.
Код: Выделить всё
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: shop
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Код: Выделить всё
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
namespace: shop
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432
Поток пакета: под-к-поду и под-к-сервису
Под-к-поду на одной ноде: пакет выходит из eth0 пода в его veth, попадает на ноду, плагин знает локальные маршруты и кладет пакет прямо в veth целевого пода. На разных нодах: CNI либо заворачивает в оверлей (VXLAN у Flannel, IP-in-IP у Calico), либо маршрутизирует нативно по BGP (Calico) или eBPF (Cilium) - source IP при этом сохраняется, что и требует модель.
Под-к-сервису сложнее, потому что ClusterIP - фикция. Когда под шлет на 10.96.0.50:80, в дело вступает kube-proxy (или eBPF у Cilium). Он заранее разложил для каждого сервиса DNAT-правило: "пакеты на этот ClusterIP подменяй на IP одного из живых эндпоинтов". Список живых подов сервиса берется из EndpointSlices - это пришедшая на смену старому Endpoints структура, которая шардит эндпоинты по 100 штук на объект, чтобы не перегружать API при тысячах подов. То есть ClusterIP - это просто стабильное имя для меняющегося набора реальных адресов, а трансляция происходит на исходящей ноде до отправки в сеть.
Диагностика сети: команды и разбор вывода
Базовый чек-лист, когда "под не ходит". Сначала смотрим, есть ли вообще эндпоинты у сервиса - пустой список означает, что селектор сервиса не совпал с метками подов или поды не готовы:
Код: Выделить всё
$ kubectl get endpointslices -l kubernetes.io/service-name=db -n shop
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
db-x7k2q IPv4 5432 10.244.1.23 12d
Код: Выделить всё
$ kubectl debug -it web-7c9f --image=nicolaka/netshoot -- bash
# nslookup db.shop.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: db.shop.svc.cluster.local
Address: 10.244.1.23
# nc -zv 10.244.1.23 5432
Connection to 10.244.1.23 5432 port [tcp/*] succeeded!
Код: Выделить всё
$ hubble observe --pod shop/web-7c9f --verdict DROPPED
... web-7c9f -> db-0 Policy denied DROPPED (TCP Flags: SYN)
Типичные грабли и антипаттерны
- Политики на CNI без поддержки. Самая коварная: NetworkPolicy на Flannel применяется без ошибок и молча не действует. Перед тем как полагаться на изоляцию, проверь, что CNI умеет политики.
- default-deny egress без разрешения DNS. Закрыли весь исходящий - и сломали резолв, потому что запрос к CoreDNS на UDP/TCP 53 тоже исходящий. Всегда добавляй allow на kube-dns.
- ndots:5 и внешние домены. Медленный DNS у приложений с обилием внешних вызовов - почти всегда это. Точка в конце или ndots:2.
- Путаница ClusterIP и Pod IP. Пинг ClusterIP не отвечает - это нормально, он не существует как интерфейс.
- И-против-ИЛИ в selector. Неверная вложенность namespaceSelector и podSelector тихо расширяет или сужает доступ.
На kind или k3s с Calico/Cilium (не Flannel - на нем шаг 4 не сработает):
- Создай namespace shop, подними два пода: api и db (например образ nginx), навесь метки app=api и app=db.
- Загляни в resolv.conf: kubectl exec в под и cat /etc/resolv.conf - найди ndots и search.
- Из api эфемерным netshoot-контейнером проверь связь до db по имени и порту - должно работать.
- Примени default-deny-all и allow-api-to-db из урока. Повтори проверку: из api доступ остался, а с третьего пода без метки app=api - таймаут.
- На Cilium запусти hubble observe --verdict DROPPED и поймай Policy denied.
- Почему модель Kubernetes запрещает NAT в трафике под-к-поду и что сломается, если его включить?
- Что произойдет с запросом api.github.com при ndots:5 и как это исправить, не трогая код приложения?
- Чем ClusterIP отличается от Pod IP на уровне того, где он "физически" существует?
- Как с помощью двух NetworkPolicy реализовать схему "по умолчанию запрещено, разрешен только api->db:5432", и почему важно не забыть про DNS?
Сеть кластера стоит на трех опорах. Модель дает плоскую сеть с уникальным IP на под и без NAT - это контракт, который выполняет CNI. Выбор kubernetes cni определяет возможности: Flannel прост, но без политик; Calico дает BGP и полноценный файрвол; Cilium на eBPF добавляет L7 и наблюдаемость, и в 2026 это магистраль. CoreDNS превращает имена в адреса, но ndots:5 портит жизнь внешним вызовам. А NetworkPolicy по схеме default-deny плюс явные allow закрывает кластер - но только если CNI это умеет. Держа в голове путь пакета под-к-поду и под-к-сервису, ты диагностируешь сетевой инцидент по эндпоинтам, DNS и вердиктам политик за минуты.