Задача:Организовать автоматическое:
- получение SSL (Let's Encrypt)
- обновление HAProxy maps
- без остановки HAProxy
- без webroot
- без ломания routing
---
📌 Архитектура:Internet
↓
:80 (HAProxy)
↓
ACME path → 127.0.0.1:8888 (certbot standalone)
↓
остальной трафик → maps → backend
---
🔧 1. HAProxy (ACME routing)В frontend на 80:
frontend http_80
bind *:80
mode http
acl letsencrypt-acl path_beg /.well-known/acme-challenge/
use_backend letsencrypt-backend if letsencrypt-acl
http-request redirect scheme https code 301 if !letsencrypt-acl
Backend:
backend letsencrypt-backend
mode http
server letsencrypt 127.0.0.1:8888
---
⚙️ 2. Скрипт получения сертификата/usr/local/bin/haproxy-cert.sh
#!/bin/bash
set -euo pipefail
DOMAIN=${1:-}
if [ -z "$DOMAIN" ]; then
echo "[CERT] no domain!"
exit 1
fi
echo "[CERT] Issuing certificate for $DOMAIN"
certbot certonly --standalone \
--http-01-port=8888 \
-d "$DOMAIN" \
--non-interactive \
--agree-tos \
--email george@cloud-life.site \
--expand
echo "[CERT] Building PEM"
cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
/etc/letsencrypt/live/$DOMAIN/privkey.pem \
> /etc/ssl/haproxy/$DOMAIN.pem
echo "[CERT] Done"
Важно:- HAProxy НЕ останавливается
- порт 80 остаётся занят HAProxy
- certbot работает через 8888
---
⚙️ 3. Основной скрипт управления maps/usr/local/bin/haproxy-map-update.sh
#!/bin/bash
GREEN="\033[0;32m"
NC="\033[0m"
RED="\033[0;31m"
usage() {
echo -e "${GREEN}"
echo "Usage:"
echo " $0 set DOMAIN BACKEND MODE"
echo " $0 del DOMAIN"
echo ""
echo "Modes:"
echo " http - обычный HTTP (hosts.map)"
echo " passth - TLS passthrough (passth.map + acme_backends.map)"
echo -e "${NC}"
exit 1
}
if [ $# -lt 2 ]; then
echo -e "${RED}[ERROR] Invalid arguments${NC}"
usage
fi
ACTION=$1
DOMAIN=$2
BACKEND=${3:-}
MODE=${4:-}
if [ "$ACTION" == "set" ]; then
if [ -z "$BACKEND" ] || [ -z "$MODE" ]; then
echo -e "${RED}[ERROR] Invalid arguments${NC}"
usage
fi
fi
set -euo pipefail
HOSTS_MAP="/etc/haproxy/maps/hosts.map"
PASSTH_MAP="/etc/haproxy/maps/passth.map"
ACME_MAP="/etc/haproxy/maps/acme_backends.map"
ACME_BACKEND="acme_${BACKEND}"
issue_cert() {
echo "[CERT] Ensuring certificate for $DOMAIN"
if /usr/local/bin/haproxy-cert.sh "$DOMAIN"; then
echo -e "${GREEN}[CERT] OK${NC}"
else
echo -e "${RED}[CERT] FAILED → aborting${NC}"
exit 1
fi
}
update_file() {
local MAP=$1
local KEY=$2
local VALUE=$3
TMP=$(mktemp)
[ -f "$MAP" ] && cp "$MAP" "$TMP"
grep -v "^$KEY " "$TMP" > "$TMP.new" || true
if [ "$ACTION" == "set" ]; then
echo "$KEY $VALUE" >> "$TMP.new"
fi
mv "$TMP.new" "$MAP"
rm -f "$TMP"
}
delete_from_all() {
update_file "$HOSTS_MAP" "$DOMAIN" ""
update_file "$PASSTH_MAP" "$DOMAIN" ""
update_file "$ACME_MAP" "$DOMAIN" ""
}
engine() {
for MAP in "$HOSTS_MAP" "$PASSTH_MAP" "$ACME_MAP"; do
if [ -f "$MAP" ]; then
awk '{ print length($1), $0 }' "$MAP" \
| sort -nr \
| cut -d" " -f2- \
> "$MAP.tmp"
mv "$MAP.tmp" "$MAP"
fi
done
}
echo "[MAP] Processing: $DOMAIN"
# 🔥 1. СНАЧАЛА сертификат
if [ "$ACTION" == "set" ]; then
issue_cert
fi
# 🔥 2. потом routing
delete_from_all
if [ "$ACTION" == "set" ]; then
if [ "$MODE" == "http" ]; then
update_file "$HOSTS_MAP" "$DOMAIN" "$BACKEND"
elif [ "$MODE" == "passth" ]; then
update_file "$PASSTH_MAP" "$DOMAIN" "$BACKEND"
update_file "$ACME_MAP" "$DOMAIN" "$ACME_BACKEND"
else
echo "Mode must be: http | passth"
exit 1
fi
fi
echo "[MAP] Sorting maps"
engine
echo "[MAP] Reloading HAProxy"
systemctl reload haproxy
echo -e "${GREEN}[MAP] DONE${NC}"
---
🚀 Использование:# HTTP сайт
haproxy-map-update.sh set site.com backend_name http
# TLS passthrough
haproxy-map-update.sh set site.com backend_name passth
# удалить
haproxy-map-update.sh del site.com
---
💡 Важные моменты:- Certbot вызывается ДО изменения maps
- Это критично (иначе будет 503)
- ACME идёт через HAProxy → 8888
- HAProxy не перезапускается дважды
- maps сортируются по длине (чтобы поддомены не ломались)
---
🏁 Результат:- Никаких 503 при выпуске сертификатов
- Нет остановки HAProxy
- Полностью автоматизированный onboarding доменов
- Production-ready схема