
HAProxy часто появляется в инфраструктуре незаметно. Сначала это просто балансировщик: принял трафик, отправил дальше — всё понятно. Потом появляется второй сервис, третий, routing по домену, path, заголовкам, SNI, а заодно canary и временные исключения. В этот момент почти всегда всплывают ACL. Кто-то использует их осознанно, кто-то — по принципу: нашёл в примере, вроде работает. Рядом с ACL неизбежно стоит mode: tcp или http. Снаружи это выглядит как простая настройка, но на деле — фундаментальное решение, от которого зависит, какие данные HAProxy вообще видит и какие условия способен проверить.
HAProxy последователен и выполняет конфигурацию ровно так, как она написана. Отсюда и классика жанра: ACL есть, но backend не выбирается, mode вроде http, но заголовки недоступны, routing работает почти всегда, кроме пятницы.
В этой статье мы разберём, как HAProxy принимает решения:
чем на самом деле отличаются
mode tcpиmode httpи где каждый из них уместенкак устроены ACL, из чего они состоят и какие данные могут использовать
где именно применяется routing и в каком порядке обрабатываются правила
какие сценарии можно (и нужно) решать ACL, а где лучше использовать maps
и какие ошибки чаще всего делают даже опытные инженеры
Цель простая: после прочтения конфиги HAProxy должны перестать выглядеть как набор случайных условий, а routing — как удачное совпадение. Всё будет предсказуемо, читаемо и, что особенно важно, объяснимо коллеге без фразы: не трогай, оно работает. Дальше начнём с базы — разберёмся, где именно в HAProxy принимаются решения и почему это важно понимать до того, как писать первую ACL.
Архитектура HAProxy: где вообще принимаются решения

Чтобы понимать, как работает 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

Разговор про 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

После того как мы договорились, что 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_regmethodhdr(name)querycook(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_success
Или применяем 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: состояние запроса

Переменные — это способ протащить состояние и данные запроса через разные стадии обработки, не изобретая велосипед с костылями из 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 вас простит, но коллеги могут и не простить.
Порядок обработки и приоритеты

Одна из особенностей — это строгий порядок обработки правил. Он несложный, но если его игнорировать, можно часами смотреть на корректные ACL и не понимать, почему они не влияют на результат.
В упрощённом виде процесс выглядит так:
HAProxy принимает соединение
Парсит запрос (в рамках режима)
Применяет правила обработки (
http-requestилиtcp-request)И только после этого выбирает backend с помощью
use_backendЕсли ни одно условие не сработало — используется
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
Что происходит:
ACL совпадает
http-request denyвозвращает 403use_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 уже не хватает

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 хорошо масштабируется не только по нагрузке, но и по сложности — при условии, что его используют как инструмент, а не как набор случайных приёмов.
