HAProxy + Ingress Controller: публикация сервисов K8s

Публикация сервисов Kubernetes: двойной HAProxy на Proxmox и Ingress Controller

(Кластер Kubernetes с 64 ГБ ОЗУ за 39 €/месяц — часть 5)

Это пятая часть серии статей о создании домашней/staging-среды Kubernetes на базе Proxmox на выделенном сервере Hetzner EX44 (14 ядер/20 потоков, 64 ГБ ОЗУ).

В четвёртой части кластер был успешно развёрнут и работает полностью за NAT. Это хорошо с точки зрения безопасности, но одновременно означает, что внешние клиенты не имеют естественного способа добраться до сервисов внутри кластера.

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

Эту роль здесь выполняет HAProxy.

Он работает на хосте Proxmox (не внутри Kubernetes), принимает входящие подключения на публичный IP-адрес и перенаправляет трафик к нужным узлам за NAT.

Схема входящего трафика через HAProxy на хосте Proxmox

Зачем использовать HAProxy как граничный шлюз (edge gateway) Proxmox?

Есть несколько практических причин, делающих такое размещение труднобиваемым:

Единая точка входа. Публичный IP-адрес есть только у хоста; у узлов его нет. Внешний трафик в любом случае приходит на хост.

Распределение нагрузки. Входящие соединения можно раскидывать по нескольким рабочим узлам (worker nodes) вместо того, чтобы зависеть от одного.

Фильтрация на границе. Удобно отклонять нежелательные домены или трафик ещё до того, как он попадёт в кластер.

TLS passthrough. HAProxy может принимать маршрутные решения на основе SNI, не завершая TLS — сертификаты и шифрование остаются полностью под управлением кластера.

Архитектура инфраструктуры: от публичного IP до Ingress в K8s

Сетевая диаграмма двойного HAProxy на хосте Proxmox EX44: маршрутизация публичного трафика (80/443) к рабочим узлам Kubernetes и API-трафика (6443) через SSH-туннель к мастер-узлам

Как показано на диаграмме выше, экземпляр HAProxy, запущенный прямо на хосте Proxmox, служит точкой входа для всего внешнего трафика.

Запросы на портах 80 и 443 пересылаются на NodePort-сервисы рабочих узлов (30080 и 30443), которые направляют трафик к подам HAProxy Ingress Controller, запущенным как DaemonSet. Ingress Controller применяет правила маршрутизации и перенаправляет запросы к сервисам внутри кластера.

Трафик на порту 6443 маршрутизируется внутренне к узлам управляющей плоскости (control plane) и доступен через проброс портов SSH, что сохраняет Kubernetes API приватным.

Автоматическая генерация конфигурации HAProxy с помощью шаблонов Terraform

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

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

Всё, что нужно сделать, — добавить следующий фрагмент в terraform/main.tf:

resource "local_file" "haproxy_config" {
depends_on = [
proxmox_virtual_environment_vm.control_planes,
proxmox_virtual_environment_vm.workers
]
filename = "${path.module}/../ansible/playbooks/roles/haproxy/files/haproxy.cfg"
content = templatefile("${path.module}/templates/haproxy.cfg.tftpl", {
control_planes = local.control_planes
workers = local.workers
})
}

Этот блок делает три простые вещи:

  1. Ждёт, пока все ВМ будут созданы (чтобы получить реальные IP-адреса).

  2. Рендерит конфигурацию HAProxy из шаблона Terraform.

  3. Записывает готовый файл прямо в директорию Ansible-роли — он сразу готов к развёртыванию.

Подробный разбор: конфигурация HAProxy для API K8s и трафика

Конфигурация HAProxy состоит из трёх логических частей:

  1. Балансировка нагрузки Kubernetes API

  2. Перенаправление HTTP-трафика

  3. Перенаправление HTTPS-трафика с SSL passthrough

Каждая часть выполняет свою отдельную функцию.

Балансировка нагрузки Kubernetes API

frontend kube_api_frontend
bind 127.0.0.1:6443
mode tcp
default_backend kube_api_backend
backend kube_api_backend
mode tcp
balance leastconn
option tcp-check
%{ for control_plane in control_planes ~}
server ${control_plane.name} ${control_plane.ip}:6443 check
%{ endfor ~}

Несколько пояснений к принятым решениям:

  • bind 127.0.0.1:6443 ограничивает доступ к API-эндпойнту уровнем хоста. Он остаётся доступным через проброс портов SSH, но не публикуется в интернет.

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

  • Список бэкендов генерируется Terraform и отражает реальные IP-адреса узлов управляющей плоскости.

HTTP frontend (порт 80): ACME и редирект

Порт 80 существует в основном по двум причинам:

  • HTTP-01-проверки cert-manager (/.well-known/acme-challenge/)

  • Перенаправление всего остального на HTTPS

frontend worker_http_frontend
bind 0.0.0.0:80
mode http
option httplog
# Don't log null connections
option dontlognull
# Block httpoxy attacks; prevents redirecting internal server traffic to an attacker
http-request del-header Proxy
# Forward real client IP to backend (for logging and access control in cluster)
option forwardfor
http-request set-header X-Real-IP %[src]
http-request set-header X-Forwarded-Proto http
# Step 1: Domain filtering - check if domain is in allowed list
# Read allowed domains from file (exact matches)
acl allowed_domain_exact hdr(host) -f /etc/haproxy/allow_domains.txt
# Wildcard domain matching (e.g., *.example.com)
# This matches domains ending with patterns from the file
# Note: For wildcards like *.example.com, the file should contain .example.com
# HAProxy will match any subdomain ending with .example.com
acl allowed_domain_wildcard hdr_end(host) -f /etc/haproxy/allow_domains.txt
# Step 2: Check if request is for Let's Encrypt HTTP-01 challenge
# Let's Encrypt uses /.well-known/acme-challenge/ path for validation
acl is_letsencrypt_challenge path_beg /.well-known/acme-challenge/
# Rule 1: Block requests from domains NOT in allowed list
# This must be checked first - reject unauthorized domains immediately
# Domain is allowed if it matches exactly OR matches wildcard pattern
http-request deny if !allowed_domain_exact !allowed_domain_wildcard
# Rule 2: Redirect allowed to HTTPS (if not ACME)
http-request redirect scheme https code 301 if !is_letsencrypt_challenge
# Rule 3: Route ACME challenge
use_backend worker_http_backend if is_letsencrypt_challenge
# Default backend (should not be reached, but kept for safety)
default_backend worker_http_backend

Дополнительно добавлено несколько базовых HTTP-защит:

  • Запросы с неизвестными или неразрешёнными заголовками Host отбрасываются с помощью строгого списка разрешённых доменов (allow_domains.txt).

  • Заголовок Proxy удаляется для предотвращения атак типа httpoxy.

  • Реальный IP-адрес клиента сохраняется через X-Real-IP и X-Forwarded-For, чтобы сервисы внутри кластера видели фактический адрес источника.

  • Пути ACME-проверки (/.well-known/acme-challenge/) пропускаются по HTTP, всё остальное перенаправляется на HTTPS.

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

Такая конфигурация блокирует неизвестные домены и уменьшает шум от случайных сканирований, однако это лишь базовое усиление защиты. Для надёжной защиты от DDoS-атак, фильтрации ботов и терминации TLS по-прежнему нужен полноценный граничный слой наподобие Cloudflare.

HTTPS frontend (порт 443): TLS passthrough с фильтрацией по SNI

Для HTTPS-трафика TLS сохраняется сквозным. HAProxy не завершает TLS — он просто перенаправляет TCP-соединения, тогда как cert-manager и Ingress Controller управляют сертификатами и шифрованием внутри кластера.

frontend worker_https_frontend
bind 0.0.0.0:443
mode tcp
option tcplog
# Wait for SNI extraction from TLS handshake
tcp-request inspect-delay 5s
# Domain filtering via SNI
acl allowed_domain_exact req_ssl_sni -f /etc/haproxy/allow_domains.txt
acl allowed_domain_wildcard req_ssl_sni -m end -f /etc/haproxy/allow_domains.txt
# Reject unauthorized domains
tcp-request content reject if !{ req_ssl_sni -m found } || !allowed_domain_exact !allowed_domain_wildcard
default_backend worker_https_backend

Ключевой элемент здесь — tcp-request inspect-delay. Он даёт HAProxy короткое окно для анализа начального TLS-рукопожатия (handshake) и извлечения SNI перед принятием решения о маршрутизации. На практике SNI приходит мгновенно — значение 5s является лишь верхней границей, а не фактической задержкой.

Соединения без SNI или нацеленные на домены, отсутствующие в списке разрешённых, отбрасываются на границе. Это гарантирует, что HAProxy перенаправляет трафик только для явно разрешённых доменов, игнорируя всё остальное, что попадает на IP сервера.

Бэкенды для рабочих узлов (NodePorts)

Следующая конфигурация бэкендов использует циклы Terraform для динамического сопоставления всех рабочих узлов с конкретными NodePort-портами, которые использует Ingress Controller:

backend worker_http_backend
mode http
balance leastconn
option tcp-check
%{ for worker in workers ~}
server ${worker.name} ${worker.ip}:30080 check
%{ endfor ~}
backend worker_https_backend
mode tcp
balance leastconn
%{ for worker in workers ~}
server ${worker.name} ${worker.ip}:30443
%{ endfor ~}

Эта конфигурация предполагает, что Ingress Controller опубликован через NodePort:

  • HTTP NodePort: 30080

  • HTTPS NodePort: 30443

HAProxy перенаправляет трафик на эти порты рабочих узлов, а Kubernetes берёт управление на себя внутри кластера.

Сохранение IP-адреса клиента: PROXY Protocol и SNI passthrough

Поскольку HAProxy стоит перед кластером, рабочие узлы будут видеть IP-адрес HAProxy как адрес клиента — даже при использовании externalTrafficPolicy: Local. Этот параметр запрещает Kubernetes переписывать исходные IP-адреса, но не может сохранить оригинальный IP клиента после того, как HAProxy открывает новое TCP-соединение к NodePort.

В данной лаборатории TLS передаётся сквозным образом, а для выдачи сертификатов используется HTTP-01. Это упрощает конфигурацию и позволяет cert-manager и Ingress Controller полностью управлять TLS внутри Kubernetes, не добавляя лишних движущихся частей на граничном уровне.

Если в будущем потребуется сохранять реальный IP-адрес клиента для HTTPS, оптимальный путь обновления — переключиться на DNS-01-проверки и включить PROXY Protocol между HAProxy и Ingress Controller. Это позволит сохранять оригинальный адрес клиента, не отказываясь от TLS passthrough.

Даже без терминации TLS HAProxy обнаруживает отказавшие узлы на уровне соединения и автоматически переключает трафик на работоспособные рабочие узлы.

Настройка списков разрешённых доменов для защиты HAProxy

Список разрешённых доменов (domain allowlist) — это простой текстовый файл, управляющий тем, какие домены HAProxy будет принимать. Он применяется как к HTTP-заголовкам Host, так и к HTTPS SNI, поэтому обслуживаются только явно разрешённые домены.

Создайте его из примера:

cd ansible/playbooks/roles/haproxy/files/
cp allow_domains.txt.example allow_domains.txt

Пример содержимого файла:

example.com
www.example.com
api.example.com
.example.com

Последняя строка заслуживает особого внимания. Маски (wildcards) реализованы как совпадение по суффиксу, поэтому .example.com будет соответствовать любому поддомену (api.example.com, app.example.com и т.д.). HAProxy здесь не использует синтаксис *.example.com — начальная точка и есть то, что включает поиск по маске.

Этот список служит простым ограничителем: HAProxy принимает трафик только для явно заданных доменов и игнорирует всё остальное, попадающее на IP сервера.

Генерация конфигурации HAProxy

Когда ресурс Terraform готов, сгенерируйте конфигурацию HAProxy:

cd terraform/
terraform plan
terraform apply

Terraform запишет итоговый файл конфигурации по пути:

ansible/playbooks/roles/haproxy/files/haproxy.cfg

Быстрая проверка:

cat ansible/playbooks/roles/haproxy/files/haproxy.cfg

Как минимум вы должны увидеть:

  • IP-адреса узлов управляющей плоскости в kube_api_backend

  • IP-адреса рабочих узлов в worker_http_backend и worker_https_backend

Автоматизация развёртывания: Ansible-роль для HAProxy

HAProxy разворачивается на хосте Proxmox через роль haproxy:

cd ansible/
ANSIBLE_CONFIG=./ansible.cfg ansible-playbook -i inventory/hosts.ini playbooks/site.yml --tags haproxy

Что делает роль

  1. Устанавливает HAProxy, если он ещё не установлен.

  2. Проверяет, что файл allow_domains.txt существует и не пуст.

  3. Копирует список разрешённых доменов в /etc/haproxy/allow_domains.txt.

  4. Загружает сгенерированный haproxy.cfg во временное место.

  5. Проверяет конфигурацию перед применением.

  6. Заменяет рабочую конфигурацию только после успешной валидации.

  7. Перезагружает HAProxy без простоя.

  8. Сохраняет резервные копии, что делает откат тривиальным при необходимости.

Валидация конфигурации происходит до того, как что-либо затронет живой сервис:

- name: Validate HAProxy configuration from temporary file
command: haproxy -c -f "{{ haproxy_config_path }}.tmp"
register: haproxy_config_check
failed_when: haproxy_config_check.rc != 0

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

Установка HAProxy Ingress Controller через Helm

На данном этапе экземпляр HAProxy на хосте Proxmox перенаправляет внешний трафик на рабочие узлы через NodePort (30080 для HTTP и 30443 для HTTPS). Но эти порты — лишь точки входа; что-то внутри кластера всё равно должно принять этот трафик и направить его к нужным сервисам.

Это задача HAProxy Ingress Controller.

Он работает на каждом рабочем узле и становится мостом между внешним трафиком и вашими приложениями. Он следит за ресурсами Ingress и автоматически обновляет правила маршрутизации по мере добавления, изменения или удаления сервисов.

Полный путь трафика теперь выглядит так:

Диаграмма потока трафика к HAProxy Ingress: публичный интернет (80/443) → шлюз Proxmox (SNI/L7 редирект) → рабочие узлы Kubernetes (NodePort 30080/30443) → HAProxy Ingress Controller → поды приложений K8s

Хост Proxmox доставляет трафик в кластер, после чего Ingress Controller берёт управление на себя и обрабатывает всё внутри Kubernetes.

Установка Ingress Controller

Сначала добавьте Helm-репозиторий:

helm repo add haproxytech https://haproxytech.github.io/helm-charts
helm repo update

Затем установите контроллер, используя предоставленный файл values:

cd examples
helm install haproxy-ingress haproxytech/kubernetes-ingress \
--namespace haproxy-controller \
--create-namespace \
-f examples/01-haproxy-ingress/values.yaml

Это развернёт Ingress Controller как DaemonSet, так что каждый рабочий узел будет запускать собственный экземпляр HAProxy, прослушивающий NodePort-сервис.

Проверка работоспособности

Проверьте поды:

kubectl get pods -n haproxy-controller -o wide

Вы должны увидеть по одному поду Ingress Controller на каждом рабочем узле.

Затем проверьте сервис:

kubectl get svc -n haproxy-controller

Вы должны увидеть NodePort-порты 30080 и 30443 — именно те порты, на которые Proxmox HAProxy перенаправляет трафик.

Детали конфигурации

Рассмотрим подробнее файл examples/01-haproxy-ingress/values.yaml:

controller:
# Use DaemonSet to run one pod per node for distributed traffic processing
kind: DaemonSet
# Network service configuration
service:
type: NodePort
# 'Local' preserves the client source IP and avoids extra hops between nodes
externalTrafficPolicy: Local
nodePorts:
http: 30080
https: 30443
# HAProxy internal configuration (via ConfigMap)
config:
use-proxy-protocol: "false"
# SSL redirect is handled by Proxmox HAProxy (not by HAProxy Ingress Controller)
# HAProxy on dedicated server redirects HTTP to HTTPS, but allows /.well-known/acme-challenge/ to pass through
# Disabling ssl-redirect here to prevent double redirect and allow HTTP-01 challenge to work
ssl-redirect: "false"
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# Enable built-in Prometheus metrics exporter
metrics:
enabled: true
# IngressClass resource configuration
# Use 'ingressClassName: haproxy' in your Ingress manifests
ingressClassResource:
enabled: true
name: haproxy
default: false

Развёртывание как DaemonSet

Контроллер работает как DaemonSet, поэтому каждый рабочий узел запускает собственный экземпляр HAProxy. Трафик, перенаправленный с хоста Proxmox, попадает на рабочий узел и обрабатывается локально, без лишних переходов через кластер.

NodePort-сервис (30080 / 30443)

Ingress Controller опубликован через:

  • 30080 → HTTP

  • 30443 → HTTPS

Эти порты совпадают с портами, настроенными в бэкендах Proxmox HAProxy. Внешний трафик сначала попадает на хост Proxmox, а затем перенаправляется на один из этих портов рабочего узла.

externalTrafficPolicy: Local

Этот параметр удерживает трафик на узле, где он прибыл, и исключает ненужную пересылку между узлами.

PROXY Protocol отключён

PROXY Protocol отключён для сохранения надёжности HTTP-01-валидации сертификатов. Его глобальное включение нарушает ACME HTTP-валидацию. Если в будущем потребуется сохранять реальные IP-адреса клиентов, переключение на DNS-01-проверки позволит безопасно включить PROXY Protocol.

SSL-редирект обрабатывается на граничном уровне

Перенаправление HTTP → HTTPS выполняет Proxmox HAProxy. Ingress Controller занимается только маршрутизацией трафика внутри кластера, что исключает конфликты редиректов и сохраняет работоспособность ACME-валидации.

IngressClass

При развёртывании регистрируется IngressClass с именем haproxy. Используйте ingressClassName: haproxy в манифестах Ingress, чтобы направлять трафик через этот контроллер.

Настройка SSL-сертификатов Let’s Encrypt

Когда Ingress Controller готов, кластер готов автоматически выдавать и управлять TLS-сертификатами с помощью cert-manager и Let’s Encrypt.

cert-manager работает внутри Kubernetes и управляет полным жизненным циклом сертификатов — запрашивает, хранит и обновляет их по мере необходимости. Он напрямую интегрируется с ресурсами Ingress, поэтому сертификаты выдаются автоматически при появлении новых доменов.

Установка cert-manager

Добавьте Helm-репозиторий:

helm repo add jetstack https://charts.jetstack.io
helm repo update

Установите cert-manager:

helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true

Проверка cert-manager и настройки ClusterIssuer

Дождитесь запуска всех подов:

kubectl get pods -n cert-manager

Создание ClusterIssuer для Let’s Encrypt

Скопируйте файл-пример и укажите свой email:

cd examples
cp 02-letsencrypt/cluster-issuer.yaml.example 02-letsencrypt/cluster-issuer.yaml
vi 02-letsencrypt/cluster-issuer.yaml

Затем примените его:

kubectl apply -f 02-letsencrypt/cluster-issuer.yaml

Проверьте:

kubectl get clusterissuer

Вы должны увидеть оба:

  • letsencrypt-staging

  • letsencrypt-prod

Staging-издатель удобен для тестирования, production-издатель выдаёт доверенные сертификаты.

Автоматизация: процесс HTTP-01-проверки с HAProxy

cert-manager использует процесс HTTP-01-проверки. При запросе сертификата он временно создаёт Ingress, открывающий эндпойнт валидации:

Proxmox HAProxy пропускает этот путь по HTTP и перенаправляет его в кластер, где cert-manager отвечает на проверку. После успешной валидации сертификат выдаётся и сохраняется как Kubernetes Secret.

После этого Ingress Controller автоматически использует сертификат для TLS.

Настройка Ingress в K8s для TLS-сертификатов

Чтобы включить TLS для домена, укажите ClusterIssuer в вашем Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
ingressClassName: haproxy
tls:
- hosts:
- example.com
secretName: example-com-tls

cert-manager выполнит следующее:

  1. Запросит сертификат.

  2. Сохранит его в Secret.

  3. Автоматически обновит его до истечения срока действия.

Когда всё заработает, можно переключиться с letsencrypt-staging на letsencrypt-prod, чтобы получить доверенные сертификаты.

Сквозной тест: развёртывание Podinfo с TLS

Когда HAProxy работает на граничном уровне, Ingress Controller запущен, а cert-manager выдаёт сертификаты, остаётся развернуть простое приложение и убедиться, что всё работает от начала до конца.

В качестве лёгкого тестового сервиса используем Podinfo — он позволит проверить полный путь ingress от интернета до пода внутри кластера и убедиться в корректной работе маршрутизации и TLS.

Развёртывание приложения

Примените Deployment и Service:

kubectl apply -f examples/03-podinfo/deployment.yaml

Убедитесь, что поды и сервис запущены:

kubectl get pods -l app=podinfo
kubectl get svc -l app=podinfo

Вы должны увидеть поды в состоянии Running и сервис, опубликованный на порту 9898.

Создание ресурса Ingress

Скопируйте пример манифеста Ingress и укажите свой домен:

cp examples/03-podinfo/ingress.yaml.example examples/03-podinfo/ingress.yaml
vi examples/03-podinfo/ingress.yaml

Задайте свой домен и начните со staging-издателя:

annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging

Примените Ingress:

kubectl apply -f examples/03-podinfo/ingress.yaml

Это запустит запрос сертификата cert-manager и автоматическую настройку TLS.

Проверка выдачи сертификата

Получение сертификата может занять минуту-другую. Наблюдать за процессом можно командой:

kubectl get certificate podinfo-tls -w

Когда сертификат будет готов, он сохранится как Secret и будет автоматически подхвачен Ingress Controller.

На этом этапе TLS полностью активен.

Тест полного пути трафика

Проверьте HTTP:

Если всё настроено правильно, запрос пройдёт через:

Интернет → Proxmox HAProxy → Рабочий узел → Ingress Controller → Сервис → Под

Переключение на production-сертификаты

Когда staging работает, обновите издатель:

annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod

Примените снова:

kubectl apply -f examples/03-podinfo/ingress.yaml

cert-manager выдаст доверенный сертификат и автоматически заменит им staging-сертификат.

На этом этапе полный путь ingress активен — HAProxy обрабатывает внешний трафик, Kubernetes маршрутизирует его внутри, а cert-manager поддерживает сертификаты в актуальном состоянии.

Финальная схема работающей инфраструктуры с HAProxy, Ingress Controller и cert-manager

Что дальше и как это автоматизировать

На данном этапе ingress работает, cert-manager автоматически выдаёт TLS-сертификаты, а HAProxy перенаправляет внешний трафик в кластер.

Приложения доступны по HTTPS, а трафик течёт из интернета в поды без каких-либо ручных шагов. Репозиторий GitHub обновлён — просто склонируйте репозиторий, переключитесь на ветку part_8 и следуйте инструкциям в README.md.

➜ В следующей статье мы познакомимся с Argo CD и перейдём на Git-ориентированный рабочий процесс развёртывания.

Вместо ручного применения манифестов Argo CD будет следить за Git-репозиторием и автоматически синхронизировать с ним состояние кластера. Это упрощает управление развёртываниями и избавляет от необходимости запускать kubectl apply с рабочей станции.

© 2026 meganuke