CI/CD с GitLab: от коммита до продакшена
Когда проектов становится больше трёх, а деплой по SSH руками начинает занимать полдня, приходит время автоматизации. GitLab CI/CD — один из самых зрелых инструментов для этого: он встроен прямо в GitLab, не требует отдельных сервисов и покрывает весь путь от коммита до продакшена.
В этой статье разберу, как я выстраиваю пайплайны на реальных проектах: от структуры .gitlab-ci.yml до стратегий деплоя.
Анатомия .gitlab-ci.yml
Всё начинается с файла .gitlab-ci.yml в корне репозитория. GitLab читает его при каждом пуше и запускает пайплайн. Базовая структура — это stages (этапы) и jobs (задачи внутри этапов):
stages:
- build
- test
- deploy
build_app:
stage: build
image: node:20-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
run_tests:
stage: test
image: node:20-alpine
script:
- npm ci
- npm run test:ci
coverage: '/Liness*:s*(d+.?d*)%/'
deploy_production:
stage: deploy
script:
- rsync -avz --delete dist/ $DEPLOY_USER@$DEPLOY_HOST:/var/www/app/
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
environment:
name: production
url: https://example.com Ключевые моменты: jobs внутри одного stage выполняются параллельно, stages идут последовательно. Если job на этапе test падает, до deploy пайплайн не дойдёт.
Настройка раннеров
GitLab Runner — это агент, который выполняет jobs. Два основных executor’а:
Shell executor — выполняет команды прямо на хосте. Прост в настройке, но опасен: jobs имеют доступ к файловой системе сервера. Подходит для деплоя, где нужен доступ к Docker или ssh-ключам.
Docker executor — каждый job запускается в отдельном контейнере. Изолированно, воспроизводимо, безопасно. Для большинства задач это правильный выбор.
Регистрация раннера:
# Установка
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt-get install gitlab-runner
# Регистрация с Docker executor
sudo gitlab-runner register
--non-interactive
--url "https://gitlab.example.com/"
--registration-token "$REGISTRATION_TOKEN"
--executor "docker"
--docker-image "alpine:latest"
--description "docker-runner-01"
--tag-list "docker,linux"
--run-untagged="true" На своих серверах я обычно ставлю два раннера: docker executor для сборки/тестов и shell executor для деплоя. Теги позволяют направлять jobs на нужный раннер.
Кэширование и артефакты
Это две разные вещи, и их часто путают. Артифакты — результаты job’а, передаются между stages внутри одного пайплайна. Кэш — ускоряет повторные запуски, переиспользуется между пайплайнами.
build_app:
stage: build
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
artifacts:
paths:
- dist/
expire_in: 30 min
script:
- npm ci
- npm run build Кэш по package-lock.json означает: пока зависимости не изменились, node_modules/ переиспользуются. Это экономит минуты на каждом запуске.
Деплой на разные окружения
На практике нужен как минимум staging (для тестирования) и production (для пользователей). GitLab environments отлично для этого подходят:
deploy_staging:
stage: deploy
script:
- ansible-playbook -i inventory/staging deploy.yml
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy_production:
stage: deploy
script:
- ansible-playbook -i inventory/production deploy.yml
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual Обратите внимание на when: manual для продакшена. Это ручное подтверждение — пайплайн остановится и будет ждать нажатия кнопки. Никаких случайных деплоев в пятницу вечером.
Секреты и CI/CD Variables
Пароли, токены, ключи — всё это хранится в Settings > CI/CD > Variables. Переменные можно пометить как protected (доступны только в protected-ветках) и masked (скрыты в логах):
deploy_production:
stage: deploy
variables:
DEPLOY_HOST: "194.87.104.208"
script:
- echo "$SSH_PRIVATE_KEY" > /tmp/deploy_key
- chmod 600 /tmp/deploy_key
- ssh -i /tmp/deploy_key -p 6622 alex@$DEPLOY_HOST "cd /opt/app && git pull && docker compose up -d"
after_script:
- rm -f /tmp/deploy_key Правило: в .gitlab-ci.yml никогда не должно быть секретов. Только ссылки на переменные через $VARIABLE_NAME.
Сборка Docker-образов в CI
Здесь два подхода: Docker-in-Docker (DinD) и Kaniko.
DinD проще, но требует привилегированного режима:
build_image:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA Kaniko безопаснее — не нужен Docker daemon и привилегированный доступ:
build_image:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.22.0-debug
entrypoint: [""]
script:
- /kaniko/executor
--context $CI_PROJECT_DIR
--dockerfile $CI_PROJECT_DIR/Dockerfile
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA На shared-раннерах Kaniko — единственный вариант. На своих серверах DinD работает нормально, но я постепенно перехожу на Kaniko просто из принципа наименьших привилегий.
Стратегии деплоя
Rolling update — обновляем инстансы по одному. Просто, но при проблемах часть пользователей увидит ошибки:
deploy_rolling:
script:
- ansible-playbook deploy.yml --limit "web_servers" --serial 1 Blue-green — поднимаем новую версию рядом со старой, переключаем трафик атомарно:
deploy_blue_green:
script:
- docker compose -f docker-compose.green.yml up -d
- ./scripts/health-check.sh green
- nginx -s reload # переключаем upstream
- docker compose -f docker-compose.blue.yml down Для большинства моих проектов хватает rolling update через Ansible. Blue-green — для сервисов с SLA, где даже секунда простоя критична.
Оптимизация пайплайна
Несколько приёмов, которые сократили время пайплайнов с 15 до 4 минут:
1. Параллельные jobs. Линтинг, юнит-тесты и e2e-тесты можно запускать одновременно, а не последовательно.
2. Rules вместо only/except. rules гибче и позволяют пропускать jobs, которые не нужны:
lint:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
changes:
- "src/**/*" 3. Needs: DAG-пайплайны. Позволяют job’у стартовать сразу после нужной зависимости, не дожидаясь всего stage:
deploy:
stage: deploy
needs: ["build_app"]
script:
- ./deploy.sh 4. Правильный .dockerignore. Не тащите в контекст сборки node_modules, .git и прочий мусор — это ускоряет docker build в разы.
Итого
GitLab CI/CD — это не просто “запуск скриптов по пушу”. Это полноценный оркестратор: environments, manual approvals, DAG-пайплайны, встроенный registry. Если вы уже используете GitLab для кода, CI/CD настраивается за вечер и окупается на первой же неделе.
Главное правило: начинайте с простого. Один stage, один job, один deploy. Усложняйте только когда появляется реальная потребность. Перегруженный пайплайн из 20 jobs, который выполняется 40 минут, хуже ручного деплоя.