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