Продолжаю эксперименты с CI/CD. После первого опыта деплоя занялся разработкой CI/CD для Mailbroker. В этом сервисе задействованы сразу несколько контейнеров, а значит простым «docker run» не обойтись — слишком длинная команда получится.
Проблематика
Я знаю, что docker-compose — это элегантный способ сделать сразу все:
- запустить контейнеры
- передать переменные окружения
- создать сети и связать контейнеры между собой
Сейчас docker-compose.yml я использую для локальной разработки. А также на «lamp-сборке», которая работает на сервере уже несколько лет. Но использовать в полноценном CI/CD такой файл нельзя:
- монтирование каталогов проекта в контейнеры — удобно для разработки, но на продакшене такой способ не приемлем
- используется «build», что также тянет зависимость в виде файлов проекта
Хотелось бы иметь такой файл, который, с одной стороны, описывает все связи, а с другой — не тянет ненужные зависимости.
И, наконец, я изобрел такой способ, а также провел эксперимент.
docker-compose.deploy.yml
Вот так выглядит оригинальный файл из отладочного проекта «hello»:
version: "3.8"
services:
nginx:
build:
dockerfile: ./docker/nginx/Dockerfile
context: .
volumes:
- ./docker/nginx/conf.d/:/etc/nginx/conf.d/
- ./public/:/var/www/
ports:
- ${HTTP_PORT}:80
restart: always
Если просто скопировать этот файл в отдельный каталог и попробовать запустить, ничего не получится: нет необходимых файлов, и не определена переменная HTTP_PORT.
А что если для CI/CD сделать отдельный файл? И он был сделан:
version: "3.8"
services:
nginx:
image: hello_nginx:${CI_COMMIT_SHA}
ports:
- ${HTTP_PORT}:80
environment:
- HTTP_PORT=${HTTP_PORT}
restart: always
В нем сборка образа заменена запуском конкретной версии на основе CI_COMMIT_SHA, значит есть возможность отката назад путем простого перезапуска прошлого деплоя. А переменная HTTP_PORT берется из GitLab variables, которые я проставил в настройках проекта.
Теперь — как это работает. Сокращенное содержимое файла .gitlab-ci.yml:
stages:
- build
- deploy
build docker image:
stage: build
tags:
- docker-image-builder
script:
- echo "Commit SHA ${CI_COMMIT_SHA}" >> public/index.html
- docker build . -f docker/nginx/Dockerfile -t hello_nginx:${CI_COMMIT_SHA} -t hello_nginx:latest
run container from docker-compose.deploy.yml:
stage: deploy
tags:
- deploy
script:
- cp docker-compose.deploy.yml /tmp/
- cd /tmp/
- docker-compose -f docker-compose.deploy.yml up -d nginx
На первом этапе происходит сборка и тегирование образа hello_nginx:${CI_COMMIT_SHA}. На втором этапе файл копируется во временный (!) каталог, и происходит запуск с подстановкой переменных окружения. Это работает.
Но что произойдет, если перезагрузиться, ведь при этом очищается каталог /tmp? И я перезагрузил сервер. На удивление, все продолжило работать. Если бы я разбирался в деталях, как работает docker-compose, вопроса бы не было. Но я не знаю, и поэтому пришлось ставить эксперимент.
А что насчет переменных окружения? Они также были сохранены в контейнере:
docker exec f7f60beaedff env
# вывод
...
HTTP_PORT=8005
...
Финальный вариант
Чтобы резюмировать все стадии деплоя с использованием docker-compose, я составил такой шорт-лист:
- прописать переменные в настройках проекта GitLab
- пипелина: собирать образы с тегами (docker build . -f docker/nginx/Dockerfile -t hello_nginx:${CI_COMMIT_SHA})
- создать отдельный docker-compose.deploy.yml, в котором: использовать теги образов (image: hello_nginx:${CI_COMMIT_SHA}), передавать ports и environment
- пипелина: запускать сборку так: docker-compose -f docker-compose.deploy.yml up -d nginx