Наверняка вы все работаете с Kubernetes, публикуете сервисы наружу через Ingress-контроллер. Уверен, что большинство из вас использует ingress-nginx. Создаете манифест, деплоите его в k8s, но не всегда получаете именно тот результат, который хотели бы. Или же все работает, но спустя какое-то время что-то идет не так. 

В этой серии статей, созданной по мотивам выступления на DevOpsConf’25, мы подробно разберемся как работает сам ingress-nginx контроллер и почему это не совсем классический nginx. Погрузимся в дебри LUA-кода чтобы понять, как реализована балансировка. А также затронем тему сниппетов, как их включить если они вам очень нужны, и почему этого делать не стоит.

Меня зовут Алексей Колосков, я Lead DevOps из Hilbert Team. Больше 15 лет я в IT: за это время админил, разрабатывал, развивал on-premise инфраструктуру, инфраструктуру в облаках и даже курсы по DevOps, Security и DataTech в Yandex Cloud. Выступал на конференции DevOpsConf. Hilbert Team — провайдер IT-решений для крупного и среднего бизнеса в области облачных технологий, DevOps, DevSecOps, DataOps, MLOps и FinOps. Партнёр Yandex Cloud со специализацией Yandex Cloud Professional по направлениям DevOps и Data Platform.

Basics: nginx

Давайте сразу определимся: 

  • nginx Ingress – контроллер от nginx.inc

  • Ingress-nginx – контроллер от community

Если вы искали в Google информацию по nginx Ingress, то часто могли попасть именно на контроллер от разработчиков самого nginx. Мы же будем говорить про реализацию от Kubernetes Community, так как на данный момент он является более популярным. 

Прежде, чем перейти к сложным вещам, начнём с основ. Вспомним, как работает классический nginx.

Пример конфигурации nginx

Представим веб-приложение, развёрнутое на нескольких виртуальных машинах. Перед VM стоит nginx, балансирующий трафик между ними. Пользователь делает запрос к нашему приложению, например, сайту dc25.corp.

Запрос сначала попадает на nginx, и далее nginx начинает обрабатывать запрос согласно своему конфигурационному файлу — nginx.conf.

Конфигурационный файл в упрощенном виде может выглядеть так:

nginx на основе значений в HTTP-заголовках запроса и настроек в конфигурационном файле  выбирает нужный server и location, после чего перенаправляет трафик на один из указанных upstream-серверов согласно заданной схеме балансировки.

Сначала nginx ищет в своем конфиге подходящий блок server. Поскольку используется протокол HTTP, по умолчанию применяется порт 80. В пользовательском запросе явно не указан другой порт, поэтому nginx ищет блок server, настроенный на прослушивание порта 80.

Далее происходит обработка значения server_name. В нашем случае — это dc25.corp, что соответствует значению, переданному в заголовке Host входящего HTTP-запроса. В итоге nginx находит блок server, в котором слушается порт 80, и server_name соответствует этому значению.

Затем nginx переходит к следующему этапу обработки. В блоке server ищется location, где определяется дальнейшая судьба нашего запроса. 

Директива proxy_pass перенаправляет нас на upstream

Имя upstream соответствует тому, что мы задали в директиве proxy_pass, и upstream у нас уже содержит список эндпоинтов наших серверов, между которыми nginx балансирует трафик. Эндпоинт в данном случае представлен как IP-адрес и номер порта.

Блоков location может быть несколько, точно так же, как и блоков server.

Каждый блок location в конфигурации nginx может использовать свой собственный upstream, либо на один upstream  могут ссылаться разные location. Аналогично, у каждого блока server может быть несколько location, каждый из которых также может ссылаться на свой отдельный upstream.

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

Basics: сеть и k8s

Как работает nginx, мы рассмотрели. Теперь перейдём к основам работы с трафиком в Kubernetes. Поскольку далее речь пойдёт об ingress-nginx, который функционирует внутри Kubernetes-кластера, важно напомнить базовые принципы движения трафика в этой среде, чтобы избежать недопонимания.

Допустим, у нас есть приложение, которое мы задеплоили в кластер Kubernetes. В результате оно запущено в виде одного или нескольких подов. Для идентификации подов мы назначаем им определённые метки, например, app: MyApp.

Чтобы направить к этому приложению внутрикластерный трафик, необходимо создать объект Service. В секции selector его конфигурации указываются те же метки (labels), что и у подов. Это позволяет сервису автоматически обнаружить нужные поды, на которые слать трафик.

Далее Kubernetes на основе этих связей формирует объект EndpointSlice, который содержит список IP-адресов и портов соответствующих подов, именно туда будет направляться трафик, приходящий на сервис.

Если вы не знакомы с объектом EndpointSlice, вдаваться в детали сейчас необязательно. Достаточно понимать, что это — структурированный список адресов эндпоинтов, на которые сервис направляет трафик. Этот объект создаётся и управляется самим Kubernetes, вручную взаимодействовать с ним не требуется.

Как управлять трафиком внутри кластера, мы разобрались. Теперь задача — обеспечить доступ к приложению извне. Для этого создаётся объект Ingress. В его конфигурации в разделе правил (rules) указывается имя Service, к которому должен быть направлен внешний HTTP-трафик. Имя сервиса в правилах Ingress указывается в явном виде и должно соответствовать имени ранее созданного Service.

В результате схема маршрутизации выглядит следующим образом:

  • Service направляет трафик на поды по заданным меткам (labels),

  • Ingress направляет трафик на Service по имени (name).

С точки зрения прохождения трафика картина выглядит следующим образом: внешний трафик сначала поступает на балансировщик нагрузки (Load Balancer), настроенный в облачной или on-premise инфраструктуре. Затем этот балансировщик передаёт запросы на поды, в которых запущен Ingress-контроллер (опустим подробности про Node Port’ы).

Далее ingress-nginx принимает входящий HTTP-трафик и, в зависимости от настроенных маршрутов (Ingress rules), распределяет и балансирует его между подами нашего приложения.

Заметьте, сервисов в этом списке нет. Думаю, дальше станет чуть понятнее, почему.

Как работает  Ingress-nginx контроллер

Теперь давайте заглянем под капот ingress-nginx контроллера. Для начала важно понять две вещи. Представим, что мы самостоятельно настраиваем nginx в Kubernetes, чтобы он начал обрабатывать входящие запросы и балансировать трафик между подами.

Классический nginx настраивается с помощью статического файла nginx.conf. Чтобы он начал работать в Kubernetes, необходимо предварительно сформировать этот конфигурационный файл. Для этого необходимо:

  • Получить из кластера Kubernetes необходимые для формирования конфигурации объекты (например, Ingress, Service, Endpoints и другие).

  • Обработать эти объекты таким образом, чтобы на их основе был сгенерирован корректный файл конфигурации nginx.

Какие объекты необходимо получить:

  • Ingress. Непосредственно влияет на итоговую конфигурацию.

  • Service. В частности, имя сервиса используется для генерации списка бэкендов. Через Service контроллер получает информацию об эндпоинтах, поскольку именно в них указаны адреса подов, на которые необходимо направлять трафик. 

  • Endpoint. Собственно список эндпоинтов сервисов

  • Secret и ConfigMap. Используются для хранения параметров конфигурации, которые могут потребоваться Ingress-контроллеру. В частности, здесь могут находиться, например, SSL-сертификаты в случае, если nginx выполняет терминацию TLS-соединений, а также другие вспомогательные настройки.

Обработка объектов

Кластер Kubernetes может быть достаточно крупным, и количество объектов в нём — значительным. Эти объекты постоянно изменяются: создаются новые Ingress, Service, Pod, какие-то из них пересоздаются или удаляются. В результате возникает ряд типичных проблем, связанных с обработкой этих изменений:

  1. Перегрузка Kubernetes API запросами. Возникает вопрос: как эффективно получать нужные объекты из кластера? Один из простых, но неэффективных подходов — периодически опрашивать API Kubernetes и каждый раз получать полный список всех объектов. Однако в крупном кластере таких объектов может быть десятки или даже сотни тысяч, и регулярный полный опрос API приведёт к высокой нагрузке на управляющие компоненты кластера.

  2. Необходимость постоянной перегенерации конфига. Поскольку объекты в кластере часто меняются, при каждом таком изменении контроллер должен полностью заново сгенерировать конфигурационный файл nginx.conf. Это создаёт дополнительную нагрузку и может существенно повлиять на производительность, особенно в крупных кластерах.

Разберёмся с каждой проблемой по-отдельности.

Проблема 1: получение объектов k8s

Чтобы избежать описанного выше поведения, ingress-nginx использует Kubernetes Informers. Сразу оговорюсь, это не сущности Kubernetes, а механизм самого контроллера. Kubernetes Informers помогают, не перегружая Kubernetes API запросами, достаточно эффективно отслеживать изменения в объектах. Напомню, изменения включают в себя также создание и удаление объектов кластера.

Проблема 2: необходимость постоянной перегенерация конфига

Контроллер не хранит в памяти полную структуру объектов k8s. Он знает только о текущей конфигурации nginx. В результате при каждом изменении этих объектов, он был бы вынужден перегенерировать эту конфигурацию с нуля.

Поскольку изменения в кластере могут происходить десятки или сотни раз в секунду, повторная полная генерация конфигурации становится ресурсоёмкой операцией, особенно в условиях большого количества Ingress-объектов и сервисов.

В качестве решения в ingress-nginx контроллере использована очередь событий.

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

Например, если изменения одного и того же объекта Ingress поступили дважды в рамках временного окна, контроллер учитывает только последнее, актуальное состояние этого объекта. Это позволяет снизить нагрузку и избежать лишних генераций конфигураций.

Итак, ingress-контроллер отбросил неактуальные в рамках временного окна изменения, сформировал nginx.conf. Теперь нужно эту конфигурацию применить.

Применяем конфиг

Возникает логичный вопрос: стоит ли применять конфигурацию каждый раз после её генерации? Ответ — нет. Перед применением новой конфигурации стоит сначала сравнить её с текущей. Если отличий нет, то никаких действий не требуется.

Если же конфигурации различаются, то возникает следующий вопрос: можем ли мы наконец уже применить конфигурацию? Всё ещё рано! 

Контроллер использует механизм reload для применения изменений в конфигурации без перезапуска nginx. Однако reload — операция затратная и может повлиять на качество обработки запросов, особенно в высоконагруженных системах. 

Чем плох reload:

  • Влияет на задержку обработки запросов, особенно если конфигурация большая.

  • Каждый раз при reload, nginx сбрасывает состояние балансировки. Соответственно, качество балансировки в результате будет страдать. Поэтому в ingress-nginx это оптимизировали.

Обработка изменений в блоке upstream в ingress-nginx реализована без необходимости выполнения reload. Это связано с тем, что наиболее частые изменения касаются именно списка эндпоинтов сервисов, этот список меняется каждый раз, когда поды создаются или удаляются. Такая реализация блока upstream позволяет избежать частой перезагрузки конфигурации и, как следствие, лишних задержек при обработке трафика.

Как это работает? 

  • Контроллер получает список эндпоинтов сервисов из k8s, который он отслеживает.

  • Отправляет этот список LUA-обработчику.

  • Обработчик определяет список эндпоинтов, которые принадлежат конкретному сервису, и  применяет сконфигурированный алгоритм балансировки, который также реализован  в LUA-обработчике.

В итоге, после того как алгоритм балансировки применён, и выбран эндпоинт, nginx сам позаботится о дальнейшей обработке запроса. 

- Если изменения в конфигурации все таки требуют применения, может уже наконец выполним reload?

- Нет! ?

А если конфиг с ошибкой?

Контроллер генерирует конфигурацию на основе всех поступающих объектов Ingress. При каждом изменении любого из них он заново обрабатывает входящие данные и формирует новую версию конфигурационного файла.

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

Хотя у манифеста Ingress — статическое описание, допустить ошибочные настройки возможно: например, если использовать аннотации. Если один из объектов Ingress содержит некорректную конфигурацию, это может привести к тому, что вся сгенерированная конфигурация nginx окажется невалидной. В результате новые объекты Ingress перестанут применяться.

Пример: 

  1. Сначала поступают первый, второй и третий объекты — они корректны.

  2. Затем появляется четвёртый объект с ошибкой в аннотации.

  3. Контроллер генерирует конфигурацию, включающую все объекты, и при этом добавляет некорректный фрагмент. 

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

  5. В результате reload будет сломан.

В качестве решения в ingress-nginx разработан компонент Validation Admission Webhook Server, который используется для проверки конфигурационного файла на корректность.

Работает достаточно просто:

  • Слушает на порту контроллера 8443.

  • Добавляет все входящие объекты  Ingress к списку Ingresses, которые уже были добавлены ранее.

  • Генерирует с нуля конфигурацию.

  • Вызывает nginx -t для проверки. 

Напомню, nginx -t — это команда валидации конфига nginx.

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

Схематично это можно представить следующим образом:

Итак, что мы имеем в итоге:

  1. контроллер получил k8s-объекты для формирования конфигурации;

  2. обработал их, и сформировал конфигурационный файл;

  3. проверил его на корректность;

  4. и, наконец, применил.

Такой подход называется Synchronization Loop Pattern.

Плюсы этого подхода:

  • Избегаем лишней перегенерации конфирурационного файла, то есть генерируем его в рамках временного окна только с определённой периодичностью. В результате снижается нагрузка на контроллер.

  • Избегаем лишних reload, что положительно влияет на задержки обработки запросов и качество балансировки в целом.

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

Взаимодействие с ingress-nginx контроллером

Что нужно знать про конфигурацию nginx в ingress-nginx контроллере:

  • nginx.conf генерируется из шаблона.

  • Шаблон расположен в файле rootfs/etc/nginx/template/nginx.tmpl

Этот шаблон представляет из себя Go Template. Пример содержимого:

Читать шаблон конфигурации глазами не всегда удобно, особенно с учётом его объёма и структуры. Поэтому проще посмотреть уже сгенерированный (отрендеренный) конфигурационный файл, который применяется внутри контейнера с контроллером.

Для этого можно воспользоваться командой kubectl exec и зайти в pod, где работает ingress-nginx, а затем вывести содержимое текущего файла конфигурации nginx. Пример команды:

kubectl exec -it -n <ingress-ns> \

<controller-pod> -- cat /etc/nginx/nginx.conf

Но есть другой способ посмотреть отрендеренный конфиг, особенно подходящий, если это не единственная задача, которую нам потребуется решить. Для этого есть: kubectl plugin ingress_nginx 

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

Также он может показать уже отрендеренный конфигурационный файл, который применяется в nginx.

Наверное, самая полезная его фича — просмотр списка бэкендов.

Здесь мы видим в каждом элементе:

  • Имя бэкенда, который формируется на основе имени сервиса, на основе которого был сгенерирован данный бэкенд.

  • Полную спецификацию этого сервиса.

  • Список endpoints, между которыми будет выполняться балансировка в рамках данного бэкенда. Эти бэкенды можно представить как аналог блоков upstream, которые LUA-кодом реализованы в Ingress-контроллере, и содержат список эндпоинтов для балансировки.

  • sessionAffinityConfig, то есть привязку сессии к эндпоинтам и подобное. 

  • Таких параметров много, рекомендую просто попробовать самим и посмотреть.

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

Итак, резюмируя первую часть статьи. Мы с вами вспомнили как работает классический nginx, а также узнали механизм работы ingress-nginx контроллера. 

Во второй части мы подробно разберем как устроено обновление бэкендов в ingress-nginx, а также погрузимся в особенности реализации балансировки на примере sticky sessions.

А пока подписывайтесь на телеграм-канал Hilbert Team ?

И ходите на профессиональные мероприятия, чтобы узнать ещё больше о веб-серверах, Kubernetes и автоматизации рутинных процессов и обсудить то, что узнали с комьюнити. Подробности на официальном сайте DevOpsConf.

Комментарии (1)


  1. olku
    02.07.2025 16:34

    До прочтения был лучшего мнения о nginx контроллере. Но нет - все те же конфиги на файлах, как в доконтейнерную эпоху. Спасибо за статью.