NodePort в Kubernetes: как опубликовать сервис на порту каждой ноды | AdminWiki

NodePort в Kubernetes: как опубликовать сервис на порту каждой ноды

30 сентября 2026 12 мин. чтения

Что такое NodePort и зачем он нужен

NodePort открывает статический TCP-порт на каждой ноде кластера и проксирует трафик с этого порта в поды приложения. Клиент обращается к IP любой ноды на заданном порту, и запрос доходит до сервиса, даже если нужный под работает на другой машине. Один и тот же номер порта используется на каждой ноде (Service | Kubernetes).

Путь запроса такой: внешний клиент, IP ноды и nodePort, kube-proxy, ClusterIP сервиса, под. Диапазон по умолчанию: 30000-32767. Маршрутизация идёт на уровне L4, то есть по TCP/UDP-соединениям, без разбора HTTP-заголовков и путей. Как соотносятся уровни L4 и L7, разбираем в статье Маршрутизация в Kubernetes: Service, Ingress и Gateway API на уровнях L4 и L7.

NodePort решает конкретную задачу: опубликовать сервис наружу, когда облачного балансировщика нет. Kubernetes поддерживает четыре базовых типа Service: ClusterIP, NodePort, LoadBalancer и ExternalName. NodePort при создании автоматически получает ClusterIP, поэтому работает как надстройка над внутренним сервисом. Если сомневаетесь, какой тип брать под задачу, начните с критериев выбора в руководстве Как выбрать тип Service в Kubernetes: ClusterIP, NodePort или LoadBalancer.

Как NodePort пробрасывает трафик на ClusterIP

Порядок обработки пакета на ноде:

  1. Клиент отправляет запрос на NodeIP:30080.
  2. kube-proxy ловит пакет в цепочке KUBE-NODEPORTS и переводит его в цепочку конкретного сервиса.
  3. Правила DNAT подменяют адрес назначения на ClusterIP сервиса, затем на IP выбранного пода и его targetPort.
  4. Ответ уходит обратно через ту же ноду, для этого kube-proxy включает masquerading (SNAT).

Пример правил iptables в режиме kube-proxy:

-A KUBE-NODEPORTS -p tcp -m comment --comment "default/web:http" -m tcp --dport 30080 -j KUBE-SVC-2QZ7A5XWQ3YH4K7T
-A KUBE-SVC-2QZ7A5XWQ3YH4K7T -m statistic --mode random --probability 0.333 -j KUBE-SEP-6B4M2LPQH7ZDXK9R

Первая цепочка принимает соединение на порту ноды, вторая распределяет его между эндпоинтами. Вероятность 0.333 в примере означает три пода: первый получает треть соединений, остаток уходит дальше по цепочке.

kube-proxy работает на каждой ноде и следит за объектами Service и EndpointSlice через API-сервер. Изменился набор готовых подов, правила переписываются автоматически. В режиме iptables балансировка выполняется через statistic random. В режиме IPVS kube-proxy использует API ядра IPVS и iptables, чтобы создавать правила перенаправления трафика с IP сервисов на IP эндпоинтов (Virtual IPs and Service Proxies | Kubernetes); планировщик задаётся полем ipvs.scheduler в конфигурации kube-proxy, а для каждого отдельного listener конфигурация IPVS содержит набор адресов бэкенд-эндпоинтов с распределением трафика по кругу (round-robin) по умолчанию (NodePort :: The Kubernetes Networking Guide). IPVS применяет хеш-таблицы вместо линейных цепочек iptables, поэтому производительность остаётся стабильной независимо от числа сервисов, тогда как в режиме iptables при тысячах сервисов число правил растёт линейно и их перебор становится узким местом (How to Configure Internal Load Balancing on Talos Linux). Посмотреть таблицу IPVS можно командой:

ipvsadm -Ln

Две детали, которые чаще всего ломают ожидания. Первая: порт открыт на всех нодах, включая те, где нет ни одного пода этого сервиса, поэтому добавляется один лишний хоп и SNAT. Вторая: из-за SNAT реальный IP клиента по умолчанию теряется, если не задать externalTrafficPolicy: Local.

Диапазон портов по умолчанию и как его изменить

Kubernetes выделяет nodePort из диапазона, который задаёт флаг --service-node-port-range; значение по умолчанию - 30000-32767 (Service | Kubernetes). Один и тот же номер порта используется на каждой ноде. Диапазон выбран в основном для того, чтобы избежать конфликтов с зарезервированными номерами портов и другими портами на ноде (Why Kubernetes NodePort Services Range From 30000–32767). Внутри диапазона есть две полосы: динамическое назначение по умолчанию использует верхнюю полосу и может задействовать нижнюю, когда верхняя исчерпана (Service | Kubernetes).

Изменить диапазон можно, перезапустив apiserver с новым значением:

kube-apiserver --service-node-port-range=20000-25000

Смена диапазона не перевыделяет порты у уже созданных сервисов, они сохраняют свои значения. Если указать nodePort явно вне диапазона, объект не создастся. Ошибка выглядит так:

The Service "web" is invalid: spec.ports[0].nodePort: Invalid value: 80: provided port is not in the valid range. The range of valid ports is 30000-32767

В managed-кластерах доступ к флагу apiserver обычно закрыт, и диапазон остаётся стандартным. Практический вывод: выбирайте порт из 30000-32767 и ведите список занятых значений, чтобы не ловить конфликты при масштабировании.

Пошаговая настройка NodePort: манифест и команды

Соберём связку из Deployment с nginx и Service типа NodePort. Оба файла лежат в одном namespace default.

Пример манифеста NodePort для веб-приложения

Deployment на три реплики с меткой app: web и readiness probe, чтобы неготовые поды не попадали в балансировку:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.27
        ports:
        - containerPort: 80
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 3
          periodSeconds: 5

Service того же имени публикует поды на порту ноды 30080:

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: NodePort
  selector:
    app: web
  ports:
  - name: http
    port: 80
    targetPort: 80
    nodePort: 30080

Разбор полей. port - порт ClusterIP, по нему сервис доступен внутри кластера. targetPort - порт, который слушает контейнер. nodePort - порт на нодах, необязательное поле. Если убрать nodePort, Kubernetes сам выберет свободное значение из 30000-32767 и запишет его в spec.ports[0].nodePort. selector обязан совпадать с метками подов: при опечатке сервис останется без эндпоинтов, а клиент получит connection refused.

Применение и проверка:

kubectl apply -f web-deploy.yaml
kubectl get pods -l app=web -o wide
kubectl apply -f web-svc.yaml
kubectl get svc web
kubectl describe svc web

Ожидаемый вывод kubectl get svc:

NAME   TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
web    NodePort   10.96.0.15             80:30080/TCP   12s

Запись 80:30080/TCP читается как «ClusterIP на 80, порт ноды 30080». Для UDP-сервисов в PORT(S) появится 30080/UDP.

Проверка доступности сервиса через IP любой ноды

Сначала получите адреса нод:

kubectl get nodes -o wide
# NAME    STATUS   ROLES    AGE   VERSION   INTERNAL-IP    EXTERNAL-IP
# node1   Ready    worker   10d   v1.31.4   192.168.1.10   203.0.113.10
# node2   Ready    worker   10d   v1.31.4   192.168.1.11   203.0.113.11

Теперь запрос на порт ноды, где подов этого сервиса может вообще не быть:

curl -I http://192.168.1.10:30080
curl -s http://203.0.113.11:30080 | head -n 5

Ответ HTTP/1.1 200 OK означает, что цепочка собрана целиком: kube-proxy, ClusterIP, под. Убедиться, что запрос дошёл до контейнера, поможет хвост логов:

kubectl logs -l app=web --tail=20 -f

Если curl возвращает connection refused или зависает, проверяйте по порядку:

  • kubectl get endpoints web или kubectl get endpointslice -l kubernetes.io/service-name=web. Пустой список означает, что selector не совпал с метками либо поды не готовы.
  • kubectl describe pod показывает readiness probe failed: такой под исключён из балансировки, пока не начнёт отвечать на проверку.
  • targetPort не совпадает с фактическим портом контейнера: пакеты приходят, а приложение их не слушает.
  • firewall на ноде или security group облака блокирует порт. Это самая частая причина, когда внутри кластера всё работает, а снаружи нет.
  • kubectl -n kube-system logs -l k8s-app=kube-proxy: проверьте, что прокси жив и пересобрал правила.

Что происходит при отказе узла

Поскольку порт слушают все ноды, падение одной машины не убирает сервис целиком: тот же nodePort отвечает на остальных нодах. Дальше работает контроллер жизненного цикла нод (node-lifecycle-controller) в составе kube-controller-manager (kube-controller-manager | Kubernetes):

  1. Node controller помечает ноду как NotReady. Окно ожидания задаёт флаг --node-monitor-grace-period: это время, в течение которого работающая нода может не отвечать, прежде чем её признают нездоровой. Значение по умолчанию - 50 секунд. Периодичность синхронизации NodeStatus задаёт --node-monitor-period, по умолчанию 5 секунд (kube-controller-manager | Kubernetes).
  2. Поды на недоступной ноде переводятся в Terminating, а ReplicaSet создаёт замену на живых нодах, если хватает ресурсов.
  3. kube-proxy на всех нодах обновляет правила после изменения EndpointSlice, и трафик уходит на новые поды.

Пока поды на упавшей ноде ещё числятся в эндпоинтах, часть запросов уходит в никуда. Readiness probe сокращает это окно: под, не отвечающий на проверку, исключается из балансировки сразу, и kube-proxy перестаёт слать на него соединения.

Отдельный риск создаёт внешний балансировщик перед NodePort. Ему нужен health check по порту ноды, например TCP 30080, иначе он продолжит отправлять клиентов на мёртвую машину. Проверить актуальный состав эндпоинтов можно командой kubectl get endpoints web -o wide, а подробнее про балансировку и health check читайте в материале Балансировка нагрузки и маршрутизация в Nginx и Kubernetes.

Ограничения и риски безопасности NodePort

NodePort открывает один и тот же порт на каждой ноде: на worker-машинах, на control plane и на хостах, где приложение не запущено вовсе. Поверхность атаки растёт вместе с числом нод. Дополнительно сам Service не даёт ни TLS, ни аутентификации, ни разграничения доступа по домену или пути.

Практический вывод: не выставляйте NodePort в интернет как продакшн-точку входа. Рабочая схема - внешний балансировщик или Ingress с TLS-терминацией перед сервисом, а прямой доступ к порту ноды ограничен по источнику. Альтернативные способы публикации с их плюсами и рисками разобраны в статье про ExternalIP в Kubernetes.

Меры защиты, которые работают вместе:

  • Firewall на нодах: разрешайте порт только доверенным подсетям.
  • NetworkPolicy внутри кластера: ограничивает трафик между подами. Сам порт ноды она не закрывает, поэтому используется в паре с firewall.
  • externalTrafficPolicy: Local: kube-proxy проксирует запросы только на локальные эндпоинты и не перенаправляет их на другие ноды, за счёт чего сохраняется IP клиента (Using Source IP | Kubernetes). Нода принимает внешний трафик только если на ней есть локальные эндпоинты этого сервиса; иначе соединение не переадресуется на другую ноду, SNAT обычно не нужен, и IP клиента сохраняется на уровне TCP. Побочный эффект: часть нод перестаёт быть точкой входа, если на них нет эндпоинтов, а внешний балансировщик должен уметь отправлять трафик только на «здоровые» ноды с локальными эндпоинтами. При включении этого режима Kubernetes также выделяет поле service.spec.healthCheckNodePort (Using Source IP | Kubernetes).
  • Ingress с TLS и внешний балансировщик вместо прямого доступа к порту ноды для HTTP-сервисов.
  • LoadBalancer с MetalLB в bare-metal кластере, если нужен стабильный внешний IP без ручного перебора нод.

Конфликты портов и как их избежать

Уникальный nodePort выделяется для сервисов типа NodePort и LoadBalancer в момент, когда API-сервер сохраняет ресурс Service в постоянное хранилище (например, etcd): для каждого порта в NodePort-сервисе API-сервер выделяет уникальный порт из диапазона NodePort (NodePort :: The Kubernetes Networking Guide). Если два сервиса явно просят один и тот же номер, второй не создастся:

The Service "web-2" is invalid: spec.ports[0].nodePort: Invalid value: 30080: provided port is already allocated

Учтите, что значения ClusterIP и NodePort неизменяемы: их нельзя поменять на протяжении жизненного цикла объекта Service (NodePort :: The Kubernetes Networking Guide). При автоматическом назначении конфликтов не возникает, но есть обратная сторона: удалите сервис и создайте заново, и порт может оказаться другим. Скрипты, внешние балансировщики и firewall-правила, привязанные к конкретному номеру, после такого сломаются. Для стабильности указывайте nodePort в манифесте явно и держите уникальность.

Список занятых портов по всем namespace:

kubectl get svc -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" "}{.spec.ports[*].nodePort}{"\n"}{end}'

Настройка firewall для NodePort

Firewall на ноде не знает о Kubernetes и блокирует порт по умолчанию. Откройте его тем инструментом, который управляет правилами в вашем дистрибутиве.

# firewalld
firewall-cmd --add-port=30080/tcp --permanent && firewall-cmd --reload

# ufw
ufw allow 30080/tcp

# iptables
-A INPUT -p tcp --dport 30080 -j ACCEPT

Ограничьте источник, если сервис нужен не всем. Правило ниже пускает только одну подсеть, остальное отбрасывает:

iptables -A INPUT -p tcp --dport 30080 -s 203.0.113.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 30080 -j DROP

В облаке добавьте те же правила в security group нод: трафик проходит через них раньше, чем доходит до iptables. Порядок важен: сначала разрешающее правило для нужной подсети, затем запрет для всего остального, иначе широкий ACCEPT перекроет ограничение.

Когда NodePort оправдан: сценарии использования

NodePort уместен там, где важна скорость запуска, а не сквозная безопасность и маршрутизация по домену:

  • dev-стенды и тестовые окружения, где сервис живёт день-два и не требует высокой доступности.
  • on-premise кластеры без облачного балансировщика и без MetalLB.
  • быстрый доступ к сервису для отладки: проверить ответ приложения снаружи, не поднимая Ingress.
  • временная публикация сервиса или передача порта коллеге на время работ.
  • инфраструктурные сервисы вроде внутренней панели мониторинга, доступ к которым ограничен по IP.

В продакшене NodePort обычно закрывают внешним балансировщиком: MetalLB в bare-metal, аппаратный LB, HAProxy или nginx. Для HTTP-трафика чаще берут Ingress-контроллер: он даёт TLS, маршрутизацию по домену и путям, единую точку входа вместо списка нод с номерными портами.

Сравнение NodePort, LoadBalancer и Ingress

КритерийClusterIPNodePortLoadBalancerIngress
УровеньL4L4L4L7, HTTP/HTTPS
Доступ снаружинетIP ноды и порт 30000-32767внешний IP от провайдерадомен и путь через контроллер
Требования к инфраструктуренетоткрытый порт на нодахоблако или MetalLB в bare-metalIngress-контроллер и DNS
TLS и маршрутизация по доменунетнеттерминация на балансировщикесертификаты и rules
Кто потребляет client IPне применимотеряется без externalTrafficPolicy: Localзависит от провайдеразаголовки X-Forwarded-For
Типичный сценарийобмен между сервисамиdev-стенд, отладка, on-premise без LBпродакшн-сервис в облакепубликация веб-приложений

Правило выбора простое. Внутрикластерный трафик - ClusterIP. Нужен внешний IP без ручной работы с нодами в облаке или с MetalLB - LoadBalancer. Нужен TLS, несколько доменов и общий балансировщик на десятки сервисов - Ingress. NodePort берите для dev, отладки и там, где других вариантов нет, но держите его за балансировщиком или firewall.

Чек-лист: настройка NodePort без ошибок

  1. Поды запущены и готовы: kubectl get pods -l app=web показывает Running и 1/1.
  2. selector в Service совпадает с метками подов символ в символ.
  3. targetPort равен порту, который реально слушает контейнер.
  4. nodePort задан явно из 30000-32767 либо оставлен пустым для автоназначения.
  5. kubectl get svc web выводит ожидаемую строку вида 80:30080/TCP.
  6. kubectl get endpoints web возвращает IP подов, а не пустой список.
  7. Порт открыт в firewalld, ufw или iptables и в security group облака, источник ограничен по IP.
  8. curl -I http://NodeIP:30080 отвечает 200 с внешней машины, а в логах пода видно запрос.
  9. Проверен отказ ноды: сервис отвечает через остальные ноды, поды переехали, эндпоинты обновились.
  10. Решено, нужен ли внешний балансировщик, Ingress с TLS и NetworkPolicy поверх NodePort.
Поделиться: