{"id":26171908,"url":"https://github.com/yu-leo/deploy-to-docker-swarm","last_synced_at":"2025-12-24T16:41:38.119Z","repository":{"id":253741074,"uuid":"841994986","full_name":"Yu-Leo/deploy-to-docker-swarm","owner":"Yu-Leo","description":null,"archived":false,"fork":false,"pushed_at":"2024-08-19T05:10:19.000Z","size":4691,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-08-19T07:39:52.705Z","etag":null,"topics":["deploy","devops","docker"],"latest_commit_sha":null,"homepage":"https://habr.com/ru/articles/836850/","language":"Makefile","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Yu-Leo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-13T13:11:15.000Z","updated_at":"2024-08-19T07:39:55.445Z","dependencies_parsed_at":"2024-08-19T07:50:54.086Z","dependency_job_id":null,"html_url":"https://github.com/Yu-Leo/deploy-to-docker-swarm","commit_stats":null,"previous_names":["yu-leo/deploy-to-docker-swarm"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yu-Leo%2Fdeploy-to-docker-swarm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yu-Leo%2Fdeploy-to-docker-swarm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yu-Leo%2Fdeploy-to-docker-swarm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Yu-Leo%2Fdeploy-to-docker-swarm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Yu-Leo","download_url":"https://codeload.github.com/Yu-Leo/deploy-to-docker-swarm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243104198,"owners_count":20236943,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deploy","devops","docker"],"created_at":"2025-03-11T19:52:32.235Z","updated_at":"2025-12-24T16:41:38.089Z","avatar_url":"https://github.com/Yu-Leo.png","language":"Makefile","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Деплой в Docker Swarm\n\n![preview.png](./img/preview.png)\n\nНедавно я занимался настройкой деплоя для одного из своих проектов. Хочу поделиться полученным опытом и знаниями в виде статьи, описывающей мою систему.\n\nРасскажу:\n- Как настроить пайплайны в **GitLab** для сборки и тестирования сервисов\n- Как настроить **удаленный сервер** для развертывания сервисов\n- Как автоматизировать **деплой** приложений **в Docker Swarm**, используя GitLab\n\nДля понимания материала желательно:\n- Уметь пользоваться Git и [GitLab](https://about.gitlab.com/), в т. ч. [GitLab Pipelines](https://docs.gitlab.com/ee/ci/pipelines/)\n- Иметь базовые навыки использования Linux-утилит\n- Понимать, что такое контейнеризация; знать, что такое [Docker](https://www.docker.com/)\n- Иметь представление о том, как устроена архитектура современных web-приложений\n\n**Дисклеймер**\n\n**Эта схема не идеальна!** Она не подойдет для больших highload систем с сотнями сервисов. Однако в моём небольшом проекте она работает хорошо. Надеюсь, подойдёт и вам.\n\nЯ не считаю себя профессионалом в DevOps. Критикуйте, **предлагайте идеи и улучшения** в комментариях к этой статье, а так же в [Issues](https://github.com/Yu-Leo/deploy-to-docker-swarm/issues) и [Pull Requests](https://github.com/Yu-Leo/deploy-to-docker-swarm/pulls).\n\nРепозиторий с исходными конфигами: https://github.com/Yu-Leo/deploy-to-docker-swarm\n\n## Деплоим?\n\n### Что деплоим?\n\nИтак, для начала нужно определиться, что именно мы будем деплоить.\n\nВ текущей версии моя система состоит из 4-х компонентов:\n- СУБД PostgreSQL\n- Backend на Go\n- Frontend на React + nginx в качестве веб-сервера\n- Nginx proxy. Проксирует запросы от клиентов, распределяя их между backend и frontend. Обеспечивает поддержку протокола HTTP**S**\n\nПомимо этих компонентов я планирую добавить и другие сервисы: систему сбора метрик (prometheus), мониторинга (grafana), in-memory хранилища (redis), вспомогательные джобы и т. д. и т. п.\n\n![services.png](./img/services.png)\n\n### Куда деплоим?\n\nДля развертывания системы я решил использовать **Docker-контейнеры**, а для их оркестрации — **[Docker Swarm](https://docs.docker.com/engine/swarm/)**. В [этой статье](https://habr.com/ru/articles/659813/)  описаны его основные возможности. Меня же в первую очередь привлекла простота его настройки и широкие возможности. То, что нужно для небольшого проекта.\n\nВ моём случае всё крутится на одном сервере, но Docker Swarm позволяет использовать несколько.\n\n#### dev и prod окружения\n\n![dev-prod-deployments.png](./img/dev-prod-deployments.png)\n\nВсю архитектуру я хочу разворачивать в **двух окружениях**: dev и prod.\n\n`prod`:\n- Боевое окружение (production)\n- Единая точка входа для пользователей в виде nginx proxy\n- Рабочие версии сервисов\n- Продовые данные\n\n`dev`:\n- Тестовое окружение\n- Несколько точек входа для разработчиков (backend, frontend, ...), если это необходимо\n- Не всегда рабочие версии сервисов. Используется для тестов и экспериментов\n- Тестовые данные\n\nВ текущей реализации и dev и prod окружения разворачиваются на одной manager-ноде Docker Swarm, но в разных [стеках](https://docs.docker.com/engine/swarm/stack-deploy/). По мере роста проекта эти окружения могут быть разделены на различные сервера или группы серверов.\n\n### Как деплоим?\n\nВ качестве сервиса для хранения исходного кода проекта я выбрал **GitLab**. И, конечно, я хочу использовать его возможности **для автоматизации** сборки, тестирования и развертывания.\n\n## Поехали!\n\nТеперь реализуем систему для деплоя этого проекта.\n### Схема\n\n![deploy-scheme.png](./img/deploy-scheme.png)\n\nВсе сервисы запускаются **в Docker-контейнерах**. А значит, нужны их образы (Docker Images). Публичные образы (например, nginx и postgresql) тянутся с Docker Hub. Приватные (backend, frontend) — с приватного GitLab Registry проекта.\n\nПрежде чем спуллить образы с GitLab Registry их нужно туда запушить. Эта операция происходит в паплайнах репозиториев `backend` и `frontend`. В них хранятся исходники сервисов, а так же инструкции по сборке их Docker-образов. При необходимости в проект можно добавлять другие аналогичные репозитории.\n\nСердце этой системы развертывания — репозиторий `deploy-manifests` (на схеме слева внизу). Он содержит параметры запуска сервисов и их конфиги. Из его пайплайнов происходит всё взаимодействие с manager-нодой Docker Swarm.\n\n### Тестирование и сборка\n\n Главная **цель** пайплайнов в репозиториях `backend` и `frontend`, ... — **собрать** из исходников **Docker-образ** сервиса(-ов). Попутно можно запускать тесты, линтеры и т. д.\n\n#### Пайплайн в backend repository\n\n**Структура репозитория**\n\n[Репозиторий](https://github.com/Yu-Leo/deploy-to-docker-swarm/tree/main/backend)\n\n```sh\n.\n├── backend  # Директория одного сервиса \n├── build  # Директория сборки\n│   └── backend  # Исполняемый файл одного сервиса\n│   └── build_version.tmp  # Временный файл с тегом\n├── common  # Общий для нескольких сервисов код\n├── Dockerfile  # Общий Dockerfile\n├── .gitignore\n├── .gitlab-ci.yml  # Описание GitLab Pipeline\n├── .golangci.yml  # Конфиг для линтера\n├── go.mod\n├── go.sum\n└── Makefile  # Файл с инструкциями по сборке и тестированию демонов\n```\n\nПо структуре это **монорепозиторией**: в одном репозитории хранится код нескольких микросервисов. У этого подхода есть свои плюсы и минусы, но их рассмотрение выходит за рамки этой статьи.\n\n**Сборка**\n\n[`.gitlab-ci.yml`](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/backend/.gitlab-ci.yml)\n\n![backend-repository-pipeline.png](./img/backend-repository-pipeline.png)\n\nПайплайн состоит из двух джоб:\n- `build-lint-test`\n- `docker-images`\n\nВ `build-lint-test`, как следует из названия, происходят три действия: **сборка** исполняемого файла, запуск **линтера** и запуск **юнит-тестов**. Если на оном из этапов джоба падает, пайплайн дальше не идёт. Если же все этапы выполнились успешно, в GitLab сохраняется артефакт работы джобы — набор исполняемых файлов для каждого микросервиса (демона).\n\nВ джобе `docker-images` происходит **сборка Docker-образов** демонов на основе исполняемых файлов, а так же Dockerfile. После сборки полученные образы пушатся в GitLab Registry. Каждый образ получает уникальный тег, состоящий из даты и 8-ми цифр от хэш-суммы коммита (`20240725_4605cc6e`).\n\nЯ учел, что разным микросервисам могут понадобиться **разные Dockerfile** для сборки. Если в директории микросервиса есть Dockerfile, образ будет собираться по нему. Иначе будет использован [дефолтный Dockerfile](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/backend/Dockerfile), расположенный в корне репозитория.\n\n**Правила запуска джоб**\n\nВ моей реализации триггером запуска джоб являются **лейблы на MR в GitLab**.\n\n- Лейбл `tests` запускает только джобу `build-lint-test`\n- Лейбл `build-\u003cservice-name\u003e`, где `\u003cservice-name\u003e` — название микросервиса, запускает обе джобы: `build-lint-test` и `docker-images`. Build и push операции будут выполняться только для сервиса `\u003cservice-name\u003e`. Если указать несколько: `build-\u003cservice-name-1\u003e`, `build-\u003cservice-name-2\u003e`, будут собраны и запушены все соответствующие образы.\n\nДжобу `build-lint-test` можно также запустить вручную, без каких либо лейблов.\n\n#### Пайплайны в других репозиториях\n\nАналогичным образом можно настроить пайплайны в других репозиториях.\n\nВ качестве примера — мой репозиторий [frontend](https://github.com/Yu-Leo/deploy-to-docker-swarm/tree/main/frontend). Его пайплайн состоит из единственной джобы, которая собирает и публикует Docker-образ.\n\n**Важно!** В настройках каждого подключаемого репозитория необходимо дать доступ до скачивания образов из пайплайнов репозтория `deploy-manifests`:\n\n![allow-pull-images.png](./img/allow-pull-images.png)\n\nФактически скачивание будет происходить не в самом пайплайне, а на удаленном сервере при развертывании, но для доступа в приватный GitLab Registry используется токен из GitLab Job-ы.\n\n#### Комментариями с тегами образов\n\nКаждый образ собирается и пушится с уникальным тегом (пример: `20240722_ac34b89g`). Далее этот тег понадобится нам для деплоя конкретной версии сервиса.\n\nЧтобы не искать сгенерированный тег вручную, я настроил их **автоматическую публикацию** в комментарии к MR.\n\n**Как это выглядит**\n\nСообщение об успешной сборке образа сервиса `ping`:\n\n![ping-success.png](./img/ping-success.png)\n\nСообщение о фейле сборки и/или публикации образа:\n\n![ping-fail.png](./img/ping-fail.png)\n\n**Как это настроить**\n\nВ файле [.gitlab-ci.yaml](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/backend/.gitlab-ci.yml#L77)\n\nЭта фишка реализована при помощи [GitLab API](https://docs.gitlab.com/ee/api/rest/). Для его использования необходимо **получить токен** доступа.\n\nВ GitLab Free недоступны токены для джоб, но вместо них можно использовать токен от персонального аккаунта. Строго говоря, этот способ **не безопасен**, однако имеет место быть для небольших проектов. При этом комментарии будут публиковаться от имени владельца токена.\n\nЧтобы это повторить, нужно:\n\n1. Добавить ещё один токен для своего аккаунта\n\n![create-user-token.png](./img/create-user-token.png)\n\n2. Указать этот токен в ENV-переменной для пайплайна в репозитории\n\n![set-user-token-1.png](./img/set-user-token-1.png)\n\n![set-user-token-2.png](./img/set-user-token-2.png)\n\n\nПосле этого станет доступна публикация комментариев к MR в репозитории.\n\n### Сервер\n\n#### Подготовка\n\n**Описание** даже базовой **настройки удаленно сервера** получилось объемным, поэтому я вынес его в отдельную статью: \"[Настройка SSH на удаленном сервере](https://yu-leo.github.io/yu0dev/posts/setup-ssh-on-server/)\". Рекомендую прочесть её, прежде чем переходить далее.\n\n#### Пользователь `gitlab`\n\nПомимо основного пользователя `www`, которого мы создавали в вышеупомянутой статье, нужно завести пользователя `gitlab`. От его имени в джобах GitLab-пайплайна будет происходить подключение к серверу.\n\n1. Создаем пользователя (на сервере)\n\n```shell\nuseradd -s /bin/bash -m gitlab\n```\n\n2. Создаем ключи (на локальной машине)\n\n```shell\nssh-keygen -t ed25519\nmv id_ed25519.pub gitlab.pub\nmv id_ed25519 gitab\n```\n\nПароль от приватного SSH-ключа задавать **не** нужно.\n\n3. Редактируем файл `/etc/ssh/sshd_config` (на сервере)\n\n```sh\nPasswordAuthentication yes  # Временно разрешаем авторизацию по паролю\nAllowUsers www gitlab  # Добавляем пользователя gitlab\n```\n\n4. Закидываем **публичный** ключ с локальной машины на сервер\n\n```shell\nssh-copy-id -i ~/.ssh/gitlab.pub gitlab@\u003cip\u003e\n```\n\n5. Редактируем файл `/etc/ssh/sshd_config` (на сервере)\n\n```sh\nPasswordAuthentication no  # Закрываем обратно\n```\n\n6. Добавляем пользователя `gitlab` в группу `docker` (на сервере)\n\n```shell\nsudo usermod -aG docker gitlab\n```\n\n#### Docker Swarm\n\n[Инициализируем](https://docs.docker.com/reference/cli/docker/swarm/init/) Docker Swarm командой\n\n```shell\ndocker swarm init\n```\n\nДалее появится сообщение\n\n```\nSwarm initialized: current node (bvz81updecsj6wjz393c09vti) is now a manager.\n\nTo add a worker to this swarm, run the following command:\n\n    docker swarm join --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-1awxwuwd3z9j1z3puu7rcgdbx 172.17.0.2:2377\n\nTo add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.\n```\n\nв котором Docker сообщает, что текущая нода будет являться manager-нодой, и показывает, каким образом можно добавить worker-ноды в кластер.\n\nДля деплоя необязательно иметь worker-ноды. Можно использовать единственную manager-ноду и разворачиваться на ней.\n\n#### Структура директорий на сервере\n\n```sh\n.  # /\n├── ...\n└── data \n    └── PROJECT-NAME  # Название проекта\n        ├── data  # Данные сервисов\n        │   └── prod  # Данные prod-сервисов\n        │       ├── nginx-proxy\n        │       │   └── ssl\n        │       │       ├── ca.crt\n        │       │       ├── hostname.crt\n        │       │       └── hostname.key\n        │       └── postgres\n        └── deploy  # Данные деплоя\n            ├── dev  # Конфиги dev-сервисов\n            │   └── backend\n            │       └── config.toml\n            ├── docker-compose.dev.yaml\n            ├── docker-compose.prod.yaml\n            ├── get-config-versions.py\n            └── prod  # Конфиги prod-сервисов\n                ├── nginx\n                │   └── default.conf\n                ├── nginx-proxy\n                │   └── default.conf\n                └── backend\n                    └── config.toml\n```\n\nДля корректной работы нужно вручную создать часть директорий перед запуском развертывания:\n\n```shell\nmkdir -p /data/PROJECT-NAME/data/prod/postgres\nmkdir -p /data/PROJECT-NAME/data/prod/nginx-proxy\nmkdir -p /data/PROJECT-NAME/deploy\n```\n\nПри необходимости структуру можно поменять, не забыв обновить пути в файлах репозитория `deploy-manifests`.\n\n#### Docker Secrets\n\n[Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) — механизм Docker для управления **приватными** данными (секретами). Типичные примеры секретов: пароли, ключи, сертификаты. Секреты монтируются к Docker-контейнерам: содержимое секрета находится в файле `/run/secrets/secret-name` внутри контейнера. Ниже я опишу их использование подробнее.\n\nРассмотрим создание секретов на примере двух кейсов:\n\n**1. Пароль для пользователя в PostgreSQL**\n\nАвтоматически сгенерируем 32-х символьный пароль для юзера PostgreSQL и запишем его в Docker Secret с названием `postgres-passwd`:\n\n```shell\ntr -dc A-Za-z0-9 \u003c/dev/urandom | head -c 32 | docker secret create postgres-passwd\n```\n\nа можно сгенерировать пароль, после чего одновременно вывести его в терминал и записать в Docker Secret:\n\n```shell\ntr -dc A-Za-z0-9 \u003c/dev/urandom | head -c 32 | tee /dev/stderr | docker secret create postgres-passwd\n```\n\n**2. Сертификат для nginx**\n\nСитуация: на локальной машине наличествует в наличии файл с SSL-сертивикатом для nginx. Нужно закинуть его в Docker Secret на удалённом сервере, чтобы в дальнейшем использовать в nginx.\n\nКопируем файл на удаленный сервер:\n\n```shell\nscp /path/to/file/on/local usename@servername:/path/to/file/on/server\n```\n\nКладем содержимое файла в Docker Secret командой:\n\n```shell\ncat /path/to/file/on/server | docker secret create nginx-crt -\n```\n\n### deploy-manifests\n\nПерейдём к главному компоненту моей системы развертывания — репозиторию `deploy-manifests`. Из его пайплайнов происходит запуск сервисов в Docker Swarm.\n\n#### Структура репозитория\n\n```sh\n.\n├── copy-files.sh  # Файл, копирующий содержимое из репозитория на сервер\n├── deploy.sh  # Файл с инструкциями по развертыванию системы\n├── dev  # Конфиги для dev-окружения\n│   └── backend  # Конфиги сервиса backend в dev-окружении\n│       └── config.toml\n├── docker-compose.dev.yaml  # Описание стека dev-окружения\n├── docker-compose.prod.yaml  # Описание стека prod-окружения\n├── get-config-versions.py  # Файл, генерирующий версии конфигов\n├── .gitignore\n├── .gitlab-ci.yml  # Описание GitLab Pipeline\n├── prod  # Конфиги для prod-окружения\n│   ├── backend  # Конфиги сервиса backend в prod-окружении\n│   │   └── config.toml\n│   └── nginx-proxy  # Конфиги сервиса nginx-proxy в prod-окружении\n│       └── default.conf\n└── requirements.txt  # Зависимости для скипта get-config-versions.py\n```\n\n#### Подключение к серверу\n\nДля подключения к удаленному серверу из джоб пайплайна необходимо **указать** следующие **ENV-переменные** в настройках репозитория:\n- `SERVER_HOST` — ip-адрес manager-ноды Docker Swarm\n- `USER_NAME` — пользователь на удаленном сервере, от имени которого будет происходить деплой. Как его создать я описывал выше, в разделе \"Сервер\" -\u003e \"Пользователь gitlab\"\n- `SSH_KEY` — приватный ssh-ключ для вышеупомянутого пользователя\n\n![set-env-vars-for-deploy.png](./img/set-env-vars-for-deploy.png)\n\n#### Пайплайн\n\n[`.gitlab-ci.yaml`](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/.gitlab-ci.yml)\n\nПайплайн состоит из двух независимых джоб: `dev-deploy` и `prod-deploy`. Как следует из названия, каждая из них **запускает деплой** соответствующего **окружения**. Обе джобы имеют одинаковую логику; отличаются только окружениями\n\nСуть каждой джобы сводится к двум действиям:\n1. **Скорпировать** на удаленный сервер **файлы** из репозитория. При этом копируются конфиги только соответствующего джобе окружения\n2. **Запустить** на удалённом сервере **скрипт** `deploy.sh` для развертывания стека сервисов. О нем подробнее далее\n#### docker-compose.mode.yml\n\nЭто файл, в котором задается **конфигурация для деплоя** соответствующего `mode` стека сервисов: `dev` либо `prod`. Если вы уже работали с docker-compose, его структура вам знакома, однако для использования в режиме Docker Swarm есть дополнительные настройки.\n\n**Описание [`docker-compose.prod.yml`](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/docker-compose.prod.yaml):**\n\nНа самом верхнем уровне `docker-compose.mode.yml` состоит из нескольких секций:\n- `services:` — сервисы, входящие в стек\n- `networks:` — связывающие их сети\n- `configs:` — [Docker-конфиги](https://docs.docker.com/reference/cli/docker/config/)\n- `secrets:` — [Docker-секреты](https://docs.docker.com/reference/cli/docker/secret/)\n\n**Services**\n\nБлок, в котором описывается набор сервисов, входящих в стек. В качестве примера рассмотрим базовые настройки сервиса [`backend`](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/docker-compose.prod.yaml#L40):\n\n```yaml\nservices:\n  # ...\n  backend:\n    hostname: backend\n    image: ${REGISTRY}/PROJECT-NAME/backend/backend:20240727_a5fa34c7\n    environment:\n      DB_PASS_FILE: /run/secrets/project-prod-postgres-passwd\n    depends_on:\n      - postgres\n```\n\n- `hostname` — имя сервиса во внутренней сети Docker-стека\n- `image: ${REGISTRY}/PROJECT-NAME/backend/backend:20240727_a5fa34c7` — путь до образа сервиса. После двоеточия указывается тег.\n- `environment` — описание переменных окружения\n- `depends_on` — зависимости между сервисами. `backend` будет запущен после сервиса `postgres`\n\n**Configs**\n\n```yaml\nservices:\n  # ...\n  backend:  \n    # ...\n    configs:  \n      - source: backend-config  \n        target: /app/config.toml\n\t# ...\nconfigs:  \n  backend-config:  \n    name: ${PROJECT_PROD_BACKEND_CONFIG}\n    file: ./prod/backend/config.toml\n```\n\nDocker-конфиги — файлы, хранящие в себе **нечувствительную** информацию. Не подходят для хранения паролей и сертификатов, но подходят (как и следует из названия) для хранения конфигов.\n\nDocker не умеет автоматически обновлять сервис, если конфиг изменился. Для решения этой проблемы придумано несколько решений; я расскажу о своём.\n\nКаждому конфигу, описанному в глобальной секции `configs:` docker-compose файла, присваивается имя (`name:`). Его значение берётся из переменной окружения (`${PROJECT_PROD_BACKEND_CONFIG}`).\n\nКаждый раз при запуске скрипта [deploy.sh](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/deploy.sh) (о нём подробнее далее) значения этих переменных инициализируются:\n1. Запускается скрипт [`get-config-versions.py`](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/get-config-versions.py) . Результатом его работы являются строки вида `PROJECT_PROD_BACKEND_CONFIG=md5(config-file)`, где `md5(config-file)` — часть хэша от содержимого файла с конфигом. Путь до файла берётся из поля `file:` docker-compose файла.\n2. В скрипте deploy.sh на основе полученных строк [инициализируются переменные окружения](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/deploy.sh#L27).\n\nКак это влияет на работу сервисов:\n\n- Конфиг не изменился =\u003e не изменился его хэш =\u003e не изменилось имя Docker-конфига =\u003e Docker не будет обновлять сервис =\u003e сервис продолжит работу со своим конфигом\n- Конфиг изменился =\u003e изменился его хэш =\u003e изменилось имя Docker-конфига =\u003e Docker будет обновлять сервис =\u003e после обновления сервис будет использовать новый конфиг\n\nОписание настроек из docker-compose файла:\n\n- `source: backend-config` — название Docker-конфига, который нужно подключить к сервису\n- `target: /app/config.toml` — путь, по которому конфиг будет расположен в контейнере сервиса\n- `name: ${PROJECT_PROD_BACKEND_CONFIG}` — имя Docker-конфига\n- `file: ./prod/backend/config.toml` — путь до конфига на manager-ноде\n\nДля работы скрипта `get-config-versions.py` требуется установить [зависимости](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/requirements.txt) (на сервере):\n\n```shell\npip3 install -r ./requirements.txt\n```\n\n**Secrets**\n\n```yaml\nservices:\n  # ...\n  backend:  \n    # ...\n    secrets:  \n      - project-prod-postgres-passwd\n\t# ...\nsecrets:  \n  project-prod-postgres-passwd:  \n    external: true\n```\n\nКак я уже говорил, Docker-секреты — это файлы, хранящие в себе **чувствительную** информацию: пароли, ключи и т. п.\n\n`external: true` указывает, что Docker-секрет заранее создан в Docker Swarm. Выше я описывал процесс создания Docker-секретов на сервере\n\nФайлы с Docker-секретами будут примонтированы в директорию `/run/secrets/` внутри контейнера с сервисом. Описанный выше секрет `project-prod-postgres-passwd` расположится в файле `/run/secrets/project-prod-postgres-passwd`.\n\n[Официальная документация](https://docs.docker.com/compose/compose-file/05-services/#secrets)\n\n**Healthcheck**\n\n```yaml\nservices:\n  # ...\n  backend:  \n    # ...\n    healthcheck:  \n      test: curl --fail http://127.0.0.1:8080/api/service/ping || exit 1\n      interval: 10s\n      timeout: 3s  \n      retries: 5\n```\n\nПроверяет работоспособность каждой реплики сервиса.\n- `test:` — команда, которая будет выполняться в контейнере. Если она завершится с кодом `0` — сервис жив и готов к работе, если c кодом `1` — сервис нездоров. В примере: стучимся на ручку `ping` сервиса; если она не отвечает, сервис не проходит healthcheck\n- `interval:` — интервал проверки\n- `timeout:` — таймаут на выполнение хелсчек-команды\n- `retries:` — сколько раз проверка должна упасть — вернуть ненулевой код ошибки — прежде чем на контейнер будет признан unhealthy\n\n[Официальная документация](https://docs.docker.com/compose/compose-file/05-services/#healthcheck)\n\n**Deploy**\n\n```yaml\nservices:\n  # ...\n  backend:  \n    # ...\n    deploy:  \n      replicas: 1  \n      restart_policy:  \n        delay: 5s  \n        condition: on-failure  \n      update_config:  \n        parallelism: 1  \n        order: start-first  \n        failure_action: rollback  \n        delay: 10s  \n      rollback_config:  \n        parallelism: 0  \n        order: stop-first  \n      resources:  \n        limits:  \n          cpus: \"0.7\"  \n          memory: \"800M\"\n```\n\nСекция, в которой описываются параметры, относящиеся к развертыванию сервиса. Разберём подробнее.\n\n[Официальная документация](https://docs.docker.com/compose/compose-file/05-services/#deploy)\n\n**RollingUpdate**\n\n```yaml\nservices:\n  # ...\n  backend:  \n    # ...\n    deploy:  \n      update_config:  \n      \tparallelism: 1  \n      \torder: start-first  \n        failure_action: rollback  \n      \tdelay: 10s  \n      rollback_config:  \n\t      parallelism: 0  \n\t      order: stop-first\n```\n\nПлавная выкатка. Механизм, при помощи которого можно **постепенно** обновить сервис с одной версии на другую.\n\nКонфигурация, приведённая выше, описывает следующее поведение: \n1. Поднимается **одна реплика новой версии**, при этом остаются все реплики старой версии.\n2. После того как реплика успешно поднялась и helthcheck-проверка успешно прошла, выключается **одна реплика старой версии**.\n3. Пункты 1 и 2 повторяются, пока не будут **обновлены все реплики**\n4. Если в какой-то момент раскатка прервётся, все новые реплики разом остановятся и будут подняты реплики старой версии.\n\n![rolling-update.png](./img/rolling-update.png)\n\nЧтобы проверить и наглядно показать работу этой функции я написал небольшой скрипт. Каждую секунду он посылает запрос на ручку `/api/service/version` моего сервиса, поле чего выводит ответ.\n\n```sh\nwhile true; do echo -n \"$(date) \"; curl -sS http://SERVER_ADDRES:8080/api/service/version; echo ''; sleep 1; done\n```\n\nДалее я запустил этот скрипт на локальной машине параллельно с запуском выкатки новой версии сервиса. Результат:\n\n```\nСр 17 июл 2024 21:52:08 MSK {\"name\":\"backend\",\"version\":\"20240704_63586a62\"}\nСр 17 июл 2024 21:52:09 MSK {\"name\":\"backend\",\"version\":\"20240704_63586a62\"}\nСр 17 июл 2024 21:52:10 MSK {\"name\":\"backend\",\"version\":\"20240717_8a4f53ae\"}\nСр 17 июл 2024 21:52:11 MSK {\"name\":\"backend\",\"version\":\"20240704_63586a62\"}\nСр 17 июл 2024 21:52:12 MSK {\"name\":\"backend\",\"version\":\"20240717_8a4f53ae\"}\nСр 17 июл 2024 21:52:13 MSK {\"name\":\"backend\",\"version\":\"20240717_8a4f53ae\"}\n```\n\nСначала все запросы попадали на поды со старой версией сервиса (`20240704_63586a62`), затем один запрос попал на новую (`20240717_8a4f53ae`) и одни на старую, после чего все запросы стали попадать только на новую версию сервиса.\n\nЭто объясняется тем, что во время раскатки нового сервиса в некоторый момент времени существовали одновременно поды старой и новой версии. Docker автоматически распределяет запросы между подами, поэтому часть запросов попала на новую версию, часть — на старую. По завершении процесса раскатки подов со старой версией не осталось и все запросы стали приходить только на поды с новой версией.\n\nЕсли во время раскатки поды не будут проходить helthcheck-проверки, произойдет rollback, то есть автоматический откат до старой версии. В таком случае в логах GitLab джобы появится сообщение:\n\n```\nrollback: update rolled back due to failure or early termination of task y4skuy8si13qy1hrzui7inzcn\n```\n\nЭтот лог можно обработать и вывести алерт о произошедшем откате.\n\n[Официальная документация по update_config](https://docs.docker.com/compose/compose-file/deploy/#update_config)\n\n[Официальная документация по rollback_config](https://docs.docker.com/compose/compose-file/deploy/#rollback_config)\n\n**Resources**\n\n```yaml\nservices:\n  # ...\n  backend:  \n    # ...\n    deploy:\n      resources:  \n        limits:  \n          cpus: \"0.7\"  \n      \t  memory: \"800M\"\n```\n\nВ этой секции задаются требования и лимиты по CPU и оперативной памяти. Подробнее — в [официальной документации](https://docs.docker.com/compose/compose-file/deploy/#resources).\n\n**Volumes**\n\n[Docker Volume](https://docs.docker.com/engine/storage/volumes/) — механизм, позволяющий расшарить данные (файлы, директории) между контейнерами и хостовой машиной. Преимущество использования Docker Volumes состоит в том, что после остановки или удаления контейнера данные остаются на хостовой ОС. Это можно использовать для хранения файлов БД и подключения их в контейнер с СУБД:\n\n```yaml\nservices:  \n  postgres:  \n    # ... \n    volumes:  \n      - /data/PROJECT-NAME/data/prod/postgres:/var/lib/postgresql/data\n```\n\nДиректория `/data/PROJECT-NAME/data/prod/postgres` на хостовой ОС будет соответствовать директории `/var/lib/postgresql/data` внутри Docker-контейнера. Таким образом все изменения, которые PostgreSQL производит внутри контейнера, будут сохраняться и на хостовой ОС. Если контейнер остановится, данные не потеряются.\n\n#### Правила запуска пайплайна\n\nЕсть 3 триггера для запуска джоб деплоя\n- Лейблы на MR. Лейбл `dev-deploy` триггерит джобы деплоя dev-окружения, `prod-deploy` — prod-окружения. Лейблы не зависят друг от друга.\n- Запуск в MR вручную. Любую джобу можно независимо запустить из любого MR\n- Merge в main-ветку. При мердже в main-ветку автоматически запускается джоба `prod-deploy`. `dev-deploy` не запускается, поскольку иногда полезно оставить в dev-среде то состояние, которое было развернуто с ещё не смердженного MR\n\n#### deploy.sh\n\nОписание файла [`deploy.sh`](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/deploy.sh).\n\nЭтот скрипт запускается на удаленном сервере и обновляет сервисы соответствующего окружения (`--dev` или `--prod` при запуске).\n\nПроцесс обновления окружения состоит из нескольких этапов:\n\n1. Подготовительный этап №1: **определение режима запуска** скрипта (`dev` или `prod`). Переход в рабочую директорию.\n\n2. Подготовительный этап №2: **обновление конфигов**. Как я уже говорил, Docker не умеет в автоматичекое обновление конфигов, поэтому эту проблему нужно решить самостоятельно (см. раздел \"docker-compose.mode.yml\" -\u003e \"configs\"). На этом этапе запускается скрипт [`get-config-versions.py`](https://github.com/Yu-Leo/deploy-to-docker-swarm/blob/main/deploy-manifests/get-config-versions.py) и инициализируются сгенерированные им переменные окружения.\n\n3. Основной этап: **обновление деплоймента** средствами Docker Swarm. Здесь стоит упомянуть, что Docker Swarm умный: он не будет убивать контейнеры и поднимать новые, если у сервиса не изменился конфиг, тег или параметры в `docker-compose.mode.yml`\n\n4. Заключительный этап: **очистка неиспользуемых объектов** Docker. В том числе удаление лишних конфигов: в скрипте запускается команда на удаление всех конфигов; Docker знает, какие из них ещё используются, и удаляет только неиспользуемые\n\n## Итоги\n\nИсходники: https://github.com/Yu-Leo/deploy-to-docker-swarm\n\nВот такая система для развертывания сервисов у меня получилась. Повторюсь: она далека от идеала, однако имеет право на жизнь и использование в небольших проектах.\n\n### А что дальше?\n\nБесконечный простор для творчества. Можно улучшать и добавлять новую функциональность как пайпалайнам сборки, так и пайплайну деплоя.\n\nВот несколько идей, которые я собираюсь реализовать в своей системе:\n- end-2-end тестирование. Поднятие полноценного тестового окружения в пайплайне репозитория backend, запуск e2e-тестов в пайплайне\n- Добавление системы метрик, графиков и алертов\n- Добавление системы сбора, фильтрации и отображения логов и ошибок\n- Улучшения системы оповещения о сбоях в процессе деплоя\n\nПредлагайте свои идеи улучшений в комментариях, а так же в [Issues](https://github.com/Yu-Leo/deploy-to-docker-swarm/issues) и [Pull Requests](https://github.com/Yu-Leo/deploy-to-docker-swarm/pulls) в репозитории!\n\n\n---\n\nЭта статья на Habr: https://habr.com/ru/articles/836850/\n\nЭта статья в моём блоге: https://yu-leo.github.io/yu0dev/posts/deploy-to-docker-swarm/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyu-leo%2Fdeploy-to-docker-swarm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyu-leo%2Fdeploy-to-docker-swarm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyu-leo%2Fdeploy-to-docker-swarm/lists"}