· 17 мин чтения

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

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

Привет! Я, Семёнов Евгений Сергеевич из ITFresh. Расскажу, как один проект начался буквально с двух тревожных ночных звонков — и оба произошли всего за три недели! Сначала техдиру крупного московского финтеха пришлось целых четыре часа биться головой о стену, пытаясь откатить неудачный ручной релиз. А уже на следующем спринте его старший бэкендер случайно отправил staging в production, просто используя rsync. Что ж, нам понадобился месяц, чтобы передать им полностью настроенный и работающий GitLab-пайплайн. Этот пайплайн, к слову, включает в себя защиту от дураков, автоматический откат и удобные уведомления прямо в Slack. Дальше мы обязательно углубимся в детали, я покажу часть кода и, конечно, поделюсь всеми граблями, на которые мы умудрились наступить.

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

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

Представляете, первый такой инцидент вылился в четыре часа простоя их платёжного шлюза? А это, между прочим, примерно 280 000 рублей упущенной комиссии. Ну а второй случай? Там вообще случилась утечка тестовых ключей: по какой-то неведомой причине staging-переменные оказались в боевом production-конфиге. После всех этих «приключений» мне, признаться честно, дали полный карт-бланш – целых две недели и абсолютную свободу действий. Никаких ограничений!

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

Знаете, когда речь заходит о CI/CD, я всегда держу в голове три фундаментальных принципа. Первый – это полная воспроизводимость процесса, без сюрпризов. Второй – стопроцентно защищённый продакшен. И, разумеется, третий, но не менее важный – мгновенный откат в случае чего. Для наших ребят из финтеха мы разработали пайплайн из четырёх чётких стадий: build → test → deploy-staging → deploy-production. Мы действительно продумали каждую мелочь. Кстати, первые три стадии запускаются абсолютно автоматически после каждого push в ветку main. Но что касается четвёртой, deploy-production, она стартует только вручную, по нажатию кнопки, и требует обязательного одобрения двумя аппруверами. Поверьте, это в разы надёжнее!

СтадияТриггерВремяРаннер
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-сервера. Ведь вы в курсе, что «не запускать сборку артефактов на управляющем хосте» – это не просто правило, это спасительная мантра в любой непредвиденной ситуации, золотой стандарт безопасности. Что для этого понадобилось? Отдельная виртуалка на 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: &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 подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.