Или CI/CD с помощью Portainer
Это вторая часть из цикла статей, посвященных инфраструктуре для стартапов. Всего их будет две, поделенные, по моему мнению, на логически законченные блоки.
Часть первая. Настройка окружения
Часть вторая. CI/CD и советы
Для кого и для чего описывать не буду, см. ч.1 этой статьи.
Предисловие
Предложенная мной архитектура действительно имеет ряд недостатков, которые важно учитывать при её использовании. Некоторые из них были отмечены в комментариях к первой статье, а с другими я столкнулся лично в процессе эксплуатации. Ниже перечислю основные слабые места и риски, которые могут стать критичными в определённых сценариях:
Swarm может “сломаться на двух серверах -- нужен минимум 3 менеджера, иначе можно поймать split-brain
Swarm хотя и жив, но сообщество давно ушло в сторону Kubernetes
Отсутствие self-healing-кластера. Docker Swarm не имеет полноценного встроенного механизма для автоматического восстановления после сбоев
> Уже были мысли писать скрипты для автоматического расширения/поднятия дополнительных нод и перезагрузки старыхОтказоустойчивость искусственная -- если оба VPS у одного хостера, это не спасёт от падения
> Больше VPS = больше точек отказа. Не факт, что несколько худых vps могут быть лучше одного хорошего, так у нас периодически падают машинки, особенно неприятно когда с traefik'омOverlay-сети в сварме на дешевых VPS могут чудить
> Мы с товарищем до сих пор не смогли найти, кто конкретно стирает часть хедеров у запросов, несколько ночей дебага не помогли. Отложили на потом.
CI/CD через Portainer
После настройки окружения сразу встал вопрос: как автоматизировать деплой, чтобы не лазить каждый раз по SSH и не обновлять сервисы руками.
Самый простой и рабочий способ — использовать Portainer Webhooks + GitHub Actions (или GitLab CI)ля автосборки и доставки.
Идейно наш процесс ci/cd заключается в следующем:
Есть репозиторий с шаблонами пайплайнов
Для проектов используем моно-репозитории, где внутри сервисы разделены по директориям
В репе сервисов лежат пайпланы на каждый сервис
Секреты лежат в GH Secrets (теперь в
Vault
). Как перенести или использовать волт - к другим статьямПайплайны делят образы сервисов на dev/prod с помощью тега
:dev
иlatest
соответственноПосле билда - дергается ручка portainer и по уникальному хешу сервиса, обновляется образ
1. Структура пайплайнов и организация репозитория
В отдельном репозитории для пайплайнов мы храним общий шаблон:

В репозитории проекта, мы переиспользуем следующим образом:
name: deploy-server
on:
workflow_dispatch:
inputs:
environment:
required: true
description: Deploy to PROD/DEV
type: choice
options: [PROD, DEV]
default: DEV
jobs:
deploy:
uses: .shampsdev/cicd-pipelines/.github/workflows/pipeline.yaml@main
permissions:
packages: write
contents: read
attestations: write
id-token: write
secrets: inherit
with:
dockerfile_path: 'server/Dockerfile.server'
context_path: 'server'
image_name: 'project-server'
environment: ${{ github.event.inputs.environment }}
secret-service-hash: ${{ github.event.inputs.environment == 'PROD' && 'SERVER_SERVICE_HASH' || 'SERVER_SERVICE_HASH_DEV' }}
Можно заметить, для билда используется Dockerfile
из директории нужного сервиса в репозитории. Его содержимое вы определите сами.
2. Разделение окружений
Для разделения DEV и PROD мы используем workflow_dispatch
с выбором окружения.
Внутри пайплайна окружение влияет на:
теги Docker-образов (
:dev
,:latest
);секреты (разные
hash
'и дляsecret-service-hash
);
В будущем можно автоматизировать запуск от пуша в ветку, но ручной запуск -- хорошая защита от случайных релизов. Мы предпочитаем гнать все по кнопочке.
Каждое окружение в отдельном Stack'e project-dev
и project-prod
3. Webhook Portainer и обновление сервиса
В Portainer для каждого сервиса можно включить отдельный webhook — он будет просто перезапускать сервис с обновлённым образом.
Где найти:
Зайди в Portainer, в Services
Выбери нужный сервис
Внизу будет блок Webhooks
Нажми "Enable webhook" -- появится уникальный URL
Он выглядит примерно так:
https://portainer.example.com/api/webhooks/abcdef1234567890

Именно abcdef1234567890
необходимо ввести в SERVICE_HASH
4. Деплоим!

В моем пайплайне следующие особенности:

- Деплой в прод только из ветки main
, так мы гарантируем для себя, что в этой ветке только рабочий код
- Деплой в дев из любой ветки. Удобно для гибкой работы в команде
- Для уведомлений используется Shoutrrr, который отправляет сообщения в телеграмм в любом случае.
Все готово, после успешного деплоя вы получите следующее сообщение в телеграмме:

5. Особенности и подводные камни
Webhook только перезапускает сервис, он не пересоздаёт его. Если образ не обновился, значит перезапуск будет бесполезным
Портейнер возвращает 204, говоря о том, что запустил процесс обновления сервиса. Обновился он или нет -- никто не знает
В Stack'e обязательно приходится указывать общий тег, без конкретной версии. Пока не нашел способа обойти это
Откаты и rolling update надо настраивать самостоятельно в файле стека.
Один webhook = одна команда обновления. Нельзя передавать параметры или обновлять сразу все (сразу Stack можно обновить только в Enterprise портейнере :) )
А что дальше?
Далее мы с командой подняли свой registry
, его также можно прямо в portainer'e добавить. Пушим и тянем образы туда.
Как накрутить логи/метрики и все прочие сервисы -- вы сможете разобраться самостоятельно. Этот процесс особо не отличается от настройки в докере в одном docker-compose
.
Заключение
Таким образом мы получили удобный способ автоматизировать деплой без лишней головной боли: сервисы обновляются по команде, без SSH и ручного docker service update
.
Если статья вам понравилась или заставила задуматься -- обязательно пишите комментарии. Мы открыты к любой конструктивной критике и нам всегда интересно узнать советы коллег с опытом!
Vaniog
А как откатывать версию приложения, если задеплоили сломанную?
MrUssy Автор
На такой случай у них есть кнопка с ролбеком. Проверял. Работает :)