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_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 и не понимать, почему они не влияют на результат.

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

  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 уже не хватает

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