Поделиться
Поделиться

Ручной деплой — это риск. Каждый раз, когда разработчик вручную заливает код на сервер, есть шанс что-то сломать: забыть переменную окружения, перепутать ветку, пропустить миграцию базы данных. CI/CD убирает человека из этого процесса. Разберём, как выстроить надёжный пайплайн в GitLab от первого коммита до production.

Структура .gitlab-ci.yml

Всё начинается с файла .gitlab-ci.yml в корне репозитория. Минимальная структура:

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"

default:
  image: node:20-alpine
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

Ключевые концепции:

| Понятие | Описание | |---|---| | stages | Упорядоченные группы jobs. Jobs одной стадии выполняются параллельно | | jobs | Атомарные задачи: тест, сборка, деплой | | runners | Агенты, которые выполняют jobs | | artifacts | Файлы, передаваемые между стадиями | | cache | Кешированные зависимости между запусками | | environments | Именованные окружения (staging, production) |

Стадии и jobs

Типичный пайплайн для веб-приложения:

stages:
  - lint
  - test
  - build
  - deploy:staging
  - deploy:production

# --- LINT ---
lint:
  stage: lint
  script:
    - npm ci
    - npm run lint
    - npm run type-check
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# --- ТЕСТЫ ---
unit_tests:
  stage: test
  script:
    - npm ci
    - npm run test:unit -- --coverage
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    expire_in: 1 week

e2e_tests:
  stage: test
  image: mcr.microsoft.com/playwright:v1.45.0-jammy
  script:
    - npm ci
    - npx playwright install --with-deps chromium
    - npm run test:e2e
  artifacts:
    when: on_failure
    paths:
      - playwright-report/
    expire_in: 3 days

# --- СБОРКА ---
build_image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - |
      docker build \
        --cache-from $CI_REGISTRY_IMAGE:latest \
        --build-arg BUILDKIT_INLINE_CACHE=1 \
        -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
        -t $CI_REGISTRY_IMAGE:latest \
        .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest
  only:
    - main
    - tags

Docker Runners

По умолчанию GitLab.com предоставляет shared runners. Для приватных серверов нужен self-hosted runner.

Установка runner на VPS

# Установка
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install gitlab-runner

# Регистрация
sudo gitlab-runner register \
  --url https://gitlab.com \
  --registration-token YOUR_TOKEN \
  --executor docker \
  --docker-image "docker:24" \
  --docker-privileged \
  --description "production-runner" \
  --tag-list "production,docker"

Конфигурация runner (/etc/gitlab-runner/config.toml)

[[runners]]
  name = "production-runner"
  url = "https://gitlab.com"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "docker:24"
    privileged = true
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/certs/client", "/cache"]
    shm_size = 0
  [runners.cache]
    Type = "s3"
    Shared = true
    [runners.cache.s3]
      ServerAddress = "minio.example.com"
      BucketName = "gitlab-runner-cache"
      BucketLocation = "us-east-1"

Артефакты и передача данных между стадиями

Артефакты — это способ передать результаты одной стадии в следующую:

build_frontend:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour    # хранить 1 час (достаточно для деплоя)

deploy_staging:
  stage: deploy:staging
  dependencies:
    - build_frontend    # явно указываем источник артефактов
  script:
    - ls dist/          # файлы доступны здесь
    - rsync -avz dist/ user@staging.example.com:/var/www/app/

Артефакты для отчётов (покрытие, тесты) не нужно передавать явно — GitLab подхватывает их автоматически:

unit_tests:
  artifacts:
    reports:
      junit: junit.xml           # результаты тестов в MR
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
    paths:
      - coverage/html/           # HTML-отчёт о покрытии
    expire_in: 30 days

Управление секретами

Никогда не храните секреты в .gitlab-ci.yml. Используйте CI/CD Variables (Settings → CI/CD → Variables):

deploy_production:
  script:
    # $DATABASE_URL, $JWT_SECRET и т.д. — CI/CD Variables
    - |
      docker run -d \
        --name app \
        -e DATABASE_URL=$DATABASE_URL \
        -e JWT_SECRET=$JWT_SECRET \
        -e REDIS_URL=$REDIS_URL \
        -p 3000:3000 \
        $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Для production используйте Protected variables — они доступны только в protected ветках (main, production):

# Переменные с пометкой Protected не будут доступны в feature-ветках
# Устанавливается в GitLab UI: Settings → CI/CD → Variables → Protected ✅

Для чувствительных данных рекомендуем HashiCorp Vault:

deploy_production:
  secrets:
    DATABASE_PASSWORD:
      vault: production/db/password@secret
      file: false

Кеширование зависимостей

Правильный кеш ускоряет пайплайн в 3-5 раз:

default:
  cache:
    # Разный кеш для разных веток
    key:
      files:
        - package-lock.json     # инвалидация при изменении lock-файла
      prefix: ${CI_COMMIT_REF_SLUG}
    paths:
      - .npm/
    policy: pull-push

# Отдельный job для обновления кеша (только в main)
update_cache:
  stage: .pre
  script:
    - npm ci --cache .npm --prefer-offline
  cache:
    key:
      files:
        - package-lock.json
      prefix: ${CI_COMMIT_REF_SLUG}
    paths:
      - .npm/
    policy: push    # только записывает, не читает
  only:
    changes:
      - package-lock.json

Окружения и деплой

Staging автоматически, Production вручную

deploy_staging:
  stage: deploy:staging
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - ./scripts/deploy.sh staging $CI_COMMIT_SHA
  only:
    - main

deploy_production:
  stage: deploy:production
  environment:
    name: production
    url: https://example.com
  script:
    - ./scripts/deploy.sh production $CI_COMMIT_SHA
  when: manual              # нажать кнопку в GitLab UI
  only:
    - main
  allow_failure: false

Динамические окружения для review apps

deploy_review:
  stage: deploy:staging
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.review.example.com
    on_stop: stop_review    # автоудаление при закрытии MR
  script:
    - helm upgrade --install review-$CI_COMMIT_REF_SLUG ./helm \
        --set image.tag=$CI_COMMIT_SHA \
        --set ingress.host=$CI_COMMIT_REF_SLUG.review.example.com
  only:
    - merge_requests

stop_review:
  stage: deploy:staging
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  script:
    - helm uninstall review-$CI_COMMIT_REF_SLUG
  when: manual
  only:
    - merge_requests

Стратегии деплоя

Blue-Green Deployment

Два идентичных окружения: Blue (текущий production) и Green (новая версия). Переключение моментальное:

#!/bin/bash
# scripts/blue-green-deploy.sh

CURRENT=$(docker inspect --format='{{.Name}}' $(docker ps -q --filter name=app-blue) 2>/dev/null | grep -c blue)

if [ "$CURRENT" -gt 0 ]; then
  # Текущий — blue, деплоим в green
  NEW_COLOR=green
  OLD_COLOR=blue
else
  NEW_COLOR=blue
  OLD_COLOR=green
fi

echo "Deploying to $NEW_COLOR..."

# Запускаем новую версию
docker run -d \
  --name app-$NEW_COLOR \
  --health-cmd="curl -f http://localhost:3000/health || exit 1" \
  --health-interval=10s \
  --health-retries=3 \
  $IMAGE_TAG

# Ждём готовности
timeout 60 bash -c "until docker inspect --format='{{.State.Health.Status}}' app-$NEW_COLOR | grep -q healthy; do sleep 2; done"

# Переключаем Nginx
sed -i "s/app-$OLD_COLOR/app-$NEW_COLOR/" /etc/nginx/conf.d/app.conf
nginx -s reload

echo "Traffic switched to $NEW_COLOR"

# Останавливаем старую версию
sleep 30   # grace period для завершения текущих запросов
docker stop app-$OLD_COLOR && docker rm app-$OLD_COLOR

Rolling Update (через Docker Swarm или Kubernetes)

# docker-compose.production.yml
services:
  app:
    image: ${IMAGE_TAG}
    deploy:
      replicas: 4
      update_config:
        parallelism: 1          # обновляем по 1 контейнеру
        delay: 30s              # ждём 30 секунд между заменами
        failure_action: rollback
        monitor: 60s
        max_failure_ratio: 0.25
      rollback_config:
        parallelism: 0          # откатываем все сразу
        failure_action: pause
# В GitLab CI
deploy_production:
  script:
    - docker stack deploy -c docker-compose.production.yml myapp --with-registry-auth
  environment:
    name: production

Полный пайплайн в итоге

# Итоговая структура
stages: [lint, test, build, deploy:staging, deploy:production]

# Lint: параллельно eslint + type-check
# Test: unit (с coverage) + e2e (с артефактами при падении)
# Build: Docker image → GitLab Registry
# Deploy staging: автоматически при пуше в main
# Deploy production: вручную через кнопку в UI

Время пайплайна с кешированием: ~4-6 минут. Без кеша — 12-15 минут.

О том, как оптимизировать Docker-образы для ускорения сборки, читайте в статье Оптимизация Dockerfile. Для Next.js-приложений также посмотрите Next.js разработка: App Router — там есть пример многоэтапного Dockerfile.

Итог

Хороший CI/CD пайплайн — это не разовая настройка, а живой инструмент команды. Начните с простого: lint → тесты → деплой на staging. Добавляйте шаги постепенно: e2e тесты, review apps, blue-green. Главное — автоматизировать то, что сейчас делается руками и регулярно ошибается.