Недавно я занимался настройкой деплоя для одного из своих проектов. Хочу поделиться полученным опытом и знаниями в виде статьи, описывающей мою систему.
Расскажу:
Как настроить пайплайны в GitLab для сборки и тестирования сервисов
Как настроить удаленный сервер для развертывания сервисов
Как автоматизировать деплой приложений в Docker Swarm, используя GitLab
Для понимания материала желательно:
Уметь пользоваться Git и GitLab, в т. ч. GitLab Pipelines
Иметь базовые навыки использования Linux-утилит
Понимать, что такое контейнеризация; знать, что такое Docker
Иметь представление о том, как устроена архитектура современных web-приложений
Дисклеймер
Эта схема не идеальна! Она не подойдет для больших highload систем с сотнями сервисов. Однако в моём небольшом проекте она работает хорошо. Надеюсь, подойдёт и вам.
Я не считаю себя профессионалом в DevOps. Критикуйте, предлагайте идеи и улучшения в комментариях к этой статье, а так же в Issues и Pull Requests.
Репозиторий с исходными конфигами: https://github.com/Yu-Leo/deploy-to-docker-swarm
Деплоим?
Что деплоим?
Итак, для начала нужно определиться, что именно мы будем деплоить.
В текущей версии моя система состоит из 4-х компонентов:
СУБД PostgreSQL
Backend на Go
Frontend на React + nginx в качестве веб-сервера
Nginx proxy. Проксирует запросы от клиентов, распределяя их между backend и frontend. Обеспечивает поддержку протокола HTTPS
Помимо этих компонентов я планирую добавить и другие сервисы: систему сбора метрик (prometheus), мониторинга (grafana), in-memory хранилища (redis), вспомогательные джобы и т. д. и т. п.
Куда деплоим?
Для развертывания системы я решил использовать Docker-контейнеры, а для их оркестрации — Docker Swarm. В этой статье описаны его основные возможности. Меня же в первую очередь привлекла простота его настройки и широкие возможности. То, что нужно для небольшого проекта.
В моём случае всё крутится на одном сервере, но Docker Swarm позволяет использовать несколько.
dev и prod окружения
Всю архитектуру я хочу разворачивать в двух окружениях: dev и prod.
prod
:
Боевое окружение (production)
Единая точка входа для пользователей в виде nginx proxy
Рабочие версии сервисов
Продовые данные
dev
:
Тестовое окружение
Несколько точек входа для разработчиков (backend, frontend, ...), если это необходимо
Не всегда рабочие версии сервисов. Используется для тестов и экспериментов
Тестовые данные
В текущей реализации и dev и prod окружения разворачиваются на одной manager-ноде Docker Swarm, но в разных стеках. По мере роста проекта эти окружения могут быть разделены на различные сервера или группы серверов.
Как деплоим?
В качестве сервиса для хранения исходного кода проекта я выбрал GitLab. И, конечно, я хочу использовать его возможности для автоматизации сборки, тестирования и развертывания.
Поехали!
Теперь реализуем систему для деплоя этого проекта.
Схема
Все сервисы запускаются в Docker-контейнерах. А значит, нужны их образы (Docker Images). Публичные образы (например, nginx и postgresql) тянутся с Docker Hub. Приватные (backend, frontend) — с приватного GitLab Registry проекта.
Прежде чем спуллить образы с GitLab Registry их нужно туда запушить. Эта операция происходит в паплайнах репозиториев backend
и frontend
. В них хранятся исходники сервисов, а так же инструкции по сборке их Docker-образов. При необходимости в проект можно добавлять другие аналогичные репозитории.
Сердце этой системы развертывания — репозиторий deploy-manifests
(на схеме слева внизу). Он содержит параметры запуска сервисов и их конфиги. Из его пайплайнов происходит всё взаимодействие с manager-нодой Docker Swarm.
Тестирование и сборка
Главная цель пайплайнов в репозиториях backend
и frontend
, ... — собрать из исходников Docker-образ сервиса(-ов). Попутно можно запускать тесты, линтеры и т. д.
Пайплайн в backend repository
Структура репозитория
.
├── backend # Директория одного сервиса
├── build # Директория сборки
│ └── backend # Исполняемый файл одного сервиса
│ └── build_version.tmp # Временный файл с тегом
├── common # Общий для нескольких сервисов код
├── Dockerfile # Общий Dockerfile
├── .gitignore
├── .gitlab-ci.yml # Описание GitLab Pipeline
├── .golangci.yml # Конфиг для линтера
├── go.mod
├── go.sum
└── Makefile # Файл с инструкциями по сборке и тестированию демонов
По структуре это монорепозиторией: в одном репозитории хранится код нескольких микросервисов. У этого подхода есть свои плюсы и минусы, но их рассмотрение выходит за рамки этой статьи.
Сборка
Пайплайн состоит из двух джоб:
build-lint-test
docker-images
В build-lint-test
, как следует из названия, происходят три действия: сборка исполняемого файла, запуск линтера и запуск юнит-тестов. Если на оном из этапов джоба падает, пайплайн дальше не идёт. Если же все этапы выполнились успешно, в GitLab сохраняется артефакт работы джобы — набор исполняемых файлов для каждого микросервиса (демона).
В джобе docker-images
происходит сборка Docker-образов демонов на основе исполняемых файлов, а так же Dockerfile. После сборки полученные образы пушатся в GitLab Registry. Каждый образ получает уникальный тег, состоящий из даты и 8-ми цифр от хэш-суммы коммита (20240725_4605cc6e
).
Я учел, что разным микросервисам могут понадобиться разные Dockerfile для сборки. Если в директории микросервиса есть Dockerfile, образ будет собираться по нему. Иначе будет использован дефолтный Dockerfile, расположенный в корне репозитория.
Правила запуска джоб
В моей реализации триггером запуска джоб являются лейблы на MR в GitLab.
Лейбл
tests
запускает только джобуbuild-lint-test
Лейбл
build-<service-name>
, где<service-name>
— название микросервиса, запускает обе джобы:build-lint-test
иdocker-images
. Build и push операции будут выполняться только для сервиса<service-name>
. Если указать несколько:build-<service-name-1>
,build-<service-name-2>
, будут собраны и запушены все соответствующие образы.
Джобу build-lint-test
можно также запустить вручную, без каких либо лейблов.
Пайплайны в других репозиториях
Аналогичным образом можно настроить пайплайны в других репозиториях.
В качестве примера — мой репозиторий frontend. Его пайплайн состоит из единственной джобы, которая собирает и публикует Docker-образ.
Важно! В настройках каждого подключаемого репозитория необходимо дать доступ до скачивания образов из пайплайнов репозтория deploy-manifests
:
Фактически скачивание будет происходить не в самом пайплайне, а на удаленном сервере при развертывании, но для доступа в приватный GitLab Registry используется токен из GitLab Job-ы.
Комментариями с тегами образов
Каждый образ собирается и пушится с уникальным тегом (пример: 20240722_ac34b89g
). Далее этот тег понадобится нам для деплоя конкретной версии сервиса.
Чтобы не искать сгенерированный тег вручную, я настроил их автоматическую публикацию в комментарии к MR.
Как это выглядит
Сообщение об успешной сборке образа сервиса ping
:
Сообщение о фейле сборки и/или публикации образа:
Как это настроить
В файле .gitlab-ci.yaml
Эта фишка реализована при помощи GitLab API. Для его использования необходимо получить токен доступа.
В GitLab Free недоступны токены для джоб, но вместо них можно использовать токен от персонального аккаунта. Строго говоря, этот способ не безопасен, однако имеет место быть для небольших проектов. При этом комментарии будут публиковаться от имени владельца токена.
Чтобы это повторить, нужно:
Добавить ещё один токен для своего аккаунта
Указать этот токен в ENV-переменной для пайплайна в репозитории
После этого станет доступна публикация комментариев к MR в репозитории.
Сервер
Подготовка
Описание даже базовой настройки удаленно сервера получилось объемным, поэтому я вынес его в отдельную статью: "Настройка SSH на удаленном сервере". Рекомендую прочесть её, прежде чем переходить далее.
Пользователь gitlab
Помимо основного пользователя www
, которого мы создавали в вышеупомянутой статье, нужно завести пользователя gitlab
. От его имени в джобах GitLab-пайплайна будет происходить подключение к серверу.
Создаем пользователя (на сервере)
useradd -s /bin/bash -m gitlab
Создаем ключи (на локальной машине)
ssh-keygen -t ed25519
mv id_ed25519.pub gitlab.pub
mv id_ed25519 gitab
Пароль от приватного SSH-ключа задавать не нужно.
Редактируем файл
/etc/ssh/sshd_config
(на сервере)
PasswordAuthentication yes # Временно разрешаем авторизацию по паролю
AllowUsers www gitlab # Добавляем пользователя gitlab
Закидываем публичный ключ с локальной машины на сервер
ssh-copy-id -i ~/.ssh/gitlab.pub gitlab@<ip>
Редактируем файл
/etc/ssh/sshd_config
(на сервере)
PasswordAuthentication no # Закрываем обратно
Добавляем пользователя
gitlab
в группуdocker
(на сервере)
sudo usermod -aG docker gitlab
Docker Swarm
Инициализируем Docker Swarm командой
docker swarm init
Далее появится сообщение
Swarm initialized: current node (bvz81updecsj6wjz393c09vti) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-1awxwuwd3z9j1z3puu7rcgdbx 172.17.0.2:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
в котором Docker сообщает, что текущая нода будет являться manager-нодой, и показывает, каким образом можно добавить worker-ноды в кластер.
Для деплоя необязательно иметь worker-ноды. Можно использовать единственную manager-ноду и разворачиваться на ней.
Структура директорий на сервере
. # /
├── ...
└── data
└── PROJECT-NAME # Название проекта
├── data # Данные сервисов
│ └── prod # Данные prod-сервисов
│ ├── nginx-proxy
│ │ └── ssl
│ │ ├── ca.crt
│ │ ├── hostname.crt
│ │ └── hostname.key
│ └── postgres
└── deploy # Данные деплоя
├── dev # Конфиги dev-сервисов
│ └── backend
│ └── config.toml
├── docker-compose.dev.yaml
├── docker-compose.prod.yaml
├── get-config-versions.py
└── prod # Конфиги prod-сервисов
├── nginx
│ └── default.conf
├── nginx-proxy
│ └── default.conf
└── backend
└── config.toml
Для корректной работы нужно вручную создать часть директорий перед запуском развертывания:
mkdir -p /data/PROJECT-NAME/data/prod/postgres
mkdir -p /data/PROJECT-NAME/data/prod/nginx-proxy
mkdir -p /data/PROJECT-NAME/deploy
При необходимости структуру можно поменять, не забыв обновить пути в файлах репозитория deploy-manifests
.
Docker Secrets
Docker Secrets — механизм Docker для управления приватными данными (секретами). Типичные примеры секретов: пароли, ключи, сертификаты. Секреты монтируются к Docker-контейнерам: содержимое секрета находится в файле /run/secrets/secret-name
внутри контейнера. Ниже я опишу их использование подробнее.
Рассмотрим создание секретов на примере двух кейсов:
1. Пароль для пользователя в PostgreSQL
Автоматически сгенерируем 32-х символьный пароль для юзера PostgreSQL и запишем его в Docker Secret с названием postgres-passwd
:
tr -dc A-Za-z0-9 </dev/urandom | head -c 32 | docker secret create postgres-passwd
а можно сгенерировать пароль, после чего одновременно вывести его в терминал и записать в Docker Secret:
tr -dc A-Za-z0-9 </dev/urandom | head -c 32 | tee /dev/stderr | docker secret create postgres-passwd
2. Сертификат для nginx
Ситуация: на локальной машине наличествует в наличии файл с SSL-сертивикатом для nginx. Нужно закинуть его в Docker Secret на удалённом сервере, чтобы в дальнейшем использовать в nginx.
Копируем файл на удаленный сервер:
scp /path/to/file/on/local usename@servername:/path/to/file/on/server
Кладем содержимое файла в Docker Secret командой:
cat /path/to/file/on/server | docker secret create nginx-crt -
deploy-manifests
Перейдём к главному компоненту моей системы развертывания — репозиторию deploy-manifests
. Из его пайплайнов происходит запуск сервисов в Docker Swarm.
Структура репозитория
.
├── copy-files.sh # Файл, копирующий содержимое из репозитория на сервер
├── deploy.sh # Файл с инструкциями по развертыванию системы
├── dev # Конфиги для dev-окружения
│ └── backend # Конфиги сервиса backend в dev-окружении
│ └── config.toml
├── docker-compose.dev.yaml # Описание стека dev-окружения
├── docker-compose.prod.yaml # Описание стека prod-окружения
├── get-config-versions.py # Файл, генерирующий версии конфигов
├── .gitignore
├── .gitlab-ci.yml # Описание GitLab Pipeline
├── prod # Конфиги для prod-окружения
│ ├── backend # Конфиги сервиса backend в prod-окружении
│ │ └── config.toml
│ └── nginx-proxy # Конфиги сервиса nginx-proxy в prod-окружении
│ └── default.conf
└── requirements.txt # Зависимости для скипта get-config-versions.py
Подключение к серверу
Для подключения к удаленному серверу из джоб пайплайна необходимо указать следующие ENV-переменные в настройках репозитория:
SERVER_HOST
— ip-адрес manager-ноды Docker SwarmUSER_NAME
— пользователь на удаленном сервере, от имени которого будет происходить деплой. Как его создать я описывал выше, в разделе "Сервер" -> "Пользователь gitlab"SSH_KEY
— приватный ssh-ключ для вышеупомянутого пользователя
Пайплайн
Пайплайн состоит из двух независимых джоб: dev-deploy
и prod-deploy
. Как следует из названия, каждая из них запускает деплой соответствующего окружения. Обе джобы имеют одинаковую логику; отличаются только окружениями
Суть каждой джобы сводится к двум действиям:
Скорпировать на удаленный сервер файлы из репозитория. При этом копируются конфиги только соответствующего джобе окружения
Запустить на удалённом сервере скрипт
deploy.sh
для развертывания стека сервисов. О нем подробнее далее
docker-compose.mode.yml
Это файл, в котором задается конфигурация для деплоя соответствующего mode
стека сервисов: dev
либо prod
. Если вы уже работали с docker-compose, его структура вам знакома, однако для использования в режиме Docker Swarm есть дополнительные настройки.
Описание docker-compose.prod.yml
:
На самом верхнем уровне docker-compose.mode.yml
состоит из нескольких секций:
services:
— сервисы, входящие в стекnetworks:
— связывающие их сетиconfigs:
— Docker-конфигиsecrets:
— Docker-секреты
Services
Блок, в котором описывается набор сервисов, входящих в стек. В качестве примера рассмотрим базовые настройки сервиса backend:
services:
# ...
backend:
hostname: backend
image: ${REGISTRY}/PROJECT-NAME/backend/backend:20240727_a5fa34c7
environment:
DB_PASS_FILE: /run/secrets/project-prod-postgres-passwd
depends_on:
- postgres
hostname
— имя сервиса во внутренней сети Docker-стекаimage: ${REGISTRY}/PROJECT-NAME/backend/backend:20240727_a5fa34c7
— путь до образа сервиса. После двоеточия указывается тег.environment
— описание переменных окруженияdepends_on
— зависимости между сервисами.backend
будет запущен после сервисаpostgres
Configs
services:
# ...
backend:
# ...
configs:
- source: backend-config
target: /app/config.toml
# ...
configs:
backend-config:
name: ${PROJECT_PROD_BACKEND_CONFIG}
file: ./prod/backend/config.toml
Docker-конфиги — файлы, хранящие в себе нечувствительную информацию. Не подходят для хранения паролей и сертификатов, но подходят (как и следует из названия) для хранения конфигов.
Docker не умеет автоматически обновлять сервис, если конфиг изменился. Для решения этой проблемы придумано несколько решений; я расскажу о своём.
Каждому конфигу, описанному в глобальной секции configs:
docker-compose файла, присваивается имя (name:
). Его значение берётся из переменной окружения (${PROJECT_PROD_BACKEND_CONFIG}
).
Каждый раз при запуске скрипта deploy.sh (о нём подробнее далее) значения этих переменных инициализируются:
Запускается скрипт get-config-versions.py . Результатом его работы являются строки вида
PROJECT_PROD_BACKEND_CONFIG=md5(config-file)
, гдеmd5(config-file)
— часть хэша от содержимого файла с конфигом. Путь до файла берётся из поляfile:
docker-compose файла.В скрипте deploy.sh на основе полученных строк инициализируются переменные окружения.
Как это влияет на работу сервисов:
Конфиг не изменился => не изменился его хэш => не изменилось имя Docker-конфига => Docker не будет обновлять сервис => сервис продолжит работу со своим конфигом
Конфиг изменился => изменился его хэш => изменилось имя Docker-конфига => Docker будет обновлять сервис => после обновления сервис будет использовать новый конфиг
Описание настроек из docker-compose файла:
source: backend-config
— название Docker-конфига, который нужно подключить к сервисуtarget: /app/config.toml
— путь, по которому конфиг будет расположен в контейнере сервисаname: ${PROJECT_PROD_BACKEND_CONFIG}
— имя Docker-конфигаfile: ./prod/backend/config.toml
— путь до конфига на manager-ноде
Для работы скрипта get-config-versions.py
требуется установить зависимости (на сервере):
pip3 install -r ./requirements.txt
Secrets
services:
# ...
backend:
# ...
secrets:
- project-prod-postgres-passwd
# ...
secrets:
project-prod-postgres-passwd:
external: true
Как я уже говорил, Docker-секреты — это файлы, хранящие в себе чувствительную информацию: пароли, ключи и т. п.
external: true
указывает, что Docker-секрет заранее создан в Docker Swarm. Выше я описывал процесс создания Docker-секретов на сервере
Файлы с Docker-секретами будут примонтированы в директорию /run/secrets/
внутри контейнера с сервисом. Описанный выше секрет project-prod-postgres-passwd
расположится в файле /run/secrets/project-prod-postgres-passwd
.
Healthcheck
services:
# ...
backend:
# ...
healthcheck:
test: curl --fail http://127.0.0.1:8080/api/service/ping || exit 1
interval: 10s
timeout: 3s
retries: 5
Проверяет работоспособность каждой реплики сервиса.
test:
— команда, которая будет выполняться в контейнере. Если она завершится с кодом0
— сервис жив и готов к работе, если c кодом1
— сервис нездоров. В примере: стучимся на ручкуping
сервиса; если она не отвечает, сервис не проходит healthcheckinterval:
— интервал проверкиtimeout:
— таймаут на выполнение хелсчек-командыretries:
— сколько раз проверка должна упасть — вернуть ненулевой код ошибки — прежде чем на контейнер будет признан unhealthy
Deploy
services:
# ...
backend:
# ...
deploy:
replicas: 1
restart_policy:
delay: 5s
condition: on-failure
update_config:
parallelism: 1
order: start-first
failure_action: rollback
delay: 10s
rollback_config:
parallelism: 0
order: stop-first
resources:
limits:
cpus: "0.7"
memory: "800M"
Секция, в которой описываются параметры, относящиеся к развертыванию сервиса. Разберём подробнее.
RollingUpdate
services:
# ...
backend:
# ...
deploy:
update_config:
parallelism: 1
order: start-first
failure_action: rollback
delay: 10s
rollback_config:
parallelism: 0
order: stop-first
Плавная выкатка. Механизм, при помощи которого можно постепенно обновить сервис с одной версии на другую.
Конфигурация, приведённая выше, описывает следующее поведение:
Поднимается одна реплика новой версии, при этом остаются все реплики старой версии.
После того как реплика успешно поднялась и helthcheck-проверка успешно прошла, выключается одна реплика старой версии.
Пункты 1 и 2 повторяются, пока не будут обновлены все реплики
Если в какой-то момент раскатка прервётся, все новые реплики разом остановятся и будут подняты реплики старой версии.
Чтобы проверить и наглядно показать работу этой функции я написал небольшой скрипт. Каждую секунду он посылает запрос на ручку /api/service/version
моего сервиса, поле чего выводит ответ.
while true; do echo -n "$(date) "; curl -sS http://SERVER_ADDRES:8080/api/service/version; echo ''; sleep 1; done
Далее я запустил этот скрипт на локальной машине параллельно с запуском выкатки новой версии сервиса. Результат:
Ср 17 июл 2024 21:52:08 MSK {"name":"backend","version":"20240704_63586a62"}
Ср 17 июл 2024 21:52:09 MSK {"name":"backend","version":"20240704_63586a62"}
Ср 17 июл 2024 21:52:10 MSK {"name":"backend","version":"20240717_8a4f53ae"}
Ср 17 июл 2024 21:52:11 MSK {"name":"backend","version":"20240704_63586a62"}
Ср 17 июл 2024 21:52:12 MSK {"name":"backend","version":"20240717_8a4f53ae"}
Ср 17 июл 2024 21:52:13 MSK {"name":"backend","version":"20240717_8a4f53ae"}
Сначала все запросы попадали на поды со старой версией сервиса (20240704_63586a62
), затем один запрос попал на новую (20240717_8a4f53ae
) и одни на старую, после чего все запросы стали попадать только на новую версию сервиса.
Это объясняется тем, что во время раскатки нового сервиса в некоторый момент времени существовали одновременно поды старой и новой версии. Docker автоматически распределяет запросы между подами, поэтому часть запросов попала на новую версию, часть — на старую. По завершении процесса раскатки подов со старой версией не осталось и все запросы стали приходить только на поды с новой версией.
Если во время раскатки поды не будут проходить helthcheck-проверки, произойдет rollback, то есть автоматический откат до старой версии. В таком случае в логах GitLab джобы появится сообщение:
rollback: update rolled back due to failure or early termination of task y4skuy8si13qy1hrzui7inzcn
Этот лог можно обработать и вывести алерт о произошедшем откате.
Официальная документация по update_config
Официальная документация по rollback_config
Resources
services:
# ...
backend:
# ...
deploy:
resources:
limits:
cpus: "0.7"
memory: "800M"
В этой секции задаются требования и лимиты по CPU и оперативной памяти. Подробнее — в официальной документации.
Volumes
Docker Volume — механизм, позволяющий расшарить данные (файлы, директории) между контейнерами и хостовой машиной. Преимущество использования Docker Volumes состоит в том, что после остановки или удаления контейнера данные остаются на хостовой ОС. Это можно использовать для хранения файлов БД и подключения их в контейнер с СУБД:
services:
postgres:
# ...
volumes:
- /data/PROJECT-NAME/data/prod/postgres:/var/lib/postgresql/data
Директория /data/PROJECT-NAME/data/prod/postgres
на хостовой ОС будет соответствовать директории /var/lib/postgresql/data
внутри Docker-контейнера. Таким образом все изменения, которые PostgreSQL производит внутри контейнера, будут сохраняться и на хостовой ОС. Если контейнер остановится, данные не потеряются.
Правила запуска пайплайна
Есть 3 триггера для запуска джоб деплоя
Лейблы на MR. Лейбл
dev-deploy
триггерит джобы деплоя dev-окружения,prod-deploy
— prod-окружения. Лейблы не зависят друг от друга.Запуск в MR вручную. Любую джобу можно независимо запустить из любого MR
Merge в main-ветку. При мердже в main-ветку автоматически запускается джоба
prod-deploy
.dev-deploy
не запускается, поскольку иногда полезно оставить в dev-среде то состояние, которое было развернуто с ещё не смердженного MR
deploy.sh
Описание файла deploy.sh.
Этот скрипт запускается на удаленном сервере и обновляет сервисы соответствующего окружения (--dev
или --prod
при запуске).
Процесс обновления окружения состоит из нескольких этапов:
Подготовительный этап №1: определение режима запуска скрипта (
dev
илиprod
). Переход в рабочую директорию.Подготовительный этап №2: обновление конфигов. Как я уже говорил, Docker не умеет в автоматичекое обновление конфигов, поэтому эту проблему нужно решить самостоятельно (см. раздел "docker-compose.mode.yml" -> "configs"). На этом этапе запускается скрипт get-config-versions.py и инициализируются сгенерированные им переменные окружения.
Основной этап: обновление деплоймента средствами Docker Swarm. Здесь стоит упомянуть, что Docker Swarm умный: он не будет убивать контейнеры и поднимать новые, если у сервиса не изменился конфиг, тег или параметры в
docker-compose.mode.yml
Заключительный этап: очистка неиспользуемых объектов Docker. В том числе удаление лишних конфигов: в скрипте запускается команда на удаление всех конфигов; Docker знает, какие из них ещё используются, и удаляет только неиспользуемые
Итоги
Исходники: https://github.com/Yu-Leo/deploy-to-docker-swarm
Вот такая система для развертывания сервисов у меня получилась. Повторюсь: она далека от идеала, однако имеет право на жизнь и использование в небольших проектах.
А что дальше?
Бесконечный простор для творчества. Можно улучшать и добавлять новую функциональность как пайпалайнам сборки, так и пайплайну деплоя.
Вот несколько идей, которые я собираюсь реализовать в своей системе:
end-2-end тестирование. Поднятие полноценного тестового окружения в пайплайне репозитория backend, запуск e2e-тестов в пайплайне
Добавление системы метрик, графиков и алертов
Добавление системы сбора, фильтрации и отображения логов и ошибок
Улучшения системы оповещения о сбоях в процессе деплоя
Предлагайте свои идеи улучшений в комментариях, а так же в Issues и Pull Requests в репозитории!
Эта статья в моём блоге: https://yu-leo.github.io/yu0dev/posts/deploy-to-docker-swarm/
Комментарии (14)
pvzh
19.08.2024 07:22+1Небольшое примечание: в текущей версии спецификации Compose-файла его имя по-умолчанию уже не содержит префикса
docker-
, там простоcompose.yaml
: https://docs.docker.com/compose/compose-application-model/Yu-Leo Автор
19.08.2024 07:22Спасибо за замечание! Работу моих скриптов это не ломает, поскольку везде явно указываются пути до файлов. На будущее буду иметь в виду
Marsezi
19.08.2024 07:22Несколько вопросов
1) у вас на схеме идёт от одной базы связь к другой из продакшена в test, что это значит?
2) зачем такая сложная схема с лейблами если образ не был изменён он и не появится соответственно и не обновится. обновиться только те образы которые появились от изменений в коде.
3) в какой момент происходит обновление продакшена - это ручное нажатие скрипта? Обновиться абсолютно весь production (для того чтобы ваше лейблы были одинаковой версии) или только те сервисы которые изменились?
4) как происходит замена тестового окружения если нужно откатить изменения в том числе на старую базу данных?
Yu-Leo Автор
19.08.2024 07:221) у вас на схеме идёт от одной базы связь к другой из продакшена в test, что это значит?
Там показана связь от сервиса "Backend API" в окружении "dev" до двух баз: в "prod" и в "dev". Строго говоря, это не совсем верно, поскольку окружения должны быть полностью изолированные.
Во время разработки и тестирования новых версий backend-а иногда было полезно сходить в продовую базу, поэтому связь осталась в конфигах и на схеме
2) зачем такая сложная схема с лейблами если образ не был изменён он и не появится соответственно и не обновится. обновиться только те образы которые появились от изменений в коде.
Предполагаю, речь про лейблы в пайплайнах репозиториев backend/frontend.
Нет, в текущей реализации сборка образа не зависит от наличия или отсутствия изменений в коде. Можно собрать два одинаковых образа на базе одного и того же кода, но они будут иметь разные теги
Иногда нет необходимости собирать образ на каждый коммит. В таком случае лейбл сборки можно отключить и добавить только в тот момент, когда реально потребуется собрать образ для выкатки
3) в какой момент происходит обновление продакшена - это ручное нажатие скрипта? Обновиться абсолютно весь production (для того чтобы ваше лейблы были одинаковой версии) или только те сервисы которые изменились?
Есть 3 триггера для запуска джобы деплоя продакшена
Лейбл prod-deploy на MR.
Ручной запуск джобы в MR
Merge в main-ветку. При мердже в main-ветку автоматически запускается джоба prod-deploy.
При запуске сервисы деплоятся с теми версиями, которые указаны в конфиге. Если версия не поменялась, сервис не перезапускается. Если поменялась - обновляется
4) Как происходит замена тестового окружения если нужно откатить изменения в том числе на старую базу данных?
Аналогично выкатке, но вместо более новой версии указывается более старая. Автоматизации управления содержимым БД в моей реализации нет, тут нужно будет обновлять вручную.
olku
19.08.2024 07:22Что сейчас актуально для скудулинга джобов по расписанию и одноразовых в Swarm?
baldr
19.08.2024 07:22+3Хороший вопрос. Я изучал его с полгода назад. На самом деле, оказывается, что не так уж и много вариантов.
swarm-cronjob. Запускается отдельный сервис, смотрит на labels у сервисов.
Ofelia. Примерно то же самое, но ещё может из конфига брать расписания.
У обоих конфигурация довольно тупая, результаты запуска можно найти только в логах, а какую-то статистику получить - ну пиши свои логопарсеры что ли..
Есть Apache AirFlow, он немного для другого, но у него есть оператор для запуска Docker/Swarm контейнеров. Можно шедулить и UI есть. Однако, это получается
немногонекрасиво: расписание запуска хардкодится на питоне в коде DAG'а, он запускает celery-таск, которая ничего не делает, но запускает ещё Docker-контейнер и висит ждёт его окончания. 40 активных тасков == 40 celery-тасков, да ещё и рестартовать их нельзя нормально. Ну как-то перебор.В принципе, наверное, можно для этого приспособить Jenkins - у него есть и UI, и скриптовать что угодно можно. Но как-то я его не очень люблю.
Я оценил варианты и написал своё. Оно и запускать может, и Docker Images из исходников строит, и секретами динамически управляет.
Скриншоты
olku
19.08.2024 07:22Выглядит заманчиво. Open source?
baldr
19.08.2024 07:22+1К сожалению, нет. Но, возможно, удастся когда-нибудь уговорить начальника на то, чтобы выпустить в OpenSource. Однако, для этого нужно потратить ещё некоторое время для того чтобы причесать код, добавить недостающие фичи и, самое главное - написать хоть какую-то документацию..
Вряд ли соберусь сделать всё это раньше чем через полгода, однако, может быть, смогу описать основные принципы в статье на хабре.
baldr
В целом, поставил вам плюс, в первую очередь за популяризацию Docker Swarm. Сейчас к вам тут набегут хейтеры, которые объявят что он умер и туда ему и кубернетес.
У меня, до недавнего времени, был целый зоопарк скриптов и сервисов (ок 100 штук), которые выполнялись на нескольких серверах в контейнерах и без. В какой-то момент я понял что надо навести в этом порядок и долго выбирал какой-нибудь оркестратор. Поскольку работаю я один, то k8s пришлось отмести из-за сложности с сопровождением. В итоге выбрал Docker Swarm и всё туда перенес за пару месяцев. Нормальной оркестрации и шедулера так и не нашел, пришлось написать самому.
В вашем проекте вы используете nginx - однако, насколько я знаю, он резолвит доменные имена при старте и, если IP сервиса меняется - то он об этом не узнает. Если вы обновляете frontend - то приходится и nginx тоже рестартовать? Именно из-за этого я выбрал Traefik для динамической маршрутизации. Он сам находит новые сервисы без рестарта. Впрочем, конфиги в labels мне не нравятся, да и вообще nginx я тоже люблю больше, конечно..
С другой стороны, у вас фронтенд - это чисто статика, насколько я вижу.. Тогда такой проблемы, наверное, нет.
Вы выставляете порты для базы данных наружу - это очень небезопасно. Тем более что они доступны по всем интерфейсам хоста. Тем более это не нужно в overlay сетях - сервисы прекрасно найдут базу внутри swarm. Вы можете возразить что вам нужно к ней иногда подключаться, но для этого можно придумать промежуточный временный контейнер, который подключится одним концом к базе, а порт прокинет наружу - только на localhost и только в моменты когда вам это нужно. Можно сделать его на socat, и можно просто на ssh tunnel.
Конфиги и секреты - да, это боль. Нельзя изменить конфиг/секрет если его использует хотя бы один сервис, то есть чтобы поменять SSL-сертификат - надо удалить сервис, удалить конфиг с сертификатом, создать новый конфиг и снова создать сервис. У меня тоже самописное решение - примерно как у вас, создаются временные конфиги, а в labels к ним пишется время обновления..
knightly_flo
Не хейтер докер сварма и не особо слежу за этим инструментом, но взгляд зацепился за то, что даже у вас в одном сообщении сначала идет:
а затем:
Может поэтому сварм и не такой популярный, раз его приходится вот так вот допиливать? Опять же, не знаю почти ничего о сварме, но мне казалось, что главная ценность оркестратора контейнеров — это, собственно, нормальная оркестрация и шедулинг... А что не так с этим у сварма из коробки?
baldr
Видите ли - я не уверен что нормальный шедулинг есть и в кубернетесе. По крайней мере я не нашёл того, что я хотел бы видеть. А хотел я видеть что-то вроде дашборда ДАГов в Apache Airflow. Но только не хотел использовать Airflow для запуска, потому что уж больно сложная схема там получается внутри. А, как известно, если программисту не нравится чужая программа, то он пишет свою (фатальный недостаток). Начальство не возражало - ну так надо пользоваться случаем.
Для докера нет родного UI (как нет и в k8s), но есть API, через который сторонние инструменты могут им управлять. Для Swarm всё, действительно, остановилось в развитии пару лет назад, но сам он достаточно законченный инструмент (хотя, есть баги и они уже чинятся плохо, я сам наткнулся на парочку). Впрочем, есть ощущение, что и сам Docker теряет позиции - приходят containerd и podman, и теснят его с рынка.
olku
Есть Portainer
baldr
Да, им пользуюсь, но у него этих фатальных недостатков полно.
Yu-Leo Автор
Большое спасибо за такой подробный и развернутый комментарий!
В моём случае всё крутится на одном сервере, пеезжать пока не приходилось. Так что все адреса, можно сказать, постоянные.
Нет, при обновлении frontend'а nginx-proxy не нужно рестартить. Он так же продолжает перенаправлять соответствующий трафик на фронтовый сервис.
Согласен. Порт был открыт именно из-за необходимости периодически подключаться к базе напрямую. Про то, что сервисы базу внутри swarm найдут и без этого - знаю.
Не знал про такие решения. Приму на вооружение, спасибо!