Canary deployments с TypeScript: бесшовный релиз без страха сломать продакшен

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему вы до сих пор боитесь нажимать "Deploy"?

Вы написали код. Проверили на локальной машине. Тесты прошли. Всё идеально. Вы нажимаете заветную кнопку Deploy... и через 5 минут видите, как падает 500-я ошибка, а пользователи в панике пишут в поддержку. Знакомо?

Традиционные стратегии деплоя (blue-green, rolling update) хороши, но у них есть один недостаток: они либо включают весь новый код сразу, либо не включают его вообще. А что, если бы вы могли отправить новую фичу всего 1% пользователей, посмотреть на метрики, и только потом, убедившись в стабильности, пустить её на 100%?

Именно так работают canary deployments (канареечные релизы). Название пошло от старой шахтёрской практики — брать с собой канарейку в шахту, чтобы вовремя заметить утечку газа. Если птичка замолкала — шахтёры знали: пора бежать наверх. В мире IT "канарейка" — это небольшая группа пользователей, на которых тестируется новый код. Если всё хорошо — релиз катится дальше. Если нет — вы откатываетесь быстрее, чем успеваете сказать "hotfix".

В этой статье мы на TypeScript напишем полноценную систему canary deployment. Вы узнаете, как настроить балансировку трафика, как автоматизировать процесс через CI/CD, и как мониторить "здоровье" вашей канарейки. Поехали!

Что такое Canary Deployment и зачем он нужен?

Canary deployment — это стратегия выкатки нового кода, при которой обновлённая версия приложения сначала отправляется небольшому проценту пользователей (обычно 1-10%). Остальные продолжают работать на старой, стабильной версии. Если у новой версии не возникает проблем (ошибок, падения производительности, роста latency), вы постепенно увеличиваете процент трафика на неё, пока она не заменит старую полностью.

Ключевые преимущества:

  • Минимизация рисков — баги затрагивают только малую часть аудитории.
  • Быстрый откат — достаточно перенаправить трафик обратно на стабильную версию.
  • A/B тестирование в реальных условиях — можно сравнить метрики старой и новой версии.
  • Плавная раскатка — вы не убиваете сервер внезапным ростом нагрузки.

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

  1. Backend на TypeScript (Express.js).
  2. Балансировщик трафика на Nginx.
  3. Автоматизация через GitHub Actions.

Шаг 1. Пишем приложение на TypeScript с поддержкой версионирования

Для начала создадим простое Express-приложение, которое будет уметь работать в двух режимах: stable и canary. Разница будет только в ответе на корневой эндпоинт, но в реальном проекте это может быть любая новая фича.

// src/app.ts
import express, { Request, Response } from 'express';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;
const VERSION = process.env.VERSION || 'stable'; // 'stable' или 'canary'

app.get('/', (req: Request, res: Response) => {
  res.json({
    message: 'Hello from API!',
    version: VERSION,
    timestamp: new Date().toISOString(),
  });
});

// Эндпоинт для health check
app.get('/health', (req: Request, res: Response) => {
  res.status(200).send('OK');
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT} in ${VERSION} mode`);
});

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

# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/app.js"]

Соберите образы для двух версий:

# Сборка stable
docker build -t myapp:stable --build-arg VERSION=stable .

# Сборка canary
docker build -t myapp:canary --build-arg VERSION=canary .

Шаг 2. Настраиваем Nginx для Canary-балансировки

Главный "дирижёр" нашего оркестра — это Nginx. Он будет решать, какой процент запросов отправлять на canary-версию. Мы используем модуль split_clients, который позволяет распределять трафик на основе хэша от IP-адреса клиента. Это гарантирует, что один и тот же пользователь будет всегда попадать на одну и ту же версию (пока мы не изменим конфигурацию).

# nginx.conf
upstream stable_backend {
    server stable_app:3000;
}

upstream canary_backend {
    server canary_app:3000;
}

split_clients "${remote_addr}" $backend {
    5%     canary_backend;   # 5% трафика на canary
    *      stable_backend;   # остальные 95% на stable
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://$backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Как это работает?

  • Nginx берёт IP-адрес клиента ($remote_addr) и вычисляет хэш.
  • Если хэш попадает в первые 5% диапазона — запрос уходит на canary_backend.
  • Иначе — на stable_backend.

Чтобы изменить процент (например, с 5% на 20%), достаточно обновить конфиг и перезагрузить Nginx. Никакого даунтайма!

Шаг 3. Автоматизация через GitHub Actions

Ручное управление canary-релизами — это путь в никуда. Давайте автоматизируем процесс с помощью GitHub Actions. Наш workflow будет делать следующее:

  1. Собирать Docker-образ для canary-версии.
  2. Пушить его в Docker Hub или GitHub Container Registry.
  3. Обновлять конфигурацию Nginx (увеличивать процент трафика).
  4. Запускать мониторинг.

Вот пример файла .github/workflows/canary-deploy.yml:

name: Canary Deployment

on:
  push:
    branches: [ canary ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      -                

Блоги

Рекомендаций по книгам