CI/CD с GitLab: от коммита до продакшена

gitlabcicddevopsautomation

Когда проектов становится больше трёх, а деплой по 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 пайплайн не дойдёт.

Схема этапов CI/CD пайплайна в GitLab

Настройка раннеров

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 для продакшена. Это ручное подтверждение — пайплайн остановится и будет ждать нажатия кнопки. Никаких случайных деплоев в пятницу вечером.

Визуализация стадий пайплайна: build, test, staging, production

Секреты и 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 минут, хуже ручного деплоя.

© 2026 Terminal Notes. Built with SvelteKit.