
В январе 2023 мне пришла в голову идея: а почему бы не управлять своими серверами так же, как я управляю своими проектами — через docker compose up.
Довольно быстро стало понятно, что до меня в эту сторону массово не ходили, если кто так уже делает, то делает это молча… т.е. на все детские грабли на этом пути мне придётся наступить лично.
А вот вам повезло: если тоже захотите пойти в эту сторону, то у вас уже есть и эта статья и пример конкретной реализации.
Кому этот подход может подойти? Тут должны сойтись звёзды несколько факторов:
Нужно быть программистом, хорошо знакомым с
docker-compose.yml.Нужно иметь 1-5 личных серверов — не важно, дома или на обычном/облачном хостинге, настоящий ли это сервер или свой десктоп/ноут, выполняющий заодно и функции «сервера».
Нужно иметь достаточно опыта настройки этих серверов вручную по ситуации, чтобы появилось понимание недостатков этого подхода и желание внедрить IaC (перенести конфигурацию серверов в git и сделать её легко воспроизводимой).
Но главное — нужно не быть админом, которому Ansible привычнее. ?
Что касается IaC, то код проектов мы тоже когда-то давно писали без git, нередко меняя его прямо на сервере, и хорошо если через scp, а не в текстовом редакторе прямо на сервере.
А потом стало очевидно, что это плохая идея, и git действительно нужен даже для личных проектов.
Не буду долго агитировать за IaC, просто спрошу: вот у вас дома «сервер», и там винт умер — вы же сможете легко и быстро настроить новый сервер с нуля так же, как был настроен старый? Если ответ "да", то дальше можете не читать, у вас уже всё хорошо!
Лично меня в этом подходе привлекла его крайняя простота: всё, что для него нужно (помимо Docker Compose) — это тривиальный шелл-скрипт (который будет rsync-ом заливать на сервер каталог с docker-compose.yml, после чего выполнять на сервере через ssh docker compose up) и какой-то способ держать зашифрованные секреты в git.
Основную сложность — приведение сервера в заданное состояние из любого текущего состояния (то, ради чего обычно используют Ansible) — берёт на себя Docker Compose.
Ну, почти — нужно будет ему немного с этим делом помочь, но всё получится!
Сначала я использовал один шелл-скрипт на 20 строк плюс git-secret.
Но недавно добавил mise (исключительно для удобства), и перешёл с git-secret на fnox (для безопасного использования AI-агентов, чтобы они не получили случайно доступ к секрету, просто прочитав файл).
Конкретный пример того, как выглядит полноценная реализация этого подхода, вы можете увидеть в https://github.com/powerman/myservers-template. Это урезанный вариант моей текущей конфигурации, почищенный от личных данных и готовый для использования в качестве шаблона для создания вашего репо. Он содержит пример достаточно сложной настройки (primary и secondary сервера для собственных DNS и email сервисов, VPN для доступа в домашнюю локалку, полноценный мониторинг Netdata с алертами в телеграм). Свои DNS и email вряд ли нужны многим, но вот файрвол/мониторинг/VPN вполне могут пригодиться и для ваших серверов.
Плюс там ещё пример того, как можно вести документацию по такой инфраструктуре, с диаграммой.
Это именно пример как такое настраивается, а не готовый к выкату на ваши сервера проект!
А теперь о собранных граблях — какие проблемы не решил Docker Compose и их пришлось решать мне:
Начальная настройка сервера — тот же docker нужно на сервер как-то установить. Я для этой цели завёл
bootstrap.sh— скрипт, который нужно однократно выполнить на новом сервере (можно передать его как cloud-init user-data script в админке облачного хостинга или запустить вручную). Я старался сделать его как можно меньше, чтобы минимизировать необходимость его изменений в будущем — потому что его изменение эквивалентно необходимости снова настраивать сервер вручную, да ещё и не забывать дублировать эти ручные изменения вbootstrap.sh. Но всё равно нашлось целых две причины его менять: обновление на следующий релиз OS (напр. черезdo-release-upgradeв Ubuntu), и необходимость обновить установленные для удобства моей работы в консоли сервера инструменты (напр. когда я перешёл с Vim на Neovim и мне на всех серверах понадобился мой конфиг в/root/.config/nvim).Идемпотентная настройка файрвола. Учитывая, как докер сам изменяет настройки файрвола, понадобилось придумать подход, состоящий из начальных правил
/etc/nftables.conf(создаваемыхbootstrap.sh) и определённой структуры файла миграции этих правилmigrate.nft(выполняемого внутри контейнера приdocker compose up), позволяющий не затрагивать текущие правила докера и при этом вносить любые необходимые изменения.Идемпотентность для контейнера WireGuard. Учитывая, что WireGuard создаёт сетевой интерфейс через ядро, при отключении контейнера необходимо этот сетевой интерфейс удалить.
Письма от сервисов OS (ошибки systemd, unattended-upgrades, etc.). Поскольку у меня на серверах всё равно используется postfix, то решил через bind-mount
/var/spool/postfixхоста в контейнер postfix, чтобы очередь писем у них стала общая и postfix внутри контейнера доставлял в т.ч. и письма от системных сервисов.Специфика конкретных серверов/OS. Пришлось для каждого сервера написать небольшой скрипт
host-facts.sh, который создаёт переменные окружения, содержащие эту специфику (названия сетевых интерфейсов, IP адреса, UID/GID системных аккаунтов, etc.), которые дальше используются вdocker-compose.yml.HEALTHCHECKкритичен и необходим — иначе при деплое не поймёшь, что сервис не запустился. Многие образы докера из коробки идут безHEALTHCHECK— приходится добавлять самому.Обновления. Да, для пакетов OS обновления ставит unattended-upgrades, но при этом подходе в OS хоста практически ничего нет, все сервисы внутри контейнеров и их тоже нужно обновлять. Renovate умеет обновлять и базовые образы (строку
FROMвDockerfile), и образы вdocker-compose.yml, и пакеты Alpine (RUN apk add), и даже сторонние приложения, установленные произвольным способом (вроде postgrey, скачиваемого по URL из GitHub) — через custom managers с regex. Так что придётся использовать именно его. И держать этот проект в приватном репо на гитхабе.
Ещё одна полезная штука — тестирование на CI и локально перед выкатом. Оно, конечно, не обязательно, но… хочется видеть ошибки не когда уже сервис упал при выкате, а чуть раньше.
В общем, я немного накидал в проект форматировщиков/линтеров (благо mise очень упростил их установку и запуск), плюс сделал поддержку локальной сборки образов в host-facts.sh и без необходимости расшифровки реальных секретов, но тут ещё можно много всякого добавить.
Покажу несколько немного упрощённых примеров реализации, для иллюстрации подхода.
Деплой — это rsync каталога сервера на хост + docker compose up, всё в одном скрипте:
rsync --recursive --links --delete "./$proj/" "${destination}:.myserver" ssh -t "${destination}" 'set -euo pipefail cd ~/.myserver docker compose up --build --detach --remove-orphans --wait'
--wait тут критичен: он ждёт, пока все HEALTHCHECK-и не станут healthy (или не упадут).
Вы видите результат прямо в терминале и можете сразу откатиться: git checkout @~ (ну или git stash) + повторный деплой.
На 2-5 личных серверов автоматический деплой не нужен: важнее видеть что происходит глазами, чем экономить минуту автоматизацией.
Плюс резервирование (primary+secondary для DNS/email) покрывает окно неудачного деплоя.
Специфика хоста определяется через host-facts.sh — скрипт, который при деплое создаёт переменные окружения для docker-compose.yml:
uid() { getent passwd "$1" | cut -d: -f3; } gid() { getent group "$1" | cut -d: -f3; } metadata() { curl -s "http://169.254.169.254/metadata/v1$1"; } set -euo pipefail if test -z "${DEVEL:-}"; then WAN_IP="$(metadata /interfaces/public/0/ipv4/address)" UID_POSTFIX="$(uid postfix)" GID_POSTFIX="$(gid postfix)" # ... else WAN_IP=0.0.0.0 UID_POSTFIX=1001 GID_POSTFIX=1001 # ... fi for _var in \ WAN_IP \ UID_POSTFIX \ GID_POSTFIX; do eval "export $_var=\"\$$_var\""; done
Ветка else с фоллбэками нужна для сборки образов локально и на CI — без реального железа.
Структура скрипта подобрана так, чтобы опечатки и пропущенные переменные ловились линтером или шеллом при выполнении, плюс его было очень просто проверить глазами.
Идемпотентный файрвол — отдельная история. Docker создаёт свои правила nftables в family ip и ip6 (отдельно для каждого протокола), поэтому все «свои» правила нужно держать в family inet (который покрывает и IPv4, и IPv6), чтобы не конфликтовать:
flush ruleset inet # Сбрасываем только свои правила. flush chain ip filter DOCKER-USER # Чистим цепочки для docker forward. flush chain ip6 filter DOCKER-USER table inet filter { chain prerouting-before-docker { type filter hook prerouting priority dstnat - 2; # Фильтруем ДО docker NAT — иначе docker dnat делает невозможной # фильтрацию по оригинальному порту назначения. ... } }
Этот migrate.nft рендерится из шаблона через dockerize (подставляя IP-адреса из переменных) и применяется контейнером при каждом docker compose up — полностью идемпотентно.
WireGuard — ещё один нюанс: контейнер создаёт сетевой интерфейс ядра, и при остановке его нужно явно удалить, иначе повторный запуск сломается:
finish() { wg-quick down "$INTERFACE" } trap finish EXIT wg-quick up "$INTERFACE" sleep inf
Для существующих серверов этот подход тоже возможно применять. Да, там не получится чистой настройки «с нуля» с bootstrap.sh, и, скорее всего, не получится сразу перенести вообще всё, но можно переносить сервисы в контейнеры по одному, постепенно — лучше так, чем никак.
Ещё один плюс этого подхода — появляется очевидное место, где удобно документировать свою инфраструктуру.
В общем, подход «docker compose up как IaC» оказался вполне рабочим.
Грабли есть, но они разовые — наступил, решил, зафиксировал в конфиге.
Дальше серверы живут в git, деплой — одна команда, восстановление сервера с нуля занимает минуты, а не бесконечность.
gtosss
Вижу что молча минусуют. Не понимаю людей — почему не написать краткий комментарий, что не так. (я не автор статьи)
Скорее всего минусуют из-за провакционно-корявой КДПВ
baldr
Извольте. Я поставил минус. На мой взгляд, подход вполне распространённый, я сам так иногда делаю. Однако, это, простите, колхоз, и писать об этом статью как о достижении - моветон.
Идемпотентность и удалённый бутстрап - это Chef/Puppet/Salt/Ansible/etc. В зависимости от масштабов. А писать свои скрипты - это несерьёзно.
Хранить секреты в git - я этого вообще не понимаю, ну да бог вам судья, у каждого свой подход.
powerman Автор
Я и не сомневался, что так делают многие, идея вполне очевидная и напрашивающаяся. Проблема, на мой взгляд, как раз в том, что все делают это молча, хотя возникающие в процессе сложности и подходы к их решению могут быть очевидны далеко не всем. Так что статья не "о достижении", а просто чтобы задокументировать этот подход и связанные с ним проблемы/решения.
Ну я вот пытался внедрить Ansible на протяжении многих лет. Не идёт. Слишком сложно для таких масштабов, как у меня. Поэтому заставить себя не просто почитать его доку и немного поиграться, а начать использовать полноценно у меня просто не получалось. А Ansible - это лучшее из всего упомянутого.
Поэтому не вижу ничего плохого в одном тривиальном скрипте, который вполне соответствует масштабу задачи, и позволил мне таки перейти на IaC. Да, пытаться колхозить скрипт для решения этой задачи на тех масштабах, где уже нужно использовать Ansible - дурная идея. Но туда тащить этот подход я и не предлагал.
fnox позволяет держать секреты где угодно. Но, мне просто интересно, а где Вы предлагаете хранить секреты для пары личных серверов? И чем, на Ваш взгляд, мой вариант принципиально отличается от Ansible Vault?
baldr
Для серверов, которые нужно пересоздавать раз в 2-3 года - можно использовать что угодно, даже просто текстовый файлик со списком софта.
Проблема со своими скриптами - и я на это уже много раз натыкался сам - в том, что через эти 2-3 года внезапно обнаруживаешь, что сами скрипты устарели. Wireguard ставится из другого репозитория.. Для установки докера в старой инструкции можно было сделать
add-apt-repository, а теперь нужно в keyrings писать ещё.. ssh слушает порт через systemd вместо конфига.. И скрипты надо править каждый раз, причём обнаруживаются все эти проблемы в тот момент, когда уже надо бы срочно всё восстановить..В нормальном плейбуке все эти обновления уже отражены и плейбук "установи докер" установит его самым правильным на текущий момент способом.
Опять же - я не критикую сам подход, но хочу указать, что рекламировать нужно не его.
Я храню в keepass. И на серверах пишу в конфиги руками если нужно.
Мне кажется очевидным, что хранить секрет в git репозитории, даже зашифрованным, очень небезопасно. Оно же всю историю коммитов хранит. Если ключ шифрования скомпрометирован, то даже если вы его смените и перешифруете секреты - предыдущие коммиты доступны со старым ключом. Разделения доступа нет - это только для одного человека с одним ключом. Если нужно временно дать доступ только на часть секретов для коллеги - это невозможно, и отозвать доступ тоже непросто. Если репозиторий не на своём сервере, а на Github - то это вообще чужое облако.. На серверах я никогда не делаю git clone чтобы не давать доступ (даже readonly). В общем, небезопасно это всё.
powerman Автор
Эта проблема касается только
bootstrap.sh, а он минимален. Да, надо поставить докер и настроить ssh - но это плюс/минус всё, и поддерживать этот набор не сложно. Да и новые сервера в концепции "пара личных" появляются не часто. Всё это по-прежнему сильно проще, чем разбираться с Ansible (который, к слову, тоже постоянно развивается, меняется, и его конфиги скорее всего тоже через пару лет нужно адаптировать к текущей версии ансибла - так что размен такой себе).Ну, свои (личные, а не которые для серверов) я тоже в Keepass держу. Но писать в конфиги сервера ручками - это ломает перевыкат сервера "одной кнопкой", разве нет? Как Вы этот подход с тем же Ansible совмещаете?
Если ключ скомпрометирован, то нужно сменить все защищённым им секреты. В любом случае, вне зависимости от того, где они хранились, и доступны ли старые коммиты - разве нет?
Ну, технически fnox всё это умеет, и пригоден даже в enterprise. Но практически, для заявленной задачи "до 5 личных серверов" разделение доступа явно лишняя фича.
baldr
У меня на продакшене 5 серверов и сетап у них примерно как у вас - тоже есть bash-скрипт с начальными шагами. Но мне это очень не нравится, и мы планируем серьёзно это всё переделать.
Повторюсь - я сам так делаю, но считаю что это неправильно и учить такому не стоит.
В общем-то да. Но кроме этого есть более простые сценарии. Например у вас украли кошелёк с флэшкой, на которой был ключ. Пока до него доберутся и поймут что это - у вас есть как минимум несколько часов, чтобы его сменить. Менять все секреты - это уж перебор. Вдобавок, ключ желательно ротировать время от времени, чтобы случайно засвеченный бэкап трёхлетней давности его не слил..
Что касается хранения секретов - это боль. Мне не нравится ни один менеджер паролей и я уже год не спеша (времени нет) пишу свой (с картами и девушками). На продакшене секреты хранятся и используются в Docker Secrets (Swarm), а чтобы их удобнее менять - у меня самописный менеджер для Swarm их умеет подменять.
powerman Автор
Эта позиция (в отношении других вещей) мне хорошо понятна. Более того, я примерно из этих соображений несколько раз пытался освоить Ansible, да и на все альтернативы смотрел - те, которые Вы упомянули плюс ещё Terraform. И хотя я в основном всё-таки архитект и программист, но у меня хватает и админского опыта: начинал со слаквари в 94-м, в 2001 даже сделал собственный дистрибутив линуха на базе LFS и поддерживал его года 2.5 примерно, после этого перешёл на Gentoo, которым пользуюсь до сих пор. И я прекрасно понимаю всю пользу IaC. Но если при таком бэкграунде, многолетнем желании внедрить IaC, и нескольким подходам к существующим инструментам, я этого так и не сделал - возможно дело не во мне (моей лени или безграмотности), а в том, что существующие инструменты действительно не подходят для такой задачи и целевой аудитории "не профессиональный админ"?
Кроме того, даже если так делать неправильно, может всё-таки лучше так, чем никак (настраивать ручками без IaC)? У этого подхода довольно узкая область применимости - но я вроде бы честно и чётко её в статье обозначил. И если ни я ни Вы не взяли Ansible, а делаем так - почему "плохо" про это честно написать? От того, что про этот подход все будут стыдливо молчать - что изменится к лучшему? У меня вот управление серверами с ним однозначно стало лучше - IaC вполне рабочий получился, с IaC и документацией стало на порядок лучше, чем без них. Почему про это плохо рассказывать и подталкивать внедрять IaC хотя бы такими, колхозными методами?
baldr
С одной стороны, я понимаю что вы имеете в виду. Однако, "я бы мог с вами согласиться, но тогда мы бы оба оказались неправы" (с)
Для меня это выглядит как легитимизация неправильного подхода. И после прочтения может остаться желание оставить всё как есть: "смотри, другие так же делают". Однако, когда я объясняю что-то коллегам, я часто говорю "я сделал это вот так, но это НЕПРАВИЛЬНО, должно быть вот так, мы это должны переделать когда сможем", заводится таск в Jira и мозолит глаза.
Я согласен, вы в начале статьи постарались об этом предупредить, но я бы даже статью писать не стал, на вашем месте, простите. Я поставил минус, кажется, с "не согласен", но не видел смысла сильно объяснять свою позицию до тех пор, пока прохожий сеньор программист не выразил недоумение.
Tony-Sol
В статье говорится про 1-5 тачек - если не это идеальная возможность поработать с ansible, то что? Проще разве что из tmux панелей подключаться ко всем тачкам разом и через pane-synchronize один shell'ник, запускать
powerman Автор
Для админов, которым ансибл всё-равно нужно освоить - безусловно. Но мне оно не профильное, я его дольше изучать-вспоминать-актуализировать перед каждым использованием буду чем собственно использовать.