· 17 мин чтения

GitLab CI/CD: пайплайн автодеплоя для финтех-команды после двух продакшн-инцидентов

Семёнов Евгений Сергеевич, АйТи Фреш. Этот проект начался с двух ночных звонков за три недели: техдир финтеха в Москве потерял четыре часа на откате кривого ручного релиза, а на следующий спринт его старший бэкендер по ошибке зарелизил staging в production через rsync. Через месяц мы передали им отлаженный GitLab-пайплайн с защитой от дураков, автооткатом и уведомлениями в Slack. Ниже — детали, код и грабли.

Почему ручной деплой — билет в 3 часа ночи

У клиента была классическая схема: код лежит в GitLab, разработчики жмут «merge», потом кто-то дежурный заходит по SSH на прод и делает git pull && systemctl restart. Звучит невинно, но:

Первый инцидент стоил им 4 часа простоя платёжного шлюза — примерно 280 000 руб. упущенной комиссии. Второй — утечка тестовых ключей, потому что staging-переменные попали в production-конфиг. После этого мне дали две недели и свободу действий.

Архитектура пайплайна

Я всегда строю CI/CD вокруг трёх принципов: воспроизводимость, защищённый прод, быстрый откат. В случае нашего финтеха пайплайн состоит из четырёх стадий: build → test → deploy-staging → deploy-production. Первые три отрабатывают автоматически на push в main, четвёртая — только по кнопке двух аппруверов.

СтадияТриггерВремяРаннер
buildpush, MR2–4 минdocker build, 8 vCPU
testafter build3–5 минdocker, 4 vCPU
deploy-stagingmain auto40 секdeploy, 1 vCPU
deploy-productionmanual + 2 approvals50 секdeploy prod
rollbackmanual/auto25–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: &notify
  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 живёт в своём контейнере и не мешает соседям.

Подпишитесь на рассылку ITfresh

Раз в неделю — практические гайды для руководителя IT и сисадмина: безопасность, 1С, миграции, резервные копии, лайфхаки из реальных проектов.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.