GitLab CI/CD: пайплайн автодеплоя для финтех-команды после двух продакшн-инцидентов
Привет! Я, Семёнов Евгений Сергеевич из ITFresh. Расскажу, как один проект начался буквально с двух тревожных ночных звонков — и оба произошли всего за три недели! Сначала техдиру крупного московского финтеха пришлось целых четыре часа биться головой о стену, пытаясь откатить неудачный ручной релиз. А уже на следующем спринте его старший бэкендер случайно отправил staging в production, просто используя rsync. Что ж, нам понадобился месяц, чтобы передать им полностью настроенный и работающий GitLab-пайплайн. Этот пайплайн, к слову, включает в себя защиту от дураков, автоматический откат и удобные уведомления прямо в Slack. Дальше мы обязательно углубимся в детали, я покажу часть кода и, конечно, поделюсь всеми граблями, на которые мы умудрились наступить.
Почему ручной деплой — билет в 3 часа ночи
У клиента была классическая схема: код лежит в GitLab, разработчики жмут «merge», потом кто-то дежурный заходит по SSH на прод и делает git pull && systemctl restart. Звучит невинно, но:
- Ну и что получаем? Никакой атомарности! Представьте: pull прошёл успешно, но рестарт службы внезапно упал. Всё, сервис мёртв. И ведь этого так легко избежать!
- Полное отсутствие истории. Кто, что и когда задеплоил? Поди разберись. Максимум, что есть — это логи bash, и то, если их вообще кто-то догадался писать. Согласитесь, это не дело.
- Забудьте об откате одной кнопкой. Если что-то пошло не так, придётся вручную делать git revert, а потом снова запускать рестарт. Сколько времени и нервов это отнимает!
- Никакого разделения обязанностей (separation of duties). Получается, любой, у кого есть доступ, может взять и зарелизить что угодно. А это прямой путь к хаосу и ошибкам. Разве мы такого хотим?
Представляете, первый такой инцидент вылился в четыре часа простоя их платёжного шлюза? А это, между прочим, примерно 280 000 рублей упущенной комиссии. Ну а второй случай? Там вообще случилась утечка тестовых ключей: по какой-то неведомой причине staging-переменные оказались в боевом production-конфиге. После всех этих «приключений» мне, признаться честно, дали полный карт-бланш – целых две недели и абсолютную свободу действий. Никаких ограничений!
Архитектура пайплайна
Знаете, когда речь заходит о CI/CD, я всегда держу в голове три фундаментальных принципа. Первый – это полная воспроизводимость процесса, без сюрпризов. Второй – стопроцентно защищённый продакшен. И, разумеется, третий, но не менее важный – мгновенный откат в случае чего. Для наших ребят из финтеха мы разработали пайплайн из четырёх чётких стадий: build → test → deploy-staging → deploy-production. Мы действительно продумали каждую мелочь. Кстати, первые три стадии запускаются абсолютно автоматически после каждого push в ветку main. Но что касается четвёртой, deploy-production, она стартует только вручную, по нажатию кнопки, и требует обязательного одобрения двумя аппруверами. Поверьте, это в разы надёжнее!
| Стадия | Триггер | Время | Раннер |
|---|---|---|---|
| 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-сервера. Ведь вы в курсе, что «не запускать сборку артефактов на управляющем хосте» – это не просто правило, это спасительная мантра в любой непредвиденной ситуации, золотой стандарт безопасности. Что для этого понадобилось? Отдельная виртуалка на Ubuntu 22.04, восемь vCPU, шестнадцать гигабайт оперативной памяти, ну и, конечно же, 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-раннер мы разместили обособленно. Он находится на бастион-хосте, внутри 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 как обычная переменная окружения, а сразу после завершения процесса просто исчезает. В итоге? В репозитории нет ни одного чувствительного кусочка данных. Полный порядок!
Автоматический откат по 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 живёт в своём контейнере и не мешает соседям.
