HA в домашнем самохостинге на примере reverse proxy и не только

Что-то в последнее время затихли обсуждения (видимо, все в отпусках), ну что-же добавлю еще тему без обсуждения, хотя, очень хотелось бы почитать мысли саомохостят по поводу кластеризации и обеспечения высокой доступности

Вариантов тут несколько

  1. K8S и аналоги
  2. Костыли и страдания

Покажу свой вариант на примере реализации простого 4. 4. reverse proxy (данный пост является результатом предыдущих моих постов и изысканий)

Итак, дано:

  1. 3 ноды Proxmox VE на основе мини ПК (у меня их больше, но для простоты будет 3)
  2. 1 Роутер для выхода в интернет и публикации сервисов в оном через DNAT
  3. Ресурсы на то, чтобы обеспечить высокую доступность и отказоустойчивость критически важных сервисов, таких как
    3.1. Reverse proxy
    3.2. Секреты
  4. Отсутствие ресурсов на то, чтобы обеспечить полное дублирование всех сервисов хомлабы
  5. Разворачивание сервисов в LXC контейнерах внутри PVE

За рамками данного описания поместим Proxmox HA, но его можно тоже обсудить

К чему я пришел, минимальная версия

Остановимся подробнее на компонентах

  1. На 3 ноде PVE развернут Komodo (почитать про него можно тут, на форуме) и в каждом LXC контейнере развернут komodo periphery, осуществляющий связь самого komodo с докером в LXC контейнерах - это надо для автоматического деплоя сервисов.
  2. На 3 ноде PVE развернут Fogejo сервер, которй хранит
    2.1. Конфигурацию сервисов в виде git репозиторя с docker-compose.yml и конфигами
    2.2. Собственные docker образы для разворачивания в лабе
    2.3. CI/CD для деплоя всего этого
    2.4. Конфигурацию traefik (CI/CD пайплайн загружает конфиги в consul)
  3. На каждой ноде PVE развернут OpenBao - открытый форк HashiCorp Vault для хранения секретов - тут мы имеем отказоустойчивый кластер
  4. На 1 Ноде PVE есть сервис Certbot, который занимается выпуском и обновлением сертификатов через Letsencrypt и Cloudflare DNS Challenge - он выпускает сертификаты и складывает их в openbao для дальнейшего переиспользования. Только он занимается выпуском сертификатов, а остальные потребители их только запрашвают
  5. На каждой PVE ноде развернут Traefik Node, который состоит из
    5.1. Vault agent - он подключается к кластеру OpenBao и вытягивает секреты и tls сертификаты. При обновлении сертификата агент отправляет traefik команду перечитать конфиги и подтягуть новый сертификат
    5.2. Нода кластера consul - это еще одно KV хранилище от HashCorp, по сути, распределенная БД, где каждая нода хранит в себе весь набор данных
    5.3. Сам traefik, который получает секреты и сертификаты из openbao, а конфигурацию роутеров, миделваров и сервисов из consul
    5.4. Authentik Autentication Proxy Outpost - сервис authentication proxy, для приложений, которые не поддерживают собственную аутентификацию (или поддерживают, но простую), например Pulse, Traefik Dashboard, Uptime Kuma Traefik может реализовать собственную аутентификацию в Authentik с перенаправлением в Authentik для аутентификации и проксированием в приложение только если есть валидная кука. В самом Authentik есть встроенный Embeded Proxy Outpost (в зависимости от разворачивания), но я развернул по инстансу в каждой ноде и Authetik Proxy ходит в Authentik только за конфигурацией и токенами
    5.5. VRRP демона (точнее kepalived демона, если быть точным), который отслеживает доступность трафика на своей ноде, связывается с другими нодами для определения мастера. На выходе мы имеем один виртуальный IP адрес, который назначается одной из нод VRRP
  6. Роутер входящий запрос направляет на VRRP адрес, который обслуживается одним их traefik node.

Зелеными стрелками показан путь HTTPS трафика:

  1. Роутер направляет входящий трафик по 80 и 443 порту на VRRP адрес
  2. На VRRP адресе висит один из traefik, который имеет в себе полную конфигурацию проксирования, он терминирует TLS и направляет запрос на конкретный сервис

Внутри локалки ситуалция похожая:

  1. ПО DNS запрос направляется на ближайший VRRP Traefik адрес
  2. Traefik нода направляет запрос на свой сервис
    Тут у меня пока в ручном режиме происходит привязывания трафика к сервису на своей ноде, но я в процессе делегирования этого netbox

Синими стрелками показаны связи внутри кластера

Что в итоге имеем (без учета кворума, но я тоже кое-что реализовал за рамками данной статьи):

  1. При наличии хотя бы одного включенного мини ПК с запущенным на нем traefik и openbao у нас имеется локальный IP адрес, на котором работает reverse proxy
  2. Поскольку IP адрес один, то на роутере легко оккрыть порст с пробросом трафика на этот IP
  3. На уровне DNS и VRRP сервис service.domain.com в локальной сети обслуживается или traefik на этой же PVE Node (для минимизации пересылки трафика по сети) или любой другой, доступной в сети
  4. Сервис certbot занимается централизованным выпуском и обновлением сертификатов, поэтому, я не сталкиваюсь с лимитами по получению сертификатов от certbot, а потребитель сертификата получает его сразу же при помощи vault-agent
  5. Обновление конфигурации роутинга трафика происходит путем правки git репозитория, который потом при помощи Ci/CD загружается в consul кластер, а их consul кластера разносится уже в traefik с автоматическим подтягиванием изменений
  6. Часть сервисов, таких, как komodo, certbot, forgejo, либо не отказоустойчивые, либо отказоустойчивость организована другими методами (например proxmox HA)
  7. Балансировка трафика частично реализована средствами traefik, например, consul и openbao в service указаны внутренние адреса всех нод, соостветственно, по адресу https://openbao.domain.com отвечает любая из доступных traefik нод (не любая, а одна из доступных в соответствии с приоритезацией), которая направляет запрос на любую из доступных нод openbao
  8. На каждой Traefik ноде развернут еще и Authentik Authentication Proxy, который занимается фильтрацией трафика для сервисов, не поддерживающих собственную аутентификацию и в “основной” Authentik ходят только за конфигурацией
  9. Traefik и OpenBao ноды, развернутые на каждой PVE ноде потребляют 400-500МБ оперативки в сумме + 1-3% CPU
  10. Для провижининга новых нод испольузется Ansible, для раскатки сервисов используется Komodo

А как вы реализуете отказоустойчивость в своих домашних лабах?

3 лайка

а как работает вррп? на проксе стоит какой-то софт? какой у тебя роутер?

Софт поднимает виртуальный /32 адрес на интерфейсе и обменивается широковещательными пакетами по сети, хост с максимальным значением веса становится мастером и активирует свой адрес, остальные помечают как бэкап, как только мастер перестает отвечать в течение секунды-двух - запускается процесс перевыбора и мастером становится хосттс максимальным весом, а при равных с максимальным значением мака основного интерфейса.
В Линукс keealived демон умеет скрипт проверки, который при успехе повышает вес, а при ошибке понижает, у меня там для traefik что-то типа curl http://localhost:8080/ping

Можно указать несколько таких адресов, например узлы имеют ip 10.110.0.120, .125, .128
А vrrp, адреса 10.110.0.80, .81, .82, соответственно, у каждой своей ноды самый высокий вес, соответственно, если трафикк поднят на 1 годе, то она забирает себе .80 адрес, если она не отвечает, то любая другая забирает себе этот адрес

Аналогично с DNS: роутер имеет IP 10.110.0.1, Adguard по dhcp получил условный 10.110.0.230, у Adguard upstream DNS 10.110.0.1 роутера, на роутере 3 меня upstream сервера провайдера, Яндекса, гугла и cloudflare по приоритету + зона .lan + переопределение сервисов публичного домена

На Adguard правила фильтров + кастомные правила
На Adguard keealived со скриптом, который вызывает что-то типа nslookup dns.lan 127.0.0.1 (сейчас не за компом, не помню точную команду) и если все ок вешается на IP 10.110.0.53 (найдите зависимость) и отвечает, на роутере тоже vrrp и если Adguard не в сети, то сам роутер отвечат по 10.110.0.53. Далее, роутер по DHCP отдает DNS сервер 10.110.0.53, а шлюз по-умолчанию 10.110.0.1 у итоге клиенты всегда имеют рабочий DNS сервер, но в нормальном состояни ещё и режется реклама.

Сам VRRP разрабатывался под роутеры (как раз следует из названия) в качестве дешёвой и открытой реализации файловера: ставится 2 железки с IP x.x.x.2 и x.x.x.3 и по VRRP они делят x.x.x.1 который отдается клиентам. В итоге при выходе основной железки вторая в течение секунды вступает в работу (есть проприетарные расширения по обмену NAT таблиц между роутерами чтобы было вообще бесшовно т.к. без этого клиенты в локалке потеряют сессию и переустановят новую)

Роутер у меня mikrotik rb4011igs+5hacq2hnd-in

Ещё из фишек роутера у него на vrrp можно навешивать скрипты и при смене статуса он отправляет в MQTT топик сообщение

traefik-pve-01.lan имеет IP 10.110.0.120 (dhcp)
pve-01.traefik.lan имеет IP 10.110.0.80 (vrrp)

traefik-pve-02.lan имеет IP 10.110.0.121 (dhcp)
pve-02.traefik.lan имеет IP 10.110.0.81 (vrrp)

traefik-pve-03.lan имеет IP 10.110.0.123 (dhcp)
pve-03.traefik.lan имеет IP 10.110.0.82 (vrrp)

DNS cloudflare
*.domain.com A 1.2.3.4

На роутере входящий трафик на 1.2.3.4 порты 80 и 443 направляются на 10.210.0.80

На роутере static DNS (для локальных пользователей)

service1.domain.com CNAME pve-01.traefik.lan
service2.domain.com CNAME pve-02.traefik.lan

traefik из consul получает конфигурацию

service1.domain.com → service1.lan:8080
service2.domain.com → service2.lan:3000

Соответственно через интернет трафик идёт на самую мощную доступную ноду трафика а с нее уже на нужный сервис

В локалке по этому же адресу идёт обращение на traefik, который расположен на том же сервере, что и сервис

Если traefik-pve-01.lan недоступен, то адрес pve-01.traefik.lan поднимает traefik-02.lan и далее по списку

1 лайк

Вчера таки добрался до GPT и переписывания скрипта загрузки правил на python, теперь это выглядит так

$ ./deploy-consul.py
📤 Загружаем конфигурацию из '.' в Consul 'http://10.110.0.121:8500'
➕ Добавлен ключ: traefik/http/services/palmr-service/loadBalancer/servers/0/url → http://palmr.lan:80
➕ Добавлен ключ: traefik/http/routers/palmr-router/rule → Host(`share.domain.com`)
➕ Добавлен ключ: traefik/http/routers/palmr-router/entryPoints/0 → websecure
➕ Добавлен ключ: traefik/http/routers/palmr-router/service → palmr-service
✅ Конфигурация успешно синхронизирована.
➕ Добавлено ключей: 4
♻️ Обновлено ключей: 0
🗑️ Удалено ключей: 0
⏭️ Пропущено ключей: 480

Скрипт запусается или руками для тестирвания (что не очень правильно) или в плайплайне CI/CD

Добавление нового сервиса у меня происходит так
demo2

  1. Прописал домен
  2. На основе домена сгенерировалось имя роута и сервиса
  3. На основе домена подставилось локальное имя lxc контейнера, я пользую servicename.domain.com обрабатывается контейнером servicename с локальным DNS именем servicename.lan
  4. А вот порт 80 не стал использовать по-умолчанию, а задал свой 8080
  5. Далее добавил мидллварку, в данном случае это разрешение доступа только из локальных сетей (локалка и VPN)
  6. После показывает все параметры и если согласен с ними, то жмешь enter
  7. Далее предлагает добавить закоммитить сгенерированные файлы в git репозиторий
  8. Далее предлагает добавить еще один домен, но тут действие по-умолчанию уже выход

git log имеет следующий коммит

commit aaf096e2dbf061ff427f91b3127f711549ba2607 (HEAD -> main)
Author: Roman Kudlay <roman@kudlay.pro>
Date:   Thu Oct 2 15:17:42 2025 +0300

    Added new domain http://test.demo.com with downstream http://test.lan:8080

Далее, git push отправляет правки в обработку в forgejo (как это выглядит показано выше)

1 лайк

А у тебя получается openbao наружу торчит? или чисто внутренние адреса? Тогда как сертификаты сделал? Или самоподписные?

Наружу не торчит

У меня 2 сценария

  1. LetsEncript
    1.1. CertBot при помощи DNS Сhallenge получает сертификат и загружает его в OpenBao. Я использую wildcard сертификаты domain.com + *.domain.com
    1.2. Vault Agent видит, что обновился ключ сертификата и выгружает его в файл, при необходимости дергает сервис для того, чтобы он подтянул конфиг c сертификатом новым
    1.3. Certbot при выпуске или перевыпуске сертификата дергает вебхук Semaphore UI
    1.4. Semaphore UI запускает Ansible playbook с указанием домена и в таске происходит деплой сертификата в микотик и HomeAssistant
  2. Self Signed Certs
    2.1. В OpenBao выпускается корневой сертификат, который надо импортировать на все узлы
    2.2. В OpenBao можно при помощи веб интерфейса или клиента сгенерировать сертификаты под произвольный домен, с подписью CA openbao. При этом, можно клиенту через политики разрешить определенные домены или опции
    2.3. В OpenBao включен Acme сервер и сервисы могут выпускать себе сертификаты при помощи CertBot

Подробнее тут описывал Централизованный выпуск SSL сертификатов для смохостинга

С точки зрения Traefik у меня получается, что

  1. Основная информация по конфигурации находится в гите и деплоится в docker-compose
  2. Динамическая информация самому реверс-проксированию находится в consul кластере и traefik ее подтягивает при обновлении
  3. Сертификаты подкладывает vault agent в ввиде файлов и он их просто использует без выпуска

Таким образом я легко могу добавлять новые узлы: появился миник и за пару кликов туда разворачивается собственный экземпляр трафика со всеми примочками

В первой версии конфигурации я пробовал хранить сертификаты в consul, но в процессе выяснил, что в новой версии этот функционал убрали т.к. были проблемы с синхронизацией: при появлении нового домена каждый traefik пытался выпустить свой сертификат и положить его в общее хранилище, в итоге они отказались от этого и каждый traefik выпускает и сохраняет сертификаты самостоятельно, что может привести к выходу за лимиты.

Поэтому я и реализовал схему, когда выпуском сертификатов занимается один сервис, а потребители их подтягивают при необходимости.

1 лайк

Посмотрел 5 пункт про централизованный выпуск сертификатов:
5. Поскольку openbao требует тоже сертификаты (в production режиме), то собрал отдельный контейнер openbao-docker
5.1. На основе openbao + docker-cli + пользователь и права для доступа к docker.sock
5.2. Рядом с openbao поднял vault-agent, который подписывается на сертификаты в openbao и стягивает их в файлы, если сертификат обновлен, то вызывает docker exec openbao kill -HUP 1 что приводит к перечитыванию сертификатов самими openbao

Т.е. у тебя для openbao используется lxc с докером, где в докере подняты openbao + vault agent и некоторая обвязка которая при обновлении сертификата агентом триггерит openbao?
Т.е. технически получается openbao подтягивает сертификаты сам через себя? А как тогда первый запуск выглядел? Я просто пытаюсь это как-то осознать, но “шарики за ролики заезжают” когда пытаюсь всё в кучу свести, хотя вроде и не рокет сайнс…

В общем ищу с чего начать подобный Dev-git/ops подход…
P.S. Я же правильно понимаю что если выпускать не wildcard сертификат а чисто на конкретный домен/поддомен то надо что бы на этот же домен хотя бы заглушка какая-то торчала?

Да, vault-agent имеет такой конфиг

template {
  source      = "/templates/cert.tpl"
  destination = "/certs/cert.crt"
  command     = "docker exec openbao kill -HUP 1"
}

Как сказал бы ChatGPT: “Да, ты совершенно прав, мы получаем классическую проблему курицы и яйца”

Вот тут загадка дыры на самом деле, я пока не нашел красивого решения.
Первый раз я подложил руками сертификат, который получил из certbot, это надо было, чтобы запустить openbao еще до того, как у меня появился certbot сервис, далее, перевыпуск уже происходит автоматически (тем более, что пока тестировал все и так несколько раз перевыпустил сертификаты)

Варианта тут 2 на самом деле

  1. Такой вот запуск “с толкача” на каждой новой ноде (пока у меня так для стабильности, но может перейду на вариант после мелкой настройки кластера)
  2. Делается 1 раз для первой ноды кластера, далее при добавлении новой ноды vault-agent подлкючается к другому узлу openbao в кластере, вытягивает из него сертификат и подсовывает своему openbao инстансу, после чего новы инстанс openbao добавляется в кластер и скачивает свой экземпляр данных

У меня openbao балансируется через traefik, но traefik тоже требует сертификаты, поэтому хочу добавит. Но каждый инстанс openbao тоже доступен напрямую, поэтому я думаю еще добавить VRRP для openbao, чтобы можно было по единому адресу подключиться к любой из работающих нод, а на этот адрес уже натравить vault-agent, который находится рядом с openbao узлом, таким образом убирается зависимость от traefik.

Ну и как минус так и плюс подхода с vault-agent и файлами в том, что эти файлы не удаляются при остановке сервиса. Это значит, что при следующем старте openbao получит сертификат, который был сохранен на диске, а если после запуска агента окажется, что локальный файл устарел, то vault-agent его обновит и сообщит об этом openbao

Единственный минус тут, как по мне, заключается в том, что если используются LE сертификаты, а не самоподписные (а тут параноики скажут, что в этой схеме свой CA просто обязан быть), то данная схема не запустится в случае протухания сертификата, но тут есть 2 варианта

  1. Временно отключить проверку сертификата определенных точках для запуска
  2. На одном из openbao узлов запустить cerbot для получения и подкладывания сертификата, чтобы запустить кластер, а потом система самостоятельно запустится.
  3. Если не планируете использовать кластер из openbao и хотите LE, то проще поднять certbot рядом с openbao.

Нет, при dns challenge должен быть просто валидный домен и доступ к API DNS провайдера, тут на форуме несколько раз подробно обсуждалось это.

Если не пользовать wildcard сертификаты и кластеризацию, то данная схема выглядит совсем как rocket science

У меня сейчас на 2х доменах с поддоменами traefik выглядит так (большая часть поддоменов одного домена)

Ну т.е. только на одном трафике примерно 85 доменов (по количеству сервисов) + еще openbao, PVE, truenas, jellyfin, forgejo, Home Assistant, authentik …
Получается, что при наличии 5 traefik узлов с автономным получением сертификатов я получу порядка 430-440 сертификатов, если их запрашивать одновременно,то гарантированно получу лимиты LE, а так их у меня 3 в certbot+openbao + и еще несколько сервисов получают их самостоятельно.

1 лайк