CD и стратегии деплоя

CD (Continuous Delivery / Continuous Deployment) — практики автоматизации подготовки и доставки изменений в тестовые и производственные окружения.

Типичный путь изменения:

commit → build → tests → security checks → image/artifact
       → staging → smoke tests → approval, если требуется
       → production → monitoring → rollback при необходимости

Содержание


Continuous Delivery и Continuous Deployment

Continuous Delivery

Pipeline автоматически выполняет сборку, тесты, проверки безопасности, создаёт артефакт и разворачивает его в staging. Production-деплой выполняется после ручного подтверждения.

автоматически: commit → тесты → образ → staging
вручную:       approval → production

Такой подход удобен для регулируемых систем, плановых релизов и команд, которым требуется дополнительная ручная проверка.

Continuous Deployment

Production-деплой запускается без ручного подтверждения после успешного прохождения всех этапов:

commit → tests → image → staging → acceptance tests → production

Для него особенно важны стабильные тесты, наблюдаемость, feature flags, быстрый rollback и ограничение области влияния ошибки.

Подход Подготовка версии Production-деплой
Continuous Delivery Автоматическая С ручным подтверждением или запуском
Continuous Deployment Автоматическая Автоматический

Оба подхода используют CI: изменения регулярно собираются, тестируются и проверяются автоматически.


Основные элементы CD-процесса

Окружения

Окружение Назначение
Development Локальная разработка
Test Автоматические проверки
Staging Проверка версии в условиях, близких к production
Production Рабочее окружение

Staging желательно делать похожим на production по ОС, runtime, сетевой конфигурации, способу запуска и внешним зависимостям. Секреты и данные при этом должны быть отдельными.

Immutable artifact

Артефакт следует собрать один раз и продвигать между окружениями без повторной сборки:

build app:1.8.4 → test app:1.8.4 → staging app:1.8.4 → production app:1.8.4

Для Docker лучше использовать версию, связанную с commit, или digest:

registry.example.com/team/app@sha256:7b4f...

latest неудобен для аудита и отката, потому что его содержимое может измениться.

Идемпотентность и блокировка

Повторный запуск деплоя одной версии должен приводить систему к тому же состоянию. Параллельные production-деплои нужно блокировать: старый pipeline не должен завершиться после нового и вернуть предыдущую версию.

GitHub Actions:

concurrency:
  group: production
  cancel-in-progress: false

GitLab CI/CD:

deploy-production:
  resource_group: production

Подготовка приложения к деплою

Health endpoints

Обычно выделяют:

GET /health/ready
200 OK

Readiness должен быть быстрым и проверять только критические зависимости. Необязательный внешний сервис не должен делать весь экземпляр «не готовым» без необходимости.

Graceful shutdown

При остановке приложение должно перестать принимать новые запросы, завершить текущие операции, закрыть соединения и корректно обработать SIGTERM.

Конфигурация отдельно от образа

image:         код и runtime
configuration: переменные, config files, feature flags
secrets:       пароли, токены, ключи

Production-секреты не должны встраиваться в образ.

Совместимость версий

Во время rolling и canary старая и новая версии работают одновременно. API, сообщения очередей и схема базы данных должны временно поддерживать обе версии.


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

Стратегия определяет, как новая версия заменяет старую и как распределяется трафик.


Rolling deployment

Экземпляры старой версии постепенно заменяются экземплярами новой:

Шаг 1: v1 v1 v1 v1
Шаг 2: v2 v1 v1 v1
Шаг 3: v2 v2 v1 v1
Шаг 4: v2 v2 v2 v1
Шаг 5: v2 v2 v2 v2

Новый экземпляр должен получать трафик только после успешной readiness-проверки.

Преимущества: не требуется второе полное окружение, возможен деплой без простоя, стратегия поддерживается большинством оркестраторов, нагрузка переключается постепенно.

Недостатки: версии некоторое время работают одновременно, откат занимает время, ошибка может постепенно затронуть весь трафик, нужна совместимость приложения и базы данных.

Важные параметры: количество одновременно обновляемых экземпляров, readiness timeout, допустимое число ошибок, число дополнительных экземпляров и условие автоматического rollback.


Blue-green deployment

Существуют два окружения:

До переключения:    users → load balancer → blue v1
                                      green v2 ← internal checks

После переключения: users → load balancer → green v2
                                      blue v1 ← standby

После smoke-тестов балансировщик переключает трафик на green.

Преимущества: быстрое переключение, простой rollback возвратом трафика, проверка новой версии до полного трафика, возможность временно сохранить старое окружение.

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

Состояние следует хранить во внешних сервисах: базе данных, object storage, cache или очереди. Локальная файловая система blue и green не должна быть единственным источником данных.


Canary deployment

Новая версия получает малую долю production-трафика, после чего доля постепенно увеличивается:

Этап 1: v1 — 95%, v2 — 5%
Этап 2: v1 — 75%, v2 — 25%
Этап 3: v1 — 50%, v2 — 50%
Этап 4: v1 — 0%,  v2 — 100%

На этапах сравниваются error rate, p95/p99 latency, перезапуски, CPU и память, ошибки в логах, бизнес-метрики и smoke-тесты.

Трафик можно выбирать по случайной доле, cookie, user ID, HTTP-заголовку, региону или группе пользователей. Стабильное закрепление пользователя за одной версией упрощает анализ.

Преимущества: небольшая область влияния ошибки, проверка на реальном трафике и возможность автоматической остановки.

Недостатки: ложная маршрутизация, требования к метрикам, совместимость версий и необходимость управлять сессиями и состоянием.

Canary можно сочетать с feature flags: первая технология управляет версией приложения, вторая — включением функции.


Сравнение стратегий

Критерий Rolling Blue-green Canary
Дополнительные ресурсы Небольшие или средние Высокие Средние
Скорость переключения Постепенная Быстрая Постепенная
Область влияния ошибки Растёт постепенно Большая после switch Сначала малая
Простота rollback Средняя Высокая до изменения данных Высокая на ранних этапах
Маршрутизация Обычный балансировщик Переключение окружений Взвешенная или выборочная
Сложность Средняя Средняя Высокая

Автоматический деплой на сервер

Для небольшого проекта CI может подключаться к Linux-серверу по SSH, менять версию образа и запускать Docker Compose.

На сервере нужны Docker Engine, Compose plugin, отдельный пользователь, каталог приложения, registry access, reverse proxy, TLS, backup и мониторинг.

Пример compose.yaml:

services:
  app:
    image: ${APP_IMAGE}:${APP_VERSION}
    restart: unless-stopped
    env_file: [.env]
    ports:
      - "127.0.0.1:8080:8080"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health/ready"]
      interval: 10s
      timeout: 3s
      retries: 6
      start_period: 20s

Привязка к 127.0.0.1 оставляет прямой доступ только на сервере; внешний трафик можно пропускать через reverse proxy.

Пример deploy.sh:

#!/usr/bin/env bash
set -Eeuo pipefail

VERSION="${1:?version is required}"
APP_DIR=/opt/myapp
cd "$APP_DIR"

PREVIOUS_VERSION="$(grep '^APP_VERSION=' .env | cut -d= -f2-)"
printf '%s\\n' "$PREVIOUS_VERSION" > releases/previous-version
sed -i "s/^APP_VERSION=.*/APP_VERSION=${VERSION}/" .env

docker compose pull app
docker compose up -d --no-deps app

for attempt in {1..30}; do
  if curl --fail --silent http://127.0.0.1:8080/health/ready >/dev/null; then
    echo "Deployment ${VERSION} succeeded"
    exit 0
  fi
  sleep 2
done

echo "Health check failed; rolling back" >&2
sed -i "s/^APP_VERSION=.*/APP_VERSION=${PREVIOUS_VERSION}/" .env
docker compose pull app
docker compose up -d --no-deps app
exit 1

Это учебный пример. Для production нужны блокировка параллельных запусков, таймауты, проверка образа, централизованные логи и работа с несколькими экземплярами.

SSH-доступ должен использовать отдельного пользователя и ключ, проверять known_hosts, отключать парольный вход и ограничивать сетевой доступ. Не следует без необходимости использовать root. Членство в группе docker фактически предоставляет высокие привилегии на сервере.


GitHub Actions

Пример workflow с тестами, сборкой образа и деплоем:

name: Build and deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  packages: write

concurrency:
  group: production
  cancel-in-progress: false

env:
  IMAGE: ghcr.io/example/myapp

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint
      - run: npm test

  build:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - id: meta
        run: echo "version=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}

  deploy-production:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy
        env:
          HOST: ${{ secrets.DEPLOY_HOST }}
          USER: ${{ secrets.DEPLOY_USER }}
          VERSION: ${{ needs.build.outputs.version }}
        run: ssh "$USER@$HOST" "/opt/myapp/deploy.sh '$VERSION'"

Для production Environment можно настроить reviewers, secrets, ограничения веток и историю деплоев. Не следует отключать проверку host key через StrictHostKeyChecking=no. Сторонние actions желательно закреплять по полному commit SHA.


GitLab CI/CD

Пример production-деплоя с ручным подтверждением:

stages: [test, build, deploy]

variables:
  IMAGE: "$CI_REGISTRY_IMAGE/app"
  VERSION: "$CI_COMMIT_SHA"

test:
  stage: test
  image: node:22-alpine
  script:
    - npm ci
    - npm run lint
    - npm test

build-image:
  stage: build
  image: docker:27
  services: [docker:27-dind]
  script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
    - docker build --pull -t "$IMAGE:$VERSION" .
    - docker push "$IMAGE:$VERSION"
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

deploy-production:
  stage: deploy
  resource_group: production
  environment:
    name: production
    url: https://app.example.com
  script:
    - ./deploy.sh "$VERSION"
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual

when: manual соответствует Continuous Delivery. Для Continuous Deployment используется when: on_success после успешного build job. Production-переменные должны быть protected и доступны только доверенным веткам, тегам и ролям.


Проверки после деплоя

Успешное выполнение команды запуска не гарантирует работоспособность приложения.

Health check:

curl --fail --show-error --silent \\
  --retry 10 --retry-delay 3 \\
  https://app.example.com/health/ready

Smoke-тесты проверяют основные сценарии:

GET /health/ready → 200
GET /api/version  → ожидаемая версия
POST /api/session → авторизация работает
GET /api/items    → основной API отвечает

Полезно иметь endpoint версии:

{
  "version": "a17c9e4",
  "buildTime": "2026-09-21T10:30:00Z"
}

После выпуска наблюдают HTTP 5xx, latency, CPU и память, перезапуски, ошибки зависимостей, длины очередей и ключевые бизнес-метрики.


Rollback-стратегии

Rollback — возврат к предыдущей работоспособной версии. План отката нужно подготовить и проверить до production-деплоя.

Redeploy предыдущего артефакта

APP_VERSION=a17c9e4 docker compose up -d

Предыдущий образ должен оставаться в registry. Политика очистки не должна удалять последние рабочие релизы.

Rolling rollback

Оркестратор повторяет обновление, но к предыдущей версии. Это не мгновенно: часть запросов некоторое время может попадать на новую версию.

Blue-green rollback

Балансировщик переключает трафик с green обратно на blue. Это быстро, если старое окружение сохранилось и новая версия не внесла несовместимых изменений в данные.

Canary rollback

Новая версия исключается из маршрутизации:

v1 — 95%, v2 — 5%
ошибки превышают порог
v1 — 100%, v2 — 0%

Roll-forward

Иногда безопаснее быстро выпустить исправление, а не возвращать старую версию. Это особенно актуально после изменения данных, при несовместимой схеме или необратимой миграции.

Автоматический откат может запускаться при превышении порога ошибок, latency или провале readiness. Порог должен учитывать нормальные значения, иначе система будет создавать ложные rollback.

Для отката сохраняют идентификатор прошлого образа, конфигурацию, историю деплоев, схему маршрутизации, состояние feature flags и информацию о миграциях.


Миграции базы данных

При rolling, blue-green и canary старая и новая версии могут обращаться к одной базе. Нельзя удалять поле, которым ещё пользуется старая версия.

Используют схему expand and contract:

  1. Expand — добавить новые поля, сохранив старые.
  2. Migrate — перенести данные и перевести приложение.
  3. Contract — удалить старые поля отдельным выпуском.

Рекомендации:


Секреты в CI/CD

Секреты — пароли, токены, приватные ключи, сертификаты, registry credentials и параметры подключения к базам.

Где хранить

Секреты хранят во встроенном secret store CI/CD или во внешнем менеджере секретов. Нельзя записывать их в Git, Dockerfile, compose.yaml, логи, artifacts и cache.

# GitHub Actions
env:
  API_TOKEN: ${{ secrets.API_TOKEN }}

Для GitLab Variables полезны свойства Masked, Protected, ограничение environment scope и тип File variable для сертификатов. Production-секреты не должны быть доступны jobs из pull request и обычных feature-веток.

Минимальные полномочия

Токен должен иметь только необходимые права и ограниченный срок действия:

хорошо: push image в один namespace
плохо: административный доступ ко всей организации

По возможности долгоживущие cloud-ключи заменяют OIDC и временными credentials:

CI job → OIDC token → cloud IAM → временные credentials

Логи

Не следует печатать переменные окружения, включать set -x рядом с секретами или передавать секреты в командной строке без необходимости. Даже маскирование CI не гарантирует скрытие преобразованного значения.

Секреты при Docker build

Плохо:

ARG NPM_TOKEN
RUN echo "$NPM_TOKEN" > /root/.npmrc

Безопаснее использовать BuildKit secret mount:

# syntax=docker/dockerfile:1
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
COPY . .
RUN npm run build
docker build --secret id=npmrc,src="$HOME/.npmrc" -t myapp:build .

Для каждого секрета определяют владельца, срок действия, процедуру ротации и отзыва.


Защита production-окружения

Рекомендуется разделять build и deploy: build создаёт и проверяет артефакт, deploy продвигает выбранный digest. Production-секреты не нужны сборке.

Production защищают:

Pull request из внешнего fork не должен получать production-секреты. Runner, выполняющий недоверенный код, не должен иметь сетевой доступ к production.

Для каждого деплоя сохраняют версию, commit, инициатора, pipeline, время, окружение, проверки и причину rollback.


Общий пример процесса

checkout
 lint и unit tests
→ integration tests
→ dependency scan
→ docker build --pull
→ image scan
→ push image:<commit-sha>
→ deploy exact image to staging
→ readiness и smoke tests
→ approval или automatic promotion
→ canary 10%
→ observe metrics
→ canary 50%
→ observe metrics
→ production 100%

При превышении порогов новая версия удаляется из маршрутизации или выполняется redeploy предыдущего digest.


Чек-лист

Процесс

Стратегия

Rollback

Секреты и доступ