ADG Оставить заявку
Блог DevOps 5 мин чтения

Эфемерные среды для каждого MR: Docker Compose поднимает стек за минуту

Строим ephemeral environments в GitLab CI: Docker Compose разворачивает изолированный стек на каждый merge request, после мержа - автоматически сносится.

Контекст момента

Практика ephemeral environments в CI/CD на базе Docker Compose и Kubernetes namespaces

Тестовая среда, которую никто не сносит - это баг, который живёт вечно. Мы несколько недель назад переключились на подход ephemeral environments: каждый merge request получает собственный изолированный стек, который поднимается при открытии MR и сносится при мерже или закрытии. Расскажем как это выглядит на Docker Compose и почему не сразу полезли в Kubernetes namespaces.

Проблема с «вечной» staging-средой

Классика: есть одна staging-среда, в неё деплоят всё подряд, состояние непредсказуемое. Разработчик А задеплоил свою ветку, разработчик Б - свою, они конфликтуют на уровне базы данных, тестировщик не понимает что вообще сейчас там крутится. Отдельная боль - «я только что деплоил, всё работало» и «у меня не работает» от двух людей одновременно.

Плюс staging-среда обычно живёт постоянно, её данные расходятся с production, и она превращается в отдельный legacy-артефакт который надо поддерживать.

Ephemeral environments - другая механика. Каждый MR получает свой стек: своя база данных, свой application server, свой набор переменных. Параллельно могут работать десять сред по числу открытых MR, они не пересекаются. Закрыл MR - среда ушла.

Почему начали с Docker Compose, а не с Kubernetes namespaces

У нас есть несколько кластеров под managed-управлением, и идея с Kubernetes namespaces выглядит логично: создал namespace на каждый MR, задеплоил через Helm, после - удалил namespace. Аккуратно, изолированно, масштабируется.

Но там есть нюансы инициализации: надо тащить в namespace секреты, настраивать RBAC, давать права CI-пользователю на создание и удаление namespaces. Для команды которая только налаживает процесс - порог входа выше чем хотелось бы. Docker Compose на выделенном CI-раннере оказался быстрее в запуске: три часа настройки вместо двух дней.

Шаблон пайплайна

Пайплайн в .gitlab-ci.yml выглядит примерно так:

stages:
  - test
  - deploy-review
  - cleanup

variables:
  COMPOSE_PROJECT_NAME: "mr-${CI_COMMIT_REF_SLUG}"
  APP_PORT: "3${CI_JOB_ID: -3}"

deploy-review:
  stage: deploy-review
  script:
    - docker-compose -f docker-compose.review.yml up -d --build
    - echo "Review env: http://${CI_RUNNER_TAGS}:${APP_PORT}"
  environment:
    name: review/${CI_COMMIT_REF_SLUG}
    url: http://review.internal:${APP_PORT}
    on_stop: stop-review
  only:
    - /^mr-/

stop-review:
  stage: cleanup
  script:
    - docker-compose -f docker-compose.review.yml down -v
  environment:
    name: review/${CI_COMMIT_REF_SLUG}
    action: stop
  when: manual
  only:
    - /^mr-/

Ключевых момента два. Первый - COMPOSE_PROJECT_NAME включает номер MR, и Docker Compose изолирует все ресурсы (контейнеры, сети, тома) под этим именем. Десять параллельных MR - десять изолированных проектов. Второй - on_stop указывает GitLab что при закрытии MR нужно запустить job stop-review. GitLab сам вызывает его когда MR мержится или закрывается.

docker-compose.review.yml отличается от production-compose несколькими вещами:

services:
  app:
    build: .
    ports:
      - "${APP_PORT}:8080"
    environment:
      - DATABASE_URL=postgresql://app:app@db:5432/app_${CI_COMMIT_REF_SLUG}
      - ENV=review

  db:
    image: postgres:10
    environment:
      - POSTGRES_DB=app_${CI_COMMIT_REF_SLUG}
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=app
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

База данных поднимается своя, пустая, с именем включающим номер MR. Никакого пересечения данных между средами. При down -v том удаляется вместе с контейнером.

Что работает хорошо

Изоляция. Десять MR - десять независимых баз. Разработчики не мешают друг другу, тестировщик знает точно что в среде для MR-47 крутится именно MR-47.

Скорость. Образы кешируются на раннере, up --build для типичного приложения - полторы-две минуты. Это быстрее чем ждать пока staging-среда освободится.

Автоматическая очистка. on_stop работает надёжно - проверили на двух-трёх циклах. Среды не накапливаются, раннер не забивается старыми контейнерами.

Ссылка в MR. GitLab показывает кнопку «View app» прямо в интерфейсе merge request - тестировщику не нужно искать адрес, он всегда в MR.

Где трётся

Порты. Простая арифметика «808 + номер» не работает при двузначных числах - порт выходит за пределы диапазона или конфликтует. Мы взяли три последних цифры CI_JOB_ID со смещением, но это самодеятельность которая требует поддержки. Нормальное решение - reverse proxy на раннере и маршрутизация по хосту вместо порта, но это следующий шаг.

Ресурсы раннера. Десять сред с postgres - это десять инстансов PostgreSQL, каждый ест память. Пока влезает, но у нас проекты небольшие. Если сред будет больше или база тяжелее - раннер станет узким местом.

Подготовка данных. Пустая база - хорошо для юнит-тестов, но тестировщику часто нужно что-то с данными. Пишем отдельный seed-скрипт который наполняет базу минимальным набором фикстур при старте среды. Это чуть усложнило deploy-review job, зато среда сразу пригодна для ручного тестирования.

Внешние зависимости. Среда изолирована внутри, но если приложение ходит во внешние сервисы - S3, внешнее API - это или мокается или ходит в тот же production-эндпоинт что и все. Для нас пока решается моками в ENV переменных, но это надо держать в голове.

Kubernetes namespaces: куда движемся

Docker Compose на раннере - рабочее решение, но с ограничениями по масштабу. Там где сред нужно много, или команды большие, или уже есть кластер - Kubernetes namespaces логичнее. Механика та же: kubectl create namespace mr-47, деплой через Helm с переопределением values, kubectl delete namespace mr-47 при закрытии MR.

Мы это пробуем параллельно на одном из кластеров - пока на уровне скриптов, без автоматики GitLab environments. Сложнее в части прав доступа, но масштабируется лучше и ресурсы управляются через request/limit а не через «сколько влезет на раннере».

Пока оба подхода живут в параллели: Docker Compose там где просто и быстро, Kubernetes там где уже есть кластер и нужна изоляция на уровне сети.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.