Canary Deployments на C#: безопасный релиз без простоев

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

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

Начать курс

Что такое Canary Deployments и почему это спасёт ваш продакшн?

Вы когда-нибудь выкатывали обновление в полночь, молясь, чтобы ничего не упало? Знакомо. А что, если я скажу, что можно тестировать новый код на живых пользователях, но так, чтобы никто не пострадал? Речь о canary deployments — стратегии развёртывания, при которой новая версия приложения сначала получает лишь малую часть трафика. Если всё хорошо — трафик увеличивается. Если нет — откат за секунды.

В мире .NET и C# это особенно актуально: микросервисы, высокие нагрузки, требования к стабильности. Мы напишем код, настроим canary deployment и разберём реальные примеры. Поехали!

Почему Canary — не просто модное слово?

Традиционный blue-green deployment переключает весь трафик сразу. Rolling update — постепенно, но без тонкого контроля. Canary даёт вам суперсилу: вы выпускаете новую версию, но направляете на неё, скажем, 5% пользователей. Остальные 95% работают на старой стабильной версии. Вы мониторите метрики, ошибки, задержки. Если всё ок — постепенно наращиваете процент до 100. Если нет — просто отключаете канарейку.

Для C#-разработчика это особенно удобно: мы можем использовать встроенные средства ASP.NET Core, Kubernetes, Feature Flags или даже простой балансировщик. Главное — правильная архитектура.

Архитектура Canary Deployment для .NET приложения

Представим, что у нас есть веб-API на ASP.NET Core, которое обрабатывает заказы. Мы хотим выкатить новую логику расчёта скидок. Вместо того чтобы обновлять все поды сразу, мы:

  • Запускаем две версии сервиса: stable (v1) и canary (v2).
  • Настраиваем балансировщик (например, Nginx или Kubernetes Service) так, чтобы 95% трафика шло на v1, а 5% — на v2.
  • Добавляем HTTP-заголовок X-Canary: true в ответах canary-версии для идентификации.
  • Собираем метрики (код 200/500, latency, количество заказов).
  • Если за 30 минут ошибок не больше, чем на v1, увеличиваем вес canary до 50%, затем до 100%.

Реализация на C#: от простого к сложному

Способ 1: Canary через HTTP-заголовки и балансировщик

Самый простой путь — использовать слой middleware в ASP.NET Core, который помечает запросы. Но сначала давайте посмотрим, как выглядит код, который отличает canary-версию.

// Startup.cs или Program.cs
public class CanaryMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _version;

    public CanaryMiddleware(RequestDelegate next, string version)
    {
        _next = next;
        _version = version;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Добавляем заголовок для идентификации версии
        context.Response.Headers.Add("X-App-Version", _version);
        await _next(context);
    }
}

// Регистрация
app.UseMiddleware<CanaryMiddleware>("v2-canary");

Теперь на уровне балансировщика (например, Nginx) мы решаем, какой процент трафика отправлять на canary-под:

upstream backend {
    server app-stable:5000 weight=95;
    server app-canary:5000 weight=5;
}

Плюсы: простота. Минусы: нет программного контроля, сложно делать A/B тестирование по пользователям.

Способ 2: Feature Flags в C#

Более гибкий подход — использовать feature flags (фича-флаги). Вы включаете новую логику только для определённой группы пользователей. В ASP.NET Core есть встроенная поддержка:

// Установите пакет Microsoft.FeatureManagement.AspNetCore
// Добавьте в Program.cs
builder.Services.AddFeatureManagement();

// Определите флаг в appsettings.json
{
  "FeatureManagement": {
    "NewDiscountLogic": false
  }
}

// Используйте в контроллере
[ApiController]
[Route("api/[controller]")]
public class DiscountController : ControllerBase
{
    private readonly IFeatureManager _featureManager;

    public DiscountController(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }

    [HttpGet("calculate")]
    public async Task<IActionResult> Calculate(decimal amount)
    {
        if (await _featureManager.IsEnabledAsync("NewDiscountLogic"))
        {
            // Новая логика
            var discount = amount * 0.2m;
            return Ok(new { discount, version = "canary" });
        }
        else
        {
            // Старая логика
            var discount = amount * 0.1m;
            return Ok(new { discount, version = "stable" });
        }
    }
}

Теперь, чтобы включить canary для 5% пользователей, используем Contextual Feature Filters. Например, фильтр по ID пользователя:

public class PercentageFilter : IFeatureFilter
{
    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
    {
        var settings = context.Parameters.Get<PercentageFilterSettings>();
        var userId = context.Context?.Request.Headers["X-User-Id"].FirstOrDefault();
        if (string.IsNullOrEmpty(userId)) return Task.FromResult(false);

        var hash = Math.Abs(userId.GetHashCode()) % 100;
        return Task.FromResult(hash < settings.Percentage);
    }
}

public class PercentageFilterSettings
{
    public int Percentage { get; set; }
}

// Регистрация
builder.Services.AddFeatureManagement()
    .AddFeatureFilter<PercentageFilter>();

Теперь canary deployment — это просто изменение процента в конфиге. Без перезапуска!

Способ 3: Kubernetes Canary Deployment для .NET

Если ваш C#-сервис работает в Kubernetes, вы можете использовать нативные механизмы. Создаём два деплоймента:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-api-stable
spec:
  replicas: 10
  selector:
    matchLabels:
      app: orders-api
      version: stable
  template:
    metadata:
      labels:
        app: orders-api
        version: stable
    spec:
      containers:
      - name: api
        image: myrepo/orders-api:1.0.0
        ports:
        - containerPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-api-canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: orders-api
      version: canary
  template:
    metadata:
      labels:
        app: orders-api
        version: canary
    spec:
      containers:
      - name: api
        image: myrepo/orders-api:2.0.0
        ports:
        - containerPort: 8080

А теперь — умный Service, который распределяет трафик:

apiVersion: v1
kind: Service
metadata:
  name: orders-api
spec:
  selector:
    app: orders-api
  ports:
  - port: 80
    targetPort: 8080

По умолчанию Service будет отправлять запросы на все поды с меткой app: orders-api. Чтобы управлять весами, используем Istio или Linkerd. Но

Блоги

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