Лаборатория на двух VPS: Semaphore и gitness
КейсыService Lab.
Наша лаборатория — это два обычных VPS с Docker Compose, а вся инфраструктура описана в одном репозитории и разворачивается с чистого сервера несколькими нажатиями кнопок. Главный критерий простой: сервер можно стереть и получить то же самое состояние, ничего не вспоминая.
Лаборатория по-прежнему полигон: изменения сначала обкатываются у нас, а не в проекте клиента. Но на ней живут настоящие сервисы — этот сайт, CRM и ещё три продукта — со своими доменами, сертификатами и мониторингом.
Два сервера и три слоя
Серверов два, у каждого своя роль:
- infra — всё, что обслуживает разработку: Traefik, Postgres, Valkey, gitness (git, реестры образов и крейтов, CI/CD), Kaneo для задач, AFFiNE для документации, ExcaliDash для досок Excalidraw и стек наблюдаемости (Prometheus, Loki, Tempo, Grafana, Alloy);
- production — то, что видят пользователи: Traefik, SurrealDB, SeaweedFS как S3-хранилище и пять веб-сервисов на Rust.
Поверх них три слоя, и каждый описан кодом: управление (Semaphore UI и ansible), доставка (gitness с пайплайнами в .harness/) и наблюдаемость (OpenTelemetry в приложениях, Grafana на infra).
Semaphore UI: кнопки вместо ручных команд
Запускаем Semaphore одной командой docker compose up. Одноразовый сервис provision создаёт по JSON два проекта — infrastructure и production — с готовым набором задач. Они пронумерованы, потому что Semaphore сортирует их по имени:
0. Full cycle
1. Install Docker
2. Deploy stack
3. Status
...
Задача «Full cycle» проходит все шаги подряд, а отдельные задачи нужны, чтобы повторить упавший шаг. Под капотом — обычные ansible-плейбуки: deploy.yml рендерит шаблоны из templates/<infra|prod>/ в /opt/stack на сервере и запускает docker compose up. Конфигурация стека — это папка с шаблонами, а не набор команд, которые кто-то когда-то выполнил руками.
Секреты в git, но зашифрованные
Все пароли, ключи и токены лежат в одном файле vault.yml, зашифрованном ansible-vault. Файл коммитится в репозиторий, а пароль от него вводится один раз в хранилище ключей Semaphore:
cd bootstrap/ansible
ansible-vault edit inventory/vars/vault.yml
Шаблоны берут значения из vault, и плейбук перед запуском проверяет, что нужные переменные есть, — ошибка про пропущенный пароль приходит сразу, а не на третьем шаге. Так же и с сертификатами: чтобы при пересоздании сервера не упереться в лимит Let's Encrypt (пять одинаковых сертификатов в неделю), мы храним публичную часть acme.json в шаблоне, а закрытые ключи — в vault. Новый сервер стартует с уже выданными сертификатами.
gitness: git, реестры и CI в одном месте
gitness заменил у нас связку из git-хостинга, реестра образов и CI. В нём живут репозиторий, Docker-реестр для образов сервисов, Cargo-реестр для общих крейтов и пайплайны. Настраивает его тоже плейбук: создаёт пространство, реестры, пользователей с минимальными правами, кладёт код в репозиторий и регистрирует пайплайны.
Код — монорепозиторий с одним Cargo-воркспейсом и одним Cargo.lock. Общие библиотеки (авторизация, почта, системные задачи, наблюдаемость) лежат в packages/ и публикуются в наш Cargo-реестр, а сервисы подключают их как обычные зависимости. Чтобы сборка не компилировала весь граф зависимостей заново, мы собираем два базовых образа: в них уже скомпилированы все зависимости, а образ сервиса добавляет только свой код.
От push до продакшена
Пайплайнов три, и названия говорят сами за себя:
main— запускается на каждый push вmain: публикует новые версии крейтов, пересобирает образы и выкатывает сервисы;build— те же шаги сборки без деплоя, запускается вручную;deploy— только выкладка, тоже вручную.
steps:
- name: publish-packages # новые версии общих крейтов в Cargo-реестр
- name: build-base # базовые образы: компилированные зависимости
- name: build-services # образы пяти сервисов
- name: deploy # ssh на production, по одному сервису
Деплой — это ssh на сервер продакшена с отдельным ключом, который умеет делать ровно одно: запускать скрипт deploy-service.sh <сервис>. В authorized_keys он выглядит так:
restrict,command="/opt/stack/deploy-service.sh" ssh-ed25519 AAAA…
Скрипт делает docker compose pull и up -d для названного сервиса; контейнер пересоздаётся, только если образ изменился. Если к ключу кто-то получит доступ, всё, что он сможет, — перезапустить наши же сервисы.
Когда кэш тёплый, от push до обновлённого сайта проходят минуты. Первая сборка на пустом реестре занимает около получаса — зато потом базовые образы пересобираются, только если изменились зависимости.
Логи, метрики и трейсы в одной Grafana
Приложения отдают метрики и трейсы по OpenTelemetry (OTLP по http), пишут логи в JSON с идентификатором трейса, а Alloy собирает логи контейнеров, на production — ещё и метрики хоста и контейнеров. Всё приходит на infra: Prometheus, Loki и Tempo сидят за общей basic-аутентификацией.
В Grafana у каждого сервиса свой дашборд, а между сигналами есть переходы: от запроса с 500-й ошибкой — к его трейсу, от трейса — к строкам лога, от лога с паникой — к бэктрейсу. Дашборды — обычные JSON-файлы в репозитории, и при пересоздании сервера они появляются сами.
Какие грабли мы собрали
Идея «стереть и повторить» хороша тем, что вытаскивает на поверхность всё, что раньше держалось на ручной памяти:
- Лимит Let's Encrypt. Несколько тестовых пересозданий подряд упираются в пять одинаковых сертификатов в неделю — отсюда схема с сертификатами в шаблоне и ключами в vault.
- Контрольные суммы крейтов. Новый реестр публикует те же версии с другими checksum, и закоммиченный
Cargo.lockперестаёт собираться. Теперь пайплайн сверяет lock с реестром и сам обновляет его отдельным коммитом. - Импорт репозитория. Фоновая задача импорта в gitness на свежем сервере стартовала только на следующей 20-минутной проверке планировщика. Мы заменили её на обычный clone и push — и ждать перестали.
- Таймаут Traefik. Тяжёлые push-ы образов обрывались на 60 секундах чтения — таймаут пришлось отключить.
Итог
Два сервера, один репозиторий, одна кнопка Full cycle.
Тот же принцип «просто и в одном месте», за который мы любим чистый монолит вместо микросервисов, мы применили к инфраструктуре: никакой магии на серверах, всё лежит в git, а сервер можно пересоздать, не боясь забыть, как он был настроен.
О том, почему собственные сервисы мы пишем на Rust, — в статье Rust и Zig в привычных приложениях.
Обсудим ваш проект?
Расскажите о вашей задаче — вернёмся с оценкой сроков и стоимости в течение двух рабочих дней.