
Статья подготовлена вместе с экспертом
Сергей Новиков, менеджер продукта Managed Kubernetes

Сеть в Kubernetes на первый взгляд выглядит набором разрозненных настроек: сегодня инженер выбирает тип Service, завтра разбирается с DNS-именами, потом пишет сетевую политику, которая почему-то не срабатывает. Каждую тему учат отдельно, и складывается впечатление, что Service, DNS, CNI и Network Policies — это независимые фичи, которые нужно зазубрить по очереди.
На самом деле всё это — реализации одной модели. В её основе три правила: у каждого пода свой IP, поды общаются без NAT, адресное пространство плоское. Service балансирует трафик к группе подов, DNS превращает имена в адреса, CNI прокладывает саму сеть, а Network Policies её сегментируют. Как только модель видна целиком, перестаёшь путать выбор типа Service с причинами, почему не срабатывает политика.
Дальше разберём модель снизу вверх: плоская сеть подов, четыре типа Service, DNS-резолвинг через CoreDNS, CNI-плагины и эволюция dataplane, сетевые политики и устройство сети в управляемом кластере VK Cloud.

Сергей Новиков, менеджер продукта Managed Kubernetes
Прежде чем говорить про Service и DNS, нужно зафиксировать фундамент. Kubernetes не изобретает сеть с нуля — он задаёт требования к ней, а реализацию отдаёт плагину. Эти требования и есть сетевая модель.
Она держится на трёх правилах:
Важно понимать, что под не знает и не должен знать, где физически крутится его собеседник. Приложение обращается к IP пода, и сеть доставляет пакет. Реализует эти правила CNI-плагин, а Kubernetes требует их соблюдения от любой реализации.
Вот как это выглядит:
text
Узел A (10.0.1.0/24) Узел B (10.0.2.0/24) ┌──────────────────────┐ ┌──────────────────────┐ │ Pod 10.244.1.5 │ │ Pod 10.244.2.7 │ │ Pod 10.244.1.6 │◄────►│ Pod 10.244.2.8 │ └──────────────────────┘ └──────────────────────┘ │ │ └────────── плоская сеть ──────┘ Каждый под — свой IP, связь без NAT, единое адресное пространство кластера
Поды на разных узлах общаются по реальным IP напрямую. Адреса узлов и адреса подов лежат в разных подсетях, и задача CNI — связать их так, чтобы под с узла A дошёл до пода на узле B без трансляции адресов.
Почему это важно для всего остального? Под — сущность эфемерная: его пересоздают при обновлении, переносят при отказе узла, масштабируют, и IP пода меняется при каждом пересоздании. Обращаться к поду по адресу напрямую нельзя, завтра адрес будет другим. Отсюда и потребность в стабильной точке входа, которая переживёт пересоздание подов. Эту роль берёт на себя Service.
Service — стабильный сетевой адрес для группы подов. Поды за ним приходят и уходят, а адрес и DNS-имя Service остаются на месте. Service выбирает поды по меткам (label selector) и балансирует на них трафик. По способу доступа различают четыре типа.
ClusterIP — адрес, доступный только внутри кластера. Это тип по умолчанию и основа сервис-дискавери: на него указывает DNS-имя сервиса. Снаружи такой Service недоступен, он существует для общения подов между собой.
text
apiVersion: v1 kind: Service metadata: name: backend spec: type: ClusterIP selector: app: backend ports: - port: 80 targetPort: 8080
NodePort открывает один и тот же порт на каждом узле кластера и открывает порт на каждом узле и проксирует трафик на поды . Диапазон портов по умолчанию — 30000–32767, его задаёт флаг --service-node-port-range. Подходит для отладки или интеграции с внешним балансировщиком, но в проде напрямую используется редко: клиенту нужно знать адреса узлов и держать в голове нестандартный порт.
text
apiVersion: v1 kind: Service metadata: name: backend-nodeport spec: type: NodePort selector: app: backend ports: - port: 80 targetPort: 8080 nodePort: 30080
LoadBalancer запрашивает у облака внешний IP и поднимает балансировщик поверх NodePort. Это штатный способ выставить сервис наружу: облако выдаёт внешний IP для сервиса, а трафик распределяется по подам. Маршрутизация по HTTP-путям и хостам — уже задача Ingress в Kubernetes, а не самого Service.
text
apiVersion: v1 kind: Service metadata: name: backend-lb spec: type: LoadBalancer selector: app: backend ports: - port: 80 targetPort: 8080
ExternalName не проксирует трафик и не имеет селектора — он отдаёт CNAME на внешнее имя, то есть даёт внутреннее имя внешнему ресурсу, например базе данных за пределами кластера.
text
apiVersion: v1 kind: Service metadata: name: external-db spec: type: ExternalName externalName: db.example.com
Все четыре типа Service отвечают на один вопрос: откуда придёт трафик — изнутри, с узла, из облака или с внешнего имени. Для внутреннего общения подходит ClusterIP, наружу выставляют через LoadBalancer, а мост к внешнему ресурсу даёт ExternalName.
Service даёт стабильный IP, но обращаться к сервисам по IP неудобно: адрес ClusterIP назначается при создании и держится за Service, а помнить его никто не хочет. Для этого работает сервис-дискавери — поиск сервисов по именам. За него отвечает DNS.
DNS-сервер в кластере — CoreDNS. Это обычный Deployment в namespace kube-system, как правило с ClusterIP 10.96.0.10. CoreDNS следит за API-сервером: появился Service — появилась DNS-запись.
Полное имя сервиса (FQDN) строится по шаблону <сервис>.
text
my-svc.my-namespace.svc.cluster.local → 10.96.x.x (ClusterIP)
Проверить резолвинг можно изнутри любого пода:
text
$ nslookup backend.production.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10#53 Name: backend.production.svc.cluster.local Address: 10.96.x.x
Здесь есть деталь, на которой многие спотыкаются. Внутри своего namespace под обращается к сервису по короткому имени — просто backend, DNS дописывает суффикс текущего namespace и резолвит его. Но если сервис живёт в другом namespace, короткое имя не сработает: нужно полное имя backend.production или лучше FQDN backend.production.svc.cluster.local. Большинство проблем вида «сервис недоступен, хотя поды живые» сводится именно к обращению в чужой namespace по короткому имени.
DNS — тоже реализация сетевой модели: имена существуют, потому что у каждого Service есть стабильный ClusterIP, а у ClusterIP — место в плоской сети. Без модели резолвить было бы просто нечего. В VK Cloud управляемый Kubernetes идёт с готовой сетью: CoreDNS, балансировщик и сетевые политики уже настроены, и разворачивать сетевой слой вручную не нужно. До 12 000 ₽ на тест → Cloud Containers.
Несколько раз выше упоминалось «реализует CNI» без объяснения, что это. CNI (Container Network Interface) — стандарт, по которому подключаемый плагин выполняет три правила сетевой модели. Именно CNI назначает подам IP, прокладывает маршруты между узлами и обеспечивает связь без NAT. Без CNI поды не получат сеть вообще.
Плагины различаются тем, как они доставляют пакет от пода на одном узле до пода на другом:
Отдельная история — как dataplane обрабатывает правила Service. Исторически kube-proxy в режиме iptables строит ruleset размером O(N) от суммы сервисов и эндпойнтов: чем больше сервисов, тем длиннее цепочки правил, через которые проходит каждый пакет. На больших масштабах, порядка 5000 сервисов, такой кластер начинает заметно проседать. Поэтому dataplane эволюционировал: iptables → IPVS → nftables (стал stable в Kubernetes 1.33) → eBPF. Cilium на eBPF использует хеш-таблицы и даёт lookup за O(1) вместо O(N) у iptables.
Практический вывод такой: пока кластер маленький, разница между подходами почти не ощущается, и можно ставить LoadBalancer, не думая о dataplane. На сотнях сервисов незнание модели уже стоит дорого: линейный рост правил iptables бьёт по задержкам, а отсутствие поддержки Network Policies в CNI оставляет кластер без сегментации.
Здесь стоит упомянуть и слой L7. Маршрутизация по HTTP, mTLS, трафик между сервисами — это уровень выше базовой сети, за него отвечает service mesh в Kubernetes и Ingress, а не CNI. CNI закрывает L3/L4 — то, чтобы пакет вообще дошёл.
По умолчанию в Kubernetes любой под может обратиться к любому другому: сеть плоская и открытая. Для прода это риск — скомпрометированный фронтенд не должен ходить напрямую в базу данных. Сегментацию задают Network Policies — правила, описывающие, кому с кем можно общаться. Подробнее про защиту кластера — в материале про безопасность Kubernetes.
Логика NetworkPolicy строится на нескольких правилах. podSelector выбирает поды, к которым применяется политика, а пустой селектор {} означает «все поды namespace». Под не изолирован до тех пор, пока его не выбрала хотя бы одна политика, но как только он попал под политику, для него начинает действовать принцип: всё, что не разрешено, запрещено. default-deny — это политика с пустым селектором, блокирующая весь ingress и egress: она изолирует все поды namespace, после чего нужный трафик открывают отдельными разрешающими политиками.
Ключевой момент в том, что политики реализует CNI. Если плагин их не поддерживает, как Flannel без надстройки, манифест NetworkPolicy применится без ошибок, но трафик ограничен не будет. Это коварно: кажется, что сегментация настроена, а на деле её нет.
Рабочий пример — default-deny плюс разрешение для бэкенда ходить только к базе, с обязательным egress на DNS, без которого под не сможет резолвить имена:
text
# 1. Закрываем весь трафик в namespace apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny namespace: production spec: podSelector: {} policyTypes: - Ingress - Egress --- # 2. Разрешаем backend ходить к db и в DNS apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: backend-to-db namespace: production spec: podSelector: matchLabels: app: backend policyTypes: - Egress egress: # доступ к базе - to: - podSelector: matchLabels: app: db ports: - protocol: TCP port: 5432 # доступ к DNS (иначе сломается резолвинг) - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - protocol: TCP port: 53
Частая ошибка — забыть про egress на порт 53. После включения default-deny под перестаёт резолвить DNS-имена, и всё внезапно ломается, хотя политика формально верна. Это прямое следствие того, что DNS — часть сетевой модели, а не отдельный сервис вне неё.
В управляемом кластере VK Cloud всё описанное выше уже собрано и настроено, и сервис Cloud Containers показывает, как сетевая модель Kubernetes ложится на реальную инфраструктуру.
Физический слой обеспечивает SDN Sprut — собственная программно-определяемая сеть VK Cloud, которую используют вместо OpenStack Neutron. Sprut работает на BGP с BFD и даёт низкую задержку между нодами кластера, реализуя плоскую сеть подов на уровне физической инфраструктуры.
За CNI отвечает Calico: сеть подов и Network Policies строятся на маршрутизации по BGP с инкапсуляцией IPIP/VXLAN при необходимости, и сетевые политики из примеров выше работают здесь без дополнительной настройки плагина. CoreDNS обслуживает сервис-дискавери, а метрики уходят в Prometheus. kube-proxy работает в режиме iptables. Load Balancer построен на Octavia (HAProxy) с поддержкой L4/L7 и SSL-терминацией, а реальный IP клиента пробрасывается через Proxy Protocol или externalTrafficPolicy: Local. Kube-in-Kube изолирует Control Plane клиента внутри системного кластера, защищая DNS и сетевые плагины от изменений со стороны пользователя.
Production-кластер Kubernetes разворачивается автоматически с изолированным control plane по модели Kubernetes-in-Kubernetes. Self-Healing автоматически заменяет отказавшие ноды без ручного вмешательства инженера, однако отказоустойчивость приложения по-прежнему зависит от корректной настройки PDB и распределения реплик.
Сеть держится на трёх правилах модели: у каждого пода уникальный IP в кластере, поды общаются без NAT, адресное пространство плоское. Реализует эти правила CNI-плагин. Поверх модели работают Service (стабильный адрес для группы подов), DNS (имена вместо адресов) и Network Policies (сегментация).
ClusterIP даёт адрес только внутри кластера и используется по умолчанию. NodePort открывает порт на каждом узле в диапазоне 30000–32767. LoadBalancer запрашивает у облака внешний IP и поднимает балансировщик поверх NodePort — это штатный способ выставить сервис наружу.
DNS обслуживает CoreDNS — Deployment в namespace kube-system, обычно с ClusterIP 10.96.0.10. Сервис резолвится по FQDN <сервис>.
CNI (Container Network Interface) — стандарт подключаемого плагина, который реализует сетевую модель: назначает подам IP, прокладывает маршруты, обеспечивает связь без NAT. Подходы различаются: overlay (Flannel), routing с BGP (Calico), eBPF (Cilium). Часть плагинов, например Flannel, не поддерживает Network Policies напрямую.
Через Network Policies. По умолчанию все поды открыты, а политика с пустым селектором и блоком ingress/egress даёт default-deny, после чего нужный трафик открывают отдельными правилами. Важно не забыть egress на DNS (порт 53), иначе сломается резолвинг имён. Политики реализует CNI, и если плагин их не поддерживает, манифест применится, но не сработает.
Сеть Kubernetes — единая модель с тремя правилами: свой IP у пода, связь без NAT, плоская сеть. Всё остальное лишь надстраивается над ней: Service даёт стабильный адрес группе подов, DNS превращает имена в адреса, CNI прокладывает саму сеть и определяет, как быстро dataplane обрабатывает правила, а Network Policies сегментируют трафик поверх плоской сети.
На небольшом кластере эта конструкция прощает почти всё: можно ткнуть LoadBalancer наугад, забыть про egress на DNS, не думать про dataplane — и всё будет работать. А вот на сотнях сервисов та же беспечность превращается в задержки, залипшие правила iptables и NetworkPolicy, которая существует только на бумаге. Разница между «сеть работает» и «сеть работает предсказуемо» — это ровно та модель, которую мы разобрали: зная её, вы выбираете тип Service не гадая, чините DNS за минуту и пишете политики, которые действительно ограничивают трафик, а не просто красиво выглядят в манифесте.
Наши специалисты свяжутся с вами в ближайшее время и ответят на все вопросы.

Будем держать в курсе новостей и облачных трендов




