Предыдущие статьи:

https://habr.com/ru/articles/1084866/ - Kubernetes просто, часть 3: зачем нужен Service и как трафик доходит до конкретного Pod
https://habr.com/ru/articles/1083100/ - Kubernetes просто, часть 2: что происходит после kubectl apply
https://habr.com/ru/articles/1082088/ - Kubernetes просто, часть 1: зачем Kubernetes, устройство кластера

В прошлой части мы разобрали для чего в Kubernetes нужен Service.

У нас закрепилась такая модель:

Client Pod
    │
    │ backend-service
    ↓
Kubernetes DNS
    │
    ↓
Service
ClusterIP :80
    │
    ↓
ready backend endpoints
    │
    ↓
Pods :8080

Идея проста:

Клиенту не нужно знать IP конкретных Pods. Он обращается к Service, а набор backend Pods может меняться независимо от него.

Это отлично работает, пока мы работаем внутри кластера,  к примеру:

Frontend Pod
      │
      ↓
Backend Service
      │
      ↓
Backend Pods

Но настоящий пользователь пользователь  обычно находится в другом месте, к примеру, он приходит из Интернета.

Здесь возникает следующий вопрос:

Как предоставить доступ к приложению снаружи кластера?

Для того, чтобы ответить на него, сегодня разберем два типа Service: NodePort и LoadBalancer

А заодно сравним его с уже известным нам ClusterIP.

Вспомним ClusterIP

В прошлой части мы создавали Service:

apiVersion: v1
kind: Service

metadata:
  name: backend-service

spec:
  selector:
    app: backend

  ports:
    - port: 80
      targetPort: 8080

И как мы уже выяснили: если поле type не указано, как в примере выше, Kubernetes автоматически использует

type: ClusterIP

То есть наш Service можно представлять, как:

apiVersion: v1
kind: Service

metadata:
  name: backend-service

spec:
  type: ClusterIP

  selector:
    app: backend

  ports:
    - port: 80
      targetPort: 8080

Service получает ClusterIP, например:

backend-service – 10.1.20.10:80

Другой Pod внутри кластера может обращаться так:

Frontend Pod
      │
      │ backend-service:80
      ↓
ClusterIP Service
      │
      ↓
Backend Pod :8080

Это прекрасный вариант для внутреннего взаимодействия.

Например:

Frontend → Backend

Backend → Redis

Application → Internal API

Однако же, обычный ClusterIP сам по себе не является публичной точкой входа в Интернет.

Если нашему приложению нужен внешний доступ, требуется другой механизм.

NodePort

Начнем с достаточно простого варианта – NodePort.

Изменим Service-манифест так:

apiVersion: v1
kind: Service

metadata:
  name: backend-service

spec:
  type: NodePort

  selector:
    app: backend

  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080
Здесь мы прописали поле type самостоятельно и присвоили ему значение NodePort.

Также мы добавили новое поле – nodePort в раздел ports.

Что же это значит?

Упрощенно,

NodePort делает Service доступным через определенный порт на нодах кластера.

К примеру, у нас есть Node с адресом:

192.168.1.10

И

nodePort: 30080

Тогда при подходящей сетевой доступности к Node клиент может обратиться:

192.168.1.10:30080

Дальше запрос будет перенаправлен к backend Service и одному из подходящих backend endpoints соответственно.

Схематично:

Три порта - не путаемся

В NodePort Service-манифесте можно увидеть сразу три значения

ports: - port: 80

targetPort: 8080

nodePort: 30080

На первый взгляд выглядит так, будто Kubernetes специально пытается проверить нас на внимательность. Но здесь у каждого порта есть своя роль. Начнём с уже знакомых нам:

port:80

Это порт Service.

targetPort: 8080

Это порт backend, куда должен попадать трафик.

Теперь добавляется:

nodePort: 30080

Это порт, через который Service становится доступным из других нод.

NodePort не означает «Pod на этой Node»

Здесь частая ошибка.

Представим две Worker-ноды:

Node 1
192.168.1.10

Node 2
192.168.1.11

Backend под был определен планировщиком на вторую из них:

Node 2 
└── Pod B

А клиент обращается

192.168.1.10:30080

То есть запрос пришел на Node 1.

Можно подумать:

Раз запрос пришёл на Node 1, значит backend Pod тоже должен находиться на Node 1.

Но это не обязательное условие.

Упрощённо возможна ситуация:

Client
   │
   ↓
Node 1 :30080
   │
   │
   └─────────────────────┐
                         ↓
                      Node 2
                         │
                         ↓
                       Pod B

Service работает с подходящими backend endpoints, а они могут находиться на других Nodes.

То есть:

NodePort определяет точку входа в кластер, но не означает, что запрос обязательно должен обслуживаться Pod на той же Node.

Это особенно важно для правильной ментальной модели.

Service работает с группой backend-подов, а не с одним конкретным подом на одной конкретной машине.

Нужно ли задавать nodePort вручную

В примере мы прописали:

nodePort: 30080

Но это поле не обязательно указывать вручную.

Можно создать:

apiVersion: v1
kind: Service

metadata:
  name: backend-service

spec:
  type: NodePort

  selector:
    app: backend

  ports:
    - port: 80
      targetPort: 8080

В таком случае Kubernetes может выделить подходящий NodePort автоматически из настроенного для кластера диапазона.

В стандартной конфигурации часто используется диапазон:

30000–32767

Но лучше воспринимать его именно как типичное значение по умолчанию, а не как универсальный закон для любого возможного кластера: диапазон может быть настроен иначе.

Значит, NodePort можно использовать для публикации приложения?

Технически – да.

Если Node доступна клиенту по сети и нужный порт разрешён через firewall и политику доступа, можно обращаться:

<NodeIP>:<NodePort>

Например:

203.0.113.10:30080

Но для пользователей веб-приложений такая схема обычно не является конечной архитектурой.

Представьте адрес магазина:

http://203.0.113.10:30080

Не удобно - не так ли?

Кроме того возникают закономерные вопросы:

1) Какой Node IP использовать?

2) Что произойдёт при проблеме с этой Node?

3) Кто предоставляет удобный публичный IP?

4) Где выполнять внешнюю балансировку?

5) Как организовать нормальный входной трафик?

Итак, NodePort предоставляет для нас механизм доступа с других нод, но сам по себе не решает задачи публикации production-сервиса.

И здесь появляется следующий тип Service.

LoadBalancer

В инфраструктуре, которая поддерживает такую интеграцию, можно создать Service:

apiVersion: v1
kind: Service

metadata:
  name: frontend-service

spec:
  type: LoadBalancer

  selector:
    app: frontend

  ports:
    - port: 80
      targetPort: 8080

Главное изменение:

type: LoadBalancer

Концептуально:

Internet
   │
   ↓
External Load Balancer
   │
   ↓
Kubernetes Service
   │
   ↓
Frontend Pods

Инфраструктурная интеграция может предоставлять внешний load balancer (балансировщик нагрузки) и внешний адрес, через которые пользователи могут обращаться к приложению.

Но откуда берётся этот Load Balancer?

Сначала разберёмся, что это вообще такое. Представим, что у нас есть 3 Worker-ноды:

Node 1
Node 2
Node 3

На них работают Pods нашего приложения.

C NodePort пользователь мог обращаться напрямую к одной из нод:

User
  │
  ↓
Node 1:30080
  │
  ↓
Service
  │
  ↓
Pods

Но хотелось бы дать пользователю один нормальный внешний адрес, а уже перед кластером поставить компонент, который будет передавать их дальше.

Вот эту роль и выполняет внешний балансировщик нагрузки.

Упрощённо:

               Internet
                   │
                   ↓
             Public IP
                   │
                   ↓
            Load Balancer
             /     |     \
            ↓      ↓      ↓
         Node 1  Node 2  Node 3
                   │
                   ↓
                Service
                   │
                   ↓
                  Pods

Теперь пользователю не нужно думать, к какой именно Node обращаться. У него есть одна внешняя точка доступа – Public IP.

А дальше инфраструктура сама доставляет запрос в кластер.

Но здесь есть важный нюанс.

Когда мы пишем:

type: LoadBalancer

Мы не создаём балаансировщик буквально самим Kubernetes.

Kubernetes скорее говорит:

Мне нужна внешняя точка входа для этого Service.

Если кластер работает, к примеру, в облаке и там настроена соответствующая интеграция, инфраструктура может создать внешний балансировщик и выдать ему публичный адрес.

Получается что-то такое:

Мы создаём:

Service
type: LoadBalancer
        │
        ↓
Kubernetes видит запрос
        │
        ↓
Инфраструктура предоставляет
внешний Load Balancer
        │
        ↓
Появляется внешний адрес

То есть LoadBalancer - это не какой-то специальный Pod, который Kubernetes обязательно запускает внутри кластера.

Это способ сказать:

Я хочу предоставить этот Service наружу через внешний балансировщик

Именно поэтому поведение зависит от того, где запущен Kubernetes.

В облачном Kubernetes такой Service часто приводит к появлению внешнего адреса.

А если мы просто запустили Kubernetes локально на своём компьютере, облачного провайдера рядом нет – значит, некому автоматически выдать нам настоящий публичный адрес для Load Balancer. Для локальных кластеров эта задача может быть решена другими способами, но сейчас важна сама идея:

ClusterIP
    ↓
доступ внутри кластера
----------------------------
NodePort
    ↓
доступ через IP и порт Node
----------------------------
LoadBalancer
    ↓
внешняя точка входа
через балансировщик

Service и Load Balancer – не одно и то же

Здесь легко запутаться из-за названия:

Здесь легко запутаться из-за названия:

type: LoadBalancer

Service у нас всё ещё остаётся Service.

Он по-прежнему знает, какие поды подходят по селекторам, предоставляет стабильную точку доступа к меняющемуся набору backend-подов.

Просто теперь перед ним появляется внешний способ попасть в кластер по публичному IP-адресу.

Internet
   │
   ↓
Load Balancer
   │
   ↓
Service
   │
   ↓
ready Pods

То есть:

Service решает, куда внутри Kubernetes должен идти трафик.

Внешний Load Balancer помогает этому трафику попасть в кластер снаружи.

Для нашей текущей модели этого вполне достаточно.

Сравнение 3 типов

Теперь можно собрать всё, что мы уже разобрали.

Cluster IP

Клиент уже находится внутри Kubernetes.

Frontend Pod
      ↓
ClusterIP Service
      ↓
Backend Pods

Например:

Frontend - Backend

NodePort

Клиент приходит снаружи через IP одной из Nodes:

Client
   ↓
NodeIP:30080
   ↓
Service
   ↓
Pods

LoadBalancerr

Перед кластером появляется единая внешняя точка входа:

Internet
   ↓
Public IP
   ↓
Load Balancer
   ↓
Service
   ↓
Pods

Открывать наружу нужно не всё!

Представим Интернет-магазин:

Frontend
   ↓
Backend
   ↓
Database

Пользователь должен иметь возможность открыть frontend, но ему совершенно не обязательно напрямую обращаться к backend и уж тем более он не должен иметь возможность обращаться напрямую к базе данных. Поэтому архитектура может выглядеть так:

   INTERNET
       ↓
 Load Balancer
       ↓
Frontend Service
       ↓
 Frontend Pods
       ↓
Backend Service
       ↓
 Backend Pods
       ↓
   Database

При этом наружу опубликован только frontend. Он в свою очередь обращается к бэку через внутренний Cluster IP.

Frontend
    ↓
backend-service
    ↓
Backend Pods

Получается довольно логичная картина:

Запомнить просто:

Не публикуйте в Интернет то, к чему внешний доступ не требуется.

Что нужно запомнить из этой части?

У нас есть три основных типа Service:

ClusterIP
NodePort
LoadBalancer

Различать их проще всего по вопросу:

Если клиент находится внутри Kubernetes:

Client Pod
    ↓
ClusterIP
    ↓
Pods

Если мы хотим попасть в Service через Node:

Client
    ↓
NodeIP:NodePort
    ↓
Service
    ↓
Pods

Если приложению нужна нормальная внешняя точка входа:

Internet
    ↓
Load Balancer
    ↓
Service
    ↓
Pods

При этом фундаментальная идея не меняется:

Service скрывает от клиента постоянно меняющиеся Pods и предоставляет стабильную точку доступа к приложению.

Проверьте себя

Попробуйте ответить своими словами, не возвращаясь к тексту статьи.

1. Почему обычного ClusterIP недостаточно, если пользователь находится за пределами Kubernetes-кластера?

2. Что меняется, когда вместо:

type: ClusterIP

мы создаём:

type: NodePort

3. В Service указано:

port: 80
targetPort: 8080
nodePort: 30080

За что отвечает каждый из этих трёх портов?

4. Есть две Nodes:

Node 1
192.168.1.10

Node 2
192.168.1.11

Backend Pod работает на Node 2, а пользователь отправляет запрос:

192.168.1.10:30080

Может ли этот запрос попасть в Pod на Node 2? Почему?

5. Обязательно ли вручную указывать:

nodePort: 30080

при создании Service типа NodePort?

6. Какую проблему решает LoadBalancer по сравнению с прямым обращением к NodeIP:NodePort?

7. Что мы на самом деле просим сделать, когда создаём:

type: LoadBalancer

И почему в локальном Kubernetes-кластере после этого не обязательно автоматически появится публичный IP?

8. Есть приложение:

Internet
   ↓
Frontend
   ↓
Backend
   ↓
Database

Какие компоненты имеет смысл сделать доступными снаружи, а какие оставить внутри кластера? Какие типы Service здесь можно использовать?