← Kubernetes: сеть
6 мин · просто · Урок 2 из 8

NodePort: порт на каждой ноде

Самый прямолинейный способ пустить трафик снаружи в кластер и его цена.

Когда ClusterIP мало

ClusterIP даёт Service внутренний адрес в кластере. Pod и другие сервисы видят его, снаружи кластера он недоступен. Для админ-задач или быстрой проверки приложения нужен внешний доступ. NodePort решает эту задачу самым прямолинейным способом: открыть порт на каждой ноде.

Что такое NodePort

Когда создаётся Service с type: NodePort, Kubernetes делает три вещи:

  1. Создаёт ClusterIP (как у обычного Service): внутренний адрес.
  2. Открывает дополнительный порт на каждой ноде кластера в диапазоне 30000-32767. Этот порт kube-proxy форвардит на ClusterIP, а оттуда на endpoints. kube-proxy - компонент на каждой ноде, который настраивает правила сети для Service.
  3. Любой запрос на <любая-нода>:<NodePort> попадает в один из Pod Service.

Минимальный манифест

apiVersion: v1
kind: Service
metadata:
  name: nodeapp
spec:
  type: NodePort
  selector:
    app: nodeapp
  ports:
    - port: 80          # внутренний порт ClusterIP
      targetPort: 8080  # порт контейнера
      nodePort: 30090   # внешний порт на каждой ноде

Поля

Диапазон портов

По умолчанию 30000-32767. Это компромисс: достаточно высокий, чтобы не конфликтовать с системными портами, и достаточно широкий, чтобы один кластер выдерживал сотни сервисов.

Если попытаться задать nodePort: 80, API-сервер ответит:

provided port is not in the valid range. The range of valid ports is 30000-32767

Диапазон расширяется флагом --service-node-port-range у kube-apiserver, но это пересборка кластера ради порта: не делают.

Как проверить

Сначала смотрят, что Service создан правильно и нашёл Pod:

kubectl get svc nodeapp
# колонка PORT(S): 80:30090/TCP

kubectl get endpoints nodeapp
# непустой список IP = Service реально указывает на под

Объект v1 Endpoints deprecated с 1.33, и kubectl печатает про это предупреждение. Тот же список адресов через актуальный объект:

kubectl get endpointslice -l kubernetes.io/service-name=nodeapp

Дёрнуть NodePort запросом можно изнутри кластера: временный pod шлёт запрос на IP-ноды:30090.

NODE_IP=$(kubectl get nodes \
  -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')

kubectl run np-check --rm -it --restart=Never \
  --image=busybox:1.36 -- wget -qO- http://$NODE_IP:30090
# должна прийти nginx welcome page

В виртуализованных учебных кластерах NodePort не всегда маршрутизируется через InternalIP ноды, и проба может не достучаться. Главный критерий, что NodePort настроен верно: Service с type: NodePort, нужный nodePort и непустые endpoints на живой под.

Что важно помнить

Один порт на сервис на всю ноду

Если два разных Service попросят nodePort: 30090, второй упадёт с port is already allocated. На одну ноду каждый NodePort уникален.

Все ноды слушают этот порт

node-1 · :30090 nodeapp node-2 · :30090 ни одного Pod этого Service node-3 · :30090 nodeapp kube-proxy шлёт на ClusterIP
Порт открыт на всех нодах, даже там, где Pod этого Service нет.

Даже те, где нет ни одного Pod этого Service. kube-proxy всё равно форвардит трафик на ClusterIP. Это упрощает балансировщики снаружи: любой адрес ноды подходит.

externalTrafficPolicy: Cluster vs Local

По умолчанию Cluster: запрос с ноды A может попасть на под на ноде B (через ClusterIP). Это балансирует нагрузку, но теряет client IP.

Local: трафик идёт только в Pod на той же ноде, куда пришёл запрос. Сохраняет client IP, но если на ноде нет Pod: Connection refused.

ЗначениеКуда попадёт запросClient IPЕсли на ноде нет Pod
Cluster (по умолчанию)в любой Pod Service, хоть на другой нодетеряетсязапрос всё равно дойдёт
Localтолько в Pod на этой же нодесохраняетсяConnection refused
spec:
  type: NodePort
  externalTrafficPolicy: Local

Когда использовать NodePort

Когда не использовать

NodePort открывает порт на всех нодах кластера, включая те, где нет ни одного Pod этого Service. Без firewall перед нодами этот порт виден из интернета. В проде ноды прячут за балансировщиком или ограничивают сетевыми правилами.

Быстрый путь без YAML

kubectl create deployment nodeapp --image=nginxinc/nginx-unprivileged:1.25
kubectl expose deployment nodeapp --port=80 --target-port=8080 --type=NodePort

# какой порт выдал кластер
kubectl get svc nodeapp -o jsonpath='{.spec.ports[0].nodePort}'

kubectl expose без явного nodePort берёт случайный порт из диапазона. Для предсказуемого адреса порт дописывают в манифест руками.

Коротко

Прошли материал до конца? Отметьте урок пройденным.