Введение: Почему вы до сих пор боитесь нажимать "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 тестирование в реальных условиях — можно сравнить метрики старой и новой версии.
- Плавная раскатка — вы не убиваете сервер внезапным ростом нагрузки.
Теперь давайте перейдём от теории к практике. Мы будем строить систему, которая состоит из трёх частей:
- Backend на TypeScript (Express.js).
- Балансировщик трафика на Nginx.
- Автоматизация через 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 будет делать следующее:
- Собирать Docker-образ для canary-версии.
- Пушить его в Docker Hub или GitHub Container Registry.
- Обновлять конфигурацию Nginx (увеличивать процент трафика).
- Запускать мониторинг.
Вот пример файла .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:
-