LoadBalancer в Kubernetes: как настроить внешний балансировщик для сервисов - MetalLB и облачные контроллеры | AdminWiki

LoadBalancer в Kubernetes: как настроить внешний балансировщик для сервисов - MetalLB и облачные контроллеры

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

Что такое Service типа LoadBalancer в Kubernetes и как он работает

Service типа LoadBalancer выдаёт приложению внешний IP-адрес и внешний балансировщик: клиент из интернета обращается на этот адрес, а запрос доходит до подов. Kubernetes сам балансировщик не создаёт. Адрес и балансирующее устройство предоставляет облачный контроллер провайдера в managed-кластере либо MetalLB на собственном железе.

Путь трафика выглядит так: клиент, внешний балансировщик с адресом EXTERNAL-IP, NodePort на узлах, kube-proxy, под. LoadBalancer опирается на NodePort как транспорт: kube-controller-manager сам выделяет сервису nodePort (по умолчанию из диапазона 30000-32767), а внешний балансировщик шлёт трафик на этот порт любого узла кластера.

Проверить состояние сервиса можно двумя командами:

kubectl get svc -o wide
kubectl describe svc my-app

Колонка EXTERNAL-IP покажет адрес от балансировщика или <pending>. Значение <pending> означает, что внешний контроллер не настроен, в пуле MetalLB закончились адреса или поды не прошли readiness-проверку. В блоке Events команды describe видно, кто пытался выдать IP и почему не смог.

Чем LoadBalancer отличается от NodePort и ClusterIP

ТипДоступностьВнешний адресКогда выбирать
ClusterIPТолько внутри кластераНетВызовы между сервисами, базы данных, внутренние API
NodePortПорт на каждом узлеIP узла и порт 30000-32767Отладка, временный доступ, свой балансировщик перед кластером
LoadBalancerИз интернета или внутренней сетиОтдельный IP от контроллера или MetalLBTCP, UDP, gRPC, отдельный внешний IP для приложения

Для HTTP-трафика с маршрутизацией по хостам и путям часто хватает Ingress с одним LoadBalancer: это дешевле, чем отдельный балансировщик на каждый сервис. Критерии выбора и манифесты для всех типов сервисов собраны в статье Как выбрать тип Service в Kubernetes: ClusterIP, NodePort или LoadBalancer.

Кто выделяет внешний IP: облачный контроллер и MetalLB

В managed-кластерах (EKS, GKE, AKS и аналоги) работает cloud-controller-manager. Он вызывает API провайдера, тот создаёт балансировщик и возвращает адрес, а контроллер записывает его в status.loadBalancer сервиса. Тот же компонент удаляет балансировщик, когда вы удаляете Service.

На bare-metal кластере облачного контроллера нет, и сервис висит с EXTERNAL-IP: <pending>. Внешний адрес выдаёт MetalLB из заранее заданного пула и анонсирует его в сети. Другие методы открыть доступ, включая ручной externalIPs, разобраны в статье Настройка External IP в Kubernetes: практические методы для доступа к сервисам.

Готовый манифест Service LoadBalancer с аннотациями для облачных провайдеров

Базовый манифест почти не отличается от ClusterIP: меняется поле type и добавляется блок annotations, который читает контроллер провайдера.

apiVersion: v1
kind: Service
metadata:
  name: my-app
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app: my-app
  ports:
    - name: http
      port: 80
      targetPort: 8080
      protocol: TCP

Аннотации задают тип и схему балансировщика. Ниже — то, что подтверждается документацией провайдеров; набор аннотаций и допустимые значения зависят от версии контроллера, поэтому перед деплоем сверяйтесь с актуальной документацией.

  • AWS: аннотация service.beta.kubernetes.io/aws-load-balancer-type задаёт тип балансировщика, а контроллер AWS Load Balancer Controller обрабатывает сервисы со значением nlb-ip или external (для nlb-ip создаётся NLB с IP-таргетами). Схему балансировщика задаёт service.beta.kubernetes.io/aws-load-balancer-scheme со значениями internet-facing или internal; по умолчанию internet-facing. Аннотация aws-load-balancer-internal устарела начиная с версии v2.2.0 в пользу aws-load-balancer-scheme, но продолжает поддерживаться, а при конфликте приоритет у aws-load-balancer-scheme (документация AWS Load Balancer Controller).
  • GCP: для внутреннего балансировщика в GKE подтверждена аннотация networking.gke.io/internal-load-balancer-subnet — она позволяет указать пользовательскую подсеть для IPv4-only Internal LoadBalancer Service и работает во всех поддерживаемых версиях GKE (документация GKE). Internal LoadBalancer Service использует IP-адрес из подсети вашего VPC, что обеспечивает приватную маршрутизацию трафика внутри VPC или через peered-сети.
  • Azure: для внутреннего балансировщика в AKS подтверждена аннотация service.beta.kubernetes.io/azure-load-balancer-internal-subnet, значение которой задаётся как имя подсети, например "apps-subnet" (документация AKS). Внутренний балансировщик не имеет публичного IP и делает сервис доступным только приложениям, которые могут достичь приватного IP — в той же виртуальной сети или в другой через virtual network peering.

Аннотации не универсальны: набор и допустимые значения зависят от провайдера и версии контроллера. Перед деплоем сверьтесь с документацией провайдера, иначе балансировщик создастся с параметрами по умолчанию, и трафик пойдёт не туда, куда вы рассчитывали.

Если не хочется поддерживать контроллер и платить за отдельный балансировщик на каждый сервис, посмотрите managed-вариант: Timeweb Cloud предоставляет облачную инфраструктуру, включая серверы, VDS/VPS, базы данных, хранилище и Kubernetes, а внешний балансировщик создаётся средствами платформы.

Как настроить health-check для внешнего балансировщика

Облачный балансировщик проверяет не под напрямую, а порт узла (NodePort). Живым считается узел, на котором есть готовые поды сервиса, поэтому корректность health-check определяется readinessProbe в манифесте пода:

readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3

Убедитесь, что endpoints заполнены:

kubectl get endpoints my-app

Пустой список означает, что поды не Ready или selector сервиса не совпадает с метками подов.

При externalTrafficPolicy: Local балансировщик проверяет только узлы с локальными endpoints: лишний хоп исчезает, но нужен запас реплик, иначе ёмкость просядет. MetalLB работает на L2 или BGP и HTTP-проверок сам не делает: за health-check отвечает readinessProbe либо Ingress-контроллер, стоящий перед сервисом. Чтобы readiness-проверки имели смысл, приложения должны корректно их выставлять, а PodDisruptionBudget помогает не потерять все endpoints сразу во время обновлений — иначе возможны миграции VIP или спад анонсов.

Сколько стоит LoadBalancer в публичных облаках и как снизить расходы

В публичных облаках за LoadBalancer взимается плата по тарифам провайдера: на итог влияют тип балансировщика, количество правил и объём обработанного трафика. Считайте бюджет по актуальному прайсу провайдера, тарифы меняются.

Три способа удержать счёт под контролем:

  • Один Ingress-контроллер с одним LoadBalancer вместо балансировщика на каждый сервис.
  • Внутренний балансировщик для трафика между сервисами, чтобы не платить за публичный адрес там, где он не нужен.
  • Service Internal Traffic Policy: параметр internalTrafficPolicy: Local ограничивает трафик внутренних клиентов конечными точками на том же узле. Kubernetes поддерживает эту политику, и она помогает снизить затраты и улучшить производительность, включая задержку и пропускную способность сети (описание политики в документации Kubernetes).

Побочный эффект internalTrafficPolicy: Local стоит знать заранее: для подов на узлах, где нет endpoints этого сервиса, сервис ведёт себя так, будто endpoints нет вообще, даже если они есть на других узлах (там же).

Настройка MetalLB в bare-metal кластере: пошагово

Без облака внешний IP выдаёт MetalLB. Порядок действий:

  1. Проверьте кластер и выберите пул адресов. Диапазон должен быть свободен в той же L2-сети, что и узлы, и не пересекаться с зоной выдачи DHCP: зарезервируйте адреса на DHCP-сервере или вынесите их за пределы пула.
  2. Установите MetalLB. Официальная документация описывает три поддерживаемых способа установки: plain Kubernetes manifests, Kustomize или Helm (документация MetalLB).
  3. Дождитесь готовности компонентов.
helm install metallb metallb/metallb -n metallb-system --create-namespace
kubectl get pods -n metallb-system

По умолчанию Helm-чарт разворачивает MetalLB в режиме FRR-K8s. В namespace metallb-system должны работать controller и speaker. Пока они не в статусе Running, заявки на внешние адреса обрабатываться не будут.

Создайте пул адресов и анонс:

kubectl apply -f ip-pool.yaml
kubectl apply -f l2-advertisement.yaml
kubectl apply -f my-app-service.yaml

Проверьте результат:

kubectl get svc my-app
kubectl get ipaddresspool -n metallb-system
kubectl get l2advertisement -n metallb-system

Для BGP-режима нужна поддержка BGP на маршрутизаторах: анонсы уходят из кластера на роутер, и он раздаёт маршрут дальше. Настройка пиров и связка с MikroTik или Cisco разобрана в статье Настройка внешней маршрутизации для Kubernetes в bare-metal: MetalLB и BGP для сервисов LoadBalancer.

IPAddressPool и L2Advertisement: минимальный рабочий конфиг

Достаточно двух объектов. IPAddressPool описывает диапазон:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: main-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.1.240-192.168.1.250

L2Advertisement привязывает пул к L2-режиму:

apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: main-l2
  namespace: metallb-system
spec:
  ipAddressPools:
    - main-pool

В L2-режиме MetalLB отвечает на ARP-запросы за выданный адрес: один узел становится владельцем IP и принимает трафик. Активным держателем IP становится узел, на котором есть готовые Endpoints; при их исчезновении (например, Deployment масштабирован в 0 или все поды неготовы) IP переедет на другой узел с рабочими Endpoints. Держите в кластере минимум три узла и проверяйте, что ARP-ответы доходят до клиентской подсети.

Выданный адрес видно через jsonpath:

kubectl get svc my-app -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

Проверка: доходит ли трафик до подов

  1. Endpoints не пустые: kubectl get endpoints my-app.
  2. Запрос извне проходит: curl -v http://EXTERNAL-IP:80/ с машины вне кластера.
  3. Запрос дошёл до приложения: kubectl logs deploy/my-app.
  4. События без ошибок: kubectl describe svc my-app.

Если curl не проходит: проверьте firewall на узлах (iptables, nftables, firewalld, ufw), достижимость ARP из подсети клиента, пересечение пула с DHCP, selector сервиса и статус readinessProbe. Частая причина - клиент находится в другой подсети, куда ARP-анонс L2 не долетает: в такой схеме нужен BGP.

Сохранение client IP и sessionAffinity: чтобы запросы доходили корректно

По умолчанию действует externalTrafficPolicy: Cluster. Трафик может прийти на узел без пода, kube-proxy перебросит его на другой узел и выполнит SNAT, поэтому приложение увидит IP узла, а не клиента. Нужен реальный адрес клиента - ставьте externalTrafficPolicy: Local.

externalTrafficPolicy: Local vs Cluster - что выбрать

ПараметрClient IPБалансировкаРиски
externalTrafficPolicy: LocalСохраняетсяТолько узлы с локальными endpointsДисбаланс и потеря ёмкости при малом числе реплик
externalTrafficPolicy: ClusterТеряется из-за SNATПо всем подам кластераЛишний хоп между узлами, в логах IP узла

Local выбирайте там, где важен адрес клиента: rate limiting, аудит, геолокация. Условие работоспособности - реплики на нескольких узлах и anti-affinity, чтобы endpoints были на большинстве узлов. При Local health-check облачного балансировщика проверяет только узлы с endpoints, поэтому при одной реплике отказ узла убирает сервис из выдачи целиком.

Для sticky-сессий добавляйте sessionAffinity: ClientIP с таймаутом:

spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

Привязка работает по IP клиента: если клиенты сидят за общим NAT, все они попадут на один под. Cookie-based sticky на Ingress такой параметр не заменяет. Для корректной работы сохранения client IP и sessionAffinity может потребоваться дополнительная настройка в зависимости от используемого балансировщика.

Как проверить, что client IP сохраняется

  • Логируйте в приложении RemoteAddr и заголовок X-Forwarded-For.
  • Выполните curl с внешней машины и сверьте адрес в логах пода.
  • Для HTTP за Ingress проверьте use-forwarded-headers и список доверенных адресов: без этого X-Forwarded-For подделывается клиентом.
  • Для TCP смотрите source IP в логах приложения или через tcpdump на поде.

Видите в логах IP узла - почти наверняка работает externalTrafficPolicy: Cluster либо балансировщик делает SNAT.

Типичные ошибки при настройке внешнего доступа и чек-лист

Когда LoadBalancer избыточен: Ingress, NodePort или ClusterIP

ЗадачаЧто выбратьСтоимость
HTTP и HTTPS с маршрутизацией по хостам и путямIngress + один LoadBalancerОдин балансировщик на все сервисы
TCP, UDP, gRPC или отдельный внешний IPService LoadBalancerБалансировщик на сервис
Обмен данными между сервисамиClusterIPПлата за внешний адрес не нужна
Отладка и временный доступNodePortПлата за балансировщик не нужна

Разницу между балансировкой TCP на L4 и маршрутизацией HTTP на L7, а также выбор между Ingress NGINX, Traefik и Gateway API мы разобрали в статье Маршрутизация в Kubernetes: Service, Ingress и Gateway API на уровнях L4 и L7. Service Internal Traffic Policy снижает расходы на внутренний трафик и заодно уменьшает задержку, но помните о поведении сервиса для подов без локальных endpoints.

Ошибки, симптомы и решения

  • EXTERNAL-IP: <pending> - нет облачного контроллера или пула MetalLB. Проверьте, установлен ли контроллер, заполнен ли IPAddressPool и готовы ли поды в metallb-system.
  • Пустой список endpoints - selector сервиса не совпадает с метками подов или поды не Ready. Смотрите kubectl get endpoints и kubectl describe pod.
  • EXTERNAL-IP есть, curl не проходит - закрыт firewall на узлах, ARP не доходит из подсети клиента или пул пересекается с DHCP.
  • В логах IP узла вместо клиента - externalTrafficPolicy: Cluster или SNAT на балансировщике. Переключитесь на Local и проверьте health-check узлов.
  • Часть подов не видит сервис - включён internalTrafficPolicy: Local, а на узлах этих подов локальных endpoints нет: сервис для них выглядит как сервис без endpoints (документация Kubernetes).
  • Неожиданный счёт в облаке - балансировщик создан на каждый сервис вместо общего Ingress-контроллера.

Чек-лист перед деплоем

  1. Выбран правильный тип Service: ClusterIP, NodePort, LoadBalancer или Ingress перед ним.
  2. Selector сервиса совпадает с метками подов, endpoints не пустые.
  3. Readiness-проба настроена и отражает реальную готовность приложения.
  4. externalTrafficPolicy выставлен осознанно: Local для сохранения client IP, Cluster для равномерности.
  5. sessionAffinity: ClientIP добавлен, если приложению нужны sticky-сессии, и известен таймаут.
  6. Пул адресов MetalLB свободен и не пересекается с DHCP, ARP или BGP-анонс доходит до клиентов.
  7. Firewall на узлах открыт для порта сервиса.
  8. Аннотации провайдера сверены с документацией и версией контроллера.
  9. Проверено curl по EXTERNAL-IP с внешней машины и запись в логах пода.
  10. Оценена тарификация: сколько балансировщиков создаётся и за что списываются деньги.
Поделиться: