Найти в Дзене
Linux | Network | DevOps

🐧HAProxy: почему ACL иногда “не работает”

HAProxy часто появляется в инфраструктуре незаметно. Сначала это просто
балансировщик: принял трафик, отправил дальше — всё понятно. Потом
появляется второй сервис, третий, routing по домену, path, заголовкам,
SNI, а заодно canary и временные исключения. В этот момент почти всегда
Оглавление

HAProxy часто появляется в инфраструктуре незаметно. Сначала это просто

балансировщик: принял трафик, отправил дальше — всё понятно. Потом

появляется второй сервис, третий, routing по домену, path, заголовкам,

SNI, а заодно canary и временные исключения. В этот момент почти всегда

всплывают ACL. Кто-то использует их осознанно, кто-то — по принципу:

нашёл в примере, вроде работает. Рядом с ACL неизбежно стоит mode: tcp или http.

Снаружи это выглядит как простая настройка, но на деле —

фундаментальное решение, от которого зависит, какие данные HAProxy

вообще видит и какие условия способен проверить.

HAProxy последователен и выполняет конфигурацию ровно так, как она написана. Отсюда и классика жанра: ACL есть, но backend не выбирается, mode вроде http, но заголовки недоступны, routing работает почти всегда, кроме

пятницы.

-2

В этой статье мы разберём, как HAProxy принимает решения:

  • чем на самом деле отличаются mode tcp и mode http и где каждый из них уместен
  • как устроены ACL, из чего они состоят и какие данные могут использовать
  • где именно применяется routing и в каком порядке обрабатываются правила
  • какие сценарии можно (и нужно) решать ACL, а где лучше использовать maps
  • и какие ошибки чаще всего делают даже опытные инженеры

Цель простая: после прочтения конфиги HAProxy должны перестать выглядеть как набор случайных условий, а routing — как удачное совпадение. Всё будет предсказуемо, читаемо и, что особенно важно, объяснимо коллеге без фразы: не трогай, оно работает. Дальше начнём с базы — разберёмся, где

именно в HAProxy принимаются решения и почему это важно понимать до

того, как писать первую ACL.

Архитектура HAProxy: где вообще принимаются решения

-3

Чтобы

понимать, как работает routing в HAProxy, нужно сначала разобраться, в

какой момент и в каком месте конфигурации принимаются решения. Конфиг

интерпретируется , как чёткий и довольно прямолинейный пайплайн

обработки соединения. Если держать его в голове, многие странные баги

внезапно перестают быть странными.

Входная точка всегда одна — frontend. Именно здесь HAProxy принимает соединение, читает данные (в рамках выбранного режима) и применяет большую часть логики: ACL, http-request, tcp-request, выбор backend. Backend сам по себе никаких решений не принимает — это скорее набор параметров: куда отправлять трафик, как проверять серверы, какие таймауты использовать. Если backend выбран неправильно, значит ошибка почти всегда на стороне frontend.

Важно понимать и порядок обработки. Сначала HAProxy анализирует соединение и запрос, затем применяет правила обработки (http-request, tcp-request), и только после этого выбирает backend (use_backend или default_backend). Это означает простую, но часто забываемую вещь: ACL могут быть корректными, но если они применены не в том месте или не в то время, результат будет неожиданным.

backend be_app

acl is_api path_beg /api

Такой конфиг синтаксически валиден, но логически бесполезен. Backend не

принимает решений — этот код никогда не повлияет на routing. Если

backend выбран неправильно, ошибка почти всегда находится выше, во

frontend.

Отсюда вытекает базовый принцип: routing — это задача

frontend, а backend — это реализация выбранного маршрута. Если упростить

ещё сильнее, работает примерно так: получил соединение → понял, что это → применил правила → выбрал маршрут → проксирую дальше.

Поэтому, когда вы видите конфигурации с десятком use_backend, раскиданных между http-request deny и redirect,

стоит задать простой вопрос: в каком именно состоянии находится запрос в

этот момент? Ответ на него обычно сразу объясняет, почему трафик едет

не туда, а отладка превращается в археологию.

Режимы работы HAProxy

-4

Разговор про ACL и routing в HAProxy имеет смысл только после одного базового вопроса — в каком режиме он работает: tcp или http.

Это не второстепенная настройка. От режима напрямую зависит, какие

данные HAProxy вообще видит, а значит — какие условия можно проверять и

какие решения принимать.

В mode tcp HAProxy работает

на уровне соединений. Он знает, кто к кому подключился, на какой порт,

использовался ли TLS, и при необходимости может заглянуть в TLS

ClientHello, чтобы получить SNI. Дальше этого он не идёт. Для него

трафик — это поток байт без структуры. Он не знает, где начинается и

заканчивается HTTP-запрос, не понимает заголовков, путей и методов — и,

что важно, даже не пытается.

Из этого напрямую следует ограничение: любые ACL, завязанные на HTTP-атрибуты (path, method, hdr()),

в mode tcp просто не могут быть вычислены. Не из-за того, что конфиг

плохо написан, а потому что нужных данных на этом уровне не существует.

Зато этот режим отлично подходит для сценариев, где HTTP не нужен вовсе:

TLS passthrough, базы данных, gRPC без терминации или любые другие

non-HTTP сервисы.

mode http — принципиально другой

уровень. Здесь HAProxy включает HTTP-парсер и начинает работать как

L7-прокси. Появляются методы, URI, query string, заголовки, cookies —

всё то, ради чего ACL обычно и используют. В этом режиме решения

принимаются на основе содержимого запроса, а не просто факта

подключения. Цена очевидна: HTTP (а чаще всего и TLS) нужно

терминировать. Для веб-сценариев это нормальный и осознанный компромисс,

поэтому подавляющее большинство ingress’ов и edge-прокси работают

именно в mode http.

Важно помнить, что режим задаётся отдельно для frontend и backend. HAProxy технически позволяет сделать frontend в http, а backend в tcp.

На практике же это почти всегда сигнал, что архитектурное решение до

конца не продумано. Хороший тон — выбирать режим осознанно, фиксировать

это решение и не менять его просто потому, что так тоже заработало.

frontend http-in

mode http

backend tcp-servers

mode tcp

Использование HAProxy перед веб-сервисами не означает автоматический переход на mode http. Даже если за ним стоят HTTP-приложения, никто не запрещает использовать mode tcp. В этом случае будут корректно проксироваться соединения, но вся HTTP-логика окажется недоступной: никаких path_beg, заголовков или методов. Он честно работает как TCP-прокси и не делает вид, что понимает больше, чем видит.

ACL: условия, а не правила

Когда режим выбран, можно

переходить к ACL. И здесь важно сразу избавиться от одного

распространённого заблуждения: ACL в HAProxy — это не правило и не

действие. ACL — это условие. Оно отвечает "да или нет" на вопрос и

больше ничего не делает. Решения принимаются уже директивами вроде use_backend, http-request deny, redirect или set-header.

Коротко это можно зафиксировать так:

  • ACL ≠ routing
  • ACL ≠ deny
  • ACL = логическое выражение

Все

действия в HAProxy выполняют директивы. ACL лишь определяет, при каком

условии эти действия будут применены. Проще всего воспринимать ACL как

именованные логические выражения. Вы описываете проверку, даёте ей

понятное имя, а затем используете это имя там, где нужно принять

решение:

acl is_api path_beg /api/

acl is_post method POST

Здесь нет ни маршрутизации, ни фильтрации. Мы просто фиксируем два факта: запрос идёт в /api, и метод у него POST. Дальше конфиг начинает читаться почти как обычный текст:

use_backend api_backend if is_api is_post

Почему ACL иногда не работают?

ACL всегда вычисляются в контексте выбранного режима и текущего этапа

обработки. Если HAProxy в этот момент не видит нужных данных, ACL просто

не может быть вычислена.

На практике ACL не влияет на результат, если:

  • выбран неподходящий mode и данные которые он не может прочитать
  • правило применяется до появления нужных данных в пайплайне
  • запрос был отклонён или изменён на этапе http-request

Основные источники данных для ACL

-5

После того как мы договорились, что ACL — это всего лишь логическое

выражение, логично задать следующий вопрос: на основании каких данных

это выражение вообще может быть вычислено? Источники данных условно

можно разделить по уровням — от соединения до содержимого HTTP-запроса.

Данные соединения (L4)

Самый базовый слой — параметры соединения. Эти данные доступны практически всегда и не требуют HTTP-парсинга.

Речь идёт о:

  • IP-адресе клиента (src)
  • адресе и порте назначения (dst, dst_port)
  • состоянии TLS на фронтенде (ssl_fc)
  • количестве текущих соединений (src_conn_cur)
  • информации из PROXY protocol

Пример фильтрации по src-заголовку ip пакета:

acl private src 10.0.0.0/8

tcp-request connection reject if private

Это тот уровень, где принимаются самые дешёвые решения. Если задачу можно решить на L4 — лучше решить её там. Меньше аллокаций, меньше CPU, выше устойчивость под нагрузкой.

Метаданные TLS (до HTTP)

Если используется TLS, но вы не переходите в полноценный HTTP-анализ,

доступны данные рукопожатия. Самый распространённый источник — SNI. Это позволяет принимать решения до разбора HTTP-запроса, что особенно важно при TLS passthrough или в сценариях, где терминация нежелательна.

tcp-request inspect-delay 5s

tcp-request content accept if { req_ssl_hello_type 1 }

use_backend bk_web1 if { req.ssl_sni -i site1.com }

use_backend bk_web2 if { req.ssl_sni -i site2.com }

Здесь важно понимать: на этом этапе доступны только метаданные TLS, а не HTTP-структура. Если конфигурация пытается читать path или hdr(), проблема не в синтаксисе — просто этих данных ещё не существует.

HTTP-атрибуты (L7)

В mode http появляются данные прикладного уровня:

  • path, path_beg, path_reg
  • method
  • hdr(name)
  • query
  • cook(name)

Здесь чаще всего строится сложная маршрутизация:

acl is_api path_beg /api/

acl is_v2 path_beg /api/v2/

use_backend api_v2 if is_api is_v2

use_backend api_v1 if is_api !is_v2

На этом уровне уже оперирует структурированным запросом. Цена —

необходимость HTTP-парсинга (и обычно TLS-терминации), но гибкость

значительно выше.

Важно помнить: каждый HTTP-атрибут вычисляется в

момент обработки запроса. Если запрос уже переписан или отклонён, ACL

ниже по конфигу могут увидеть совсем другие данные.

Состояние и динамические данные

Кроме сырого запроса, ACL могут использовать вычисленные или накопленные данные.

Например, частоту запросов из stick table:

stick-table type ip size 100k expire 10s store http_req_rate(10s)

http-request track-sc0 src

acl abuse sc_http_req_rate(0) gt 50

http-request deny deny_status 429 if abuse

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

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

HTTP deny vs TCP deny: где резать трафик

В HAProxy отклонять трафик можно на разных уровнях стека, и от этого сильно зависит как поведение системы, так и её производительность. tcp-request connection reject / tcp-request content reject

работают на L4: соединение рвётся максимально рано, без разбора

HTTP-протокола. Это самый быстрый и дешёвый вариант — минимум CPU,

минимум аллокаций, идеален для защиты от мусорного трафика, сканеров и

очевидного злоупотребления.

HTTP deny (через http-request deny, deny_status, return)

— это уже L7: HAProxy парсит HTTP-заголовки, может смотреть Host, Path,

Headers, Cookies и принимать более осознанные решения, но за это платим

временем и ресурсами. Здесь правило простое: если решение можно принять

до HTTP, делай TCP deny — он быстрее и масштабируется лучше. Если нужна

логика на уровне запроса или корректный HTTP-ответ клиенту (403, 429 и

т.д.) — без HTTP deny не обойтись. И да, резать раньше почти всегда

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

Отдельный кейс — silent drop.

Это уже не просто отказать, а сделать вид, что тебя вообще нет. В

отличие от reject, который отправляет RST и мгновенно завершает

соединение, silent drop просто прекращает обработку и не отвечает

клиенту. Для сканера это выглядит как таймаут: порт вроде есть, но

сервис молчит.

И есть ещё один инструмент — tarpit.

Это уже более педагогическая мера. Вместо мгновенного отказа HAProxy

принимает соединение и держит его открытым заданное время, прежде чем

вернуть ошибку (обычно 403). По сути, мы сознательно тратим ресурсы

злоумышленника, заставляя его держать сокет занятым. Полезно против

брутфорса и агрессивных клиентов, но нужно понимать: каждое такое

соединение потребляет и наши ресурсы тоже.

Чем это отличается на практике:

  • TCP reject (L4) — быстро, дёшево, соединение закрыто сразу. Клиент мгновенно понимает, что порт живой.
  • TCP silent-drop (L4)
  • — ответа нет вообще. Усложняет массовое сканирование и снижает шум, но
  • клиент будет держать соединение до таймаута. При больших объёмах это
  • может влиять на таблицу conntrack и лимиты файловых дескрипторов.
  • Tarpit (L7) — соединение принято, но залипает на заданное время.
  • HTTP deny (L7) — полный разбор запроса и корректный HTTP-ответ. Дороже по CPU, зато предсказуемо и управляемо.

# --- TCP-level deny (L4) ---

# Отрезаем подозрительные IP ещё до разбора HTTP

acl bad_ip src -f /etc/haproxy/blacklist.ip

tcp-request connection reject if bad_ip

# Можно и по количеству соединений, всё ещё без HTTP-парсинга

acl too_many_conn src_conn_cur gt 100

tcp-request content reject if too_many_conn

# Полностью игнорировать соединение (silent drop)

acl scanner src -f /etc/haproxy/scanners.ip

http-request silent-drop if scanner

# --- HTTP-level deny (L7) ---

# Закрываем /admin для внешнего мира

acl is_admin path_beg /admin

acl not_internal src ! 10.0.0.0/8

http-request deny deny_status 403 if is_admin not_internal

# Отдать в tarpit агрессивных клиентов

stick-table type ip size 100k expire 2m store http_req_rate(1m)

http-request track-sc0 src

acl too_many_requests sc_http_req_rate(0) gt 2

timeout tarpit 10s

http-request tarpit deny_status 403 if brute_force

# Rate limit с вежливым ответом

stick-table type ip size 100k expire 10s store http_req_rate(10s)

http-request track-sc0 src

acl abuse sc_http_req_rate(0) gt 50

http-request deny deny_status 429 if abuse

Сначала рубим топором (TCP deny), если нужно — делаем вид, что топора вообще нет (silent drop), а если хочется чуть потроллить агрессивного клиента — отправляем его в tarpit. И только потом аккуратно режем (HTTP deny).

ACL в фазе http-response: когда backend уже всё сделал, а мы — ещё нет

Если в http-request мы работаем с тем, что клиент попросил, то в http-response — с тем, что backend вернул. И вот здесь многие недооценивают HAProxy.

Потому что после выбора backend’а кажется, что всё, решение принято. На

самом деле — нет. У нас всё ещё есть контроль над тем, что именно уйдёт

клиенту.

Важно понимать контекст:

  • backend уже выбран
  • балансировка завершена
  • соединение с сервером установлено
  • ответ получен

Routing на этом этапе изменить нельзя. Но вот сам ответ — можно.

Добавление security-заголовков

Классический сценарий — централизованное управление безопасностью. Не каждый сервис аккуратно выставляет HSTS, CSP или X-Frame-Options. Например, добавляем HSTS только для успешных ответов:

acl is_success status 200

http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains" if is_succes

Или применяем CSP только к HTML:

acl is_html res.hdr(Content-Type) -m sub text/html

http-response set-header Content-Security-Policy "default-src 'self'" if is_html

Backend’ы при этом остаются нетронутыми. Централизация — 1, зоопарк сервисов — 0.

Маскировка технологического стека

Иногда backend слишком разговорчив:

X-Powered-By: PHP/7.4

Можно привести всё к единому профилю:

acl has_server_hdr res.hdr(X-Powered-By) -m found

http-response del-header X-Powered-By if has_server_hdr

Реакция на коды ответа

ACL позволяют анализировать статус ответа и действовать по результату.

Например, если backend вернул 503 — отправляем пользователя на

статус-страницу:

acl backend_unavailable status 503

http-response redirect location https://status.example.com if backend_unavailable

Или добавляем диагностический заголовок для всех 5xx:

acl is_5xx status 500:599

http-response add-header X-Debug-Error true if is_5xx

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

Контроль cookies

Очень практичный кейс — принудительное добавление флагов безопасности:

acl has_set_cookie res.hdr(Set-Cookie) -m found

http-response replace-header Set-Cookie (.*) \1;\ HttpOnly;\ Secure if has_set_cookie

Если backend забыл поставить Secure или HttpOnlyэто компенсируется. Особенно актуально в микросервисной среде, где стандарты иногда трактуются творчески.

Переменные в HAProxy: состояние запроса

-6

Переменные — это способ протащить состояние и данные запроса через разные стадии обработки, не изобретая велосипед с костылями из ACL. Они задаются через set-var и читаются как обычные fetch-выражения, живут в строго определённом скоупе и времени жизни. Основные типы: req. (живут в рамках запроса), txn. (на транзакцию, переживают retry), sess. (на сессию, например keep-alive), conn. (на соединение).

В HTTP-режиме чаще всего используют req.* и txn.* для хранения заголовков, path, user-id, feature-флагов, результатов lua или map-файлов.

В TCP-режиме — conn.* и sess.*, когда нужно один раз распарсить SNI, IP или первые байты payload и дальше просто читать значение.

Переменные можно трогать на разных стадиях: http-request (разбор и нормализация запроса), http-response (логика ответа), tcp-request content (L4/L5), tcp-response, а также в log-format.

Пример ниже — классический кейс, посмотрели заголовок, сохранили,

использовали дальше, чтобы не вычислять одно и то же несколько раз:

frontend http

http-request set-var(req.user_id) req.hdr(X-User-ID)

http-request set-var(txn.is_mobile) bool(req.hdr(User-Agent),sub -i mobile)

acl is_mobile var(txn.is_mobile) -m bool

use_backend mobile if is_mobile

backend mobile

http-response set-header X-Debug-User %[var(req.user_id)]

В итоге: вычисления — один раз, логика — читаемая, конфиг —

поддерживаемый. А если вы всё ещё парсите один и тот же заголовок в пяти

ACL подряд — HAProxy вас простит, но коллеги могут и не простить.

Порядок обработки и приоритеты

-7

Одна из особенностей — это строгий порядок обработки правил. Он несложный, но если его игнорировать, можно часами смотреть на корректные ACL и не понимать, почему они не влияют на результат.

В упрощённом виде процесс выглядит так:

  1. HAProxy принимает соединение
  2. Парсит запрос (в рамках режима)
  3. Применяет правила обработки (http-request или tcp-request)
  4. И только после этого выбирает backend с помощью use_backend
  5. Если ни одно условие не сработало — используется default_backend

Отсюда важный момент: если запрос был отклонён, переписан или перенаправлен на этапе http-request, до выбора backend он может вообще не дойти. И наоборот — если backend уже выбран, никакая ACL дальше не сможет это изменить.

Приоритеты также зависят от расположения правил в конфигурации. Читаются они сверху вниз, и первое подходящее правило часто оказывается последним, которое имеет значение. Поэтому порядок use_backend важен не меньше, чем сами условия. Сначала — более специфичные ACL, потом — более общие. Иначе внезапно окажется, что /api/admin всегда уезжает туда же, куда и /api.

Хорошая практика здесь простая, но эффективная: думать о конфиге, как о

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

Специфичное vs общее правило (path_beg)

# Более специфичное правило — выше

acl is_admin_api path_beg /api/admin

use_backend admin_api if is_admin_api

# Менее специфичное — ниже

acl is_api path_beg /api

use_backend api if is_api

# Если ничего не подошло

default_backend web

Запрос:

GET /api/admin/users

  • совпадает с path_beg /api/admin
  • совпадает с path_beg /api

Но backend выбирается на первом use_backend, который сработал.

Результат:

/api/admin/users → admin_api

Теперь поменяем порядок:

acl is_api path_beg /api

use_backend api if is_api

acl is_admin_api path_beg /api/admin

use_backend admin_api if is_admin_api

Теперь:

/api/admin/users → api

До admin_api уже не дойдёт. Более специфичные ACL должны быть выше более общих. Иначе получите всё уезжает не туда, хотя ACL корректные.

Wildcard по Host (hdr_end)

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

acl is_admin_host hdr_end(host) -i admin.example.com

use_backend admin if is_admin_host

acl is_example hdr_end(host) -i example.com

use_backend main if is_example

Конструкция:

hdr_end(host) -i example.com

Означает, что заголовок Host заканчивается на example.com

(без учёта регистра). Это строковое сравнение, не DNS-логика, не

FQDN-матчинг, не самое специфичное правило выигрывает. Если поменять

порядок:

acl is_example hdr_end(host) -i example.com

use_backend main if is_example

acl is_admin_host hdr_end(host) -i admin.example.com

use_backend admin if is_admin_host

Теперь:

Host: admin.example.com → main

Хотя вы явно написали ACL для admin. Почему так? Потому что hdr_end — это просто проверка окончания строки. HAProxy не делает наиболее точное совпадение. Он делает первое совпадение.

Регулярные выражения (path_reg) и перекрытие условий

Теперь более интересный пример — versioned API.

acl is_v2 path_reg ^/api/v2/

use_backend api_v2 if is_v2

acl is_api path_beg /api

use_backend api_v1 if is_api

Запрос:

GET /api/v2/users

Совпадает:

  • с path_reg ^/api/v2/
  • с path_beg /api

Но порядок решает. Результат:

/api/v2/users → api_v2

Теперь поменяем строки:

acl is_api path_beg /api

use_backend api_v1 if is_api

acl is_v2 path_reg ^/api/v2/

use_backend api_v2 if is_v2

Теперь:

/api/v2/users → api_v1

Регулярка вообще не будет обработана. Что здесь важно:

  • path_reg не имеет приоритета над path_beg
  • более сложное условие не значит более приоритетное

Он просто идёт сверху вниз.

http-request deny — прерывание пайплайна

Многие думают, что deny — это просто фильтр перед backend. На самом деле это завершение обработки.

acl is_internal path_beg /internal

http-request deny if is_internal

use_backend web

Запрос:

GET /internal/status

Что происходит:

  1. ACL совпадает
  2. http-request deny возвращает 403
  3. use_backend web даже не рассматривается

Никакого может дальше посмотрим — нет. Это не условная ветка — это жёсткий stop.

Комбинация ACL: когда логика начинает ломаться

Более реальный пример:

acl is_api path_beg /api

acl is_admin path_beg /api/admin

acl is_auth path_beg /api/auth

use_backend auth if is_auth

use_backend admin if is_admin

use_backend api if is_api

Запрос:

/api/admin/login

Совпадает с:

  • /api
  • /api/admin

Но не совпадает с /api/auth.

Результат:

→ admin

Если же случайно переставить строки:

use_backend api if is_api

use_backend admin if is_admin

use_backend auth if is_auth

Теперь:

/api/admin/login → api

И это очень легко пропустить при рефакторинге конфига.

Maps vs ACL: когда ACL уже не хватает

-8

ACL отлично подходят для описания логики, но они плохо масштабируются,

когда данных становится много. Десятки доменов, сотни путей или длинные

списки значений быстро превращают конфиг в нечитаемую простыню. В этот

момент стоит остановиться и задать вопрос: а это точно задача для ACL?

Для таких случаев в HAProxy есть maps. По сути, это таблицы соответствий: входное значение → результат.

Вместо множества однотипных ACL используется один map-файл, а конфиг

становится короче и очевиднее. Пример routing по Host с использованием

maps:

api.example.com be_api

admin.example.com be_admin

static.example.com be_static

Маршрутизация:

use_backend %[req.hdr(host),lower,map(/etc/haproxy/hosts.map,be_default)]

Здесь берутся значение заголовка Host, нормализует его и ищет соответствие в map-файле. Если ключ найден — используется указанный backend. Если нет — применяется be_default. Добавление нового домена теперь не требует правки основного конфига и перезапуска всей логики routing.

Еще ситуация: один домен, но разные сервисы живут под разными префиксами.

/api/ be_api

/admin/ be_admin

/static/ be_static

Маршрутизация:

use_backend %[path,map_beg(/etc/haproxy/paths.map)]

Здесь используется map_beg, который ищет совпадение по началу строки. Это удобнее, чем писать десятки ACL вида:

acl is_api path_beg /api/

acl is_admin path_beg /admin/

Или routing по кастомному header

(например, X-Tenant-ID). В multi-tenant системах часто требуется

направлять трафик в зависимости от tenant’а. Делать это через ACL с

длинным списком значений — сомнительное удовольствие.

tenant-a be_tenant_a

tenant-b be_tenant_b

tenant-c be_tenant_c

Маршрутизация:

use_backend %[req.hdr(X-Tenant-ID),lower,map(/etc/haproxy/tenants.map)]

Теперь onboarding нового клиента — это добавление одной строки в tenants.map.В условиях CI/CD это особенно приятно: изменения локализованы, дифф читаемый, риск задеть соседний сервис минимальный.

Maps особенно хорошо подходят для routing по host, path и различным

идентификаторам. ACL в таком случае остаются как обвязка: проверить

наличие ключа, отфильтровать нестандартные случаи или обработать

дефолтный сценарий.

Дополнительный плюс maps — управляемость.

Добавление нового домена или пути сводится к правке map-файла без

переписывания логики routing. Это уменьшает риск ошибок и делает

изменения заметно безопаснее при reload-ах.

Практическое правило

здесь простое: ACL — для логики, maps — для данных. Если ACL начинает

напоминать справочник, значит инструмент используется не по назначению.

HAProxy в этом плане честен: он даёт оба механизма, но не пытается

скрыть, где какой подходит лучше.

Заключение

HAProxy часто воспринимают как простой и надёжный

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

простотой скрывается довольно строгая и хорошо продуманная модель

принятия решений. Режим работы определяет, какие данные доступны, ACL

формализуют условия, а routing оказывается не самостоятельной сущностью,

а прямым следствием правильно выстроенного пайплайна обработки.

Если свести всё к нескольким тезисам:

  • режим определяет, какие данные HAProxy вообще видит
  • ACL — это условия, а не действия
  • routing — следствие порядка обработки, а не отдельный механизм
  • читаемый и продуманный конфиг всегда работает предсказуемо

Практика показывает, что большинство проблем с routing-ом упираются не в

сложность инструмента, а в отсутствие чёткой модели в голове: где именно

принимаются решения, в каком порядке обрабатываются правила и какие

данные доступны в конкретный момент времени. Как только эти три вещи

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

Рецепт в итоге довольно прагматичный: выбирайте режим осознанно, используйте ACL для логики, maps — для данных, держите routing во frontend-ах и не бойтесь делать конфигурацию чуть длиннее, если она от этого становится понятнее. HAProxy хорошо масштабируется не только по нагрузке, но и по сложности — при условии, что его используют как инструмент, а не как набор случайных приёмов.