10Cloud-Native .NET

Уровень 1: Foundation

Cloud Fundamentals

Содержание

  • IaaS vs PaaS vs SaaS — где находится .NET application
  • Managed services — database, cache, message broker, storage
  • Serverless — Azure Functions, AWS Lambda с .NET
  • Service mesh — Istio, Linkerd basics
  • Cloud provider comparison: Azure, AWS, GCP для .NET workload'ов

IaaS vs PaaS vs SaaS — где находится .NET application

Модель управления облачными ресурсами

┌─────────────────────────────────────────────────────────────┐
        │                      Полнота контроля                        │
        │                                                              │
        │  IaaS    │██████████████████░░░░░░░░░░│  Вы управляете ОС,  │
        │           │                           │  middleware, app     │
        │  PaaS    │███████████░░░░░░░░░░░░░░░░│  Вы управляете только│
        │           │                           │  app code & config   │
        │  SaaS    │██░░░░░░░░░░░░░░░░░░░░░░░░│  Вы ничего не управляете│
        │           ────────────────────────────┘                      │
        │           Управление провайдером →                            │
        └─────────────────────────────────────────────────────────────┘

Сравнение моделей

КритерийIaaSPaaSSaaS
Управление ОСВыПровайдерПровайдер
Управление middlewareВыПровайдерПровайдер
Управление приложениемВыВыПровайдер
Управление даннымиВыВыПровайдер
ГибкостьМаксимальнаяСредняяМинимальная
Скорость деплояМедленнаяБыстраяМгновенная
Стоимость управленияВысокаяСредняяНизкая
Примеры для .NETAzure VM, EC2Azure App Service, Elastic BeanstalkOffice 365, Salesforce

.NET в каждой модели

IaaS: Azure Virtual Machines
        ┌─────────────────────────────────┐
        │  Azure Network                  │
        │  ┌───────────────────────────┐  │
        │  │  Windows/Linux VM         │  │ ← Вы управляете всем
        │  │  ┌─────────────────────┐  │  │
        │  │  │  IIS / Kestrel      │  │  │
        │  │  │  .NET Runtime       │  │  │
        │  │  │  SQL Server (local) │  │  │
        │  │  └─────────────────────┘  │  │
        │  └───────────────────────────┘  │
        └─────────────────────────────────┘

        PaaS: Azure App Service
        ┌─────────────────────────────────┐
        │  Azure Network                  │
        │  ┌───────────────────────────┐  │
        │  │  App Service Plan         │  │ ← Провайдер управляет ОС
        │  │  ┌─────────────────────┐  │  │
        │  │  │  Kestrel            │  │  │ ← Вы загружаете только DLL
        │  │  │  .NET Runtime       │  │  │    (managed)
        │  │  │  MyApp.dll          │  │  │
        │  │  └─────────────────────┘  │  │
        │  └───────────────────────────┘  │
        └─────────────────────────────────┘

        Serverless: Azure Functions
        ┌─────────────────────────────────┐
        │  Azure Event Grid / HTTP        │
        │  ┌───────────────────────────┐  │
        │  │  Functions Host           │  │ ← Нет сервера для управления
        │  │  ┌─────────────────────┐  │  │
        │  │  │  Function App       │  │  │
        │  │  │  - ProcessOrder()   │  │  │
        │  │  │  - SendEmail()      │  │  │
        │  │  └─────────────────────┘  │  │
        │  └───────────────────────────┘  │
        └─────────────────────────────────┘

Когда что выбирать

СценарийРекомендацияПочему
Greenfield microservicePaaS (App Service / Container Apps)Быстрый старт, мало ops
Legacy .NET Framework migrationIaaS (VM) → PaaS (App Service)Поэтапная миграция
Batch processingAzure Batch / VM Scale SetsНужен контроль над ОС
Event-driven processingServerless (Functions)Pay-per-execution
Real-time streamingAKS / Service FabricНужен full control
Low-latency APIPaaS (App Service Premium)Pre-warmed instances

Managed Services — database, cache, message broker, storage

Managed Services в облаке

Managed services — это облачные сервисы, где провайдер управляет инфраструктурой, патчингом, бэкапами и масштабированием. Вы работаете только с API.

Основные категории managed services для .NET

КатегорияAzureAWSGCP.NET SDK
DatabaseAzure SQL, Cosmos DBRDS, DynamoDBCloud SQL, SpannerMicrosoft.Data.SqlClient, Azure.Cosmos
CacheAzure Cache for RedisElastiCacheMemorystoreStackExchange.Redis
Message BrokerService Bus, Event HubsSQS, SNS, MQPub/SubAzure.Messaging.ServiceBus
StorageBlob, File, QueueS3, EFS, SQSCloud StorageAzure.Storage.Blobs
ComputeApp Service, FunctionsEC2, LambdaGCE, Cloud RunAzure SDK
AuthEntra ID (AAD)Cognito, IAMIdentity PlatformMicrosoft.Identity.Client

Пример: .NET приложение с managed services

// Program.cs — конфигурация с managed services
        var builder = WebApplication.CreateBuilder(args);

        // Managed Identity для аутентификации (без connection strings в коде)
        builder.Services.AddAuthentication(AzureIdentityDefaults.AuthenticationScheme)
            .AddAzureIdentity();

        // Azure Key Vault для secrets
        builder.Configuration.AddAzureKeyVault(
            new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
            new DefaultAzureCredential());

        // Azure Cache for Redis (через Managed Identity)
        builder.Services.AddStackExchangeRedisCache(options =>
        {
            options.Configuration = $"{builder.Configuration["RedisName"]}.redis.cache.windows.net,ssl=true," +
                                   $"accessKey={builder.Configuration["RedisKey"]}";
            options.InstanceName = "MyApp:";
        });

        // Azure SQL Database (через Managed Identity)
        builder.Services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(builder.Configuration["SqlConnectionString"],
                sql => sql.EnableRetryOnFailure(5)));

        // Azure Cosmos DB (через Managed Identity)
        builder.Services.AddSingleton(new CosmosClient(
            builder.Configuration["CosmosEndpoint"],
            new DefaultAzureCredential()));

        // Azure Service Bus (через Managed Identity)
        builder.Services.AddSingleton<ServiceBusClient>(sp =>
            new ServiceBusClient(
                new Uri($"https://{builder.Configuration["ServiceBusName"]}.servicebus.windows.net/"),
                new DefaultAzureCredential()));

        var app = builder.Build();

Azure SDK для .NET — основные пакеты

# Database
        dotnet add package Microsoft.EntityFrameworkCore.SqlServer
        dotnet add package Azure.Cosmos

        # Cache
        dotnet add package StackExchange.Redis

        # Storage
        dotnet add package Azure.Storage.Blobs
        dotnet add package Azure.Storage.Queues

        # Messaging
        dotnet add package Azure.Messaging.ServiceBus
        dotnet add package Azure.Messaging.EventHubs

        # Auth & Identity
        dotnet add package Azure.Identity
        dotnet add package Azure.Security.KeyVault.Secrets

        # Core SDK
        dotnet add package Azure.Core
        dotnet add package Azure.Storage.Common

Serverless — Azure Functions, AWS Lambda с .NET

Что такое Serverless

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

Azure Functions vs AWS Lambda для .NET

КритерийAzure FunctionsAWS Lambda
.NET поддержкаОтличная (родная)Хорошая (.NET 8+)
ПланыConsumption, Premium, Dedicated, FlexOn-Demand, Provisioned, EC2
Cold start~1-5s (Consumption), ~100ms (Premium)~500ms-2s ( .NET)
Max execution10 минут (Consumption)15 минут
Trigger types50+ triggers20+ triggers
Durable FunctionsВстроеноStep Functions (отдельно)
Local developmentExcellent (FuncCoreTool)Good (SAM CLI)
CI/CDAzure DevOps, GitHub ActionsCodePipeline, GitHub Actions

Azure Functions — типы триггеров

┌──────────────────────────────────────────────────────┐
        │                 Azure Functions Triggers              │
        │                                                      │
        │  HTTP Trigger                                       │
        │  ┌──────────────────────────────────────────────┐   │
        │  │  [Function1] HTTP → ProcessOrder()           │   │
        │  └──────────────────────────────────────────────┘   │
        │                                                      │
        │  Queue Trigger                                      │
        │  ┌──────────────────────────────────────────────┐   │
        │  │  Service Bus Queue → SendEmail()             │   │
        │  └──────────────────────────────────────────────┘   │
        │                                                      │
        │  Timer Trigger                                      │
        │  ┌──────────────────────────────────────────────┐   │
        │  │  Every 5 min → CleanupTempFiles()            │   │
        │  └──────────────────────────────────────────────┘   │
        │                                                      │
        │  Blob Trigger                                       │
        │  ┌──────────────────────────────────────────────┐   │
        │  │  New blob → ResizeImage()                    │   │
        │  └──────────────────────────────────────────────┘   │
        │                                                      │
        │  Event Grid Trigger                                 │
        │  ┌──────────────────────────────────────────────┐   │
        │  │  Storage event → NotifySubscribers()         │   │
        │  └──────────────────────────────────────────────┘   │
        │                                                      │
        │  Cosmos DB Trigger                                  │
        │  ┌──────────────────────────────────────────────┐   │
        │  │  Document change → SyncCache()               │   │
        │  └──────────────────────────────────────────────┘   │
        └──────────────────────────────────────────────────────┘

Azure Functions — Model Binding (.NET Isolated)

// .NET 8 isolated worker model
        using Microsoft.Azure.Functions.Worker;
        using Microsoft.Azure.Functions.Worker.Http;
        using Microsoft.Extensions.Logging;
        using Azure.Messaging.ServiceBus;

        public class OrderFunctions
        {
            private readonly ServiceBusSender _orderSender;
            private readonly ILogger _logger;

            public OrderFunctions(ServiceBusSender orderSender, ILoggerFactory loggerFactory)
            {
                _orderSender = orderSender;
                _logger = loggerFactory.CreateLogger<OrderFunctions>();
            }

            // HTTP Trigger — REST API endpoint
            [Function("CreateOrder")]
            public async Task<HttpResponseData> CreateOrder(
                [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
            {
                var order = await req.ReadFromJsonAsync<Order>();
        
                var message = new ServiceBusMessage(JsonSerializer.Serialize(order))
                {
                    MessageId = Guid.NewGuid().ToString(),
                    ContentType = "application/json",
                    Subject = "OrderCreated"
                };

                await _orderSender.SendMessageAsync(message);
        
                var response = req.CreateResponse(HttpStatusCode.Created);
                await response.WriteStringAsync($"Order {order.Id} queued");
                return response;
            }

            // Service Bus Trigger — async processor
            [Function("ProcessOrder")]
            public async Task ProcessOrder(
                [ServiceBusTrigger("orders", Connection = "ServiceBusConnection")] 
                ServiceBusMessage message)
            {
                var order = JsonSerializer.Deserialize<Order>(message.Body);
                _logger.LogInformation("Processing order {OrderId}", order?.Id);
        
                // Process the order...
                await CompleteMessageAsync(message);
            }

            // Timer Trigger — scheduled job
            [Function("DailyCleanup")]
            public void DailyCleanup([TimerTrigger("0 0 * * *")] TimerInfo myTimer)
            {
                _logger.LogInformation($"Cleanup started at {DateTime.UtcNow}");
                // Cleanup logic...
            }
        }

Durable Functions — оркестрация

using Microsoft.Azure.Functions.Worker.Durable;
        using Microsoft.Extensions.Logging;

        public class OrderProcessingOrchestrator
        {
            [Function(nameof(OrderProcessingOrchestrator))]
            public async Task<List<string>> RunOrchestration(
                [DurableClient] DurableTaskClient client,
                [OrchestrationTrigger] OrchestrationContext context)
            {
                var orderId = context.Input as string;
                var results = new List<string>();

                // Step 1: Validate order (parallel)
                var validationTask = context.CallActivityAsync<bool>(
                    nameof(ValidateOrderActivity), orderId);

                // Step 2: Check inventory (parallel)
                var inventoryTask = context.CallActivityAsync<bool>(
                    nameof(CheckInventoryActivity), orderId);

                // Step 3: Process payment
                var paymentTask = context.CallActivityAsync<bool>(
                    nameof(ProcessPaymentActivity), orderId);

                // Ждём все параллельные задачи
                await Task.WhenAll(validationTask, inventoryTask);

                if (!validationTask.Result || !inventoryTask.Result)
                {
                    await context.CallActivityAsync(nameof(RejectOrderActivity), orderId);
                    results.Add("Order rejected");
                    return results;
                }

                if (!paymentTask.Result)
                {
                    await context.CallActivityAsync(nameof(RejectOrderActivity), orderId);
                    results.Add("Payment failed");
                    return results;
                }

                // Step 4: Ship order
                await context.CallActivityAsync(nameof(ShipOrderActivity), orderId);
                results.Add("Order completed");

                return results;
            }
        }

        // Activities — отдельные функции
        public class OrderActivities
        {
            private readonly ILogger _logger;

            public OrderActivities(ILoggerFactory loggerFactory)
            {
                _logger = loggerFactory.CreateLogger<OrderActivities>();
            }

            [Function("ValidateOrderActivity")]
            public bool ValidateOrder([ActivityTrigger] string orderId)
            {
                _logger.LogInformation("Validating order {OrderId}", orderId);
                return true; // validation logic
            }

            [Function("CheckInventoryActivity")]
            public bool CheckInventory([ActivityTrigger] string orderId)
            {
                _logger.LogInformation("Checking inventory for {OrderId}", orderId);
                return true; // inventory logic
            }

            [Function("ProcessPaymentActivity")]
            public bool ProcessPayment([ActivityTrigger] string orderId)
            {
                _logger.LogInformation("Processing payment for {OrderId}", orderId);
                return true; // payment logic
            }

            [Function("ShipOrderActivity")]
            public void ShipOrder([ActivityTrigger] string orderId)
            {
                _logger.LogInformation("Shipping order {OrderId}", orderId);
            }

            [Function("RejectOrderActivity")]
            public void RejectOrder([ActivityTrigger] string orderId)
            {
                _logger.LogInformation("Rejecting order {OrderId}", orderId);
            }
        }

AWS Lambda с .NET

// AWS Lambda с .NET — функция обработки S3 события
        using Amazon.Lambda.Core;
        using Amazon.Lambda.S3Events;
        using Amazon.Lambda.RuntimeSupport;
        using Amazon.S3;
        using Amazon.S3.Model;

        // Function handler
        public class Function
        {
            private readonly AmazonS3Client _s3Client = new();

            /// <summary>
            /// Default function — S3 event handler
            /// </summary>
            public string HandleS3Event(S3Event ev, ILambdaContext context)
            {
                foreach (var record in ev.Records)
                {
                    context.Logger.LogLine($"Bucket: {record.S3.Bucket.Name}");
                    context.Logger.LogLine($"Key: {record.S3.Object.Key}");
            
                    // Process the object
                    var response = _s3Client.GetObjectAsync(
                        record.S3.Bucket.Name, record.S3.Object.Key).Result;
            
                    // Process content...
                }
                return "Processed";
            }

            /// <summary>
            /// HTTP API handler — API Gateway integration
            /// </summary>
            public async Task<APIGatewayHttpApiV2ProxyResponse> HandleRequest(
                APIGatewayHttpApiV2ProxyRequest request, 
                ILambdaContext context)
            {
                var body = JsonSerializer.Deserialize<Order>(request.Body);
        
                // Business logic...
        
                return new APIGatewayHttpApiV2ProxyResponse
                {
                    StatusCode = 200,
                    Body = JsonSerializer.Serialize(new { status = "ok" }),
                    Headers = new Dictionary<string, string>
                    {
                        { "Content-Type", "application/json" }
                    }
                };
            }
        }

        // Program.cs — Lambda bootstrapper
        public class Program
        {
            public static void Main(string[] args) 
                => LambdaBootstrapBuilder.Create<Function, APIGatewayHttpApiV2ProxyRequest>
                    (.Build())
                    .StartAsync().Wait();
        }

Service mesh — Istio, Linkerd basics

Что такое Service Mesh

Service mesh — это выделенный слой инфраструктуры для обработки коммуникации между сервисами. Каждый сервис получает sidecar-прокси, который управляет сетевым трафиком, безопасностью и наблюдаемостью.

Архитектура Service Mesh

┌─────────────────────────────────────────────────────────┐
        │                    Kubernetes Pod                        │
        │                                                         │
        │  ┌─────────────────────┐    ┌────────────────────────┐ │
        │  │  App Container      │    │  Sidecar Proxy (Envoy) │ │
        │  │                     │    │                        │ │
        │  │  Order Service      │◄──►│  - mTLS                │ │
        │  │  .NET Process       │    │  - Load balancing      │ │
        │  │                     │    │  - Retry / Circuit     │ │
        │  │                     │    │  - Tracing             │ │
        │  └─────────────────────┘    └────────────────────────┘ │
        │                                                         │
        │  Control Plane (Istio/Pilot) → управляет всеми sidecar'ами
        └─────────────────────────────────────────────────────────┘

Istio для .NET микросервисов

# Istio VirtualService — routing rules для .NET services
        apiVersion: networking.istio.io/v1beta1
        kind: VirtualService
        metadata:
          name: order-service
        spec:
          hosts:
          - order-service
          http:
          - match:
            - headers:
                x-canary:
                  exact: "true"
            route:
            - destination:
                host: order-service
                subset: canary
                port:
                  number: 8080
              weight: 10
          - route:
            - destination:
                host: order-service
                subset: stable
                port:
                  number: 8080
              weight: 90
        ---
        # Istio DestinationRule — определение subsets
        apiVersion: networking.istio.io/v1beta1
        kind: DestinationRule
        metadata:
          name: order-service
        spec:
          host: order-service
          subsets:
          - name: stable
            labels:
              version: v1
          - name: canary
            labels:
              version: v2
        ---
        # Istio DestinationRule — circuit breaker
        apiVersion: networking.istio.io/v1beta1
        kind: DestinationRule
        metadata:
          name: payment-service
        spec:
          host: payment-service
          trafficPolicy:
            connectionPool:
              tcp:
                maxConnections: 100
              http:
                h2UpgradePolicy: DEFAULT
                http1MaxPendingRequests: 100
                http2MaxRequests: 1000
            outlierDetection:
              consecutive5xxErrors: 5
              interval: 30s
              baseEjectionTime: 30s
              maxEjectionPercent: 50

.NET client с Istio mTLS

// .NET HTTP client автоматически работает с Istio mTLS
        // Sidecar proxy обрабатывает шифрование/дешифрование

        public class OrderClient
        {
            private readonly HttpClient _httpClient;

            public OrderClient(IHttpClientFactory factory)
            {
                _httpClient = factory.CreateClient("OrderService");
                // Istio sidecar автоматически обрабатывает mTLS
                // Не нужно настраивать сертификаты в коде
            }

            public async Task<Order?> GetOrderAsync(string orderId)
            {
                var response = await _httpClient.GetAsync($"/api/orders/{orderId}");
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadFromJsonAsync<Order>();
            }
        }

        // Program.cs — настройка HttpClient с retry и circuit breaker
        builder.Services.AddHttpClient("OrderService", client =>
        {
            client.BaseAddress = new Uri("http://order-service");
            client.Timeout = TimeSpan.FromSeconds(10);
        })
        .AddPolicyHandler(GetRetryPolicy())
        .AddPolicyHandler(GetCircuitBreakerPolicy());

        static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
        {
            return HttpPolicyExtensions
                .HandleTransientHttpError()
                .OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests)
                .WaitAndRetryAsync(3, retryAttempt => 
                    TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
        }

        static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
        {
            return HttpPolicyExtensions
                .HandleTransientHttpError()
                .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
        }

Linkerd vs Istio для .NET

КритерийIstioLinkerd
ПроксиEnvoy (Go)Linkerd2-proxy (Rust)
Размер sidecar~40 MB~5 MB
Задержка~1ms~0.1ms
ФункцииПолный набор (routing, mTLS, tracing)Базовые (mTLS, observability, retry)
СложностьВысокаяНизкая
Для .NETХорошая поддержкаОтличная (низкий overhead)
РекомендацияEnterprise, сложные routing rulesВысокая нагрузка, low-latency

Cloud provider comparison: Azure, AWS, GCP для .NET workload'ов

Сравнение провайдеров для .NET

КритерийAzureAWSGCP
.NET интеграцияРодная (Microsoft)Хорошая (.NET 8+)Хорошая (.NET 8+)
ServerlessAzure Functions (лучшая)LambdaCloud Functions
ContainersAKS, Container AppsEKS, ECSGKE
PaaSApp ServiceElastic BeanstalkCloud Run
DatabaseSQL Database, Cosmos DBRDS, DynamoDBCloud SQL, Spanner
MessagingService Bus, Event HubsSQS, SNS, MQPub/Sub
MonitoringApplication InsightsCloudWatch, X-RayCloud Trace, Monitoring
IdentityEntra ID (AAD)Cognito, IAMIdentity Platform
DevOpsAzure DevOpsCodePipelineCloud Build
Free tier$200 credit + free tier12 months free$300 credit

Выбор провайдера для .NET

┌────────────────────────────────────────────────────────┐
        │           Выбор облачного провайдера для .NET           │
        │                                                        │
        │  Microsoft stack в проекте?                            │
        │  ├── Да → Azure (лучшая интеграция)                    │
        │  │   ├── Entra ID + AAD → нативная auth                │
        │  │   ├── Visual Studio → нативная интеграция           │
        │  │   ├── .NET SDK → лучшие пакеты                      │
        │  │   └── Azure DevOps → лучший CI/CD                   │
        │  │                                                      │
        │  ├── AWS уже используется?                             │
        │  │   ├── Да → AWS (меньше миграция)                    │
        │  │   │   ├── .NET на Lambda / ECS / EKS                │
        │  │   │   └── AWS SDK for .NET                          │
        │  │   └── Нет → GCP                                   │
        │  │                                                      │
        │  └── Kubernetes-first стратегия?                       │
        │      ├── Да → GKE или AKS (оба отличные)               │
        │      └── Multi-cloud → абстракция через Ports/Adapters │
        └────────────────────────────────────────────────────────┘

.NET SDK пакеты по провайдерам

# Azure — основные пакеты
        dotnet add package Azure.Identity
        dotnet add package Azure.Storage.Blobs
        dotnet add package Azure.Messaging.ServiceBus
        dotnet add package Azure.Cosmos
        dotnet add package Azure.Security.KeyVault.Secrets
        dotnet add package Azure.Messaging.EventHubs
        dotnet add package Microsoft.Azure.Functions.Worker
        dotnet add package Microsoft.Azure.Functions.Worker.Sdk

        # AWS — основные пакеты
        dotnet add package AWSSDK.S3
        dotnet add package AWSSDK.Lambda
        dotnet add package AWSSDK.SQS
        dotnet add package AWSSDK.SNS
        dotnet add package AWSSDK.DynamoDBv2
        dotnet add package Amazon.Lambda.Core
        dotnet add package Amazon.Lambda.RuntimeSupport
        dotnet add package Amazon.Lambda.APIGatewayEvents

        # GCP — основные пакеты
        dotnet add package Google.Cloud.Storage.V1
        dotnet add package Google.Cloud.PubSub.V1
        dotnet add package Google.Cloud.Spanner.Data
        dotnet add package Google.Cloud.SecretManager.V1

Сводная таблица: Best Practices

ПрактикаРекомендация
Выбор моделиGreenfield → PaaS, Legacy → IaaS → PaaS
Managed servicesПредпочитать managed over self-hosted
ServerlessEvent-driven workloads, batch processing
Service meshIstio для complex routing, Linkerd для low-latency
Provider выборMicrosoft stack → Azure, Kubernetes-first → GKE/AKS
.NET SDKИспользовать официальные Azure/AWS/GCP SDK
ConfigurationManaged Identity + Key Vault, no connection strings
MonitoringApplication Insights (Azure) / CloudWatch (AWS)
DeploymentCI/CD с GitHub Actions / Azure DevOps
ScalingAuto-scale на основе queue depth / CPU / custom metrics

Практика


Managed Identity и Cloud Security

Содержание

  • Azure Managed Identity — zero-secret authentication к cloud services
  • AWS IAM Roles for service-to-service auth
  • Key Vault / Secrets Manager integration
  • Network security — VNet, private endpoints, service endpoints
  • Firewall rules и allowed IP ranges для managed databases

Azure Managed Identity — zero-secret authentication к cloud services

Что такое Managed Identity

Managed Identity — это автоматически управляемая Azure служба, которая даёт ресурсам Azure аутентифицироваться в других сервисах, поддерживающих Azure AD, без необходимости хранить credentials в коде.

Типы Managed Identity

ТипЖизненный циклУправлениеСценарий
System-assignedПривязан к ресурсуAzure управляетApp Service, VM, Functions
User-assignedНезависимый ресурсВы управляетеНесколько ресурсов, миграция

System-assigned vs User-assigned

System-assigned:
        ┌─────────────────────┐
        │  App Service         │
        │  ┌────────────────┐  │
        │  │  .NET App      │  │ ← Один identity на ресурс
        │  │  (managed)     │  │
        │  └────────────────┘  │
        └─────────────────────┘
            │
            └──► Azure AD (один principal)

        User-assigned:
        ┌─────────────────────┐    ┌─────────────────────┐
        │  User-assigned      │    │  User-assigned      │
        │  Identity           │    │  Identity           │
        │  (независимый)       │    │  (независимый)       │
        └─────────────────────┘    └─────────────────────┘
                 │                           │
            ┌────┴────┐              ┌──────┴──────┐
            │         │              │             │
          ┌─▼─┐    ┌─▼─┐          ┌─▼─┐       ┌─▼─┐
          │App│    │VM │          │App│       │Fn │
          └───┘    └───┘          └───┘       └───┘
          (оба используют один identity)

Настройка Managed Identity

# Включение system-assigned identity для App Service
        az webapp identity assign --name myapp --resource-group my-rg

        # Включение user-assigned identity
        az identity create --name my-app-identity --resource-group my-rg

        # Привязка user-assigned identity к App Service
        az webapp identity assign --name myapp --resource-group my-rg \
            --identities <user-identity-resource-id>

Managed Identity в .NET коде

// Program.cs — аутентификация через Managed Identity
        using Azure.Identity;
        using Azure.Security.KeyVault.Secrets;
        using Azure.Storage.Blobs;
        using Azure.Messaging.ServiceBus;
        using Microsoft.Data.SqlClient;

        var builder = WebApplication.CreateBuilder(args);

        // DefaultAzureCredential автоматически выбирает лучший метод:
        // 1. Managed Identity (в Azure)
        // 2. Visual Studio / Azure CLI (локально)
        // 3. Environment variables (CI/CD)
        var credential = new DefaultAzureCredential();

        // Key Vault — без connection string
        var keyVaultUri = new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/");
        builder.Configuration.AddAzureKeyVault(keyVaultUri, credential);

        // Blob Storage — через Managed Identity
        builder.Services.AddSingleton<BlobServiceClient>(sp =>
            new BlobServiceClient(
                new Uri($"https://{builder.Configuration["StorageAccount"]}.blob.core.windows.net/"),
                credential));

        // Service Bus — через Managed Identity
        builder.Services.AddSingleton<ServiceBusClient>(sp =>
            new ServiceBusClient(
                new Uri($"https://{builder.Configuration["ServiceBusName"]}.servicebus.windows.net/"),
                credential));

        // SQL Database — через Azure AD Managed Identity
        var sqlConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
        builder.Services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(sqlConnectionString, sql =>
            {
                sql.EnableRetryOnFailure(5);
                sql.UseAzureADAccessToken(async (context, token) =>
                {
                    var result = await credential.GetTokenAsync(
                        new Azure.Core.TokenRequestContext(new[] { "https://database.windows.net/.default" }),
                        token);
                    context.SetAccessToken(result.Token);
                });
            }));

        var app = builder.Build();

Назначение ролей для Managed Identity

# Дать App Service доступ к Key Vault
        az keyvault set-policy --name my-kv --resource-group my-rg \
            --object-id <app-service-object-id> \
            --secret-permissions get list

        # Дать App Service доступ к Storage Account
        az storage account set-policy --name my-storage --resource-group my-rg \
            --object-id <app-service-object-id> \
            --roles storage-blob-data-reader storage-blob-data-contributor

        # Дать App Service доступ к Service Bus
        az servicebus namespace set-policy --name my-ns --resource-group my-rg \
            --object-id <app-service-object-id> \
            --action Microsoft.ServiceBus/namespaces/queues/read \
            --action Microsoft.ServiceBus/namespaces/queues/write \
            --action Microsoft.ServiceBus/namespaces/queues/send

Managed Identity chain — service-to-service authentication

┌──────────┐    ┌──────────┐    ┌──────────┐
        │  Web App │───►│  Service │───►│  Cosmos  │
        │  (MI)    │    │  Bus     │    │  DB      │
        └──────────┘    └──────────┘    └──────────┘
             │               │
             │               ▼
             │          ┌──────────┐
             │          │  Worker  │
             │          │  (MI)    │
             │          └──────────┘
             │               │
             ▼               ▼
        ┌──────────┐    ┌──────────┐
        │ Key Vault│    │  Blob    │
        │  (MI)    │    │ Storage  │
        └──────────┘    └──────────┘

        Все сервисы аутентифицируются через Managed Identity.
        Нет connection strings, нет secrets в коде.

AWS IAM Roles for service-to-service auth

AWS IAM Roles для .NET

┌─────────────────────────────────────────────────────────┐
        │              AWS IAM Role Structure                      │
        │                                                          │
        │  EC2 / ECS / Lambda                                      │
        │  ┌──────────────────────────┐                            │
        │  │  .NET Application        │                            │
        │  │                          │                            │
        │  │  IAM Role: LambdaExec    │◄─── Trust Policy           │
        │  │  ┌────────────────────┐  │    (кто может использовать)│
        │  │  │  Policy: S3Access  │  │                            │
        │  │  │  Policy: DynamoDB  │  │                            │
        │  │  │  Policy: CloudWatch│  │                            │
        │  │  └────────────────────┘  │                            │
        │  └──────────────────────────┘                            │
        │                                                          │
        │  AWS выдает временные credentials                        │
        │  (Access Key + Secret Key + Session Token)               │
        │  Автоматически ротация                                   │
        └─────────────────────────────────────────────────────────┘

AWS IAM Role в .NET

// AWS SDK for .NET автоматически использует IAM Role
        // При запуске в EC2/ECS/Lambda — credentials получаются из IMDS

        using Amazon.S3;
        using Amazon.DynamoDBv2;
        using Amazon.Lambda.Core;

        // При запуске в Lambda / ECS — IAM Role используется автоматически
        // SDK получает временные credentials из IMDS (EC2 Metadata Service)
        var s3Client = new AmazonS3Client();       // IAM Role credentials
        var dynamoClient = new AmazonDynamoDBv2Client(); // IAM Role credentials

        // Нет необходимости хранить Access Key / Secret Key в коде
        // AWS автоматически ротация credentials каждые 15-60 минут

IAM Policy для .NET приложения

{
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "s3:GetObject",
                        "s3:PutObject",
                        "s3:ListBucket"
                    ],
                    "Resource": [
                        "arn:aws:s3:::my-app-bucket",
                        "arn:aws:s3:::my-app-bucket/*"
                    ]
                },
                {
                    "Effect": "Allow",
                    "Action": [
                        "dynamodb:GetItem",
                        "dynamodb:PutItem",
                        "dynamodb:UpdateItem",
                        "dynamodb:DeleteItem",
                        "dynamodb:Query",
                        "dynamodb:Scan"
                    ],
                    "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/MyAppTable"
                },
                {
                    "Effect": "Allow",
                    "Action": [
                        "secretsmanager:GetSecretValue",
                        "secretsmanager:DescribeSecret"
                    ],
                    "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/*"
                },
                {
                    "Effect": "Allow",
                    "Action": [
                        "logs:CreateLogGroup",
                        "logs:CreateLogStream",
                        "logs:PutLogEvents",
                        "logs:DescribeLogStreams"
                    ],
                    "Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/myapp:*"
                }
            ]
        }

AWS Secrets Manager vs Azure Key Vault

КритерийAzure Key VaultAWS Secrets Manager
ТипыSecrets, Keys, CertificatesSecrets only
АутентификацияManaged Identity, RBACIAM Role, Resource Policy
Автоматическая ротацияДа (Function + Key Vault)Да (Lambda)
.NET SDKAzure.Security.KeyVault.SecretsAWSSDK.SecretsManager
Multi-regionДаДа
Soft deleteДа (14-90 дней)Да (7-30 дней)
HSM поддержкаДа (Managed HSM)Да (CloudHSM)

Key Vault / Secrets Manager integration

Azure Key Vault в .NET — продвинутая конфигурация

// Program.cs — Key Vault integration с кэшированием и retry
        using Azure.Identity;
        using Azure.Security.KeyVault.Secrets;
        using Microsoft.Extensions.Caching.Memory;

        var builder = WebApplication.CreateBuilder(args);

        // Key Vault с кэшированием — уменьшает количество запросов к KV
        builder.Services.AddAzureKeyVaultSecrets((services, config) =>
        {
            config.KeyVaultUrl = new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/");
            config.Credential = new DefaultAzureCredential();
            config.CacheOptions = new AzureKeyVaultCacheOptions
            {
                ExpirationTimeout = TimeSpan.FromMinutes(4),      // Кэш на 4 минуты
                RefreshAfter = TimeSpan.FromMinutes(3),             // Обновить после 3 минут
                OnError = AzureKeyVaultCacheOnErrorOptions.Ignore    // Игнорировать ошибки кэша
            };
        });

        // Или вручную с кэшированием
        builder.Services.AddSingleton<ISecretProvider, CachingSecretProvider>();

        public class CachingSecretProvider : ISecretProvider
        {
            private readonly SecretClient _client;
            private readonly IMemoryCache _cache;
            private readonly ILogger<CachingSecretProvider> _logger;

            public CachingSecretProvider(SecretClient client, IMemoryCache cache, ILogger<CachingSecretProvider> logger)
            {
                _client = client;
                _cache = cache;
                _logger = logger;
            }

            public string GetSecret(string name)
            {
                return _cache.GetOrCreate($"secret:{name}", entry =>
                {
                    entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(4));
                    _logger.LogDebug("Fetching secret '{Name}' from Key Vault", name);
                    return _client.GetSecret(name).Value.Value;
                }) ?? throw new KeyNotFoundException($"Secret '{name}' not found");
            }
        }

Configuration с Key Vault

// Program.cs — Key Vault как источник configuration
        builder.Configuration
            .AddEnvironmentVariables()           // 1. Environment variables (lowest)
            .AddUserSecrets<Program>()           // 2. User secrets (local dev)
            .AddAzureKeyVault(                 // 3. Key Vault (production)
                new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
                new DefaultAzureCredential());

Key Vault Best Practices

// Best Practice 1: Использовать DefaultAzureCredential
        // — Автоматически выбирает лучший метод аутентификации
        // — Local dev: Visual Studio / Azure CLI
        // — Azure: Managed Identity
        // — CI/CD: Service Principal через env vars

        // Best Practice 2: Кэшировать secrets
        // — Key Vault имеет rate limits (200 RPS на vault)
        // — Кэшировать на 3-4 минуты
        // — Refresh after: 3 минуты

        // Best Practice 3: Использовать separate vaults per environment
        // — dev, staging, production — отдельные vaults
        // — Разные RBAC permissions для каждого

        // Best Practice 4: Использовать soft delete + purge protection
        // — Soft delete: recovery 14-90 дней
        // — Purge protection: защита от accidental deletion

        // Best Practice 5: Monitor Key Vault access
        // — Azure Monitor + Log Analytics
        // — Alert на подозрительную активность
        // — Audit log для compliance

Network security — VNet, private endpoints, service endpoints

Network Security в Azure

┌──────────────────────────────────────────────────────────────┐
        │                    Azure Virtual Network                      │
        │                                                              │
        │  ┌──────────────────────────────────────────────────────┐   │
        │  │                  Subnet: App                          │   │
        │  │                                                      │   │
        │  │  ┌──────────┐  ┌──────────┐  ┌──────────┐          │   │
        │  │  │ App Svc  │  │ App Svc  │  │ App Svc  │          │   │
        │  │  │ (VM)     │  │ (VM)     │  │ (VM)     │          │   │
        │  │  └────┬─────┘  └────┬─────┘  └────┬─────┘          │   │
        │  │       └──────────────┼──────────────┘               │   │
        │  │                      │                               │   │
        │  │              ┌───────▼────────┐                      │   │
        │  │              │  NSG (App)     │                      │   │
        │  │              │  Inbound: 443  │                      │   │
        │  │              │  Outbound: KV  │                      │   │
        │  │              └───────┼────────┘                      │   │
        │  └──────────────────────┼──────────────────────────────┘   │
        │                          │                                  │
        │  ┌───────────────────────┼──────────────────────────────┐  │
        │  │                  Subnet: Data                         │  │
        │  │                                                       │  │
        │  │                    Private Endpoint                    │  │
        │  │                    ┌──────────┐                        │  │
        │  │                    │  SQL DB  │  ← Private IP        │  │
        │  │                    └──────────┘                        │  │
        │  │                    ┌──────────┐                        │  │
        │  │                    │ Cosmos DB│  ← Private IP        │  │
        │  │                    └──────────┘                        │  │
        │  │                    ┌──────────┐                        │  │
        │  │                    │ KV       │  ← Private IP        │  │
        │  │                    └──────────┘                        │  │
        │  └───────────────────────────────────────────────────────┘  │
        │                                                              │
        │  ┌──────────────────────────────────────────────────────┐   │
        │  │  Azure Firewall / NVA — outbound control             │   │
        │  └──────────────────────────────────────────────────────┘   │
        └──────────────────────────────────────────────────────────────┘

Private Endpoint для Azure Services

# Terraform — Private Endpoint для SQL Database
        resource "azurerm_private_endpoint" "sql" {
          name                = "sql-private-endpoint"
          resource_group_name = azurerm_resource_group.main.name
          location            = azurerm_resource_group.main.location
          subnet_id           = azurerm_subnet.data.id

          private_service_connection {
            name                           = "sql-connection"
            private_connection_resource_id = azurerm_sql_database.main.id
            is_manual_connection           = false
            subresource_name               = "sqlServer"
          }

          private_dns_zone_group {
            name                 = "sql-dns-zone"
            private_dns_zone_ids = [azurerm_private_dns_zone.sql.id]
          }
        }

        resource "azurerm_private_dns_zone" "sql" {
          name                = "privatelink.database.windows.net"
          resource_group_name = azurerm_resource_group.main.name
        }
// .NET — подключение к SQL через Private Endpoint
        // Приложение в VNet → Private Endpoint → SQL Database
        // Трафик НЕ выходит в интернет

        builder.Services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(
                "Server=tcp:mydb.privatelink.database.windows.net,1433;" +
                "Initial Catalog=mydb;" +
                "Authentication=Active Directory Managed Identity;" +
                "Encrypt=True;TrustServerCertificate=False;",
                sql => sql.EnableRetryOnFailure(5)));

        // Connection flow:
        // App (VNet subnet) → Private Endpoint (10.0.2.4) → SQL Database
        // Весь трафик остаётся в Azure backbone network

Network Security Groups (NSG)

# NSG rules для App subnet
        # Inbound:
        #   - Allow 443 from Application Gateway
        #   - Deny all other
        # Outbound:
        #   - Allow 443 to Key Vault (Private Endpoint)
        #   - Allow 443 to SQL DB (Private Endpoint)
        #   - Allow 443 to Service Bus (Private Endpoint)
        #   - Deny all other

        # Azure Policy — обязательное использование Private Endpoints
        {
          "mode": "All",
          "policyRule": {
            "if": {
              "allOf": [
                { "field": "type", "equals": "Microsoft.Sql/servers" },
                { "field": "Microsoft.Sql/servers/networkRuleSet.appliedEvaluations", 
                  "count": 0, "equals": 1 }
              ]
            },
            "then": { "effect": "deny" }
          }
        }

Firewall rules и allowed IP ranges для managed databases

Database Security Patterns

┌─────────────────────────────────────────────────────────────┐
        │              Database Security Patterns                      │
        │                                                              │
        │  Pattern 1: Public + Firewall                               │
        │  ┌──────────┐    ┌──────────┐    ┌──────────┐              │
        │  │  App     │───►│  Firewall│───►│  SQL DB  │              │
        │  │  IP:     │    │  Rules:  │    │          │              │
        │  │  203.0.. │    │  203.0.. │    │          │              │
        │  └──────────┘    └──────────┘    └──────────┘              │
        │                                                              │
        │  Pattern 2: Private Endpoint (рекомендуется)                │
        │  ┌──────────┐    ┌──────────┐    ┌──────────┐              │
        │  │  App     │───►│  Private │───►│  SQL DB  │              │
        │  │  VNet    │    │  Endpoint│    │  (no pub)│              │
        │  │  10.0.2..│    │  10.0.2..│    │          │              │
        │  └──────────┘    └──────────┘    └──────────┘              │
        │                                                              │
        │  Pattern 3: Hybrid (on-prem + cloud)                        │
        │  ┌──────────┐    ┌──────────┐    ┌──────────┐              │
        │  │  On-prem │───►│  Express │───►│  SQL DB  │              │
        │  │  Network │    │  Route   │    │          │              │
        │  └──────────┘    └──────────┘    └──────────┘              │
        └─────────────────────────────────────────────────────────────┘

SQL Database — Security Configuration

-- SQL Database — Managed Identity authentication
        -- 1. Создать Azure AD Admin
        EXEC sp_configure 'azure_ad_only_authentication', 1;
        RECONFIGURE;

        -- 2. Создать user из Azure AD
        CREATE USER [myapp-service-principal] FROM EXTERNAL PROVIDER;

        -- 3. Дать права
        ALTER ROLE db_datareader ADD MEMBER [myapp-service-principal];
        ALTER ROLE db_datawriter ADD MEMBER [myapp-service-principal];

        -- 4. Проверить firewall rules
        SELECT * FROM sys.firewall_rules;

        -- 5. Разрешить только VNet (через Azure Portal/CLI)
        -- az sql db firewall-rule create --resource-group my-rg 
        --   --server mydb --name "VNetRule" 
        --   --start-ip-address 10.0.0.0 --end-ip-address 10.0.255.255

Cosmos DB — Security Configuration

// Cosmos DB — аутентификация через Managed Identity
        using Azure.Cosmos;
        using Azure.Identity;

        var credential = new DefaultAzureCredential();

        // Cosmos DB поддерживает Azure AD authentication
        var cosmosClient = new CosmosClient(
            new Uri($"https://{accountName}.documents.azure.com/"),
            credential,
            new CosmosClientOptions
            {
                ConnectionMode = Gateway,  // Gateway mode для Azure AD
                SerializerOptions = new CosmosSerializationOptions
                {
                    PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
                }
            });

        // Или через connection string (не рекомендуется для production)
        // var cosmosClient = new CosmosClient(connectionString);

Redis Cache — Security Configuration

// Azure Cache for Redis — аутентификация через Access Key
        // Redis не поддерживает Managed Identity напрямую
        // Используйте Key Vault для хранения Access Key

        using StackExchange.Redis;
        using Azure.Security.KeyVault.Secrets;

        var kvClient = new SecretClient(
            new Uri($"https://{kvName}.vault.azure.net/"),
            new DefaultAzureCredential());

        var redisKey = await kvClient.GetSecretAsync("RedisAccessKey");

        var configuration = new ConfigurationOptions
        {
            Endpoints = { $"{redisName}.redis.cache.windows.net:6380" },
            Ssl = true,
            AbortOnConnectFail = false,
            AllowAdmin = false,
            Password = redisKey.Value.Value,
            ConnectRetry = 3,
            SyncTimeout = 5000
        };

        var redis = ConnectionMultiplexer.Connect(configuration);
        var db = redis.GetDatabase();

Сводная таблица: Best Practices

ПрактикаРекомендация
АутентификацияManaged Identity везде, нигде не хранить secrets
Key VaultКэшировать на 3-4 минуты, separate vaults per env
NetworkPrivate Endpoints для всех managed services
NSGМинимальные правила, только нужные порты
DatabaseAzure AD auth + Private Endpoint + Firewall
Cosmos DBGateway mode для Azure AD auth
RedisAccess Key в Key Vault (нет MI support)
AWSIAM Roles + Secrets Manager + VPC Endpoints
MonitoringAudit log для Key Vault / IAM
ComplianceSoft delete + Purge Protection для KV

Практика


Cloud Storage Patterns

Содержание

  • Blob storage — Azure Blob, AWS S3 — tiered storage, lifecycle policies
  • File storage — Azure Files, EFS — shared file system patterns
  • Queue storage — Azure Queue Storage, SQS — simple messaging
  • Table storage / DynamoDB — NoSQL key-value patterns

Blob storage — Azure Blob, AWS S3

Azure Blob Storage — структура

Storage Account
        ├── Container (bucket)
        │   ├── Folder/
        │   │   ├── file1.txt (Block Blob — 0..190 GB)
        │   │   └── file2.pdf (Block Blob)
        │   └── images/
        │       ├── photo.jpg (Block Blob)
        │       └── video.mp4 (Append Blob — logging)
        ├── Container
        │   └── logs/
        │       └── app.log (Append Blob)
        └── Container
            └── pages/
                └── index.html (Page Blob — VM disks)

Blob Types и когда использовать

ТипРазмерИспользование.NET SDK класс
Block Blob0–190 GBДокументы, изображения, видеоBlobClient
Append Blob0–190 GBЛогирование, мониторингAppendBlobClient
Page Blob0–8 TBVM disks (VHD)PageBlobClient

.NET Blob Storage — основные операции

using Azure.Storage.Blobs;
        using Azure.Storage.Blobs.Models;
        using Azure.Storage.Blobs.Specialized;

        public class BlobStorageService
        {
            private readonly BlobServiceClient _blobServiceClient;
            private readonly ILogger<BlobStorageService> _logger;

            public BlobStorageService(BlobServiceClient blobServiceClient, ILogger<BlobStorageService> logger)
            {
                _blobServiceClient = blobServiceClient;
                _logger = logger;
            }

            // Создание контейнера
            public async Task CreateContainerAsync(string containerName, BlobContainerPublicAccessType accessType = BlobContainerPublicAccessType.None)
            {
                var container = _blobServiceClient.GetContainerClient(containerName);
                await container.CreateAsync(accessType);
                _logger.LogInformation("Container '{ContainerName}' created", containerName);
            }

            // Upload файла
            public async Task<string> UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType = "application/octet-stream")
            {
                var blobClient = _blobServiceClient.GetBlobContainerClient(containerName)
                    .GetBlobClient(blobName);

                await blobClient.UploadAsync(fileStream, new BlobHttpHeaders
                {
                    ContentType = contentType
                });

                _logger.LogInformation("Uploaded '{BlobName}' to '{ContainerName}'", blobName, containerName);
                return blobClient.Uri.ToString();
            }

            // Download файла
            public async Task<Stream> DownloadFileAsync(string containerName, string blobName)
            {
                var blobClient = _blobServiceClient.GetBlobContainerClient(containerName)
                    .GetBlobClient(blobName);

                var download = await blobClient.DownloadAsync();
                return download.Content;
            }

            // Upload с chunking для больших файлов
            public async Task UploadLargeFileAsync(string containerName, string blobName, Stream fileStream, int chunkSize = 4 * 1024 * 1024)
            {
                var blobClient = _blobServiceClient.GetBlobContainerClient(containerName)
                    .GetBlobClient(blobName);

                // Azure SDK автоматически chunking для файлов > 256 MB
                // Для ручного chunking используйте UploadAsync с TransferManager
                await blobClient.UploadAsync(fileStream, new BlobUploadOptions
                {
                    TransferOptions = new BlobTransferOptions
                    {
                        SingleUploadBlockSize = chunkSize,
                        MaximumConcurrency = 4,
                        OnProgress = bytes => _logger.LogDebug("Uploaded {Bytes} bytes", bytes)
                    }
                });
            }

            // List файлов
            public async Task<IList<string>> ListFilesAsync(string containerName, string? prefix = null)
            {
                var container = _blobServiceClient.GetContainerClient(containerName);
                var files = new List<string>();

                await foreach (BlobItem blob in container.ListBlobsAsync(prefix: prefix))
                {
                    files.Add(blob.Name);
                }

                return files;
            }

            // Delete файла
            public async Task DeleteFileAsync(string containerName, string blobName, bool softDelete = true)
            {
                var blobClient = _blobServiceClient.GetBlobContainerClient(containerName)
                    .GetBlobClient(blobName);

                if (softDelete)
                {
                    await blobClient.DeleteAsync(DeleteSnapshotsOptionType.Include);
                }
                else
                {
                    await blobClient.DeleteAsync(DeleteSnapshotsOptionType.Include, 
                        new BlobDeleteOptions { Permalink = null });
                }
            }

            // Generate SAS URL для временного доступа
            public async Task<string> GenerateSasUrlAsync(string containerName, string blobName, TimeSpan expiry)
            {
                var blobClient = _blobServiceClient.GetBlobContainerClient(containerName)
                    .GetBlobClient(blobName);

                var sasBuilder = new BlobSasBuilder
                {
                    BlobContainerName = containerName,
                    BlobName = blobName,
                    Resource = "b",
                    StartsOn = DateTimeOffset.UtcNow,
                    ExpiresOn = DateTimeOffset.UtcNow.Add(expiry)
                };

                sasBuilder.SetPermissions(BlobSasPermissions.Read);

                var sasToken = await blobClient.GenerateSasUriAsync(sasBuilder);
                return sasToken.ToString();
            }
        }

Tiered Storage — оптимизация стоимости

┌────────────────────────────────────────────────────────────┐
        │                Blob Storage Tiers                           │
        │                                                            │
        │  Hot Tier                                                  │
        │  ┌────────────────────────────────────────────────────┐   │
        │  │  Доступ: Частый                                   │   │
        │  │  Цена хранения: $0.018/GB                          │   │
        │  │  Цена чтения: $0.01/10K operations                 │   │
        │  │  Использование: Активные данные                    │   │
        │  └────────────────────────────────────────────────────┘   │
        │                                                            │
        │  Cool Tier                                                 │
        │  ┌────────────────────────────────────────────────────┐   │
        │  │  Доступ: Редкий (30+ дней в cool)                  │   │
        │  │  Цена хранения: $0.01/GB (на 40% дешевле)          │   │
        │  │  Цена чтения: $0.02/10K operations (дороже)        │   │
        │  │  Использование: Бэкапы, архивы                     │   │
        │  └────────────────────────────────────────────────────┘   │
        │                                                            │
        │  Cold Tier                                                   │
        │  ┌────────────────────────────────────────────────────┐   │
        │  │  Доступ: Очень редкий (90+ дней в cold)            │   │
        │  │  Цена хранения: $0.004/GB (на 78% дешевле)         │   │
        │  │  Использование: Compliance, audit logs             │   │
        │  └────────────────────────────────────────────────────┘   │
        │                                                            │
        │  Archive Tier                                              │
        │  ┌────────────────────────────────────────────────────┐   │
        │  │  Доступ: Экстремально редкий                       │   │
        │  │  Цена хранения: $0.00099/GB (на 95% дешевле)       │   │
        │  │  Регидрейд: 2 часа, стоимость чтения               │   │
        │  │  Использование: Долгосрочное хранение              │   │
        │  └────────────────────────────────────────────────────┘   │
        └────────────────────────────────────────────────────────────┘

Lifecycle Management Policy

// Lifecycle Management Policy — автоматический transition между tiers
        {
          "rules": [
            {
              "name": "HotToCool",
              "enabled": true,
              "type": "Lifecycle",
              "definition": {
                "actions": {
                  "baseBlob": {
                    "transitionToCool": {
                      "daysAfterModificationGreaterThan": 30
                    },
                    "transitionToCold": {
                      "daysAfterModificationGreaterThan": 90
                    },
                    "delete": {
                      "daysAfterModificationGreaterThan": 365
                    }
                  }
                },
                "filters": {
                  "blobTypes": ["blockBlob"],
                  "prefixes": ["uploads/"]
                }
              }
            },
            {
              "name": "ArchiveOldLogs",
              "enabled": true,
              "type": "Lifecycle",
              "definition": {
                "actions": {
                  "baseBlob": {
                    "transitionToArchive": {
                      "daysAfterModificationGreaterThan": 180
                    }
                  }
                },
                "filters": {
                  "blobTypes": ["blockBlob"],
                  "prefixes": ["logs/"]
                }
              }
            }
          ]
        }
// .NET — приложение lifecycle policy
        using Azure.Storage.Blobs.Administration;

        public class LifecycleManagementService
        {
            private readonly BlobLifecycleClient _lifecycleClient;

            public LifecycleManagementService(BlobServiceClient blobServiceClient)
            {
                _lifecycleClient = blobServiceClient.GetLifecycleManagementClient();
            }

            public async Task ApplyLifecyclePolicyAsync(string policyJson)
            {
                await _lifecycleClient.SetPolicyAsync(policyJson);
            }

            public async Task<string> GetLifecyclePolicyAsync()
            {
                var policy = await _lifecycleClient.GetPolicyAsync();
                return policy.Value;
            }
        }

File storage — Azure Files, EFS

Azure Files vs AWS EFS

КритерийAzure FilesAWS EFS
ПротоколSMB 3.0, NFS 4.1NFS 4.1
МонтированиеDisk mount, AzCopy, REST APINFS mount
PerformanceUp to 115 GB/sUp to 100 GB/s
СтоимостьPremium / StandardPerformance + Request
.NET SDKAzure.Storage.Files.SharesAWS SDK for EFS
Best forWindows apps, file sharesLinux apps, shared storage

Azure Files — .NET пример

using Azure.Storage.Files.Shares;
        using Azure.Storage.Files.Shares.Models;

        public class FileShareService
        {
            private readonly ShareClient _shareClient;

            public FileShareService(ShareClient shareClient)
            {
                _shareClient = shareClient;
            }

            // Создание share
            public async Task CreateShareAsync(int quotaGb = 1024)
            {
                await _shareClient.CreateIfNotExistsAsync(new ShareCreateOptions
                {
                    Quota = quotaGb,
                    Metadata = { ["created-by"] = ".NET Service" }
                });
            }

            // Upload файла в file share
            public async Task UploadFileAsync(string fileName, Stream fileStream)
            {
                var fileClient = _shareClient.GetFileClient(fileName);
                await fileClient.CreateAsync(fileStream.Length);
                await fileClient.UploadRangeAsync(
                    new HttpRange(0, fileStream.Length), 
                    fileStream);
            }

            // Download файла
            public async Task<Stream> DownloadFileAsync(string fileName)
            {
                var fileClient = _shareClient.GetFileClient(fileName);
                var download = await fileClient.DownloadAsync();
                return download.Content;
            }

            // List файлов
            public async Task<IList<string>> ListFilesAsync(string? path = null)
            {
                var files = new List<string>();
                await foreach (ShareFileItem file in _shareClient.GetFilesAndDirectoriesAsync(path))
                {
                    if (file.IsDirectory == false)
                    {
                        files.Add(file.Name);
                    }
                }
                return files;
            }
        }

Queue storage — Azure Queue Storage, SQS

Azure Queue Storage — .NET SDK

using Azure.Messaging.ServiceBus;  // Для Service Bus
        using Azure.Storage.Queues;       // Для Queue Storage (простой вариант)

        public class QueueStorageService
        {
            private readonly QueueClient _queueClient;
            private readonly ILogger<QueueStorageService> _logger;

            public QueueStorageService(QueueClient queueClient, ILogger<QueueStorageService> logger)
            {
                _queueClient = queueClient;
                _logger = logger;
            }

            // Создание очереди
            public async Task CreateQueueAsync()
            {
                await _queueClient.CreateIfNotExistsAsync();
            }

            // Отправка сообщения
            public async Task SendMessageAsync(string message)
            {
                await _queueClient.SendMessageAsync(message);
                _logger.LogInformation("Message sent to queue");
            }

            // Batch send
            public async Task SendMessagesAsync(IEnumerable<string> messages)
            {
                foreach (var msg in messages)
                {
                    await _queueClient.SendMessageAsync(msg);
                }
            }

            // Получение и обработка сообщения
            public async Task ProcessMessagesAsync(CancellationToken ct)
            {
                while (!ct.IsCancellationRequested)
                {
                    var messages = await _queueClient.ReceiveMessagesAsync(
                        maxMessages: 32,
                        visibilityTimeout: TimeSpan.FromSeconds(30));

                    foreach (var message in messages.Value)
                    {
                        try
                        {
                            _logger.LogInformation("Processing message: {MessageId}", message.MessageId);
                    
                            // Process the message...
                            await ProcessMessageAsync(message.Body);

                            // Remove from queue
                            await _queueClient.DeleteMessageAsync(
                                message.MessageId, 
                                message.PopReceipt);
                        }
                        catch (Exception ex)
                        {
                            _logger.LogError(ex, "Error processing message {MessageId}", message.MessageId);
                            // Message will become visible again after visibility timeout
                        }
                    }
                }
            }

            // Get message count
            public async Task<long> GetMessageCountAsync()
            {
                var props = await _queueClient.GetPropertiesAsync();
                return props.Value.ApproximateCount;
            }

            private Task ProcessMessageAsync(BinaryData body)
            {
                // Business logic
                return Task.CompletedTask;
            }
        }

Queue Storage vs Service Bus

КритерийQueue StorageService Bus Queue
СтоимостьНизкаяСредняя
Max message size64 KB256 KB (Standard), 1 MB (Premium)
Max queue size5 TB80 GB (Standard), 5 TB (Premium)
Delivery guaranteesAt-least-onceAt-least-once / Exactly-once (transactions)
Topics/SubscriptionsНетДа
Dead-letter queueНетДа
SessionsНетДа
TransactionНетДа
Best forSimple queuing, cost-sensitiveEnterprise messaging, complex routing

Table storage / DynamoDB — NoSQL key-value

Azure Cosmos DB Table API

using Azure.Data.Tables;

        public class TableStorageService
        {
            private readonly TableClient _tableClient;
            private readonly ILogger<TableStorageService> _logger;

            public TableStorageService(TableClient tableClient, ILogger<TableStorageService> logger)
            {
                _tableClient = tableClient;
                _logger = logger;
            }

            // Создание таблицы
            public async Task CreateTableAsync()
            {
                await _tableClient.CreateIfNotExistsAsync();
            }

            // Add/Update entity (upsert)
            public async Task UpsertEntityAsync<T>(T entity, string partitionKey, string rowKey) where T : class, ITableEntity, new()
            {
                var tableEntity = new TableEntity(partitionKey, rowKey);
        
                // Copy properties from entity to TableEntity
                foreach (var prop in typeof(T).GetProperties())
                {
                    var value = prop.GetValue(entity);
                    if (value != null && prop.Name != nameof(ITableEntity.PartitionKey) && 
                        prop.Name != nameof(ITableEntity.RowKey))
                    {
                        tableEntity[prop.Name] = value;
                    }
                }

                await _tableClient.UpsertEntityAsync(tableEntity);
                _logger.LogInformation("Upserted entity PK={PartitionKey}, RK={RowKey}", partitionKey, rowKey);
            }

            // Get entity
            public async Task<T?> GetEntityAsync<T>(string partitionKey, string rowKey) where T : class, new()
            {
                try
                {
                    var entity = await _tableClient.GetEntityAsync<TableEntity>(partitionKey, rowKey);
                    var result = Activator.CreateInstance<T>();
            
                    foreach (var prop in typeof(T).GetProperties())
                    {
                        if (entity.Properties.TryGetValue(prop.Name, out var value))
                        {
                            prop.SetValue(result, Convert.ChangeType(value, prop.PropertyType));
                        }
                    }
            
                    return result;
                }
                catch (RequestFailedException ex) when (ex.Status == 404)
                {
                    return default;
                }
            }

            // Query with filter
            public async Task<IEnumerable<T>> QueryAsync<T>(string filter) where T : class, new()
            {
                var results = new List<T>();
        
                await foreach (TableEntity entity in _tableClient.QueryAsync<TableEntity>(filter))
                {
                    var item = Activator.CreateInstance<T>();
                    foreach (var prop in typeof(T).GetProperties())
                    {
                        if (entity.Properties.TryGetValue(prop.Name, out var value) && value != null)
                        {
                            prop.SetValue(item, Convert.ChangeType(value, prop.PropertyType));
                        }
                    }
                    results.Add(item);
                }
        
                return results;
            }

            // Delete entity
            public async Task DeleteEntityAsync(string partitionKey, string rowKey)
            {
                await _tableClient.DeleteEntityAsync(partitionKey, rowKey);
            }
        }

AWS DynamoDB — .NET SDK

using Amazon.DynamoDBv2;
        using Amazon.DynamoDBv2.DataModel;

        public class DynamoDbService
        {
            private readonly DynamoDBContext _context;

            public DynamoDbService(AmazonDynamoDBClient ddbClient)
            {
                _context = new DynamoDBContext(ddbClient);
            }

            // Save entity
            public async Task SaveAsync(Order order)
            {
                await _context.SaveAsync(order);
            }

            // Get by partition key + sort key
            public async Task<Order?> GetAsync(string userId, string orderId)
            {
                return await _context.LoadAsync<Order>(userId, orderId);
            }

            // Query by partition key
            public async Task<IEnumerable<Order>> QueryByUserAsync(string userId)
            {
                var queryOp = new QueryOperation
                {
                    Filter = Expression.Build(e => e.UserId == userId)
                };
        
                var results = new List<Order>();
                await queryOp.GetRemainingAsync(results);
                return results;
            }

            // Scan (не рекомендуется для production)
            public async Task<IEnumerable<Order>> ScanAllAsync()
            {
                var scanOp = new ScanOperation
                {
                    Limit = 100
                };
        
                var results = new List<Order>();
                await scanOp.GetRemainingAsync(results);
                return results;
            }
        }

        // Entity definition
        [DynamoDBTable("Orders")]
        public class Order
        {
            [DynamoDBHashKey] // Partition Key
            public string UserId { get; set; } = string.Empty;

            [DynamoDBRangeKey] // Sort Key
            public string OrderId { get; set; } = string.Empty;

            [DynamoDBProperty("orderDate")]
            public DateTime OrderDate { get; set; }

            [DynamoDBProperty("total")]
            public decimal Total { get; set; }

            [DynamoDBProperty("status")]
            public string Status { get; set; } = string.Empty;
        }

Cosmos DB SQL API — для сложных запросов

using Azure.Cosmos;

        public class CosmosDbService
        {
            private readonly CosmosDatabase _database;
            private readonly CosmosContainer _container;

            public CosmosDbService(CosmosClient client)
            {
                _database = client.GetDatabase("mydb");
                _container = _database.GetContainer("orders");
            }

            // Create item
            public async Task CreateOrderAsync(Order order)
            {
                await _container.CreateItemAsync(order, new PartitionKey(order.UserId));
            }

            // Read by ID
            public async Task<Order?> GetOrderAsync(string orderId)
            {
                try
                {
                    var response = await _container.ReadItemAsync<Order>(
                        orderId, new PartitionKey("default"));
                    return response.Resource;
                }
                catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                {
                    return null;
                }
            }

            // SQL query
            public async Task<IEnumerable<Order>> GetOrdersByStatusAsync(string status)
            {
                var query = new QueryDefinition(
                    "SELECT * FROM c WHERE c.status = @status")
                    .WithParameter("@status", status);

                var results = new List<Order>();
                using var iterator = _container.GetItemQueryIterator<Order>(query);
        
                while (iterator.HasMoreResults)
                {
                    var response = await iterator.ReadNextAsync();
                    results.AddRange(response.Resource);
                }
        
                return results;
            }

            // Upsert
            public async Task UpsertOrderAsync(Order order)
            {
                await _container.UpsertItemAsync(order, new PartitionKey(order.UserId));
            }

            // Delete
            public async Task DeleteOrderAsync(string orderId)
            {
                await _container.DeleteItemAsync<Order>(orderId, new PartitionKey("default"));
            }
        }

Storage Selection Guide

Какой storage использовать?
        │
        ├── Нужна структурированная БД с SQL?
        │   └── Да → Azure SQL / Cosmos DB SQL API / RDS
        │
        ├── Нужен key-value store?
        │   ├── Простой → Azure Table / DynamoDB / Cosmos DB Table
        │   └── С транзакциями → Cosmos DB SQL API
        │
        ├── Нужен document store?
        │   └── Да → Cosmos DB SQL API / MongoDB / DynamoDB (JSON)
        │
        ├── Нужны бинарные данные?
        │   ├── Небольшие → Azure Blob / S3
        │   └── Большие (TB) → Azure Blob / S3 + lifecycle policy
        │
        ├── Нужен file share (SMB/NFS)?
        │   └── Да → Azure Files / AWS EFS
        │
        └── Нужна простая очередь?
            ├── Нет сложных требований → Queue Storage / SQS
            └── Нужны topics, DLQ, sessions → Service Bus / SNS+SQS

Сводная таблица: Best Practices

ПрактикаРекомендация
Blob tieringHot → Cool (30 дней) → Cold (90 дней) → Archive (180 дней)
Lifecycle policyАвтоматический transition, не вручную
SAS URLsДля временного доступа, не для production auth
Large uploadsSDK auto-chunking для файлов > 256 MB
Queue vs Service BusSimple → Queue Storage, Enterprise → Service Bus
Table vs Cosmos DBSimple KV → Table, Complex queries → Cosmos DB SQL
DynamoDBQuery по PK, избегайте Scan
CDNДля публичных blob — CDN для кэширования
MonitoringMonitor storage metrics (RU/s, IOPS, throughput)
SecurityPrivate Endpoints, RBAC, SAS с ограниченным TTL

Практика


Cloud Messaging Services

Содержание

  • Azure Service Bus — queues, topics, subscriptions, sessions
  • AWS SQS/SNS — simple queue vs pub/sub patterns
  • Event Grid / EventBridge — event routing и filtering
  • Dead-letter queues — poison message handling strategies
  • Message ordering guarantees — когда гарантируется, когда нет

Azure Service Bus — queues, topics, subscriptions, sessions

Архитектура Service Bus

Service Bus Namespace
        │
        ├── Queue (FIFO, single consumer per message)
        │   ├── Messages
        │   ├── Dead-letter Queue ($deadletterqueue)
        │   └── Properties: LockDuration, MaxDeliveryCount, DeadLetteringOnMessageExpiration
        │
        ├── Topic (pub/sub, multiple consumers)
        │   ├── Messages
        │   └── Subscriptions (each gets a copy)
        │       ├── Subscription 1 (with filter)
        │       ├── Subscription 2 (with filter)
        │       └── Subscription 3 ($Default filter)
        │           └── Dead-letter Queue
        │
        └── Event Hubs (high-throughput streaming)
            ├── Partitions
            ├── Consumer Groups
            └── Retention: 1-7 days

Queue — Pattern и .NET реализация

using Azure.Messaging.ServiceBus;

        // Producer — отправка в очередь
        public class QueueProducer
        {
            private readonly ServiceBusSender _sender;
            private readonly ILogger<QueueProducer> _logger;

            public QueueProducer(ServiceBusClient client, string queueName, ILogger<QueueProducer> logger)
            {
                _sender = client.CreateSender(queueName);
                _logger = logger;
            }

            public async Task SendMessageAsync(string message)
            {
                var serviceBusMessage = new ServiceBusMessage(message)
                {
                    MessageId = Guid.NewGuid().ToString(),
                    ContentType = "application/json",
                    TimeToLive = TimeSpan.FromHours(1)
                };

                await _sender.SendMessageAsync(serviceBusMessage);
                _logger.LogInformation("Message sent to queue, Id={MessageId}", serviceBusMessage.MessageId);
            }

            public async Task SendBatchAsync(IEnumerable<string> messages)
            {
                var batch = await _sender.CreateMessageBatchAsync();
        
                foreach (var msg in messages)
                {
                    if (!batch.TryAddMessage(new ServiceBusMessage(msg)))
                    {
                        var singleBatch = await _sender.CreateMessageBatchAsync();
                        singleBatch.TryAddMessage(new ServiceBusMessage(msg));
                        await _sender.SendMessagesAsync(singleBatch);
                    }
                }

                await _sender.SendMessagesAsync(batch);
            }
        }

        // Consumer — получение из очереди
        public class QueueConsumer : BackgroundService
        {
            private readonly ServiceBusProcessor _processor;
            private readonly ILogger<QueueConsumer> _logger;

            public QueueConsumer(
                ServiceBusClient client, 
                string queueName,
                IMessageHandler handler,
                ILogger<QueueConsumer> logger)
            {
                var options = new ServiceBusProcessorOptions
                {
                    MaxConcurrentCalls = 16,
                    AutoCompleteMessages = false,
                    MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(5)
                };

                _processor = client.CreateProcessor(queueName, options);
                _processor.ProcessMessageAsync += handler.OnMessageAsync;
                _processor.ProcessErrorAsync += handler.OnErrorAsync;
                _logger = logger;
            }

            protected override async Task StartAsync(CancellationToken ct)
            {
                await _processor.StartProcessingAsync(ct);
                _logger.LogInformation("Queue consumer started");
            }

            protected override async Task StopAsync(CancellationToken ct)
            {
                await _processor.StopProcessingAsync();
                await _processor.CloseAsync();
                _logger.LogInformation("Queue consumer stopped");
            }
        }

        // Message Handler — обработка сообщений
        public interface IMessageHandler
        {
            Task OnMessageAsync(ProcessMessageEventArgs args);
            Task OnErrorAsync(ProcessErrorEventArgs args);
        }

        public class OrderMessageHandler : IMessageHandler
        {
            private readonly ILogger<OrderMessageHandler> _logger;
            private readonly IOrderProcessor _orderProcessor;

            public OrderMessageHandler(ILogger<OrderMessageHandler> logger, IOrderProcessor orderProcessor)
            {
                _logger = logger;
                _orderProcessor = orderProcessor;
            }

            public async Task OnMessageAsync(ProcessMessageEventArgs args)
            {
                var message = args.Message;
                _logger.LogInformation("Processing message {MessageId}, DeliveryCount={DeliveryCount}", 
                    message.MessageId, message.DeliveryCount);

                try
                {
                    var order = JsonSerializer.Deserialize<Order>(message.Body);
                    await _orderProcessor.ProcessAsync(order!);
            
                    // Complete message — удалить из очереди
                    await args.CompleteMessageAsync(message);
                    _logger.LogInformation("Message {MessageId} completed", message.MessageId);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error processing message {MessageId}", message.MessageId);
            
                    // Abandon — вернуть в очередь (increment delivery count)
                    await args.AbandonMessageAsync(message);
                    // Или dead-letter:
                    // await args.DeadLetterMessageAsync(message, reason: "ProcessingError", description: ex.Message);
                }
            }

            public Task OnErrorAsync(ProcessErrorEventArgs args)
            {
                _logger.LogError(args.ErrorSource, "Service Bus error: {ErrorMessage}", args.ErrorMessage);
                return Task.CompletedTask;
            }
        }

Topic и Subscription — Pub/Sub Pattern

// Publisher — отправка в Topic
        public class TopicPublisher
        {
            private readonly ServiceBusSender _sender;

            public TopicPublisher(ServiceBusClient client, string topicName)
            {
                _sender = client.CreateSender(topicName);
            }

            public async Task PublishAsync(string subject, object data, Dictionary<string, string>? properties = null)
            {
                var message = new ServiceBusMessage(JsonSerializer.Serialize(data))
                {
                    Subject = subject,
                    ContentType = "application/json",
                    MessageId = Guid.NewGuid().ToString()
                };

                // User-defined properties для filtering
                if (properties != null)
                {
                    foreach (var prop in properties)
                    {
                        message.UserProperties[prop.Key] = prop.Value;
                    }
                }

                await _sender.SendMessageAsync(message);
            }
        }

        // Subscriber — получение из Subscription
        public class TopicSubscriber : BackgroundService
        {
            private readonly ServiceBusProcessor _processor;

            public TopicSubscriber(
                ServiceBusClient client,
                string topicName,
                string subscriptionName,
                Func<ProcessMessageEventArgs, Task> handler,
                ILogger<TopicSubscriber> logger)
            {
                var options = new ServiceBusProcessorOptions
                {
                    MaxConcurrentCalls = 8,
                    AutoCompleteMessages = false
                };

                _processor = client.CreateProcessor(topicName, subscriptionName, options);
                _processor.ProcessMessageAsync += handler;
                _processor.ProcessErrorAsync += async (args) => 
                    logger.LogError(args.ErrorMessage, "Topic subscriber error");
            }

            protected override async Task StartAsync(CancellationToken ct)
            {
                await _processor.StartProcessingAsync(ct);
            }

            protected override async Task StopAsync(CancellationToken ct)
            {
                await _processor.StopProcessingAsync();
                await _processor.CloseAsync();
            }
        }

        // Admin — создание Subscription с фильтрами
        public class TopicAdmin
        {
            private readonly ServiceBusAdministrationClient _adminClient;

            public TopicAdmin(ServiceBusAdministrationClient adminClient)
            {
                _adminClient = adminClient;
            }

            public async Task CreateSubscriptionWithSqlFilterAsync(
                string topicName, 
                string subscriptionName, 
                string filterExpression)
            {
                await _adminClient.CreateSubscriptionAsync(topicName, subscriptionName, 
                    new CreateSubscriptionOptions
                    {
                        MaxDeliveryCount = 10,
                        DeadLetteringOnMessageExpiration = true,
                        LockDuration = TimeSpan.FromMinutes(5)
                    });

                // Добавить SQL filter rule
                await _adminClient.CreateRuleAsync(topicName, subscriptionName, 
                    new CreateRuleOptions("OrderFilter", new SqlRuleFilter(filterExpression)));
            }

            public async Task CreateSubscriptionWithCorrelationFilterAsync(
                string topicName,
                string subscriptionName,
                string orderType)
            {
                await _adminClient.CreateSubscriptionAsync(topicName, subscriptionName);

                // Correlation filter — более эффективен чем SQL filter
                await _adminClient.CreateRuleAsync(topicName, subscriptionName,
                    new CreateRuleOptions("TypeFilter", 
                        new CorrelationRuleFilter { UserProperties = { ["OrderType"] = orderType } }));
            }
        }

Sessions — Guaranteed Ordering

// Sessions обеспечивают FIFO ordering для сообщений с одинаковым SessionId
        public class SessionConsumer : BackgroundService
        {
            private readonly ServiceBusProcessor _processor;
            private readonly ILogger<SessionConsumer> _logger;

            public SessionConsumer(
                ServiceBusClient client,
                string queueName,
                ILogger<SessionConsumer> logger)
            {
                var options = new ServiceBusProcessorOptions
                {
                    MaxConcurrentCalls = 1,          // Один call на session
                    AutoCompleteMessages = false,
                    AutoLockRenewalInterval = TimeSpan.FromMinutes(2)
                };

                _processor = client.CreateProcessor(queueName, options);
                _processor.ProcessSessionMessageAsync += OnSessionMessageAsync;
                _processor.ProcessErrorAsync += OnErrorAsync;
                _logger = logger;
            }

            private async Task OnSessionMessageAsync(ProcessSessionMessageEventArgs args)
            {
                var message = args.Message;
                var session = args.Session;
        
                _logger.LogInformation("Processing message in session {SessionId}", session.SessionId);

                try
                {
                    // Session гарантирует порядок — обрабатываем по порядку
                    var order = JsonSerializer.Deserialize<OrderUpdate>(message.Body);
                    await ProcessOrderUpdateAsync(order!, session);
            
                    await args.CompleteMessageAsync(message);
                    await args.CloseSessionAsync(); // Завершаем session
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error in session {SessionId}", session.SessionId);
                    await args.AbandonMessageAsync(message);
                    // Session lock автоматически renewed
                }
            }

            private Task OnErrorAsync(ProcessErrorEventArgs args)
            {
                _logger.LogError(args.ErrorMessage, "Session consumer error");
                return Task.CompletedTask;
            }

            protected override async Task StartAsync(CancellationToken ct) => 
                await _processor.StartProcessingAsync(ct);

            protected override async Task StopAsync(CancellationToken ct)
            {
                await _processor.StopProcessingAsync();
                await _processor.CloseAsync();
            }
        }

        // Publisher с SessionId
        public class SessionPublisher
        {
            private readonly ServiceBusSender _sender;

            public SessionPublisher(ServiceBusClient client, string queueName)
            {
                _sender = client.CreateSender(queueName);
            }

            public async Task SendOrderedMessageAsync(string sessionId, OrderUpdate update)
            {
                var message = new ServiceBusMessage(JsonSerializer.Serialize(update))
                {
                    SessionId = sessionId,  // Все сообщения с одинаковым SessionId — FIFO
                    MessageId = Guid.NewGuid().ToString(),
                    ContentType = "application/json"
                };

                await _sender.SendMessageAsync(message);
            }
        }

AWS SQS/SNS — simple queue vs pub/sub

SQS (Simple Queue Service)

// AWS SQS — .NET SDK
        using Amazon.SQS;
        using Amazon.SQS.Model;

        public class SqsService
        {
            private readonly AmazonSQSClient _sqsClient;
            private readonly ILogger<SqsService> _logger;

            public SqsService(AmazonSQSClient sqsClient, ILogger<SqsService> logger)
            {
                _sqsClient = sqsClient;
                _logger = logger;
            }

            // Получить URL очереди по имени
            private async Task<string> GetQueueUrlAsync(string queueName)
            {
                var response = await _sqsClient.GetQueueUrlAsync(queueName);
                return response.QueueUrl;
            }

            // Отправить сообщение
            public async Task SendMessageAsync(string queueName, string message, Dictionary<string, string>? attributes = null)
            {
                var url = await GetQueueUrlAsync(queueName);
        
                var request = new SendMessageRequest
                {
                    QueueUrl = url,
                    MessageBody = message,
                    DelaySeconds = 0,
                    MessageAttributes = attributes
                };

                await _sqsClient.SendMessageAsync(request);
            }

            // Batch send (до 10 сообщений)
            public async Task SendBatchAsync(string queueName, IEnumerable<SqsMessage> messages)
            {
                var url = await GetQueueUrlAsync(queueName);
        
                var entries = messages.Select((m, i) => new SendMessageBatchRequestEntry
                {
                    Id = i.ToString(),
                    MessageBody = m.Body,
                    DelaySeconds = m.DelaySeconds,
                    MessageAttributes = m.Attributes
                }).ToList();

                var request = new SendMessageBatchRequest(url, entries);
                await _sqsClient.SendMessageBatchAsync(request);
            }

            // Receive messages (long polling)
            public async Task<List<ReceiveMessageResponse>> ReceiveMessagesAsync(string queueName, int maxMessages = 10)
            {
                var url = await GetQueueUrlAsync(queueName);
        
                var request = new ReceiveMessageRequest
                {
                    QueueUrl = url,
                    MaxNumberOfMessages = maxMessages,
                    VisibilityTimeout = 30,   // Секунды до повторной видимости
                    WaitTimeSeconds = 20      // Long polling
                };

                return await _sqsClient.ReceiveMessageAsync(request);
            }

            // Delete message после обработки
            public async Task DeleteMessageAsync(string queueName, string receiptHandle)
            {
                var url = await GetQueueUrlAsync(queueName);
        
                await _sqsClient.DeleteMessageAsync(new DeleteMessageRequest
                {
                    QueueUrl = url,
                    ReceiptHandle = receiptHandle
                });
            }

            // Dead-letter queue — получить сообщения из DLQ
            public async Task<List<ReceiveMessageResponse>> ReceiveFromDlqAsync(string dlqName)
            {
                var url = await GetQueueUrlAsync(dlqName);
        
                return await _sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest
                {
                    QueueUrl = url,
                    MaxNumberOfMessages = 10,
                    WaitTimeSeconds = 5
                });
            }
        }

        public class SqsMessage
        {
            public string Body { get; set; } = string.Empty;
            public int? DelaySeconds { get; set; }
            public Dictionary<string, string>? Attributes { get; set; }
        }

SNS (Simple Notification Service)

// AWS SNS — pub/sub
        using Amazon.SimpleNotificationService;
        using Amazon.SimpleNotificationService.Model;

        public class SnsService
        {
            private readonly AmazonSimpleNotificationServiceClient _snsClient;

            public SnsService(AmazonSimpleNotificationServiceClient snsClient)
            {
                _snsClient = snsClient;
            }

            // Create topic
            public async Task<string> CreateTopicAsync(string topicName)
            {
                var response = await _snsClient.CreateTopicAsync(new CreateTopicRequest
                {
                    Name = topicName
                });
                return response.TopicArn;
            }

            // Publish message
            public async Task PublishAsync(string topicArn, string message, Dictionary<string, MessageAttributeValue>? attributes = null)
            {
                var request = new PublishRequest
                {
                    TopicArn = topicArn,
                    Message = message,
                    MessageAttributes = attributes
                };

                await _snsClient.PublishAsync(request);
            }

            // Subscribe SQS to SNS
            public async Task SubscribeQueueAsync(string topicArn, string queueArn)
            {
                await _snsClient.SubscribeAsync(new SubscribeRequest
                {
                    TopicArn = topicArn,
                    Protocol = "sqs",
                    Endpoint = queueArn
                });
            }
        }

SQS vs Service Bus

КритерийAWS SQSAzure Service Bus
TopicsНет (только SNS)Да (встроенные)
SessionsНетДа
Dead-letterДа (separate queue)Да (встроенная)
Max message size256 KB256 KB / 1 MB
Visibility timeout0–12 hours30 sec – 7 days
CostНизкаяСредняя
AWS nativeДаНет

Event Grid / EventBridge — event routing

Azure Event Grid Architecture

┌─────────────────────────────────────────────────────────────┐
        │                  Azure Event Grid                           │
        │                                                             │
        │  Events Sources:                                            │
        │  ├── Storage Account (blob created/deleted)                 │
        │  ├── Resource Groups (resource created/updated)             │
        │  ├── Azure SQL (change tracking)                           │
        │  ├── Custom Events (your app)                              │
        │  └── Service Bus (message events)                          │
        │                                                             │
        │  Event Routing:                                             │
        │  ├── Topic → Subscriptions → Handlers                      │
        │  │   ├── HTTP Endpoint                                     │
        │  │   ├── WebHook                                           │
        │  │   ├── Function App                                      │
        │  │   ├── Event Hub                                         │
        │  │   └── Storage Queue                                     │
        │  └── Domain → Topic Filters → Subscriptions                │
        │                                                             │
        │  Event Schema:                                              │
        │  ├── CloudEvents v1.0 (recommended)                        │
        │  ├── EventGrid Schema (default)                            │
        │  └── Custom Schema                                        │
        └─────────────────────────────────────────────────────────────┘

Event Grid в .NET

// Event Grid Schema
        public class EventGridEvent
        {
            [JsonPropertyName("id")]
            public string Id { get; set; } = string.Empty;

            [JsonPropertyName("topic")]
            public string Topic { get; set; } = string.Empty;

            [JsonPropertyName("subject")]
            public string Subject { get; set; } = string.Empty;

            [JsonPropertyName("eventType")]
            public string EventType { get; set; } = string.Empty;

            [JsonPropertyName("eventTime")]
            public DateTimeOffset EventTime { get; set; }

            [JsonPropertyName("data")]
            public JsonElement Data { get; set; }

            [JsonPropertyName("dataVersion")]
            public string DataVersion { get; set; } = string.Empty;
        }

        // Custom Events Publisher
        public class EventPublisher
        {
            private readonly EventGridPublisherClient _client;

            public EventPublisher(Uri endpoint, TokenCredential credential)
            {
                _client = new EventGridPublisherClient(endpoint, credential);
            }

            public async Task PublishAsync(string subject, object data, string eventType = "Custom.Event")
            {
                var events = new[]
                {
                    new CloudEvent
                    {
                        Source = "myapp",
                        Type = eventType,
                        Subject = subject,
                        Data = data
                    }
                };

                await _client.SendEventsAsync(events);
            }
        }

        // Azure Function — Event Grid Trigger
        public class EventGridFunction
        {
            private readonly ILogger _logger;

            public EventGridFunction(ILoggerFactory loggerFactory)
            {
                _logger = loggerFactory.CreateLogger<EventGridFunction>();
            }

            [Function("ProcessEvent")]
            public void ProcessEvent(
                [EventGridTrigger] EventGridEvent eventGridEvent)
            {
                _logger.LogInformation("Event: {EventType}, Subject: {Subject}", 
                    eventGridEvent.EventType, eventGridEvent.Subject);

                switch (eventGridEvent.EventType)
                {
                    case "Microsoft.Storage.BlobCreated":
                        HandleBlobCreated(eventGridEvent.Data);
                        break;
                    case "Custom.OrderCreated":
                        HandleOrderCreated(eventGridEvent.Data);
                        break;
                }
            }
        }

Dead-letter queues — Poison Message Handling

Стратегии обработки poison messages

Message Flow:
        ┌─────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
        │ Producer │───► │  Queue   │───► │ Consumer │───► │  DLQ     │
        └─────────┘     └──────────┘     └──────────┘     └──────────┘
                                                    │              ▲
                                                    │  MaxDelivery │
                                                    │  Count Exceeded│
                                                    ▼              │
                                             ┌──────────┐          │
                                             │  Retry   │──────────┘
                                             │  N times │
                                             └──────────┘

DLQ Handler — .NET реализация

public class DeadLetterQueueHandler : BackgroundService
        {
            private readonly ServiceBusClient _client;
            private readonly ServiceBusSender _reprocessSender;
            private readonly ILogger<DeadLetterQueueHandler> _logger;

            public DeadLetterQueueHandler(
                ServiceBusClient client,
                ServiceBusSender reprocessSender,
                ILogger<DeadLetterQueueHandler> logger)
            {
                _client = client;
                _reprocessSender = reprocessSender;
                _logger = logger;
            }

            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    try
                    {
                        // Получаем сообщения из DLQ
                        var receiver = _client.CreateReceiver("orders", 
                            new ServiceBusReceiverOptions
                            {
                                SubQueue = SubQueue.DeadLetter
                            });

                        var messages = await receiver.ReceiveMessagesAsync(
                            maxMessages: 10,
                            maxWaitTime: TimeSpan.FromSeconds(30),
                            cancellationToken: stoppingToken);

                        foreach (var message in messages)
                        {
                            await ProcessDeadLetterAsync(message, receiver);
                        }

                        await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Error processing DLQ");
                        await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
                    }
                }
            }

            private async Task ProcessDeadLetterAsync(ServiceBusReceivedMessage message, ServiceBusReceiver receiver)
            {
                var deadLetterReason = message.DeadLetterReason;
                var deadLetterErrorDescription = message.DeadLetterErrorDescription;
        
                _logger.LogWarning(
                    "DLQ message: Id={MessageId}, Reason={Reason}, Description={Description}, " +
                    "DeliveryCount={DeliveryCount}, OriginalEnqueuedTime={EnqueuedTime}",
                    message.MessageId, deadLetterReason, deadLetterErrorDescription,
                    message.DeliveryCount, message.EnqueuedTime);

                // Категоризация
                var action = CategorizeMessage(message, deadLetterReason);

                switch (action)
                {
                    case DlqAction.Retry:
                        // Re-schedule for retry
                        var retryMessage = new ServiceBusMessage(message.Body)
                        {
                            MessageId = message.MessageId,
                            ContentType = message.ContentType,
                            Subject = message.Subject,
                            ScheduledEnqueueTime = DateTimeOffset.UtcNow.AddMinutes(5)
                        };
                        await _reprocessSender.SendMessageAsync(retryMessage);
                        await receiver.CompleteMessageAsync(message);
                        _logger.LogInformation("Rescheduled message {MessageId} for retry", message.MessageId);
                        break;

                    case DlqAction.MoveToReview:
                        // Move to review queue for manual inspection
                        await receiver.CompleteMessageAsync(message);
                        _logger.LogWarning("Message {MessageId} moved to review", message.MessageId);
                        break;

                    case DlqAction.Drop:
                        // Drop the message
                        await receiver.CompleteMessageAsync(message);
                        _logger.LogWarning("Dropped message {MessageId}: {Reason}", message.MessageId, deadLetterReason);
                        break;
                }
            }

            private DlqAction CategorizeMessage(ServiceBusReceivedMessage message, string? reason)
            {
                return reason switch
                {
                    "TTLExpiredException" => DlqAction.Retry,
                    "MaxDeliveryCountExceeded" when message.DeliveryCount < 50 => DlqAction.Retry,
                    "MaxDeliveryCountExceeded" => DlqAction.MoveToReview,
                    "MessageLockLostException" => DlqAction.Retry,
                    _ => DlqAction.Drop
                };
            }
        }

        public enum DlqAction
        {
            Retry,
            MoveToReview,
            Drop
        }

Message Ordering Guarantees

Guarantees по типу messaging

ТипOrderingDeduplicationDelivery
Service Bus QueueNo (except sessions)NoAt-least-once
Service Bus Queue + SessionsFIFO within sessionNoAt-least-once
Service Bus TopicNoNoAt-least-once per subscription
SQSNoNoAt-least-once
Event GridNoNoAt-least-once
Event HubsWithin partitionNoAt-least-once

Как обеспечить ordering в .NET

// Pattern 1: Sessions для Service Bus
        // Все сообщения с одинаковым SessionId обрабатываются по порядку

        // Pattern 2: Partition Key для Event Hubs
        // Все сообщения с одинаковым PartitionKey попадают в один partition
        // и сохраняют порядок

        // Pattern 3: Single consumer для SQS
        // Одна SQS queue — один consumer instance

        // Pattern 4: Database-level ordering
        // Если messaging не гарантирует порядок — использовать DB для final ordering
        public class OrderProcessor
        {
            private readonly AppDbContext _context;

            public async Task ProcessWithOrdering(IEnumerable<OrderMessage> messages)
            {
                // Group by aggregate root, process in order
                var grouped = messages
                    .OrderBy(m => m.Timestamp)
                    .GroupBy(m => m.AggregateId);

                foreach (var group in grouped)
                {
                    using var transaction = await _context.Database.BeginTransactionAsync();
            
                    foreach (var message in group.OrderBy(m => m.SequenceNumber))
                    {
                        await ProcessSingleMessageAsync(message);
                    }
            
                    await transaction.CommitAsync();
                }
            }
        }

Сводная таблица: Best Practices

ПрактикаРекомендация
Queue vs TopicSingle consumer → Queue, Multiple → Topic
OrderingSessions для FIFO, Partition Key для Event Hubs
DLQОбрабатывать автоматически: Retry → Review → Drop
MaxDeliveryCountStandard: 10, Premium: 14
Concurrent Calls16 для queue, 8 для topic
Message Size< 256 KB, для больших — Blob + reference
Batch SendГруппировать для throughput
MonitoringAlert на DLQ size и delivery count
IdempotencyВсегда — at-least-once delivery
TTLSet TTL для автоматической очистки

Практика


Cloud-Native Architecture Patterns

Содержание

  • 12-Factor App — каждый принцип в контексте .NET
  • Sidecar pattern — log aggregation, service proxy
  • Ambassador pattern — external service abstraction
  • Strangler Fig — incremental migration to cloud-native
  • Backend for Frontend (BFF) в cloud environment

12-Factor App — каждый принцип в контексте .NET

Twelve-Factor App Methodology

┌──────────────────────────────────────────────────────────┐
        │              12-Factor App Principles                     │
        │                                                           │
        │  1. Codebase     │  One codebase, many deploys           │
        │  2. Dependencies │  Explicitly declared, isolated        │
        │  3. Config       │  Stored in environment                │
        │  4. Backing svc  │  Attached resources, not internal     │
        │  5. Build/Run    │  Strictly separated stages            │
        │  6. Processes    │  Stateless, state in backing stores   │
        │  7. Port binding │  Export services via port binding     │
        │  8. Concurrency  │  Scale out via process model          │
        │  9. Disposability│  Fast startup, graceful shutdown      │
        │  10. Dev/Prod    │  Keep parity between environments     │
        │  11. Logs        │  Event streams, not file logs         │
        │  12. Admin       │  One-off management processes         │
        └──────────────────────────────────────────────────────────┘

-Factor в .NET — детальное руководство

Factor 1: Codebase — один код, много деплоев
# Один Git repo → несколько деплоев (dev, staging, prod)
        # Используем configuration для различий, не код

        # .NET: один solution, разные deployment targets
        MyApp.sln
        ├── src/
        │   ├── MyApp.Api/
        │   ├── MyApp.Services/
        │   └── MyApp.Common/
        ├── tests/
        │   ├── MyApp.Tests/
        │   └── MyApp.IntegrationTests/
        └── infra/
            ├── azure/
            │   ├── dev/
            │   ├── staging/
            │   └── prod/
            └── terraform/
Factor 2: Dependencies — явно объявлены
<!-- .csproj — явно объявленные зависимости -->
        <Project Sdk="Microsoft.NET.Sdk.Web">
          <PropertyGroup>
            <TargetFramework>net9.0</TargetFramework>
            <Nullable>enable</Nullable>
            <ImplicitUsings>enable</ImplicitUsings>
          </PropertyGroup>

          <!-- Явные зависимости с версиями -->
          <ItemGroup>
            <PackageReference Include="Azure.Identity" Version="1.13.1" />
            <PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
            <PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
            <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
            <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
          </ItemGroup>
        </Project>
Factor 3: Config — в environment variables
// BAD: Hardcoded configuration
        var app = WebApplication.CreateBuilder(args);
        app.Configuration["ConnectionStrings:Default"] = "Server=...";

        // GOOD: Configuration из environment / Key Vault
        var builder = WebApplication.CreateBuilder(args);

        builder.Configuration
            .AddEnvironmentVariables()
            .AddAzureKeyVault(new Uri(kvUri), new DefaultAzureCredential());

        // Access via strongly-typed options
        builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));

        public class AppSettings
        {
            public string ApiName { get; set; } = string.Empty;
            public int MaxRequestSize { get; set; }
            public string RedisConnection { get; set; } = string.Empty;
        }
# .env (local development)
        ASPNETCORE_ENVIRONMENT=Development
        ConnectionStrings__DefaultConnection=Host=localhost;Database=myapp
        REDIS_CONNECTION=localhost:6379
        LOGGING__LEVEL=Debug
Factor 4: Backing services — как attached resources
// Backing services (БД, Cache, Queue) подключаются как resources
        // Не hardcode адреса — использовать configuration

        builder.Services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(builder.Configuration["SqlConnectionString"]));

        builder.Services.AddStackExchangeRedisCache(options =>
            options.Configuration = builder.Configuration["RedisConnection"]);

        builder.Services.AddSingleton<ServiceBusClient>(sp =>
            new ServiceBusClient(new Uri(builder.Configuration["ServiceBusEndpoint"])));
        // Все connection strings — из configuration, не из кода
Factor 5: Build, Release, Run — строго разделены
# Build Stage
        FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
        WORKDIR /src
        COPY *.sln .
        COPY **/*.csproj ./
        RUN dotnet restore
        COPY . .
        RUN dotnet publish -c Release -o /app/publish

        # Release Stage (publish output)
        FROM build AS release
        WORKDIR /app
        RUN dotnet publish -c Release -o /app/publish --no-restore

        # Run Stage (runtime)
        FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
        WORKDIR /app
        COPY --from=release /app/publish .
        ENV ASPNETCORE_ENVIRONMENT=Production
        ENTRYPOINT ["dotnet", "MyApp.dll"]
Factor 6: Processes — stateless
// Stateless processes — state хранится в backing store
        // Не хранить state в memory или на disk

        // BAD: In-memory state
        public class OrderService
        {
            private readonly Dictionary<string, Order> _orders = new(); // State in memory!
        }

        // GOOD: State в database / cache
        public class OrderService
        {
            private readonly AppDbContext _context;
            private readonly IMemoryCache _cache; // Cache — backing store, не state

            public async Task<Order> GetOrderAsync(string id)
            {
                // Cache-aside pattern
                return await _cache.GetOrCreateAsync($"order:{id}", async entry =>
                {
                    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
                    return await _context.Orders.FindAsync(id);
                });
            }
        }
Factor 7: Port binding — экспорт через порт
// .NET — Kestrel слушает порт, не hardcoded
        // Конфигурация через environment variables

        // Program.cs
        var builder = WebApplication.CreateBuilder(args);

        builder.WebHost.ConfigureKestrel(options =>
        {
            options.ListenAnyIP(
                int.Parse(builder.Configuration["PORT"] ?? "8080"),
                listenOptions => { listenOptions.Protocols = HttpProtocols.Http1AndHttp2; });
        });

        var app = builder.Build();

        // Kubernetes — порт из environment
        // env:
        //   - name: PORT
        //     value: "8080"
Factor 8: Concurrency — scale out
// Stateless processes легко масштабируются
        // Один instance обрабатывает N concurrent requests

        // .NET — Kestrel по умолчанию поддерживает concurrency
        // Scale out через горизонтальное масштабирование

        // Kubernetes HPA — horizontal pod autoscaler
        // apiVersion: autoscaling/v2
        // kind: HorizontalPodAutoscaler
        // spec:
        //   scaleTargetRef:
        //     apiVersion: apps/v1
        //     kind: Deployment
        //     name: myapp
        //   minReplicas: 2
        //   maxReplicas: 20
        //   metrics:
        //   - type: Resource
        //     resource:
        //       name: cpu
        //       target:
        //         type: Utilization
        //         averageUtilization: 70
Factor 9: Disposability — fast startup, graceful shutdown
// Graceful shutdown в .NET
        var builder = WebApplication.CreateBuilder(args);
        var app = builder.Build();

        // Register hosted service для graceful shutdown
        app.Services.AddHostedService<GracefulShutdownService>();

        public class GracefulShutdownService : IHostedService
        {
            private readonly IHostApplicationLifetime _appLifetime;
            private readonly ILogger<GracefulShutdownService> _logger;

            public GracefulShutdownService(
                IHostApplicationLifetime appLifetime,
                ILogger<GracefulShutdownService> logger)
            {
                _appLifetime = appLifetime;
                _logger = logger;
            }

            public Task StartAsync(CancellationToken ct) => Task.CompletedTask;

            public async Task StopAsync(CancellationToken ct)
            {
                _logger.LogInformation("Graceful shutdown started");
        
                // Дождаться завершения текущих запросов
                // Кэшировать active requests count
                await Task.Delay(TimeSpan.FromSeconds(30), ct);
        
                _logger.LogInformation("Graceful shutdown completed");
            }
        }

        // Kubernetes — graceful termination
        // spec:
        //   template:
        //     spec:
        //       terminationGracePeriodSeconds: 30
Factor 10: Dev/Prod parity
# docker-compose.yml — dev environment похож на prod
        services:
          api:
            build: .
            environment:
              - ASPNETCORE_ENVIRONMENT=Production  # Same as prod!
              - ConnectionStrings__DefaultConnection=Host=postgres;...
            depends_on:
              postgres:
                condition: service_healthy
              redis:
                condition: service_healthy

          postgres:
            image: postgres:16-alpine
            # Same version as prod!

          redis:
            image: redis:7-alpine
            # Same version as prod!
Factor 11: Logs — event streams
// .NET structured logging — logs как event stream
        // Не писать в файлы — отправлять в centralized log aggregator

        builder.Services.AddLogging(builder =>
        {
            builder.ClearProviders();
            builder.AddAzureAppConfiguration(); // или Application Insights
            builder.AddConsole(); // local dev
            builder.AddEventSourceLogger();
        });

        // Structured logging
        public class OrderController
        {
            private readonly ILogger<OrderController> _logger;

            [HttpPost]
            public async Task<IActionResult> CreateOrder([FromBody] OrderRequest request)
            {
                _logger.LogInformation("Creating order {OrderId} for customer {CustomerId}",
                    request.OrderId, request.CustomerId); // Structured log
        
                // ...
        
                _logger.LogInformation("Order {OrderId} created successfully", request.OrderId);
                return CreatedAtAction(nameof(GetOrder), new { id = request.OrderId }, result);
            }
        }

        // Application Insights — automatic distributed tracing
        builder.Services.AddApplicationInsightsTelemetry();
Factor 12: Admin processes — one-off
// .NET Worker Service — admin/maintenance tasks
        // Запускаются как one-off processes

        // Program.cs — Worker Service
        var builder = Host.CreateApplicationBuilder(args);
        builder.Services.AddHostedService<DataMigrationWorker>();
        builder.Services.AddHostedService<ReportGeneratorWorker>();
        builder.Services.AddHostedService<CacheWarmupWorker>();

        var host = builder.Build();
        host.Run();

        // DataMigrationWorker — one-off migration task
        public class DataMigrationWorker : IHostedService
        {
            private readonly ILogger<DataMigrationWorker> _logger;

            public DataMigrationWorker(ILogger<DataMigrationWorker> logger)
            {
                _logger = logger;
            }

            public async Task StartAsync(CancellationToken ct)
            {
                _logger.LogInformation("Running data migration...");
        
                // Migration logic — runs once
                await MigrateOrdersAsync();
        
                _logger.LogInformation("Migration completed");
            }

            public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
        }

Sidecar pattern — log aggregation, service proxy

Sidecar Pattern в .NET

┌─────────────────────────────────────────────────────────┐
        │                    Kubernetes Pod                         │
        │                                                         │
        │  ┌──────────────────────────────────────────────────┐   │
        │  │  Sidecar Container (shared network, volumes)     │   │
        │  │                                                   │   │
        │  │  ┌────────────────────────────────────────────┐  │   │
        │  │  │  Main Container (.NET App)                 │  │   │
        │  │  │                                             │  │   │
        │  │  │  Kestrel → Port 8080                       │  │   │
        │  │  │  Writes logs to /app/logs                  │  │   │
        │  │  └────────────────────────────────────────────┘  │   │
        │  │                                                   │   │
        │  │  ┌────────────────────────────────────────────┐  │   │
        │  │  │  Fluentd / Vector / Logstash               │  │   │
        │  │  │  Reads /app/logs                           │  │   │
        │  │  │  Sends to Elasticsearch / Log Analytics    │  │   │
        │  │  └────────────────────────────────────────────┘  │   │
        │  └──────────────────────────────────────────────────┘   │
        └─────────────────────────────────────────────────────────┘

.NET App + Sidecar для логирования

# Kubernetes deployment — Sidecar pattern для логирования
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: order-service
        spec:
          template:
            spec:
              containers:
              - name: order-service      # Main container
                image: myapp/order-service:latest
                ports:
                - containerPort: 8080
                volumeMounts:
                - name: log-volume
                  mountPath: /app/logs
      
              - name: log-sidecar        # Sidecar container
                image: fluent/fluentd:latest
                volumeMounts:
                - name: log-volume
                  mountPath: /logs
      
              volumes:
              - name: log-volume
                emptyDir: {}
// fluent.conf — Sidecar configuration
        <source>
          @type tail
          path /app/logs/*.log
          pos_file /app/logs/fluentd.pos
          tag order-service.*
          <parse>
            @type json
          </parse>
        </source>

        <match order-service.**>
          @type azure_loganalytics
          workspace_id ${WORKSPACE_ID}
          workspace_key ${WORKSPACE_KEY}
          log_name OrderServiceLog
        </match>

Sidecar для Rate Limiting / Security

// .NET — встроенный rate limiting (вместо sidecar)
        // .NET 7+ имеет встроенный rate limiting

        builder.Services.AddRateLimiter(options =>
        {
            options.AddFixedWindowLimiter("fixed", limiterOptions =>
            {
                limiterOptions.Window = TimeSpan.FromSeconds(10);
                limiterOptions.PermitLimit = 100;
                limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
                limiterOptions.QueueLimit = 10;
            });
    
            options.OnRejected = async (context, token) =>
            {
                context.HttpContext.Response.StatusCode = 429;
                await context.HttpContext.Response.WriteAsync("Rate limit exceeded", token);
            };
        });

        app.UseRateLimiter();

Ambassador pattern — external service abstraction

Ambassador Pattern

┌───────────────────────────────────────────────────────┐
        │                 Ambassador Pattern                     │
        │                                                       │
        │  ┌─────────────┐    ┌──────────────┐    ┌──────────┐ │
        │  │  .NET App   │───►│  Ambassador  │───►│ External │ │
        │  │             │    │  Container   │    │ Service  │ │
        │  │             │    │              │    │          │ │
        │  │  Internal   │    │  - Auth      │    │  3rd     │ │
        │  │  API Call   │    │  - Retry     │    │  Party   │ │
        │  │             │    │  - TLS       │    │  API     │ │
        │  └─────────────┘    └──────────────┘    └──────────┘ │
        │                                                       │
        │  Ambassador — изолирует external service integration │
        └───────────────────────────────────────────────────────┘

.NET Ambassador Implementation

// Ambassador — dedicated HTTP client с retry, timeout, auth
        public class PaymentGatewayAmbassador
        {
            private readonly HttpClient _httpClient;
            private readonly ITokenProvider _tokenProvider;
            private readonly ILogger<PaymentGatewayAmbassador> _logger;
            private readonly ResiliencePipeline _pipeline;

            public PaymentGatewayAmbassador(
                IHttpClientFactory factory,
                ITokenProvider tokenProvider,
                ILogger<PaymentGatewayAmbassador> logger,
                ResiliencePipeline pipeline)
            {
                _httpClient = factory.CreateClient("PaymentGateway");
                _tokenProvider = tokenProvider;
                _logger = logger;
                _pipeline = pipeline;
            }

            public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
            {
                try
                {
                    // Ambassador handles: auth, retry, timeout, logging
                    var token = await _tokenProvider.GetAccessTokenAsync();
                    _httpClient.DefaultRequestHeaders.Authorization = 
                        new AuthenticationHeaderValue("Bearer", token);

                    var response = await _pipeline.ExecuteAsync(async ct =>
                        await _httpClient.PostAsJsonAsync("/api/payments", request, ct));

                    response.EnsureSuccessStatusCode();
                    return await response.Content.ReadFromJsonAsync<PaymentResult>()!;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Payment processing failed");
                    throw;
                }
            }
        }

        // Registration
        builder.Services.AddResiliencePipeline("payment-gateway", builder =>
        {
            builder
                .AddRetry(new HttpRetryStrategyOptions
                {
                    MaxRetries = 3,
                    Delay = TimeSpan.FromSeconds(2),
                    BackoffType = DelayBackoffType.Exponential,
                    UseJitter = true
                })
                .AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
                {
                    SamplingDuration = TimeSpan.FromMinutes(1),
                    FailureRatio = 0.5,
                    MinimumThroughput = 10,
                    ShouldHandle = args => 
                        ValueTask.FromResult(args.Outcome.Exception is HttpRequestException)
                });
        });

        builder.Services.AddHttpClient("PaymentGateway", client =>
        {
            client.BaseAddress = new Uri("http://payment-ambassador:8080/");
            client.Timeout = TimeSpan.FromSeconds(10);
        });

Strangler Fig — incremental migration to cloud-native

Strangler Fig Pattern

┌──────────────────────────────────────────────────────────┐
        │              Strangler Fig Pattern                        │
        │                                                          │
        │  Phase 1: All traffic → Monolith                        │
        │  ┌──────────────────────────────────────────────┐       │
        │  │  Gateway → [Monolith]                        │       │
        │  └──────────────────────────────────────────────┘       │
        │                                                          │
        │  Phase 2: Extract first service                          │
        │  ┌──────────────────────────────────────────────┐       │
        │  │  Gateway → /orders → [New Service]           │       │
        │  │        → /users  → [Monolith]                │       │
        │  └──────────────────────────────────────────────┘       │
        │                                                          │
        │  Phase 3: Extract more services                          │
        │  ┌──────────────────────────────────────────────┐       │
        │  │  Gateway → /orders → [Order Service]         │       │
        │  │        → /payments → [Payment Service]       │       │
        │  │        → /users  → [Monolith]                │       │
        │  └──────────────────────────────────────────────┘       │
        │                                                          │
        │  Phase 4: Monolith eliminated                            │
        │  ┌──────────────────────────────────────────────┐       │
        │  │  Gateway → /orders → [Order Service]         │       │
        │  │        → /payments → [Payment Service]       │       │
        │  │        → /users  → [User Service]            │       │
        │  └──────────────────────────────────────────────┘       │
        └──────────────────────────────────────────────────────────┘

.NET Implementation — API Gateway routing

# YARP (YARP Reverse Proxy) — routing rules
        # appsettings.json
        {
          "ReverseProxy": {
            "Routes": {
              "orders-route": {
                "ClusterId": "orders-cluster",
                "Match": {
                  "Path": "/api/orders{**catch-all}"
                },
                "Transforms": [
                  { "PathPattern": "/api/orders{**catch-all}" }
                ]
              },
              "monolith-route": {
                "ClusterId": "monolith-cluster",
                "Match": {
                  "Path": "{**catch-all}"
                }
              }
            },
            "Clusters": {
              "orders-cluster": {
                "Destinations": {
                  "order-service": {
                    "Address": "http://order-service:8080/"
                  }
                }
              },
              "monolith-cluster": {
                "Destinations": {
                  "monolith": {
                    "Address": "http://monolith:80/"
                  }
                }
              }
            }
          }
        }
// Program.cs — YARP setup
        builder.Services.AddReverseProxy()
            .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

        app.MapReverseProxy();

        // Strangler Fig — health check для мониторинга
        app.MapHealthChecks("/health/strangler", new HealthCheckOptions
        {
            Predicate = check => check.Name.Contains("strangler")
        });

        // Custom health check для мониторинга миграции
        public class StranglerHealthCheck : IHealthCheck
        {
            private readonly IConfiguration _config;

            public StranglerHealthCheck(IConfiguration config)
            {
                _config = config;
            }

            public Task<HealthCheckResult> CheckHealthAsync(
                HealthCheckContext context,
                CancellationToken cancellationToken = default)
            {
                var routes = _config.GetSection("ReverseProxy:Routes").GetChildren();
                var stats = new Dictionary<string, object>();
        
                foreach (var route in routes)
                {
                    stats[route.Key] = route["ClusterId"];
                }
        
                return Task.FromResult(HealthCheckResult.Healthy(
                    $"Strangler Fig routes: {string.Join(", ", stats)}"));
            }
        }

Backend for Frontend (BFF) в cloud environment

BFF Pattern Architecture

┌───────────────────────────────────────────────────────────┐
        │                    BFF Architecture                        │
        │                                                           │
        │  ┌─────────┐    ┌──────────────┐    ┌─────────────────┐  │
        │  │  Web    │───►│  Web BFF     │───►│  API Gateway    │  │
        │  │  App    │    │  (.NET)      │    │  (YARP)         │  │
        │  └─────────┘    └──────────────┘    └─────────────────┘  │
        │                                                         │  │
        │  ┌─────────┐    ┌──────────────┐    ┌─────────────────┐  │
        │  │  Mobile │───►│  Mobile BFF  │───►│  API Gateway    │  │
        │  │  App    │    │  (.NET)      │    │  (YARP)         │  │
        │  └─────────┘    └──────────────┘    └─────────────────┘  │
        │                                                         │  │
        │  ┌──────────────────────────────────────────────────┐   │
        │  │              Backend Services                     │   │
        │  │  ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐  │   │
        │  │  │Order   │ │Payment │ │User    │ │Product   │  │   │
        │  │  │Service │ │Service │ │Service │ │Service   │  │   │
        │  │  └────────┘ └────────┘ └────────┘ └──────────┘  │   │
        │  └──────────────────────────────────────────────────┘   │
        └───────────────────────────────────────────────────────────┘

.NET BFF Implementation

// Web BFF — ASP.NET Core с Cookie Authentication
        // BFF хранит tokens в cookies, не в frontend

        builder.Services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            options.Authority = builder.Configuration["Authority"];
            options.ClientId = builder.Configuration["ClientId"];
            options.ClientSecret = builder.Configuration["ClientSecret"];
            options.ResponseType = "code";
            options.SaveTokens = true;  // Сохраняем tokens в cookies
            options.Scope.Clear();
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.Scope.Add("orders.read");
            options.Scope.Add("orders.write");
        });

        // BFF → Backend API — прокси с токеном
        builder.Services.AddHttpClient("BackendApi", client =>
        {
            client.BaseAddress = new Uri(builder.Configuration["BackendApiUrl"]!);
        })
        .AddHttpMessageHandler<TokenPropagationHandler>();

        public class TokenPropagationHandler : DelegatingHandler
        {
            private readonly IHttpContextAccessor _httpContextAccessor;

            public TokenPropagationHandler(IHttpContextAccessor httpContextAccessor)
            {
                _httpContextAccessor = httpContextAccessor;
            }

            protected override async Task<HttpResponseMessage> SendAsync(
                HttpRequestMessage request, 
                CancellationToken cancellationToken)
            {
                var token = await _httpContextAccessor.HttpContext!
                    .GetTokenAsync("access_token");
        
                if (token != null)
                {
                    request.Headers.Authorization = 
                        new AuthenticationHeaderValue("Bearer", token);
                }
        
                return await base.SendAsync(request, cancellationToken);
            }
        }

Сводная таблица: Best Practices

ПрактикаРекомендация
12-FactorConfig в environment, stateless processes, structured logging
SidecarЛогирование, observability, rate limiting
AmbassadorExternal API isolation, retry, auth
Strangler FigПоэтапная миграция, API Gateway routing
BFFSeparate BFF per client type, cookie auth
MicroservicesStart with modular monolith, extract when needed
DeploymentBlue-green, canary, feature flags
MonitoringDistributed tracing, structured logs, metrics

Практика


Cloud Monitoring и Observability

Содержание

  • Application Insights / CloudWatch — APM для .NET apps
  • Log Analytics / X-Ray — distributed tracing в cloud environment
  • Custom metrics — business KPIs tracking
  • Alert rules — threshold, dynamic, anomaly detection
  • Dashboard design — operational vs executive views

Application Insights / CloudWatch — APM для .NET apps

Observability — три столпа

┌──────────────────────────────────────────────────────────┐
        │              Observability Pillars                        │
        │                                                           │
        │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
        │  │  Metrics     │  │  Logs        │  │  Traces      │   │
        │  │              │  │              │  │              │   │
        │  │  Numbers     │  │  Events      │  │  Flow        │   │
        │  │  Aggregated  │  │  Structured  │  │  End-to-end  │   │
        │  │  Over time   │  │  With context│  │  Through     │   │
        │  │              │  │              │  │  services    │   │
        │  └──────────────┘  └──────────────┘  └──────────────┘   │
        │       Questions:        Questions:       Questions:      │
        │       "What?"           "Why?"           "Where?"        │
        │       "How much?"       "What happened?" "Which path?"   │
        └──────────────────────────────────────────────────────────┘

Application Insights — настройка

// Program.cs — Application Insights integration
        using Microsoft.ApplicationInsights.AspNetCore;

        var builder = WebApplication.CreateBuilder(args);

        // Application Insights — automatic instrumentation
        builder.Services.AddApplicationInsightsTelemetry(options =>
        {
            options.InstrumentationKey = builder.Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
            // Или через connectionString (рекомендуется)
            // options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
        });

        // Additional telemetry processors
        builder.Services.AddHttpClientTelemetry();
        builder.Services AddEntityFrameworkCoreTelemetry();

        var app = builder.Build();

        // Enable distributed tracing
        app.UseHttpsRedirection();

        // Custom middleware для tracing
        app.Use(async (context, next) =>
        {
            var telemetryClient = context.RequestServices.GetRequiredService<TelemetryClient>();
            telemetryClient.Context.Operation.Id = Guid.NewGuid().ToString();
    
            telemetryClient.TrackRequest(context.Request.Method, context.Request.Path, 
                DateTimeOffset.UtcNow, context.Response.StatusCode, true);
    
            await next();
        });

        app.Run();

Application Insights — Telemetry Types

// Custom telemetry в .NET
        public class TelemetryService
        {
            private readonly TelemetryClient _telemetryClient;
            private readonly ILogger<TelemetryService> _logger;

            public TelemetryService(TelemetryClient telemetryClient, ILogger<TelemetryService> logger)
            {
                _telemetryClient = telemetryClient;
                _logger = logger;
            }

            // Track custom event
            public void TrackOrderCreated(Order order)
            {
                _telemetryClient.TrackEvent("OrderCreated", new Dictionary<string, string>
                {
                    { "OrderId", order.Id },
                    { "CustomerId", order.CustomerId },
                    { "Total", order.Total.ToString() },
                    { "Currency", order.Currency }
                });
            }

            // Track custom metric
            public void TrackOrderProcessingTime(double milliseconds)
            {
                _telemetryClient.TrackMetric("OrderProcessingTime", milliseconds);
            }

            // Track dependency
            public async Task TrackDatabaseCallAsync(Func<Task> databaseCall)
            {
                var stopwatch = Stopwatch.StartNew();
                try
                {
                    await databaseCall();
                    _telemetryClient.TrackDependency("SQL", "GetOrder", 
                        "SELECT * FROM Orders", 
                        stopwatch.Elapsed, true, DependencySuccess);
                }
                catch (Exception ex)
                {
                    _telemetryClient.TrackDependency("SQL", "GetOrder", 
                        "SELECT * FROM Orders", 
                        stopwatch.Elapsed, false, DependencyFailure);
                    throw;
                }
            }

            // Track custom exception
            public void TrackPaymentFailure(Order order, Exception ex)
            {
                _telemetryClient.TrackException(ex, new Dictionary<string, string>
                {
                    { "OrderId", order.Id },
                    { "PaymentMethod", order.PaymentMethod },
                    { "Amount", order.Total.ToString() }
                });
            }

            // Set property for all telemetry in scope
            public T WithOrderContext<T>(string orderId, Func<T> action)
            {
                var telemetryScope = _telemetryClient.CreateTelemetryConfiguration();
                telemetryScope.Properties["OrderId"] = orderId;
        
                return action();
            }
        }

AWS CloudWatch — APM для .NET

// AWS CloudWatch — .NET integration
        using Amazon.CloudWatch;
        using Amazon.CloudWatch.Model;

        public class CloudWatchService
        {
            private readonly IAmazonCloudWatch _cloudWatchClient;
            private readonly string _namespace;

            public CloudWatchService(IAmazonCloudWatch cloudWatchClient, string namespace)
            {
                _cloudWatchClient = cloudWatchClient;
                _namespace = namespace;
            }

            // Custom metric
            public async Task PutMetricAsync(string metricName, double value, string unit = "Count")
            {
                await _cloudWatchClient.PutMetricDataAsync(new PutMetricDataRequest
                {
                    Namespace = _namespace,
                    MetricData = new[]
                    {
                        new MetricDatum
                        {
                            MetricName = metricName,
                            Value = value,
                            Unit = MetricUnit.Count,
                            Timestamp = DateTime.UtcNow
                        }
                    }
                });
            }

            // Custom log
            public async Task PutLogAsync(string logGroupName, string logStreamName, string message)
            {
                await _cloudWatchClient.PutLogEventsAsync(new PutLogEventsRequest
                {
                    LogGroupName = logGroupName,
                    LogStreamName = logStreamName,
                    LogEvents = new[]
                    {
                        new InputLogEvent
                        {
                            Message = message,
                            Timestamp = DateTime.UtcNow
                        }
                    }
                });
            }

            // Custom alarm
            public async Task CreateAlarmAsync(string alarmName, string metricName, 
                double threshold, ComparisonOperator comparison)
            {
                await _cloudWatchClient.PutMetricAlarmAsync(new PutMetricAlarmRequest
                {
                    AlarmName = alarmName,
                    MetricName = metricName,
                    Namespace = _namespace,
                    Statistic = Statistic.Average,
                    Period = 300,
                    EvaluationPeriods = 2,
                    Threshold = threshold,
                    ComparisonOperator = comparison,
                    AlarmActions = new[] { "arn:aws:sns:us-east-1:123456789012:alerts" }
                });
            }
        }

Log Analytics / X-Ray — distributed tracing

Distributed Tracing Architecture

┌──────────────────────────────────────────────────────────────┐
        │              Distributed Tracing Flow                         │
        │                                                               │
        │  Request → [Web API] → [Order Service] → [Payment Service]  │
        │            │               │                  │               │
        │            │               │                  │               │
        │            ▼               ▼                  ▼               │
        │         TraceId          TraceId            TraceId           │
        │         SpanId           SpanId            SpanId            │
        │         ParentSpan       ParentSpan       (root)            │
        │                                                               │
        │  CorrelationId = один и тот же TraceId across all services   │
        │                                                               │
        │  Application Insights:                                       │
        │  - Automatic correlation                                    │
        │  - Dependency tracking                                       │
        │  - Performance monitoring                                    │
        │  - Live metrics stream                                       │
        └──────────────────────────────────────────────────────────────┘

OpenTelemetry — .NET

// OpenTelemetry — industry standard для distributed tracing
        // Работает с Application Insights, Jaeger, Zipkin, AWS X-Ray

        builder.Services.AddOpenTelemetry()
            .WithTracing(tracing =>
            {
                tracing.AddSource("MyApp.*")  // .NET Activity sources
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddEntityFrameworkCoreInstrumentation()
                    .AddServiceBusInstrumentation()
                    .AddSQLClientInstrumentation()
                    .AddSource("OrderService")
                    .AddSource("PaymentService")
            
                    // Exporter — выбор в зависимости от cloud provider
                    .AddAzureMonitorTraceExporter(options =>
                    {
                        options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
                    })
                    // Или для AWS X-Ray:
                    // .AddXRayExporter(options => { ... })
                    // Или для Jaeger:
                    // .AddJaegerExporter(options => { ... });
            })
            .WithMetrics(metrics =>
            {
                metrics.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation()
                    .AddMeter("MyApp.*")  // Custom metrics
                    .AddAzureMonitorMetricExporter();
            });

AWS X-Ray — .NET

// AWS X-Ray — .NET SDK
        using Amazon.XRay.Recorder.Core;
        using Amazon.XRay.Recorder.AspectCore.Extensions.AspectInjector;

        // Program.cs
        builder.Services.AddXRay(builder.Configuration["AWS_REGION"]!);

        // Auto-instrumentation
        builder.Services.AddControllers()
            .AddXRay();

        // Manual segment
        public class OrderService
        {
            private readonly ISegmentContext _segmentContext;

            public async Task ProcessOrderAsync(Order order)
            {
                using var subsegment = _segmentContext.BeginSubsegment("OrderProcessing");
        
                try
                {
                    // Business logic
                    await ValidateOrderAsync(order);
                    await ChargePaymentAsync(order);
                    await SaveOrderAsync(order);
            
                    subsegment.AddAnnotation("OrderId", order.Id);
                    subsegment.AddMetadata("OrderTotal", order.Total.ToString());
                }
                catch (Exception ex)
                {
                    subsegment.AddException(ex);
                    throw;
                }
                finally
                {
                    subsegment.Close();
                }
            }
        }

Custom metrics — business KPIs tracking

Custom Metrics в .NET

// Custom business metrics
        public class BusinessMetrics
        {
            private readonly TelemetryClient _telemetryClient;
            private readonly Meter _meter;

            public BusinessMetrics(TelemetryClient telemetryClient)
            {
                _telemetryClient = telemetryClient;
                _meter = new Meter("MyApp.Business");
            }

            // Histogram — distribution of values
            private readonly Histogram<double> _orderValueHistogram = 
                _meter.CreateHistogram<double>("order.value");

            // Counter — cumulative count
            private readonly Counter<long> _orderCountCounter = 
                _meter.CreateCounter<long>("order.count");

            // UpDownCounter — can increase and decrease
            private readonly UpDownCounter<long> _activeOrdersCounter = 
                _meter.CreateUpDownCounter<long>("active.orders");

            public void RecordOrder(Order order)
            {
                // Record histogram
                _orderValueHistogram.Record(order.Total, 
                    new KeyValuePair<string, object?>("OrderId", order.Id),
                    new KeyValuePair<string, object?>("Currency", order.Currency));

                // Record counter
                _orderCountCounter.Add(1,
                    new KeyValuePair<string, object?>("OrderId", order.Id));

                // Update active orders
                _activeOrdersCounter.Add(1,
                    new KeyValuePair<string, object?>("OrderId", order.Id));
            }

            public void CompleteOrder(string orderId)
            {
                _activeOrdersCounter.Add(-1,
                    new KeyValuePair<string, object?>("OrderId", orderId));
            }
        }

        // KPI Dashboard Metrics
        public class KpiMetrics
        {
            private readonly TelemetryClient _telemetryClient;

            public void TrackKpi(string kpiName, double value, Dictionary<string, string>? tags = null)
            {
                _telemetryClient.TrackMetric(kpiName, value);
        
                // Для Application Insights — custom property
                _telemetryClient.Context.Properties["KpiName"] = kpiName;
                _telemetryClient.Context.Properties["KpiValue"] = value.ToString();
            }

            // Common KPIs
            public void TrackRevenue(decimal revenue, string currency)
            {
                _telemetryClient.TrackMetric("Revenue", revenue, 
                    new Dictionary<string, string> { { "Currency", currency } });
            }

            public void TrackConversionRate(double rate, string funnelStep)
            {
                _telemetryClient.TrackMetric("ConversionRate", rate * 100,
                    new Dictionary<string, string> { { "FunnelStep", funnelStep } });
            }

            public void TrackCustomerLifetimeValue(decimal clv, string segment)
            {
                _telemetryClient.TrackMetric("CustomerLifetimeValue", clv,
                    new Dictionary<string, string> { { "Segment", segment } });
            }
        }

Alert rules — threshold, dynamic, anomaly detection

Alert Rules Configuration

# Application Insights Alert Rules
        # Azure Portal / ARM Template / Terraform

        # Threshold Alert
        alerts:
        - name: HighErrorRate
          condition:
            metric: DependencyFailures
            threshold: 10
            operator: GreaterThan
            timeWindow: 5m
            frequency: 1m
          actions:
          - type: Webhook
            uri: https://hooks.slack.com/services/xxx
          - type: Email
            email: team@company.com

        # Dynamic Alert (anomaly detection)
        - name: AnomalousLatency
          condition:
            metric: ServerRequests
            threshold: 3  # 3 standard deviations
            operator: GreaterThan
            timeWindow: 15m
            anomalyDetection:
              granularity: Auto
              sensitivity: 50
          actions:
          - type: Webhook
            uri: https://hooks.slack.com/services/xxx

.NET — Custom Alert Check

// Custom health check для alerting
        public class PerformanceHealthCheck : IHealthCheck
        {
            private readonly IMetricsService _metricsService;
            private readonly ILogger<PerformanceHealthCheck> _logger;

            public PerformanceHealthCheck(IMetricsService metricsService, 
                ILogger<PerformanceHealthCheck> logger)
            {
                _metricsService = metricsService;
                _logger = logger;
            }

            public async Task<HealthCheckResult> CheckHealthAsync(
                HealthCheckContext context,
                CancellationToken cancellationToken = default)
            {
                var p95Latency = await _metricsService.GetP95LatencyAsync();
                var errorRate = await _metricsService.GetErrorRateAsync();
                var activeConnections = await _metricsService.GetActiveConnectionsAsync();

                // Threshold checks
                if (p95Latency > 2000)  // 2 seconds
                {
                    _logger.LogWarning("P95 latency is {P95Latency}ms", p95Latency);
                    return HealthCheckResult.Degraded(
                        $"P95 latency: {p95Latency}ms (threshold: 2000ms)");
                }

                if (errorRate > 0.05)  // 5%
                {
                    _logger.LogWarning("Error rate is {ErrorRate:P}", errorRate);
                    return HealthCheckResult.Degraded(
                        $"Error rate: {errorRate:P} (threshold: 5%)");
                }

                if (activeConnections > 1000)
                {
                    _logger.LogWarning("Active connections: {ActiveConnections}", activeConnections);
                    return HealthCheckResult.Degraded(
                        $"Active connections: {activeConnections} (threshold: 1000)");
                }

                return HealthCheckResult.Healthy("All metrics within thresholds");
            }
        }

Dashboard design — operational vs executive views

Dashboard Types

┌─────────────────────────────────────────────────────────────┐
        │              Dashboard Types                                │
        │                                                             │
        │  Operational Dashboard (Real-time)                          │
        │  ┌────────────────────────────────────────────────────┐    │
        │  │  CPU    │ Memory  │ Requests/sec  │ Error Rate    │    │
        │  │ [===   ] │ [====]  │  1,234/sec    │  0.5%         │    │
        │  │  45%    │  62%    │  ↑ 12%        │  ↓ 0.1%       │    │
        │  │                                        │            │    │
        │  │  P50: 120ms  P95: 450ms  P99: 1.2s  │            │    │
        │  │                                        │            │    │
        │  │  Active Users: 1,234  │  Orders/min: 456  │        │    │
        │  │                                        │            │    │
        │  │  Services: [OK] [OK] [OK] [WARNING]   │            │    │
        │  └────────────────────────────────────────────────────┘    │
        │                                                             │
        │  Executive Dashboard (Daily/Weekly)                         │
        │  ┌────────────────────────────────────────────────────┐    │
        │  │  Revenue: $123,456  │  ↑ 12% vs last week          │    │
        │  │  Orders: 5,678      │  ↑ 8% vs last week           │    │
        │  │  Conversion: 3.2%   │  ↑ 0.3% vs last week         │    │
        │  │  Active Users: 45,678 │  ↑ 5% vs last week         │    │
        │  │  Avg Response Time: 230ms │ ↓ 20ms vs last week    │    │
        │  │  Uptime: 99.97%     │  ↑ 0.01% vs last week        │    │
        │  └────────────────────────────────────────────────────┘    │
        └─────────────────────────────────────────────────────────────┘

Application Insights Dashboards

// KQL — Kusto Query Language для Log Analytics

        // Top 10 slowest dependencies
        dependencies
        | where timestamp > ago(1h)
        | project name, duration, success, operation_Name
        | summarize avgDuration = avg(duration) by name
        | top 10 by avgDuration desc

        // Error rate by endpoint
        requests
        | where timestamp > ago(1h)
        | where success == false
        | project name, resultCode, count = 1
        | summarize errors = sum(count) by name, resultCode
        | top 10 by errors desc

        // P95 latency by service
        traces
        | where timestamp > ago(1h)
        | where message startswith "Latency:"
        | parse message with "Latency: " latency "ms"
        | summarize p95 = percentile(to doubles(latency), 95) by operation_Name
        | sort by p95 desc

Сводная таблица: Best Practices

ПрактикаРекомендация
APMApplication Insights (Azure) / CloudWatch (AWS)
TracingOpenTelemetry — industry standard
LoggingStructured logging, send to centralized aggregator
MetricsCustom business KPIs + infrastructure metrics
AlertsThreshold + anomaly detection
DashboardsOperational (real-time) + Executive (summary)
CorrelationTraceId across all services
Health Checks/health, /health/ready, /health/live
PerformanceP50, P95, P99 — не average
CostRetention policies для logs и traces

Практика


Multi-Cloud и Hybrid Strategies

Содержание

  • Multi-cloud anti-patterns — когда это вредит больше, чем помогает
  • Cloud abstraction layers — Ports and Adapters для cloud services
  • Data sovereignty и compliance — regional requirements
  • Egress cost optimization — minimizing cross-cloud data transfer
  • Disaster recovery across clouds

Multi-cloud anti-patterns — когда это вредит больше, чем помогает

Когда Multi-Cloud имеет смысл

┌──────────────────────────────────────────────────────────┐
        │           Multi-Cloud Decision Matrix                     │
        │                                                           │
        │  ✅ РЕКОМЕНДУЕТСЯ:                                       │
        │  ├── Enterprise с глобальным присутствием                │
        │  ├── Regulatory requirements (data residency)             │
        │  ├── Vendor lock-in avoidance (strategic)                 │
        │  └── Specific service superiority (best-of-breed)        │
        │                                                           │
        │  ❌ НЕ РЕКОМЕНДУЕТСЯ:                                    │
        │  ├── Startup / Small team                                 │
        │  ├── Single market deployment                             │
        │  ├── Limited cloud expertise                              │
        │  ├── Budget-constrained                                   │
        │  └── "Because everyone does multi-cloud"                  │
        └──────────────────────────────────────────────────────────┘

Anti-Pattern #1: Cloud-agnostic everything

// ANTI-PATTERN: Абстрагируем ВСЁ от облака
        // Проблема: теряем managed features, усложняем код

        // ❌ Плохо: полная абстракция от облака
        public interface IStorageService
        {
            Task UploadAsync(Stream data, string fileName);
            Task<Stream> DownloadAsync(string fileName);
        }

        public class BlobStorageService : IStorageService
        {
            private readonly BlobContainerClient _client;
    
            public BlobStorageService(BlobContainerClient client)
            {
                _client = client;
            }
    
            // Реализация для Azure Blob — но interface не знает об этом
        }

        public class S3StorageService : IStorageService
        {
            private readonly AmazonS3Client _client;
    
            // Реализация для AWS S3 — но interface не знает об этом
        }

        // ❌ Проблема: 
        // - теряем lifecycle policies, CDN, tiered storage
        // - теряем managed identity integration
        // - дублируем код для каждой cloud provider
        // - нет доступа к provider-specific features

        // ✅ Хорошо: абстрагируем только business logic
        public interface IFileStorageService
        {
            Task<string> UploadDocumentAsync(Document document, CancellationToken ct);
            Task<Stream> GetDocumentAsync(string documentId, CancellationToken ct);
            Task DeleteDocumentAsync(string documentId, CancellationToken ct);
        }

        // Реализация специфична для провайдера, но business logic чистая
        public class AzureBlobFileStorageService : IFileStorageService
        {
            private readonly BlobContainerClient _container;
            private readonly ILogger<AzureBlobFileStorageService> _logger;

            public AzureBlobFileStorageService(BlobContainerClient container, 
                ILogger<AzureBlobFileStorageService> logger)
            {
                _container = container;
                _logger = logger;
            }

            public async Task<string> UploadDocumentAsync(Document document, CancellationToken ct)
            {
                // Используем Azure-specific features: lifecycle, tiering, CDN
                var blobClient = _container.GetBlobClient(document.Id);
                await blobClient.UploadAsync(document.Content, overwrite: true, 
                    new BlobUploadOptions { TransferOptions = /* Azure-specific options */ }, ct);
        
                // Set metadata for CDN invalidation
                await blobClient.SetMetadataAsync(new Dictionary<string, string>
                {
                    { "DocumentType", document.Type },
                    { "UploadedAt", DateTime.UtcNow.ToString("o") }
                }, ct: ct);

                _logger.LogInformation("Document uploaded: {DocumentId}", document.Id);
                return blobClient.Uri.ToString();
            }
        }

Anti-Pattern #2: Multi-cloud для redundancy

┌──────────────────────────────────────────────────────────┐
        │           Multi-cloud для Redundancy — Cost vs Benefit   │
        │                                                           │
        │  Cost:                                                   │
        │  ├── 2x infrastructure                                   │
        │  ├── 2x engineering team                                 │
        │  ├── Data egress costs                                   │
        │  └── Complexity overhead                                 │
        │                                                           │
        │  Benefit:                                                │
        │  ├── Full provider outage coverage                       │
        │  └── Geographic diversity                                  │
        │                                                           │
        │  Verdict: ❌ Over-engineering для большинства            │
        │  Better: Multi-region within single cloud provider       │
        └──────────────────────────────────────────────────────────┘
// ✅ Better: Multi-region within single cloud
        // Azure: Active-Active geo-redundant
        // AWS: Multi-AZ + Cross-Region Replication
        // GCP: Multi-region deployment

        // Geo-redundant SQL Database — автоматически
        // Azure SQL Database с ZRS (Zone-Redundant Storage)
        // + Geo-replication для DR

        // Multi-region App Service — автоматически
        // Azure Traffic Manager / Azure Front Door
        // AWS Route 53 latency-based routing

Cloud abstraction layers — Ports and Adapters для cloud services

Ports and Adapters Pattern

┌─────────────────────────────────────────────────────────┐
        │              Ports and Adapters Architecture              │
        │                                                         │
        │  ┌──────────────────────────────────────────────────┐   │
        │  │              Application Core                     │   │
        │  │                                                   │   │
        │  │  ┌──────────┐  ┌──────────┐  ┌──────────────┐   │   │
        │  │  │ Domain   │  │ Services │  │ Use Cases    │   │   │
        │  │  │ Entities │  │          │  │              │   │   │
        │  │  └──────────┘  └──────────┘  └──────────────┘   │   │
        │  └──────────────────────┬──────────────────────────┘   │
        │                         │                               │
        │              ┌──────────▼──────────┐                    │
        │              │    Ports (Interfaces)│                   │
        │              │                     │                    │
        │              │  IStoragePort       │                    │
        │              │  IMessagingPort     │                    │
        │              │  ICachingPort       │                    │
        │              └──────────┬──────────┘                    │
        │                         │                               │
        │              ┌──────────▼──────────┐                    │
        │              │   Adapters (Impl)   │                    │
        │              │                     │                    │
        │              │  AzureBlobAdapter   │                    │
        │              │  ServiceBusAdapter  │                    │
        │              │  RedisAdapter       │                    │
        │              └─────────────────────┘                    │
        └─────────────────────────────────────────────────────────┘

Реализация Ports and Adapters

// Port — interface, не зависит от cloud provider
        public interface IStoragePort
        {
            Task<string> UploadAsync(Stream data, string fileName, string contentType, 
                CancellationToken ct);
            Task<Stream> DownloadAsync(string fileName, CancellationToken ct);
            Task DeleteAsync(string fileName, CancellationToken ct);
            Task<bool> ExistsAsync(string fileName, CancellationToken ct);
        }

        // Port — messaging
        public interface IMessagingPort
        {
            Task PublishAsync<T>(T message, string subject, CancellationToken ct) 
                where T : class;
            Task SubscribeAsync<T>(Func<T, CancellationToken, Task> handler, 
                string subscription, CancellationToken ct) where T : class;
        }

        // Port — caching
        public interface ICachingPort
        {
            Task<T?> GetAsync<T>(string key, CancellationToken ct);
            Task SetAsync<T>(string key, T value, TimeSpan? expiry, CancellationToken ct);
            Task RemoveAsync(string key, CancellationToken ct);
        }

        // Adapter — Azure Implementation
        public class AzureBlobStorageAdapter : IStoragePort
        {
            private readonly BlobContainerClient _container;
            private readonly ILogger<AzureBlobStorageAdapter> _logger;

            public AzureBlobStorageAdapter(BlobContainerClient container,
                ILogger<AzureBlobStorageAdapter> logger)
            {
                _container = container;
                _logger = logger;
            }

            public async Task<string> UploadAsync(Stream data, string fileName, 
                string contentType, CancellationToken ct)
            {
                var blobClient = _container.GetBlobClient(fileName);
                await blobClient.UploadAsync(data, overwrite: true, new BlobUploadOptions
                {
                    HttpHeaders = new BlobHttpHeaders { ContentType = contentType }
                }, ct);

                return blobClient.Uri.ToString();
            }

            public async Task<Stream> DownloadAsync(string fileName, CancellationToken ct)
            {
                var blobClient = _container.GetBlobClient(fileName);
                var download = await blobClient.DownloadAsync(ct);
                return download.Content;
            }

            public async Task DeleteAsync(string fileName, CancellationToken ct)
            {
                var blobClient = _container.GetBlobClient(fileName);
                await blobClient.DeleteAsync(ct: ct);
            }

            public async Task<bool> ExistsAsync(string fileName, CancellationToken ct)
            {
                var blobClient = _container.GetBlobClient(fileName);
                return await blobClient.ExistsAsync(ct);
            }
        }

        // Adapter — AWS Implementation
        public class S3StorageAdapter : IStoragePort
        {
            private readonly AmazonS3Client _s3Client;
            private readonly string _bucketName;
            private readonly ILogger<S3StorageAdapter> _logger;

            public S3StorageAdapter(AmazonS3Client s3Client, string bucketName,
                ILogger<S3StorageAdapter> logger)
            {
                _s3Client = s3Client;
                _bucketName = bucketName;
                _logger = logger;
            }

            public async Task<string> UploadAsync(Stream data, string fileName,
                string contentType, CancellationToken ct)
            {
                await _s3Client.PutObjectAsync(new PutObjectRequest
                {
                    BucketName = _bucketName,
                    Key = fileName,
                    InputStream = data,
                    ContentType = contentType
                }, ct);

                return $"s3://{_bucketName}/{fileName}";
            }

            public async Task<Stream> DownloadAsync(string fileName, CancellationToken ct)
            {
                var response = await _s3Client.GetObjectAsync(
                    new GetObjectRequest { BucketName = _bucketName, Key = fileName }, ct);
                return response.ResponseStream;
            }

            public async Task DeleteAsync(string fileName, CancellationToken ct)
            {
                await _s3Client.DeleteObjectAsync(
                    new DeleteObjectRequest { BucketName = _bucketName, Key = fileName }, ct);
            }

            public async Task<bool> ExistsAsync(string fileName, CancellationToken ct)
            {
                try
                {
                    await _s3Client.HeadObjectAsync(
                        new HeadObjectRequest { BucketName = _bucketName, Key = fileName }, ct);
                    return true;
                }
                catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                {
                    return false;
                }
            }
        }

        // Dependency Injection — выбор adapter по environment
        builder.Services.AddSingleton<IStoragePort>(sp =>
        {
            var provider = sp.GetRequiredService<IConfiguration>()["CloudProvider"] ?? "azure";
    
            return provider.ToLower() switch
            {
                "azure" => sp.GetRequiredService<AzureBlobStorageAdapter>(),
                "aws" => sp.GetRequiredService<S3StorageAdapter>(),
                _ => throw new InvalidOperationException($"Unknown cloud provider: {provider}")
            };
        });

Data sovereignty и compliance — regional requirements

Data Residency Requirements

┌──────────────────────────────────────────────────────────┐
        │           Data Sovereignty by Region                      │
        │                                                           │
        │  Region          │ Requirements                         │
        │  ────────────────│──────────────────────────────────────│
        │  EU (GDPR)       │ Data must stay in EU                 │
        │  China           │ Data must stay in China              │
        │  Germany         │ Data must stay in Germany            │
        │  US Federal      │ FedRAMP compliance required          │
        │  Healthcare      │ HIPAA compliance required            │
        │  Financial       │ PCI DSS compliance required          │
        └──────────────────────────────────────────────────────────┘

        // Azure — Region Binding
        // appsettings.json
        {
          "DataResidency": {
            "AllowedRegions": ["eastus2", "westeurope", "japanwest"],
            "EnforceRegion": true
          }
        }

        // Validation middleware
        public class DataResidencyMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly IConfiguration _config;

            public DataResidencyMiddleware(RequestDelegate next, IConfiguration config)
            {
                _next = next;
                _config = config;
            }

            public async Task InvokeAsync(HttpContext context)
            {
                var enforceRegion = _config.GetValue<bool>("DataResidency:EnforceRegion");
                var allowedRegions = _config.GetSection("DataResidency:AllowedRegions")
                    .Get<string[]>();

                if (enforceRegion && allowedRegions != null)
                {
                    var requestedRegion = context.Request.Headers["X-Data-Region"].FirstOrDefault();
            
                    if (!string.IsNullOrEmpty(requestedRegion) && 
                        !allowedRegions.Contains(requestedRegion))
                    {
                        context.Response.StatusCode = 403;
                        await context.Response.WriteAsync(
                            $"Data region '{requestedRegion}' not allowed");
                        return;
                    }
                }

                await _next(context);
            }
        }

Egress cost optimization — minimizing cross-cloud data transfer

Egress Cost by Provider

ProviderEgress Cost (first 10TB/month)
Azure$0.08/GB
AWS$0.09/GB
GCP$0.085/GB

Optimization Strategies

// Strategy 1: CDN для уменьшения egress
        // Azure CDN / AWS CloudFront / GCP Cloud CDN
        // Кэширование контента на edge nodes

        // Strategy 2: Private Link / VPC Peering
        // Трафик между сервисами в одной cloud — бесплатный
        // Избегаем public internet egress

        // Strategy 3: Data compression
        builder.Services.AddResponseCompression(options =>
        {
            options.EnableForHttps = true;
            options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "application/json", "text/plain" });
        });

        app.UseResponseCompression();

        // Strategy 4: Pagination и filtering
        // Не отправлять всё, отправлять только нужное
        [HttpGet]
        public async Task<IActionResult> GetOrders(
            [FromQuery] int page = 1,
            [FromQuery] int pageSize = 50,
            [FromQuery] DateTime? fromDate = null)
        {
            var query = _context.Orders.AsQueryable();
    
            if (fromDate.HasValue)
                query = query.Where(o => o.OrderDate >= fromDate.Value);
    
            var total = await query.CountAsync();
            var orders = await query
                .OrderByDescending(o => o.OrderDate)
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .ToListAsync();
    
            return Ok(new 
            {
                Data = orders,
                Pagination = new 
                {
                    Page = page,
                    PageSize = pageSize,
                    TotalItems = total,
                    TotalPages = (int)Math.Ceiling(total / (double)pageSize)
                }
            });
        }

Disaster Recovery across clouds

DR Strategies

┌──────────────────────────────────────────────────────────┐
        │           Disaster Recovery Strategies                    │
        │                                                           │
        │  Strategy     │ RTO      │ RPO      │ Cost              │
        │  ─────────────│──────────│──────────│───────────────────│
        │  Backup&Restore│ Hours   │ Hours    │ Низкая            │
        │  Pilot Light  │ Minutes  │ Minutes  │ Средняя           │
        │  Warm Standby │ Minutes  │ Seconds  │ Высокая           │
        │  Active-Active│ Seconds  │ Seconds  │ Очень высокая     │
        └──────────────────────────────────────────────────────────┘

        RTO = Recovery Time Objective — сколько времени на восстановление
        RPO = Recovery Point Objective — сколько данных можно потерять

Multi-Region DR Plan

# Terraform — Multi-region deployment
        # Primary: East US
        # Secondary: West Europe

        provider "azurerm" {
          features {}
        }

        # Primary region
        resource "azurerm_resource_group" "primary" {
          name     = "myapp-rg-primary"
          location = "East US"
        }

        # Secondary region
        resource "azurerm_resource_group" "secondary" {
          name     = "myapp-rg-secondary"
          location = "West Europe"
        }

        # SQL Database — Geo-replication
        resource "azurerm_sql_database" "primary" {
          name                = "myapp-db"
          server_id           = azurerm_sql_server.primary.id
          collation           = "SQL_Latin1_General_CP1_CI_AS"
          max_size_gb         = 250
          license_type        = "LicenseIncluded"
  
          geo_backup_enabled            = true
          zone_redundant                = true
          read_scale                    = true
        }

        resource "azurerm_sql_database" "secondary" {
          name                = "myapp-db-secondary"
          server_id           = azurerm_sql_server.secondary.id
          collation           = "SQL_Latin1_General_CP1_CI_AS"
          max_size_gb         = 250
  
          # Geo-replication link
          replica {
            primary_database_id = azurerm_sql_database.primary.id
          }
        }

        # App Service — Geo-redundant deployment
        resource "azurerm_app_service" "primary" {
          name                = "myapp-primary"
          location            = azurerm_resource_group.primary.location
          resource_group_name = azurerm_resource_group.primary.name
          app_service_plan_id = azurerm_app_service_plan.primary.id
        }

        resource "azurerm_app_service" "secondary" {
          name                = "myapp-secondary"
          location            = azurerm_resource_group.secondary.location
          resource_group_name = azurerm_resource_group.secondary.name
          app_service_plan_id = azurerm_app_service_plan.secondary.id
        }

        # Traffic Manager — failover routing
        resource "azurerm_traffic_manager_profile" "failover" {
          name                = "myapp-failover"
          resource_group_name = azurerm_resource_group.primary.name
          traffic_routing_method = "Failover"

          monitor_config {
            protocol = "HTTPS"
            port     = 443
            path     = "/health"
          }

          external_endpoint {
            name = "primary"
            target = azurerm_app_service.primary.default_site_hostname
            endpoint_status = "Enabled"
            priority = 1
          }

          external_endpoint {
            name = "secondary"
            target = azurerm_app_service.secondary.default_site_hostname
            endpoint_status = "Enabled"
            priority = 2
          }
        }

Сводная таблица: Best Practices

ПрактикаРекомендация
Multi-cloudТолько при реальной необходимости
AbstractionBusiness logic, не managed features
Data residencyValidate region at runtime
EgressCDN, compression, private links
DRMulti-region в одном cloud → проще
RTO/RPODefine SLAs before architecture
CostMonitor egress, use private endpoints
ComplianceRegion-locked resources

Практика


Serverless .NET Advanced

Содержание

  • Cold start mitigation — warm-up strategies, provisioned concurrency
  • Function composition — durable functions, orchestration patterns
  • State management в serverless — external state stores
  • Performance tuning — memory allocation, timeout configuration
  • Cost optimization — right-sizing function resources

Cold start mitigation — warm-up strategies, provisioned concurrency

Cold Start Problem

┌────────────────────────────────────────────────────────────┐
        │              Cold Start Lifecycle                           │
        │                                                             │
        │  Event ──► [Scale Out] ──► [Pull Image] ──► [Init] ──► [Run]│
        │                    │              │           │           │   │
        │                    │         5-30s     1-5s      0ms      │   │
        │                    │         (network) (init)              │   │
        │                                                             │
        │  Warm Request: 0-50ms                                       │
        │  Cold Request: 1-10s (first request after scale out)        │
        └────────────────────────────────────────────────────────────┘

Cold Start Mitigation Strategies

┌──────────────────────────────────────────────────────────┐
        │           Cold Start Mitigation Strategies                │
        │                                                           │
        │  Strategy                │ Impact    │ Complexity         │
        │  ────────────────────────│───────────│───────────────────│
        │  Premium Plan            │ High      │ Low                │
        │  Provisioned Concurrency │ High      │ Low                │
        │  Flex Consumption        │ Medium    │ Medium             │
        │  Custom Warm-up          │ Medium    │ Medium             │
        │  Native AOT              │ High      │ Medium             │
        │  Function Grouping       │ Low       │ Low                │
        └──────────────────────────────────────────────────────────┘

Azure Functions — Plans и cold start

┌──────────────────────────────────────────────────────────┐
        │           Azure Functions Plans                           │
        │                                                           │
        │  Consumption Plan                                        │
        │  ├── Scale: 0 → 1000+ instances                         │
        │  ├── Pay: per execution + GB-seconds                      │
        │  ├── Cold start: 1-5s (first request)                    │
        │  └── Best for: sporadic, unpredictable workloads         │
        │                                                           │
        │  Premium Plan                                              │
        │  ├── Scale: pre-warmed instances                         │
        │  ├── Pay: instance-hours + executions                     │
        │  ├── Cold start: ~100ms (pre-warmed)                    │
        │  └── Best for: latency-sensitive APIs                    │
        │                                                           │
        │  Flex Consumption Plan (NEW)                               │
        │  ├── Scale: instant, fine-grained                        │
        │  ├── Pay: per vCPU/GB-second                              │
        │  ├── Cold start: ~200ms (fast scale-out)                 │
        │  └── Best for: balanced cost/performance                 │
        │                                                           │
        │  Dedicated (App Service Plan)                              │
        │  ├── Scale: manual or auto                                │
        │  ├── Pay: instance-hours                                  │
        │  ├── Cold start: 0 (always running)                      │
        │  └── Best for: long-running, consistent load             │
        └──────────────────────────────────────────────────────────┘

Provisioned Concurrency

// host.json — Provisioned Concurrency
        {
          "version": "2.0",
          "extensionBundle": {
            "id": "Microsoft.Azure.Functions.ExtensionBundle",
            "version": "[4.*, 5.0.0)"
          },
          "concurrency": {
            "dynamicConcurrencyEnabled": true,
            "snapshotPersistenceEnabled": true
          }
        }

        // Premium Plan — Minimum instances для предотвращения cold start
        // Azure Portal → App Service Plan → Scaling (Inbound and Outbound)
        // Minimum: 2 (pre-warmed instances)
        // Maximum: 10 (auto-scale)

        // Или через ARM / Terraform:
        resource "azurerm_function_app" "main" {
          ...
          zone_balancing_enabled = true
  
          site_config {
            min_elastic_instance_count = 2  // Pre-warmed instances
          }
        }

Custom Warm-up Function

// Warm-up function — вызывается каждые N минут для поддержания warm
        public class WarmUpFunction
        {
            private readonly ILogger<WarmUpFunction> _logger;

            public WarmUpFunction(ILoggerFactory loggerFactory)
            {
                _logger = loggerFactory.CreateLogger<WarmUpFunction>();
            }

            [Function("WarmUp")]
            public void WarmUp(
                [TimerTrigger("0 */5 * * * *")] TimerInfo myTimer)  // Каждые 5 минут
            {
                _logger.LogInformation("Warm-up triggered");
        
                // Pre-warm dependencies
                // - Load models into memory
                // - Initialize connections
                // - Warm up HttpClient pools
            }
        }

        // Или HTTP warm-up через Azure Monitor Health Check
        // Azure Monitor → Health Check → Ping endpoint
        // Endpoint: /api/warmup
        // Interval: 60 seconds

Native AOT для Serverless

# Native AOT — компиляция в native code, нет JIT
        # .NET 8+ — стабильная поддержка

        # Уменьшение cold start на 50-70%
        dotnet publish -c Release -r win-x64 --self-contained -p:AotCompiler=true -o out

        # Размер: 20-40 MB (vs 100+ MB для JIT)
        # Cold start: ~100ms (vs ~1s для JIT)
        # Memory: 30-50 MB (vs 150-300 MB для JIT)

Function composition — Durable Functions, orchestration patterns

Durable Functions — Orchestrator Pattern

// Durable Functions — оркестрация serverless workflows
        // .NET Isolated Worker Model

        public class OrderOrchestrator
        {
            [Function("OrderOrchestrator")]
            public async Task<OrderResult> Run(
                [OrchestrationTrigger] OrchestrationContext context)
            {
                var orderId = context.GetInput<string>();
                var steps = new List<string>();

                // Step 1: Validate (parallel)
                var validationTask = context.CallActivityAsync<bool>(
                    nameof(ValidateOrderActivity), orderId);

                var inventoryTask = context.CallActivityAsync<bool>(
                    nameof(CheckInventoryActivity), orderId);

                await Task.WhenAll(validationTask, inventoryTask);
        
                if (!validationTask.Result || !inventoryTask.Result)
                {
                    await context.CallActivityAsync(nameof(RejectOrderActivity), orderId);
                    return new OrderResult { Status = "Rejected" };
                }

                // Step 2: Payment
                var paymentResult = await context.CallActivityAsync<PaymentResult>(
                    nameof(ProcessPaymentActivity), orderId);

                if (!paymentResult.Success)
                {
                    await context.CallActivityAsync(nameof(RejectOrderActivity), orderId);
                    return new OrderResult { Status = "PaymentFailed" };
                }

                // Step 3: Ship (sequential)
                await context.CallActivityAsync(nameof(ShipOrderActivity), orderId);
        
                // Step 4: Notify (parallel)
                var notifyTasks = new[]
                {
                    context.CallActivityAsync(nameof(SendEmailActivity), orderId),
                    context.CallActivityAsync(nameof(SendSmsActivity), orderId),
                    context.CallActivityAsync(nameof(UpdateInventoryActivity), orderId)
                };
        
                await Task.WhenAll(notifyTasks);

                return new OrderResult { Status = "Completed", OrderId = orderId };
            }
        }

        // Activities — атомарные функции
        public class OrderActivities
        {
            [Function("ValidateOrderActivity")]
            public bool ValidateOrder([ActivityTrigger] string orderId) => true;

            [Function("CheckInventoryActivity")]
            public bool CheckInventory([ActivityTrigger] string orderId) => true;

            [Function("ProcessPaymentActivity")]
            public PaymentResult ProcessPayment([ActivityTrigger] string orderId) => 
                new PaymentResult { Success = true };

            [Function("ShipOrderActivity")]
            public void ShipOrder([ActivityTrigger] string orderId) { }

            [Function("SendEmailActivity")]
            public void SendEmail([ActivityTrigger] string orderId) { }

            [Function("SendSmsActivity")]
            public void SendSms([ActivityTrigger] string orderId) { }

            [Function("UpdateInventoryActivity")]
            public void UpdateInventory([ActivityTrigger] string orderId) { }

            [Function("RejectOrderActivity")]
            public void RejectOrder([ActivityTrigger] string orderId) { }
        }

        public class OrderResult
        {
            public string Status { get; set; } = string.Empty;
            public string OrderId { get; set; } = string.Empty;
        }

        public class PaymentResult
        {
            public bool Success { get; set; }
            public string TransactionId { get; set; } = string.Empty;
        }

Fan-out / Fan-in Pattern

public class FanOutOrchestrator
        {
            [Function("FanOutOrchestrator")]
            public async Task<List<string>> Run(
                [OrchestrationTrigger] OrchestrationContext context)
            {
                var orderId = context.GetInput<string>();
        
                // Получаем список продуктов заказа
                var products = await context.CallActivityAsync<List<Product>>(
                    nameof(GetOrderProductsActivity), orderId);

                // Fan-out: обработка каждого продукта параллельно
                var tasks = products.Select(product => 
                    context.CallActivityAsync<ProductResult>(
                        nameof(ProcessProductActivity), 
                        new { orderId, product }))
                    .ToList();

                // Fan-in: ждём все задачи
                var results = await Task.WhenAll(tasks);

                return results.Select(r => r.Status).ToList();
            }

            [Function("GetOrderProductsActivity")]
            public List<Product> GetOrderProducts([ActivityTrigger] string orderId) => 
                new List<Product>();

            [Function("ProcessProductActivity")]
            public ProductResult ProcessProduct([ActivityTrigger] dynamic input)
            {
                return new ProductResult { Status = "Processed" };
            }
        }

        public class Product
        {
            public string Id { get; set; } = string.Empty;
            public string Name { get; set; } = string.Empty;
            public decimal Price { get; set; }
        }

        public class ProductResult
        {
            public string Status { get; set; } = string.Empty;
        }

Human-in-the-Loop Pattern

public class ApprovalOrchestrator
        {
            [Function("ApprovalOrchestrator")]
            public async Task<string> Run(
                [OrchestrationTrigger] OrchestrationContext context)
            {
                var orderId = context.GetInput<string>();

                // Отправляем на утверждение
                await context.CallActivityAsync(nameof(SubmitForApprovalActivity), orderId);

                // Ждём ответ от человека
                var approvalEvent = await context.WaitForExternalEvent<bool>("ApprovalDecision");

                if (approvalEvent)
                {
                    await context.CallActivityAsync(nameof(ApproveOrderActivity), orderId);
                    return "Approved";
                }
                else
                {
                    await context.CallActivityAsync(nameof(RejectOrderActivity), orderId);
                    return "Rejected";
                }
            }

            [Function("SubmitForApprovalActivity")]
            public void SubmitForApproval([ActivityTrigger] string orderId) { }

            [Function("ApproveOrderActivity")]
            public void ApproveOrder([ActivityTrigger] string orderId) { }

            [Function("RejectOrderActivity")]
            public void RejectOrder([ActivityTrigger] string orderId) { }
        }

        // Вызов из внешнего API
        public class ApprovalController
        {
            private readonly DurableTaskClient _client;

            public ApprovalController(DurableTaskClient client)
            {
                _client = client;
            }

            [HttpPost("orders/{orderId}/approve")]
            public async Task<IActionResult> Approve(string orderId)
            {
                await _client.RaiseEventAsync("approval-instance-id", "ApprovalDecision", true);
                return Ok();
            }

            [HttpPost("orders/{orderId}/reject")]
            public async Task<IActionResult> Reject(string orderId)
            {
                await _client.RaiseEventAsync("approval-instance-id", "ApprovalDecision", false);
                return Ok();
            }
        }

State management в serverless — external state stores

State in Serverless

┌──────────────────────────────────────────────────────────┐
        │           Serverless State Management                     │
        │                                                           │
        │  Functions — stateless!                                   │
        │  State хранится в external stores:                        │
        │                                                           │
        │  ┌──────────┐  ┌──────────┐  ┌──────────┐               │
        │  │ Cosmos   │  │ Azure    │  │ Redis    │               │
        │  │ DB       │  │ Table    │  │ Cache    │               │
        │  │ (chron)  │  │ (simple) │  │ (fast)   │               │
        │  └──────────┘  └──────────┘  └──────────┘               │
        │                                                           │
        │  Durable Functions — встроенный state:                    │
        │  ┌────────────────────────────────────────────────────┐   │
        │  │  Orchestration State Store (Cosmos DB / Table)    │   │
        │  │                                                     │   │
        │  │  - Orchestrator state                              │   │
        │  │  - Activity history                                │   │
        │  │  - Custom state                                    │   │
        │  └────────────────────────────────────────────────────┘   │
        └──────────────────────────────────────────────────────────┘

Durable Functions — Custom State

public class StatefulOrchestrator
        {
            [Function("StatefulOrchestrator")]
            public async Task<string> Run(
                [OrchestrationTrigger] OrchestrationContext context)
            {
                var orderId = context.GetInput<string>();

                // Get custom state
                var state = context.GetCustomState<OrderState>() ?? new OrderState();

                // Update state
                state.CurrentStep = "Processing";
                state.LastUpdated = DateTime.UtcNow;
                context.SetCustomState(state);

                // Process...
                await context.CallActivityAsync(nameof(ProcessOrderActivity), orderId);

                // Update again
                state.CurrentStep = "Completed";
                state.CompletedAt = DateTime.UtcNow;
                context.SetCustomState(state);

                return "Completed";
            }

            [Function("ProcessOrderActivity")]
            public void ProcessOrder([ActivityTrigger] string orderId) { }
        }

        public class OrderState
        {
            public string CurrentStep { get; set; } = string.Empty;
            public DateTime LastUpdated { get; set; }
            public DateTime? CompletedAt { get; set; }
            public Dictionary<string, object> Metadata { get; set; } = new();
        }

External State Store — Cosmos DB

// External state store для serverless functions
        public class ServerlessStateService
        {
            private readonly CosmosContainer _container;

            public ServerlessStateService(CosmosClient cosmosClient)
            {
                _container = cosmosClient.GetContainer("serverless", "states");
            }

            public async Task<T?> GetStateAsync<T>(string partitionKey, string id)
            {
                try
                {
                    var response = await _container.ReadItemAsync<StateDocument<T>>(
                        id, new PartitionKey(partitionKey));
                    return response.Resource.Data;
                }
                catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                {
                    return default;
                }
            }

            public async Task SaveStateAsync<T>(string partitionKey, string id, T data, 
                TimeSpan? ttl = null)
            {
                var document = new StateDocument<T>
                {
                    Id = id,
                    PartitionKey = partitionKey,
                    Data = data,
                    Version = 1,
                    UpdatedAt = DateTime.UtcNow
                };

                await _container.UpsertItemAsync(document, new PartitionKey(partitionKey),
                    new ItemUpsertOptions { TTL = (int)ttl?.TotalSeconds });
            }

            public async Task IncrementAndGetVersionAsync(string partitionKey, string id)
            {
                var patchOperations = new[]
                {
                    PatchOperation.Increment("/Version", 1),
                    PatchOperation.Set("/UpdatedAt", DateTime.UtcNow.ToString("o"))
                };

                await _container.PatchItemAsync<StateDocument<object>>(
                    id, new PartitionKey(partitionKey), patchOperations);
            }
        }

        public class StateDocument<T>
        {
            [JsonPropertyName("id")]
            public string Id { get; set; } = string.Empty;

            [JsonPropertyName("partitionKey")]
            public string PartitionKey { get; set; } = string.Empty;

            [JsonPropertyName("data")]
            public T Data { get; set; } = default!;

            [JsonPropertyName("version")]
            public int Version { get; set; }

            [JsonPropertyName("updatedAt")]
            public string UpdatedAt { get; set; } = string.Empty;
        }

Performance tuning — memory allocation, timeout configuration

Function App Configuration

// host.json — Performance tuning
        {
          "version": "2.0",
          "functionTimeout": "00:05:00",
          "maxConcurrentFunctions": 100,
          "extensionBundle": {
            "id": "Microsoft.Azure.Functions.ExtensionBundle",
            "version": "[4.*, 5.0.0)"
          },
          "logging": {
            "applicationInsights": {
              "samplingSettings": {
                "isEnabled": true,
                "excludedTypes": "Request"
              }
            }
          },
          "concurrency": {
            "dynamicConcurrencyEnabled": true,
            "snapshotPersistenceEnabled": true
          }
        }

        // function.json — Individual function config
        {
          "scriptFile": "../bin/MyFunctions.dll",
          "entryPoint": "MyFunctions.OrderFunction.ProcessOrder",
          "bindings": [
            {
              "authLevel": "function",
              "type": "httpTrigger",
              "direction": "in",
              "name": "req",
              "methods": ["post"]
            },
            {
              "type": "http",
              "direction": "out",
              "name": "res"
            }
          ]
        }

.NET Performance Tuning for Serverless

// Performance tips для serverless .NET

        // 1. Async all the way
        public async Task<IActionResult> ProcessAsync([HttpTrigger] HttpRequest req)
        {
            // ❌ BAD: Blocking call
            // var result = task.Result;
    
            // ✅ GOOD: async/await
            var result = await ProcessDataAsync();
            return Ok(result);
        }

        // 2. Minimize allocations
        public string FormatOrderId(int orderId)
        {
            // ❌ BAD: string concatenation в цикле
            // var result = "";
            // foreach (var c in digits) result += c;
    
            // ✅ GOOD: Span<T> / StringBuilder
            Span<char> span = stackalloc char[10];
            orderId.ToString(span);
            return new string(span);
        }

        // 3. Connection pooling
        // HttpClient — использовать IHttpClientFactory
        // DB connections — Entity Framework DbContext (scoped)
        // Redis — StackExchange.Redis (_singleton)

        // 4. Lazy initialization
        private static Lazy<BlobContainerClient> _lazyClient = 
            new(() => new BlobContainerClient(/* ... */));

        // 5. Static clients для повторного использования
        private static readonly SemaphoreSlim _semaphore = new(1, 1);

Cost optimization — right-sizing function resources

Cost Optimization Strategies

┌──────────────────────────────────────────────────────────┐
        │           Serverless Cost Optimization                    │
        │                                                           │
        │  1. Right-sizing memory                                  │
        │     - More memory = more CPU = faster execution          │
        │     - Often cheaper to use more memory                   │
        │                                                           │
        │  2. Reduce execution time                                │
        │     - Cold start mitigation                              │
        │     - Connection pooling                                 │
        │     - Lazy loading                                       │
        │                                                           │
        │  3. Batch processing                                     │
        │     - Process multiple items per invocation              │
        │     - Reduce number of invocations                       │
        │                                                           │
        │  4. Event filtering                                      │
        │     - Filter at source, not in function                  │
        │     - Reduce unnecessary invocations                     │
        │                                                           │
        │  5. Choose right plan                                    │
        │     - Consumption: sporadic workloads                    │
        │     - Premium: latency-sensitive                         │
        │     - Flex: balanced                                     │
        └──────────────────────────────────────────────────────────┘

Memory vs Cost Analysis

┌──────────────────────────────────────────────────────────┐
        │  Memory   │  CPU    │  Avg Time │  Cost/1M invocations  │
        │  ─────────│─────────│───────────│───────────────────────│
        │  128 MB   │  0.2x   │  800ms    │  $15.00               │
        │  256 MB   │  0.4x   │  600ms    │  $14.50               │
        │  512 MB   │  0.8x   │  400ms    │  $14.00               │
        │  1024 MB  │  1.5x   │  250ms    │  $13.80  ← Optimal    │
        │  2048 MB  │  3.0x   │  180ms    │  $15.50               │
        │  3008 MB  │  5.0x   │  150ms    │  $18.00               │
        └──────────────────────────────────────────────────────────┘

        Вывод: 1024 MB — оптимальный баланс cost/performance
// Function App — configure memory via environment
        // appsettings.json или Azure Portal

        // Memory-optimized function
        public class MemoryOptimizedFunction
        {
            // Use struct for small data
            public struct OrderSummary
            {
                public string OrderId;
                public decimal Total;
                public DateTime Date;
            }

            // Use span for string processing
            public string ProcessText(ReadOnlySpan<char> input)
            {
                return input.ToString().Trim();
            }

            // Pre-allocate collections
            private readonly List<Order> _orders = new(1000);
        }

Сводная таблица: Best Practices

ПрактикаРекомендация
Cold startPremium/Flex plan, provisioned concurrency
OrchestrationDurable Functions для long-running workflows
StateExternal store (Cosmos DB, Redis), не в function
Memory1024 MB — оптимальный баланс cost/performance
TimeoutSet explicit timeout, default varies by plan
ConcurrencyDynamic concurrency enabled
AOTNative AOT для минимального cold start
Warm-upTimer trigger для поддержания warm
BatchingProcess multiple items per invocation
MonitoringTrack cold starts, execution time, memory

Практика


Cloud Cost Architecture

Содержание

  • Total Cost of Ownership (TCO) analysis — cloud vs on-premise
  • Reserved capacity planning — 1-year, 3-year commitments
  • Auto-scaling cost optimization — right-sizing min/max/desired
  • Spot instance strategies для batch processing и CI/CD
  • FinOps practices — showback, chargeback, anomaly detection

Total Cost of Ownership (TCO) analysis — cloud vs on-premise

TCO Components

┌──────────────────────────────────────────────────────────┐
        │           TCO Components                                  │
        │                                                           │
        │  On-Premise:                                             │
        │  ├── Hardware (servers, storage, networking)             │
        │  ├── Data center (power, cooling, space)                 │
        │  ├── Staff (sysadmins, network engineers)                │
        │  ├── Software licenses (OS, DB, middleware)              │
        │  ├── Maintenance & upgrades                              │
        │  └── Downtime cost                                       │
        │                                                           │
        │  Cloud:                                                  │
        │  ├── Compute (VMs, serverless, containers)               │
        │  ├── Storage (blob, database, backup)                    │
        │  ├── Networking (bandwidth, CDN, VPN)                    │
        │  ├── Managed services (PaaS, SaaS)                       │
        │  ├── Staff (cloud engineers)                             │
        │  └── Training & migration                                │
        └──────────────────────────────────────────────────────────┘

TCO Calculator — .NET Example

// TCO Analysis для .NET application
        public class TcoCalculator
        {
            // On-Premise Costs (annual)
            public class OnPremiseCosts
            {
                public decimal Hardware { get; set; }        // Серверы, storage, networking
                public decimal DataCenter { get; set; }       // Power, cooling, space
                public decimal Staff { get; set; }            // Sysadmins, engineers
                public decimal Licenses { get; set; }         // Windows Server, SQL Server
                public decimal Maintenance { get; set; }      // Maintenance contracts
                public decimal Downtime { get; set; }         // Cost per hour × expected downtime
            }

            // Cloud Costs (annual)
            public class CloudCosts
            {
                public decimal Compute { get; set; }          // VMs, App Service, Functions
                public decimal Storage { get; set; }          // Blob, DB, Backup
                public decimal Networking { get; set; }       // Bandwidth, CDN, VPN
                public decimal ManagedServices { get; set; }  // PaaS services
                public decimal Staff { get; set; }            // Cloud engineers
                public decimal Training { get; set; }         // Training, migration
            }

            // Calculate TCO
            public TcoResult Calculate(OnPremiseCosts onPrem, CloudCosts cloud)
            {
                var onPremTotal = onPrem.Hardware + onPrem.DataCenter + onPrem.Staff +
                                 onPrem.Licenses + onPrem.Maintenance + onPrem.Downtime;

                var cloudTotal = cloud.Compute + cloud.Storage + cloud.Networking +
                                cloud.ManagedServices + cloud.Staff + cloud.Training;

                var savings = onPremTotal - cloudTotal;
                var savingsPercent = (savings / onPremTotal) * 100;

                return new TcoResult
                {
                    OnPremiseAnnual = onPremTotal,
                    CloudAnnual = cloudTotal,
                    AnnualSavings = savings,
                    SavingsPercent = savingsPercent,
                    BreakEvenMonths = cloud.Training > 0 
                        ? (cloud.Training / (cloudTotal > 0 ? cloudTotal / 12 : 1)) 
                        : 0
                };
            }
        }

        public class TcoResult
        {
            public decimal OnPremiseAnnual { get; set; }
            public decimal CloudAnnual { get; set; }
            public decimal AnnualSavings { get; set; }
            public decimal SavingsPercent { get; set; }
            public decimal BreakEvenMonths { get; set; }
        }

Пример TCO для .NET Application

┌─────────────────────────────────────────────────────────────┐
        │  TCO Analysis: 10 .NET Microservices                        │
        │                                                             │
        │  On-Premise (Annual):                                       │
        │  ├── Hardware:        $120,000                              │
        │  ├── Data Center:     $60,000                               │
        │  ├── Staff (5):       $300,000                              │
        │  ├── Licenses:        $50,000                               │
        │  ├── Maintenance:     $30,000                               │
        │  ├── Downtime:        $40,000                               │
        │  ─────────────────────────────────                          │
        │  │  Total:            $600,000                              │
        │  └────────────────────────────────                          │
        │                                                             │
        │  Azure (Annual):                                            │
        │  ├── Compute:         $180,000 (App Service + VMs)         │
        │  ├── Storage:         $30,000 (Blob + SQL)                 │
        │  ├── Networking:      $20,000 (CDN + VPN)                  │
        │  ├── Managed Services:$50,000 (Service Bus, Redis, etc)   │
        │  ├── Staff (3):       $180,000                              │
        │  ├── Training:        $20,000                               │
        │  ─────────────────────────────────                          │
        │  │  Total:            $480,000                              │
        │  └────────────────────────────────                          │
        │                                                             │
        │  Annual Savings:   $120,000 (20%)                           │
        │  Break-even:       5 months                                 │
        └─────────────────────────────────────────────────────────────┘

Reserved capacity planning — 1-year, 3-year commitments

Reserved Instances vs Spot vs On-Demand

┌──────────────────────────────────────────────────────────┐
        │           Pricing Models                                  │
        │                                                           │
        │  On-Demand                                               │
        │  ├── Pay by the second/hour                              │
        │  ├── No commitment                                       │
        │  ├── Highest price                                       │
        │  └── Best for: sporadic, unpredictable workloads         │
        │                                                           │
        │  Reserved Instances (RI)                                 │
        │  ├── 1-year:  ~30-40% discount                           │
        │  ├── 3-year:  ~50-60% discount                           │
        │  ├── Upfront / Partial / No upfront                      │
        │  └── Best for: steady-state, predictable workloads       │
        │                                                           │
        │  Spot Instances                                          │
        │  ├── 60-90% discount                                     │
        │  ├── Can be interrupted with 2-minute warning            │
        │  └── Best for: batch, fault-tolerant, flexible workloads │
        └──────────────────────────────────────────────────────────┘

Reserved Capacity Strategy

// Reserved Instance Calculator
        public class ReservedCapacityCalculator
        {
            public ReservedCapacityResult Calculate(
                string service,
                int instanceCount,
                decimal onDemandPricePerHour,
                int years = 1,
                PaymentType paymentType = PaymentType.NoUpfront)
            {
                var discountRate = years switch
                {
                    1 => 0.35m,
                    3 => 0.55m,
                    _ => throw new ArgumentException("Years must be 1 or 3")
                };

                var upfrontPayment = paymentType switch
                {
                    PaymentType.AllUpfront => onDemandPricePerHour * 24 * 365 * years * (1 - discountRate - 0.05m),
                    PaymentType.PartialUpfront => onDemandPricePerHour * 24 * 365 * years * (1 - discountRate - 0.05m) * 0.5m,
                    PaymentType.NoUpfront => 0m,
                    _ => throw new ArgumentException("Invalid payment type")
                };

                var hourlyRate = onDemandPricePerHour * (1 - discountRate);
                var annualCost = hourlyRate * 24 * 365 * instanceCount;
                var totalCost = annualCost * years + upfrontPayment;

                return new ReservedCapacityResult
                {
                    Service = service,
                    InstanceCount = instanceCount,
                    Years = years,
                    PaymentType = paymentType,
                    OnDemandAnnual = onDemandPricePerHour * 24 * 365 * instanceCount,
                    ReservedAnnual = annualCost,
                    UpfrontPayment = upfrontPayment,
                    TotalCost = totalCost,
                    Savings = onDemandAnnual * years - totalCost,
                    SavingsPercent = discountRate
                };
            }
        }

        public enum PaymentType
        {
            AllUpfront,
            PartialUpfront,
            NoUpfront
        }

        public class ReservedCapacityResult
        {
            public string Service { get; set; } = string.Empty;
            public int InstanceCount { get; set; }
            public int Years { get; set; }
            public PaymentType PaymentType { get; set; }
            public decimal OnDemandAnnual { get; set; }
            public decimal ReservedAnnual { get; set; }
            public decimal UpfrontPayment { get; set; }
            public decimal TotalCost { get; set; }
            public decimal Savings { get; set; }
            public decimal SavingsPercent { get; set; }
        }

Auto-scaling cost optimization — right-sizing min/max/desired

Auto-Scaling Strategies

┌──────────────────────────────────────────────────────────┐
        │           Auto-Scaling for Cost Optimization              │
        │                                                           │
        │  Scale Out (Vertical)                                    │
        │  ├── Add more instances                                  │
        │  ├── Horizontal scaling                                  │
        │  └── Best for: stateless services                       │
        │                                                           │
        │  Scale Up (Horizontal)                                   │
        │  ├── Bigger VM / instance                                │
        │  ├── More CPU / Memory                                   │
        │  └── Best for: stateful services, databases             │
        │                                                           │
        │  Scale In (Down)                                         │
        │  ├── Remove idle instances                               │
        │  ├── Min instances = cost baseline                       │
        │  └── Best for: reducing cost during low traffic         │
        └──────────────────────────────────────────────────────────┘

Kubernetes HPA — Cost-Aware

# Kubernetes Horizontal Pod Autoscaler — cost-aware
        apiVersion: autoscaling/v2
        kind: HorizontalPodAutoscaler
        metadata:
          name: order-service-hpa
        spec:
          scaleTargetRef:
            apiVersion: apps/v1
            kind: Deployment
            name: order-service
          minReplicas: 2      # Минимум для availability
          maxReplicas: 20     # Максимум для cost control
          metrics:
          # CPU-based scaling
          - type: Resource
            resource:
              name: cpu
              target:
                type: Utilization
                averageUtilization: 70  # Scale при 70% CPU
          # Memory-based scaling
          - type: Resource
            resource:
              name: memory
              target:
                type: Utilization
                averageUtilization: 80
          # Custom metric — cost-aware
          - type: Object
            object:
              metric:
                name: cost-per-request
              describedObject:
                apiVersion: batch/v1
                kind: Job
              target:
                type: Value
                value: 0.001  # $0.001 per request
          behavior:
            scaleDown:
              stabilizationWindowSeconds: 300  # 5 минут перед scale down
              policies:
              - type: Percent
                value: 50
                periodSeconds: 60
            scaleUp:
              stabilizationWindowSeconds: 0    # Immediate scale up
              policies:
              - type: Percent
                value: 100
                periodSeconds: 60
              - type: Pods
                value: 4
                periodSeconds: 60
              selectPolicy: Max

Azure App Service — Auto-scale

// Azure Resource Manager — Auto-scale settings
        {
          "profiles": [
            {
              "name": "Default",
              "capacity": {
                "minimum": 2,
                "maximum": 10,
                "default": 2
              },
              "rules": [
                {
                  "scaleAction": {
                    "direction": "Increase",
                    "type": "ChangeCount",
                    "value": 1,
                    "cooldown": "PT5M"
                  },
                  "trigger": {
                    "metricTrigger": {
                      "metricName": "CpuPercentage",
                      "metricResourceUri": "[resourceId('Microsoft.Web/serverfarms', 'myapp-plan')]",
                      "timeGrain": "PT1M",
                      "statistic": "Average",
                      "timeWindow": "PT5M",
                      "timeAggregation": "Average",
                      "operator": "GreaterThan",
                      "threshold": 70
                    },
                    "scaleAction": {
                      "direction": "Increase",
                      "type": "ChangeCount",
                      "value": 1,
                      "cooldown": "PT5M"
                    }
                  }
                },
                {
                  "scaleAction": {
                    "direction": "Decrease",
                    "type": "ChangeCount",
                    "value": 1,
                    "cooldown": "PT15M"
                  },
                  "trigger": {
                    "metricTrigger": {
                      "metricName": "CpuPercentage",
                      "metricResourceUri": "[resourceId('Microsoft.Web/serverfarms', 'myapp-plan')]",
                      "timeGrain": "PT1M",
                      "statistic": "Average",
                      "timeWindow": "PT5M",
                      "timeAggregation": "Average",
                      "operator": "LessThan",
                      "threshold": 30
                    }
                  }
                }
              ]
            }
          ]
        }

Spot instance strategies для batch processing и CI/CD

Spot Instance Strategy

┌──────────────────────────────────────────────────────────┐
        │           Spot Instance Strategy                          │
        │                                                           │
        │  Suitable Workloads:                                     │
        │  ├── Batch processing                                    │
        │  ├── CI/CD build agents                                  │
        │  ├── Data processing / ETL                               │
        │  ├── ML training                                         │
        │  ├── Unit tests                                          │
        │  └── Development environments                            │
        │                                                           │
        │  NOT Suitable:                                           │
        │  ├── Production APIs                                     │
        │  ├── Databases                                           │
        │  ├── Stateful services                                   │
        │  └── Latency-sensitive workloads                         │
        │                                                           │
        │  Strategy:                                               │
        │  ├── Use spot for non-critical workloads                │
        │  ├── Implement checkpointing                             │
        │  ├── Use spot fleets / mixed instances                  │
        │  └── Have fallback to on-demand                         │
        └──────────────────────────────────────────────────────────┘

.NET Batch Processing с Spot Instances

// Batch processor — checkpointing для spot instances
        public class BatchProcessor
        {
            private readonly ILogger<BatchProcessor> _logger;
            private readonly IBatchCheckpointService _checkpointService;

            public BatchProcessor(ILogger<BatchProcessor> logger, 
                IBatchCheckpointService checkpointService)
            {
                _logger = logger;
                _checkpointService = checkpointService;
            }

            public async Task ProcessBatchAsync(IEnumerable<BatchItem> items, CancellationToken ct)
            {
                var processedCount = 0;
                var checkpointInterval = 100; // Checkpoint каждые 100 items

                await foreach (var item in items.ToAsyncEnumerable().WithCancellation(ct))
                {
                    try
                    {
                        await ProcessSingleItemAsync(item, ct);
                        processedCount++;

                        // Checkpoint — для восстановления после spot interruption
                        if (processedCount % checkpointInterval == 0)
                        {
                            await _checkpointService.SaveCheckpointAsync(processedCount);
                            _logger.LogInformation("Checkpoint saved at {Count}", processedCount);
                        }
                    }
                    catch (OperationCanceledException)
                    {
                        // Spot interruption — save checkpoint и выйти gracefully
                        await _checkpointService.SaveCheckpointAsync(processedCount);
                        _logger.LogWarning("Batch interrupted at {Count}, checkpoint saved", processedCount);
                        return;
                    }
                }

                _logger.LogInformation("Batch completed: {Count} items", processedCount);
            }

            private Task ProcessSingleItemAsync(BatchItem item, CancellationToken ct)
            {
                // Process item...
                return Task.CompletedTask;
            }
        }

        // Checkpoint service — сохранение прогресса
        public interface IBatchCheckpointService
        {
            Task SaveCheckpointAsync(int processedCount);
            Task<int> LoadCheckpointAsync();
        }

        public class BlobCheckpointService : IBatchCheckpointService
        {
            private readonly BlobClient _checkpointBlob;

            public BlobCheckpointService(BlobServiceClient blobServiceClient, string container, string checkpointName)
            {
                _checkpointBlob = blobServiceClient.GetContainerClient(container)
                    .GetBlobClient(checkpointName);
            }

            public async Task SaveCheckpointAsync(int processedCount)
            {
                await _checkpointBlob.UploadAsync(
                    new MemoryStream(Encoding.UTF8.GetBytes(processedCount.ToString())));
            }

            public async Task<int> LoadCheckpointAsync()
            {
                try
                {
                    var download = await _checkpointBlob.DownloadAsync();
                    using var reader = new StreamReader(download.Content);
                    return int.Parse(await reader.ReadToEndAsync());
                }
                catch
                {
                    return 0; // No checkpoint
                }
            }
        }

FinOps practices — showback, chargeback, anomaly detection

FinOps Framework

┌──────────────────────────────────────────────────────────┐
        │              FinOps Framework                             │
        │                                                           │
        │  Phase 1: Inform                                        │
        │  ├── Visibility — understand cloud spend                 │
        │  ├── Showback — allocate costs to teams                 │
        │  ├── Benchmarking — compare against industry             │
        │  └── Anomaly detection — spot unexpected spikes          │
        │                                                           │
        │  Phase 2: Optimize                                      │
        │  ├── Right-sizing — match resources to needs             │
        │  ├── Reserved capacity — commit for discounts            │
        │  ├── Architecture — optimize for cost                    │
        │  └── Waste reduction — eliminate unused resources        │
        │                                                           │
        │  Phase 3: Operate                                       │
        │  ├── Budgeting — forecast spend                          │
        │  ├── Procurement — manage commitments                    │
        │  ├── Integration — FinOps into CI/CD                     │
        │  └── Continuous improvement — regular reviews            │
        └──────────────────────────────────────────────────────────┘

Cost Allocation by Team

// Cost allocation — chargeback / showback
        public class CostAllocator
        {
            public CostAllocationResult Allocate(
                IEnumerable<CloudResource> resources,
                Dictionary<string, List<string>> teamResourceMap)
            {
                var allocation = new Dictionary<string, decimal>();

                foreach (var (team, resourceIds) in teamResourceMap)
                {
                    var teamCost = resources
                        .Where(r => resourceIds.Contains(r.Id))
                        .Sum(r => r.MonthlyCost);

                    allocation[team] = teamCost;
                }

                return new CostAllocationResult
                {
                    Allocations = allocation,
                    TotalCost = resources.Sum(r => r.MonthlyCost),
                    ReportDate = DateTime.UtcNow
                };
            }
        }

        public class CloudResource
        {
            public string Id { get; set; } = string.Empty;
            public string Name { get; set; } = string.Empty;
            public string ServiceType { get; set; } = string.Empty;
            public decimal MonthlyCost { get; set; }
        }

        public class CostAllocationResult
        {
            public Dictionary<string, decimal> Allocations { get; set; } = new();
            public decimal TotalCost { get; set; }
            public DateTime ReportDate { get; set; }
        }

Anomaly Detection

// Anomaly detection для cloud costs
        public class CostAnomalyDetector
        {
            private readonly ILogger<CostAnomalyDetector> _logger;
            private readonly ICostRepository _costRepository;

            public CostAnomalyDetector(ILogger<CostAnomalyDetector> logger,
                ICostRepository costRepository)
            {
                _logger = logger;
                _costRepository = costRepository;
            }

            public async Task DetectAnomaliesAsync(DateTime? since = null)
            {
                since ??= DateTime.UtcNow.AddDays(-7);

                var dailyCosts = await _costRepository.GetDailyCostsAsync(since.Value, DateTime.UtcNow);
        
                if (dailyCosts.Count < 7) return;

                // Simple anomaly detection: compare with 7-day average
                var average = dailyCosts.Average(d => d.TotalCost);
                var standardDeviation = Math.Sqrt(
                    dailyCosts.Average(d => Math.Pow(d.TotalCost - average, 2)));

                var today = dailyCosts.Last();
                var threshold = average + (2 * standardDeviation); // 2 sigma

                if (today.TotalCost > threshold)
                {
                    var deviation = ((today.TotalCost - average) / average) * 100;
            
                    _logger.LogWarning("Cost anomaly detected! Today: ${TodayCost}, " +
                        "Average: ${Average}, Deviation: {Deviation:P}",
                        today.TotalCost, average, deviation / 100);

                    // Send alert
                    await SendCostAlertAsync(today.TotalCost, average, deviation);
                }
            }

            private Task SendCostAlertAsync(decimal todayCost, decimal average, decimal deviationPercent)
            {
                // Send alert via Teams, Slack, Email
                return Task.CompletedTask;
            }
        }

Cost Monitoring в .NET Application

// Custom middleware для мониторинга cost per request
        public class CostTrackingMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly TelemetryClient _telemetry;

            public CostTrackingMiddleware(RequestDelegate next, TelemetryClient telemetry)
            {
                _next = next;
                _telemetry = telemetry;
            }

            public async Task InvokeAsync(HttpContext context)
            {
                var sw = Stopwatch.StartNew();
        
                try
                {
                    await _next(context);
                }
                finally
                {
                    sw.Stop();
            
                    // Track cost per request
                    _telemetry.TrackMetric("RequestCost", 
                        sw.Elapsed.TotalSeconds * 0.0001, // $0.0001 per second of compute
                        new Dictionary<string, string>
                        {
                            { "Endpoint", context.Request.Path },
                            { "Method", context.Request.Method },
                            { "StatusCode", context.Response.StatusCode.ToString() }
                        });
                }
            }
        }

Сводная таблица: Best Practices

ПрактикаРекомендация
TCOCalculate before migration, include hidden costs
Reserved1-year for predictable, 3-year for steady-state
Auto-scaleSet min/max, use cost-aware metrics
SpotFor batch, CI/CD, fault-tolerant workloads
FinOpsInform → Optimize → Operate
AllocationChargeback by team, showback by service
AnomalyAlert on >2σ deviation from average
MonitoringCost per request, per service, per team
Right-sizingRegular review of resource utilization

Практика


Контрольная точка модуля 10

Содержание

  • Проект: Cloud-Native Microservice Platform
  • Критерии прохождения
  • Архитектура решения
  • Пошаговая реализация

Проект: Cloud-Native Microservice Platform

Описание проекта

Создать cloud-native платформу микросервисов на Azure (или AWS) с:

  • Managed identity для zero-secret service authentication
  • Cloud message broker для async communication
  • Serverless functions для event-driven processing
  • Full observability через cloud APM tools
  • Cost monitoring и alerting setup
  • Multi-region deployment strategy document

Архитектура

┌─────────────────────────────────────────────────────────────────────┐
        │                     Cloud-Native Platform                            │
        │                                                                      │
        │  Clients:                                                            │
        │  ┌──────────┐  ┌──────────┐  ┌──────────┐                          │
        │  │  Web     │  │  Mobile  │  │  Partner │                          │
        │  │  App     │  │  App     │  │  API     │                          │
        │  └────┬─────┘  └────┬─────┘  └────┬─────┘                          │
        │       │              │              │                                │
        │       └──────────────┼──────────────┘                                │
        │                      │                                               │
        │              ┌───────▼────────┐                                     │
        │              │  API Gateway   │  (Azure APIM / YARP)                │
        │              │  + BFF         │                                     │
        │              └───────┬────────┘                                     │
        │                      │                                               │
        │  ┌───────────────────┼──────────────────────────────────────────┐   │
        │  │                   │                                           │   │
        │  ▼                   ▼                                           │   │
        │ ┌──────────┐   ┌──────────────┐   ┌──────────┐                  │   │
        │ │Order     │   │  Payment     │   │  User    │                  │   │
        │ │Service   │   │  Service     │   │  Service │                  │   │
        │ │(App Svc) │   │(App Svc)     │   │(App Svc) │                  │   │
        │ └────┬─────┘   └──────┬───────┘   └────┬─────┘                  │   │
        │      │                │                │                         │   │
        │      ▼                ▼                ▼                         │   │
        │ ┌────────────────────────────────────────────────────────────┐   │   │
        │ │              Azure Service Bus (Topics)                    │   │   │
        │ │  ┌─────────────┐  ┌─────────────┐  ┌──────────────────┐  │   │   │
        │ │  │ OrderEvents │  │ PaymentEvts │  │  UserEvents      │  │   │   │
        │ │  └──────┬──────┘  └──────┬──────┘  └────────┬─────────┘  │   │   │
        │ └─────────┼────────────────┼──────────────────┼────────────┘   │   │
        │           │                │                  │                │   │
        │      ┌────▼────┐    ┌─────▼─────┐    ┌───────▼──────┐        │   │
        │      │Order    │    │ Payment   │    │ Notification │        │   │
        │      │Processor│    │ Processor │    │ Service      │        │   │
        │      │(Worker) │    │(Worker)   │    │(Functions)   │        │   │
        │      └────┬────┘    └─────┬─────┘    └───────┬──────┘        │   │
        │           │               │                   │               │   │
        │      ┌────▼───────────────▼───────────────────▼──────────┐   │   │
        │      │             Managed Data Layer                     │   │   │
        │      │  ┌──────────┐  ┌──────────┐  ┌────────────────┐  │   │   │
        │      │  │ SQL DB   │  │ Cosmos DB│  │ Redis Cache    │  │   │   │
        │      │  │(Private) │  │(Private) │  │(Private)       │  │   │   │
        │      │  └──────────┘  └──────────┘  └────────────────┘  │   │   │
        │      └───────────────────────────────────────────────────┘   │   │
        │                                                               │   │
        │  ┌──────────────────────────────────────────────────────────┐│   │
        │  │              Observability Layer                         ││   │
        │  │  ┌─────────────┐  ┌─────────────┐  ┌────────────────┐  ││   │
        │  │  │ App Insights│  │ Log Analytics│  │ Custom Metrics │  ││   │
        │  │  └─────────────┘  └─────────────┘  └────────────────┘  ││   │
        │  └──────────────────────────────────────────────────────────┘│   │
        │                                                               │   │
        │  ┌──────────────────────────────────────────────────────────┐│   │
        │  │              Security Layer                              ││   │
        │  │  ┌─────────────┐  ┌─────────────┐  ┌────────────────┐  ││   │
        │  │  │  Managed    │  │  Key Vault  │  │  Private       │  ││   │
        │  │  │  Identity   │  │             │  │  Endpoints     │  ││   │
        │  │  └─────────────┘  └─────────────┘  └────────────────┘  ││   │
        │  └──────────────────────────────────────────────────────────┘│   │
        └─────────────────────────────────────────────────────────────────────┘

Критерии прохождения

Обязательные критерии

#КритерийСтатус
1Zero hardcoded credentials в codebase (managed identity everywhere)
2All external dependencies use managed cloud services
3End-to-end distributed tracing across all cloud services
4Monthly cost report с per-service breakdown
5Disaster recovery plan tested с measured RTO/RPO

Дополнительные критерии

#КритерийСтатус
6Topic-based notification system с subscription filtering
7Dead-letter queue handling для poison messages
8Serverless function с event-driven processing
9Auto-scaling с cost-aware thresholds
10Cost alerting при превышении budget

Пошаговая реализация

Шаг 1: Infrastructure Setup

# 1.1. Create Resource Group
        az group create --name cloud-native-platform --location eastus2

        # 1.2. Create Service Bus Namespace
        az servicebus namespace create \
            --name cnplatform-sbns \
            --resource-group cloud-native-platform \
            --location eastus2 \
            --sku Premium

        # 1.3. Create Topic and Subscriptions
        az servicebus topic create \
            --namespace cnplatform-sbns \
            --name order-events \
            --resource-group cloud-native-platform

        az servicebus topic subscription create \
            --namespace cnplatform-sbns \
            --topic order-events \
            --name order-processor

        az servicebus topic subscription create \
            --namespace cnplatform-sbns \
            --topic order-events \
            --name notification-service

        # 1.4. Create SQL Database
        az sql server create \
            --name cnplatform-sql \
            --resource-group cloud-native-platform \
            --location eastus2 \
            --admin-user cloudadmin \
            --admin-password 'YourStrong@Passw0rd'

        az sql db create \
            --name platform-db \
            --server cnplatform-sql \
            --resource-group cloud-native-platform \
            --sku Standard_S3

        # 1.5. Create Key Vault
        az keyvault create \
            --name cnplatform-kv \
            --resource-group cloud-native-platform \
            --location eastus2 \
            --enabled-for-deployment true \
            --enabled-for-template-deployment true

        # 1.6. Create App Service Plans
        az appservice plan create \
            --name cnplatform-asp \
            --resource-group cloud-native-platform \
            --sku P1v3 \
            --is-linux

        # 1.7. Create Application Insights
        az monitor app-insights component create \
            --app cnplatform-ai \
            --resource-group cloud-native-platform \
            --location eastus2

Шаг 2: .NET Solution Structure

CloudNativePlatform/
        ├── src/
        │   ├── CloudNativePlatform.Api/          # Основной API Gateway / BFF
        │   ├── CloudNativePlatform.OrderService/  # Order microservice
        │   ├── CloudNativePlatform.PaymentService/ # Payment microservice
        │   ├── CloudNativePlatform.OrderProcessor/ # Worker — order processing
        │   ├── CloudNativePlatform.NotificationService/ # Serverless notification
        │   ├── CloudNativePlatform.Shared/         # Shared models & interfaces
        │   └── CloudNativePlatform.Infrastructure/ # IAC, deployment scripts
        ├── tests/
        │   ├── CloudNativePlatform.UnitTests/
        │   └── CloudNativePlatform.IntegrationTests/
        └── infra/
            ├── terraform/
            └── bicep/

Шаг 3: Shared Library — Interfaces (Ports)

// CloudNativePlatform.Shared/Ports/IOrderPort.cs
        namespace CloudNativePlatform.Shared.Port;

        public interface IOrderPort
        {
            Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct);
            Task<Order?> GetOrderAsync(string orderId, CancellationToken ct);
            Task UpdateOrderStatusAsync(string orderId, OrderStatus status, CancellationToken ct);
        }

        // CloudNativePlatform.Shared/Ports/IMessagingPort.cs
        namespace CloudNativePlatform.Shared.Port;

        public interface IMessagingPort
        {
            Task PublishOrderEventAsync(OrderEvent @event, CancellationToken ct);
            Task SubscribeToOrderEventsAsync(Func<OrderEvent, CancellationToken, Task> handler, CancellationToken ct);
        }

        // CloudNativePlatform.Shared/Models/Order.cs
        namespace CloudNativePlatform.Shared.Models;

        public record Order(
            string Id,
            string CustomerId,
            string ProductId,
            int Quantity,
            decimal Total,
            OrderStatus Status,
            DateTime CreatedAt,
            DateTime? UpdatedAt);

        public enum OrderStatus
        {
            Pending,
            Processing,
            Paid,
            Shipped,
            Delivered,
            Cancelled
        }

        public record OrderEvent(
            string OrderId,
            string EventType,
            string AggregateId,
            DateTime OccurredAt,
            Dictionary<string, string> Properties);

        public record CreateOrderRequest(
            string CustomerId,
            string ProductId,
            int Quantity);

Шаг 4: Order Service — App Service with Managed Identity

// CloudNativePlatform.OrderService/Program.cs
        using Azure.Identity;
        using Azure.Messaging.ServiceBus;
        using CloudNativePlatform.Shared.Port;

        var builder = WebApplication.CreateBuilder(args);

        // Managed Identity для аутентификации
        var credential = new DefaultAzureCredential();

        // Configuration из Key Vault
        var keyVaultUri = new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/");
        builder.Configuration.AddAzureKeyVault(keyVaultUri, credential);

        // Application Insights
        builder.Services.AddApplicationInsightsTelemetry();

        // Service Bus (Managed Identity)
        builder.Services.AddSingleton<ServiceBusClient>(sp =>
            new ServiceBusClient(
                new Uri($"https://{builder.Configuration["ServiceBusName"]}.servicebus.windows.net/"),
                credential));

        // SQL Database (Managed Identity)
        builder.Services.AddScoped<IOrderPort, OrderPort>();

        // OpenTelemetry
        builder.Services.AddOpenTelemetry()
            .WithTracing(tracing => tracing
                .AddAspNetCoreInstrumentation()
                .AddServiceBusInstrumentation()
                .AddAzureMonitorTraceExporter());

        var app = builder.Build();

        app.MapPost("/api/orders", async (CreateOrderRequest request, IOrderPort port, 
            CancellationToken ct) =>
        {
            var order = await port.CreateOrderAsync(request, ct);
            return Results.Created($"/api/orders/{order.Id}", order);
        });

        app.MapGet("/api/orders/{orderId}", async (string orderId, IOrderPort port, 
            CancellationToken ct) =>
        {
            var order = await port.GetOrderAsync(orderId, ct);
            return order is null ? Results.NotFound() : Results.Ok(order);
        });

        app.Run();

        // OrderPort — реализация с Managed Identity
        public class OrderPort : IOrderPort
        {
            private readonly AppDbContext _context;
            private readonly IMessagingPort _messaging;
            private readonly ILogger<OrderPort> _logger;

            public OrderPort(AppDbContext context, IMessagingPort messaging, 
                ILogger<OrderPort> logger)
            {
                _context = context;
                _messaging = messaging;
                _logger = logger;
            }

            public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
            {
                var order = new Order(
                    Guid.NewGuid().ToString(),
                    request.CustomerId,
                    request.ProductId,
                    request.Quantity,
                    await CalculateTotalAsync(request.ProductId, request.Quantity),
                    OrderStatus.Pending,
                    DateTime.UtcNow);

                _context.Orders.Add(order);
                await _context.SaveChangesAsync(ct);

                // Publish event
                var @event = new OrderEvent(
                    order.Id,
                    "OrderCreated",
                    order.Id,
                    DateTime.UtcNow,
                    new Dictionary<string, string>
                    {
                        { "CustomerId", order.CustomerId },
                        { "Total", order.Total.ToString() }
                    });

                await _messaging.PublishOrderEventAsync(@event, ct);

                _logger.LogInformation("Order created: {OrderId}", order.Id);
                return order;
            }

            private Task<decimal> CalculateTotalAsync(string productId, int quantity) =>
                Task.FromResult(quantity * 9.99m);

            public Task<Order?> GetOrderAsync(string orderId, CancellationToken ct) =>
                _context.Orders.FindAsync([orderId], ct).ThenAs(order => order is null ? null : order);

            public Task UpdateOrderStatusAsync(string orderId, OrderStatus status, CancellationToken ct)
            {
                // Update logic...
                return Task.CompletedTask;
            }
        }

Шаг 5: Worker Service — Order Processor

// CloudNativePlatform.OrderProcessor/BackgroundService.cs
        using Azure.Messaging.ServiceBus;
        using CloudNativePlatform.Shared.Models;

        public class OrderProcessorService : BackgroundService
        {
            private readonly ServiceBusProcessor _processor;
            private readonly ILogger<OrderProcessorService> _logger;
            private readonly IOrderPort _orderPort;

            public OrderProcessorService(
                ServiceBusClient client,
                ILogger<OrderProcessorService> logger,
                IOrderPort orderPort)
            {
                _processor = client.CreateProcessor("order-events", "order-processor", 
                    new ServiceBusProcessorOptions
                    {
                        MaxConcurrentCalls = 8,
                        AutoCompleteMessages = false
                    });

                _processor.ProcessMessageAsync += OnOrderEventAsync;
                _processor.ProcessErrorAsync += OnErrorAsync;
                _logger = logger;
                _orderPort = orderPort;
            }

            private async Task OnOrderEventAsync(ProcessMessageEventArgs args)
            {
                var body = args.Message.Body.ToString();
                // Deserialize and process
                await _orderPort.UpdateOrderStatusAsync(
                    args.Message.MessageId, 
                    OrderStatus.Processing, 
                    CancellationToken.None);
        
                await args.CompleteMessageAsync(args.Message);
            }

            private Task OnErrorAsync(ProcessErrorEventArgs args)
            {
                _logger.LogError(args.ErrorMessage, "Order processor error");
                return Task.CompletedTask;
            }

            protected override async Task StartAsync(CancellationToken ct)
            {
                await _processor.StartProcessingAsync(ct);
            }

            protected override async Task StopAsync(CancellationToken ct)
            {
                await _processor.StopProcessingAsync();
                await _processor.CloseAsync();
            }
        }

Шаг 6: Serverless Notification Function

// CloudNativePlatform.NotificationService/NotificationFunction.cs
        using Microsoft.Azure.Functions.Worker;
        using Microsoft.Azure.Functions.Worker.Http;
        using Microsoft.Extensions.Logging;

        public class NotificationFunction
        {
            private readonly ILogger<NotificationFunction> _logger;

            public NotificationFunction(ILoggerFactory loggerFactory)
            {
                _logger = loggerFactory.CreateLogger<NotificationFunction>();
            }

            [Function("SendNotification")]
            public async Task Run(
                [ServiceBusTrigger("order-events", "notification-service", 
                    Connection = "ServiceBusConnection")] 
                ServiceBusMessage message)
            {
                var eventType = message.Subject;
                _logger.LogInformation("Processing event: {EventType}, Order: {OrderId}", 
                    eventType, message.MessageId);

                // Send notification (email, SMS, push)
                await SendNotificationAsync(eventType, message.Body.ToString());
            }

            [Function("HttpNotification")]
            public async Task<HttpResponseData> RunHttp(
                [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
            {
                var response = req.CreateResponse(HttpStatusCode.OK);
                await response.WriteStringAsync("Notification triggered");
                return response;
            }

            private Task SendNotificationAsync(string eventType, string body)
            {
                _logger.LogInformation("Sending notification for: {EventType}", eventType);
                return Task.CompletedTask;
            }
        }

Шаг 7: Observability Setup

// Application Insights + OpenTelemetry
        builder.Services.AddOpenTelemetry()
            .WithTracing(tracing => tracing
                .AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddServiceBusInstrumentation()
                .AddEntityFrameworkCoreInstrumentation()
                .AddAzureMonitorTraceExporter())
            .WithMetrics(metrics => metrics
                .AddAspNetCoreInstrumentation()
                .AddRuntimeInstrumentation()
                .AddAzureMonitorMetricExporter());

        // Custom business metrics
        var meter = new Meter("CloudNativePlatform");
        var orderCounter = meter.CreateCounter<long>("orders.created");
        var orderValueHistogram = meter.CreateHistogram<double>("order.value");

        // Health checks
        builder.Services.AddHealthChecks()
            .AddDbContextCheck<AppDbContext>("database")
            .AddCheck<ServiceBusHealthCheck>("servicebus")
            .AddCheck<KeyVaultHealthCheck>("keyvault");

Deployment

Azure Deployment

# Deploy services
        az webapp create \
            --name cnplatform-orders \
            --resource-group cloud-native-platform \
            --plan cnplatform-asp \
            --deployment-source-url https://github.com/your/repo \
            --deployment-source-branch main

        # Enable Managed Identity
        az webapp identity assign --name cnplatform-orders

        # Grant permissions
        az keyvault set-policy --name cnplatform-kv \
            --object-id $(az webapp identity show --name cnplatform-orders --query principalId --output tsv) \
            --secret-permissions get list

        az servicebus namespace set-policy --name cnplatform-sbns \
            --object-id $(az webapp identity show --name cnplatform-orders --query principalId --output tsv) \
            --action Microsoft.ServiceBus/namespaces/queues/send \
            --action Microsoft.ServiceBus/namespaces/queues/read

Документация

Disaster Recovery Plan

ПараметрЗначение
RTO15 минут
RPO5 минут
StrategyActive-Passive geo-redundant
Primary RegionEast US 2
Secondary RegionWest Europe
DR TestingQuarterly

Cost Monitoring

MetricAlert ThresholdAction
Monthly budget80%Notify team
Monthly budget100%Alert + auto-scale down
Monthly budget120%Emergency review
P95 Latency> 2sInvestigate
Error Rate> 5%Page on-call

Checklist для сдачи проекта

  • [ ] Solution builds successfully
  • [ ] All services use Managed Identity (no secrets in code)
  • [ ] Service Bus topics/subscriptions configured
  • [ ] Application Insights enabled on all services
  • [ ] Distributed tracing works end-to-end
  • [ ] Health check endpoints implemented
  • [ ] Auto-scaling configured
  • [ ] Cost monitoring setup
  • [ ] DR plan documented
  • [ ] Monthly cost report generated
  • [ ] All tests passing