GitLab CI/CD: пайплайн автодеплоя для финтех-команды после двух продакшн-инцидентов
Семёнов Евгений Сергеевич, АйТи Фреш. Этот проект начался с двух ночных звонков за три недели: техдир финтеха в Москве потерял четыре часа на откате кривого ручного релиза, а на следующий спринт его старший бэкендер по ошибке зарелизил staging в production через rsync. Через месяц мы передали им отлаженный GitLab-пайплайн с защитой от дураков, автооткатом и уведомлениями в Slack. Ниже — детали, код и грабли.
Почему ручной деплой — билет в 3 часа ночи
У клиента была классическая схема: код лежит в GitLab, разработчики жмут «merge», потом кто-то дежурный заходит по SSH на прод и делает git pull && systemctl restart. Звучит невинно, но:
- Нет атомарности. Pull прошёл, restart упал — сервис мёртв.
- Нет истории: что и когда задеплоили — только по логам bash, если их вообще писали.
- Нет отката одной кнопкой. Нужно руками делать git revert и снова рестартить.
- Никакого separation of duties. Кто угодно с доступом может зарелизить что угодно.
Первый инцидент стоил им 4 часа простоя платёжного шлюза — примерно 280 000 руб. упущенной комиссии. Второй — утечка тестовых ключей, потому что staging-переменные попали в production-конфиг. После этого мне дали две недели и свободу действий.
Архитектура пайплайна
Я всегда строю CI/CD вокруг трёх принципов: воспроизводимость, защищённый прод, быстрый откат. В случае нашего финтеха пайплайн состоит из четырёх стадий: build → test → deploy-staging → deploy-production. Первые три отрабатывают автоматически на push в main, четвёртая — только по кнопке двух аппруверов.
| Стадия | Триггер | Время | Раннер |
|---|---|---|---|
| build | push, MR | 2–4 мин | docker build, 8 vCPU |
| test | after build | 3–5 мин | docker, 4 vCPU |
| deploy-staging | main auto | 40 сек | deploy, 1 vCPU |
| deploy-production | manual + 2 approvals | 50 сек | deploy prod |
| rollback | manual/auto | 25–30 сек | deploy prod |
Установка GitLab Runner
Раннер мы поставили отдельно от GitLab-сервера — принцип «не запускать артефактную сборку на управляющем хосте» спасает в момент ЧП. Отдельная виртуалка 8 vCPU, 16 ГБ RAM, Ubuntu 22.04, Docker-in-Docker executor:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.fintech.local/" \
--registration-token "GLRT-XXXX" \
--executor "docker" \
--docker-image "docker:24" \
--docker-privileged \
--docker-volumes "/var/run/docker.sock:/var/run/docker.sock" \
--description "build-runner-01" \
--tag-list "docker,build"
Deploy-раннер поставили отдельный, на bastion-хосте внутри production-сети, с минимальным доступом — только к двум целевым серверам через SSH. Это важно: build-раннер имеет доступ в интернет для apt/npm, а deploy — ни одной лишней связности.
Файл .gitlab-ci.yml целиком
Показываю рабочую версию (упрощённую, без лишних деталей финтеха):
stages:
- build
- test
- deploy-staging
- deploy-production
variables:
DOCKER_DRIVER: overlay2
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
build:
stage: build
tags: [docker, build]
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $IMAGE_TAG .
- docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
- docker push $IMAGE_TAG
- docker push $CI_REGISTRY_IMAGE:latest
unit-tests:
stage: test
image: $IMAGE_TAG
tags: [docker, build]
script:
- pytest --cov=app --cov-fail-under=80
deploy-staging:
stage: deploy-staging
tags: [deploy, staging]
environment:
name: staging
url: https://staging.fintech.local
script:
- ssh deploy@stg-01 "docker pull $IMAGE_TAG && docker-compose up -d"
- ./scripts/healthcheck.sh https://staging.fintech.local/health
only: [main]
deploy-production:
stage: deploy-production
tags: [deploy, production]
environment:
name: production
url: https://fintech.local
on_stop: rollback-production
script:
- ./scripts/deploy-prod.sh $IMAGE_TAG
when: manual
only: [main]
rollback-production:
stage: deploy-production
tags: [deploy, production]
environment:
name: production
action: stop
script:
- ./scripts/rollback.sh
when: manual
Защищённые переменные и secrets
Хранить пароли в gitlab-ci.yml — грех. Переменные пачкой ложатся в Settings → CI/CD → Variables с двумя галками: «Protected» (виден только на защищённых ветках) и «Masked» (не показывается в логах). Для особо чувствительных вещей — API-ключи платёжных шлюзов, токены банковских API — подключаем HashiCorp Vault:
deploy-production:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.fintech.local
secrets:
DB_PASSWORD:
vault: fintech/prod/db@kv
token: $VAULT_ID_TOKEN
script:
- echo "DB_PASSWORD=$DB_PASSWORD" > /run/app.env
- ./scripts/deploy-prod.sh $IMAGE_TAG
JWT от GitLab Runner обменивается на токен Vault на лету, секрет попадает в job как переменная окружения и исчезает после завершения. В репозитории — ноль sensitive data.
Автоматический откат по health-check
Вот та самая часть, ради которой всё это затевалось. Скрипт deploy-prod.sh после запуска нового контейнера опрашивает /health-эндпоинт раз в две секунды. Если 30 проверок подряд возвращают код не 200 — запускает redeploy предыдущего SHA:
#!/bin/bash
set -euo pipefail
NEW_TAG=$1
PREV_TAG=$(docker inspect --format='{{.Config.Image}}' app-current 2>/dev/null || echo "")
ssh deploy@prod-01 "docker pull $NEW_TAG && \
docker rename app-current app-prev 2>/dev/null || true && \
docker run -d --name app-current -p 8080:8080 $NEW_TAG"
for i in {1..30}; do
code=$(curl -s -o /dev/null -w "%{http_code}" https://fintech.local/health || echo "000")
if [[ "$code" == "200" ]]; then
ssh deploy@prod-01 "docker rm -f app-prev 2>/dev/null || true"
echo "Deploy OK: $NEW_TAG"; exit 0
fi
sleep 2
done
echo "Health check failed — rolling back to $PREV_TAG"
ssh deploy@prod-01 "docker rm -f app-current && docker rename app-prev app-current"
curl -X POST "$SLACK_WEBHOOK" -d "{\"text\":\"Deploy rolled back: $NEW_TAG\"}"
exit 1
Уведомления в Slack и Telegram
Последний штрих — каждая удачная и неудачная job отправляет алерт. Slack через Incoming Webhook, Telegram через бота:
.notify_template: ¬ify
after_script:
- |
if [ "$CI_JOB_STATUS" = "failed" ]; then
curl -s -X POST "$TG_WEBHOOK" -d "chat_id=$TG_CHAT" \
-d "text=CI FAIL: $CI_PROJECT_PATH job $CI_JOB_NAME branch $CI_COMMIT_REF_NAME"
fi
deploy-production:
<<: *notify
# ... остальная конфигурация
Результат у клиента
Проект занял 12 рабочих дней. За первые два месяца после запуска команда сделала 94 релиза в production без единого инцидента. Среднее время выкатки фичи упало с 45 минут ручной возни до 6 минут автоматики. Два раза сработал автоматический rollback — оба раза команда даже не узнала, пока не пришло уведомление в Slack. Стоимость внедрения вышла 180 000 руб., что клиент окупил на первом же сохранённом инциденте.
Построим GitLab CI/CD, который нельзя сломать кнопкой
Настраиваю GitLab под ключ: установка self-hosted инстанса на ваших серверах, раннеры под Docker и Kubernetes, пайплайны build/test/deploy с защищённым продом и автооткатом, Vault для секретов, интеграция со Slack и Telegram. Для команд 5–40 человек — обычно 7–15 рабочих дней.
Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш
FAQ — вопросы по GitLab CI/CD
- GitLab CI/CD — это платно?
- GitLab Community Edition, развёрнутый у себя, бесплатен полностью: пайплайны, environments, Container Registry, переменные. На gitlab.com бесплатный план даёт 400 минут CI/CD в месяц на shared runners. Для большинства команд этого хватает на первый квартал тестов.
- Чем GitLab отличается от GitHub Actions и Jenkins?
- GitLab — это единая платформа: код, issues, CI/CD, registry, мониторинг, security scanning. GitHub Actions — только экосистема GitHub. Jenkins универсальнее, но требует кучу плагинов и ручной настройки. Для команды, которая и так живёт в GitLab, встроенный CI/CD — очевидный выбор.
- Как защитить продакшн от случайного деплоя?
- Слоев несколько: when: manual для production-job, protected environments с ограничением по роли Maintainer, protected branches с обязательным MR и code review, required approvals на 2 аппрувера. Все это уже встроено, ничего дополнительно ставить не надо.
- Как быстро откатить сломанный релиз?
- У нас двухуровневый откат: автоматический — если health-check валится 60 секунд подряд, контейнер возвращается на предыдущий образ. Ручной — кнопка Rollback прямо в GitLab UI, запускает redeploy предыдущей ревизии. На практике откат укладывается в 30 секунд.
- Сколько ресурсов надо GitLab Runner?
- Build runner — 4 vCPU, 8 ГБ RAM (комфортно параллелить 3–4 проекта). Deploy runner — 1 vCPU, 1 ГБ RAM (docker pull + run, не больше). Docker executor добавляет небольшой оверхед, каждая job живёт в своём контейнере и не мешает соседям.