Уровень 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 │██░░░░░░░░░░░░░░░░░░░░░░░░│ Вы ничего не управляете│
│ ────────────────────────────┘ │
│ Управление провайдером → │
└─────────────────────────────────────────────────────────────┘
Сравнение моделей
| Критерий | IaaS | PaaS | SaaS |
| Управление ОС | Вы | Провайдер | Провайдер |
| Управление middleware | Вы | Провайдер | Провайдер |
| Управление приложением | Вы | Вы | Провайдер |
| Управление данными | Вы | Вы | Провайдер |
| Гибкость | Максимальная | Средняя | Минимальная |
| Скорость деплоя | Медленная | Быстрая | Мгновенная |
| Стоимость управления | Высокая | Средняя | Низкая |
| Примеры для .NET | Azure VM, EC2 | Azure App Service, Elastic Beanstalk | Office 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 microservice | PaaS (App Service / Container Apps) | Быстрый старт, мало ops |
| Legacy .NET Framework migration | IaaS (VM) → PaaS (App Service) | Поэтапная миграция |
| Batch processing | Azure Batch / VM Scale Sets | Нужен контроль над ОС |
| Event-driven processing | Serverless (Functions) | Pay-per-execution |
| Real-time streaming | AKS / Service Fabric | Нужен full control |
| Low-latency API | PaaS (App Service Premium) | Pre-warmed instances |
Managed Services — database, cache, message broker, storage
Managed Services в облаке
Managed services — это облачные сервисы, где провайдер управляет инфраструктурой, патчингом, бэкапами и масштабированием. Вы работаете только с API.
Основные категории managed services для .NET
| Категория | Azure | AWS | GCP | .NET SDK |
| Database | Azure SQL, Cosmos DB | RDS, DynamoDB | Cloud SQL, Spanner | Microsoft.Data.SqlClient, Azure.Cosmos |
| Cache | Azure Cache for Redis | ElastiCache | Memorystore | StackExchange.Redis |
| Message Broker | Service Bus, Event Hubs | SQS, SNS, MQ | Pub/Sub | Azure.Messaging.ServiceBus |
| Storage | Blob, File, Queue | S3, EFS, SQS | Cloud Storage | Azure.Storage.Blobs |
| Compute | App Service, Functions | EC2, Lambda | GCE, Cloud Run | Azure SDK |
| Auth | Entra ID (AAD) | Cognito, IAM | Identity Platform | Microsoft.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 Functions | AWS Lambda |
| .NET поддержка | Отличная (родная) | Хорошая (.NET 8+) |
| Планы | Consumption, Premium, Dedicated, Flex | On-Demand, Provisioned, EC2 |
| Cold start | ~1-5s (Consumption), ~100ms (Premium) | ~500ms-2s ( .NET) |
| Max execution | 10 минут (Consumption) | 15 минут |
| Trigger types | 50+ triggers | 20+ triggers |
| Durable Functions | Встроено | Step Functions (отдельно) |
| Local development | Excellent (FuncCoreTool) | Good (SAM CLI) |
| CI/CD | Azure DevOps, GitHub Actions | CodePipeline, 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
| Критерий | Istio | Linkerd |
| Прокси | 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
| Критерий | Azure | AWS | GCP |
| .NET интеграция | Родная (Microsoft) | Хорошая (.NET 8+) | Хорошая (.NET 8+) |
| Serverless | Azure Functions (лучшая) | Lambda | Cloud Functions |
| Containers | AKS, Container Apps | EKS, ECS | GKE |
| PaaS | App Service | Elastic Beanstalk | Cloud Run |
| Database | SQL Database, Cosmos DB | RDS, DynamoDB | Cloud SQL, Spanner |
| Messaging | Service Bus, Event Hubs | SQS, SNS, MQ | Pub/Sub |
| Monitoring | Application Insights | CloudWatch, X-Ray | Cloud Trace, Monitoring |
| Identity | Entra ID (AAD) | Cognito, IAM | Identity Platform |
| DevOps | Azure DevOps | CodePipeline | Cloud Build |
| Free tier | $200 credit + free tier | 12 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 |
| Serverless | Event-driven workloads, batch processing |
| Service mesh | Istio для complex routing, Linkerd для low-latency |
| Provider выбор | Microsoft stack → Azure, Kubernetes-first → GKE/AKS |
| .NET SDK | Использовать официальные Azure/AWS/GCP SDK |
| Configuration | Managed Identity + Key Vault, no connection strings |
| Monitoring | Application Insights (Azure) / CloudWatch (AWS) |
| Deployment | CI/CD с GitHub Actions / Azure DevOps |
| Scaling | Auto-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 Vault | AWS Secrets Manager |
| Типы | Secrets, Keys, Certificates | Secrets only |
| Аутентификация | Managed Identity, RBAC | IAM Role, Resource Policy |
| Автоматическая ротация | Да (Function + Key Vault) | Да (Lambda) |
| .NET SDK | Azure.Security.KeyVault.Secrets | AWSSDK.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 |
| Network | Private Endpoints для всех managed services |
| NSG | Минимальные правила, только нужные порты |
| Database | Azure AD auth + Private Endpoint + Firewall |
| Cosmos DB | Gateway mode для Azure AD auth |
| Redis | Access Key в Key Vault (нет MI support) |
| AWS | IAM Roles + Secrets Manager + VPC Endpoints |
| Monitoring | Audit log для Key Vault / IAM |
| Compliance | Soft 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 Blob | 0–190 GB | Документы, изображения, видео | BlobClient |
| Append Blob | 0–190 GB | Логирование, мониторинг | AppendBlobClient |
| Page Blob | 0–8 TB | VM 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 Files | AWS EFS |
| Протокол | SMB 3.0, NFS 4.1 | NFS 4.1 |
| Монтирование | Disk mount, AzCopy, REST API | NFS mount |
| Performance | Up to 115 GB/s | Up to 100 GB/s |
| Стоимость | Premium / Standard | Performance + Request |
| .NET SDK | Azure.Storage.Files.Shares | AWS SDK for EFS |
| Best for | Windows apps, file shares | Linux 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 Storage | Service Bus Queue |
| Стоимость | Низкая | Средняя |
| Max message size | 64 KB | 256 KB (Standard), 1 MB (Premium) |
| Max queue size | 5 TB | 80 GB (Standard), 5 TB (Premium) |
| Delivery guarantees | At-least-once | At-least-once / Exactly-once (transactions) |
| Topics/Subscriptions | Нет | Да |
| Dead-letter queue | Нет | Да |
| Sessions | Нет | Да |
| Transaction | Нет | Да |
| Best for | Simple queuing, cost-sensitive | Enterprise 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 tiering | Hot → Cool (30 дней) → Cold (90 дней) → Archive (180 дней) |
| Lifecycle policy | Автоматический transition, не вручную |
| SAS URLs | Для временного доступа, не для production auth |
| Large uploads | SDK auto-chunking для файлов > 256 MB |
| Queue vs Service Bus | Simple → Queue Storage, Enterprise → Service Bus |
| Table vs Cosmos DB | Simple KV → Table, Complex queries → Cosmos DB SQL |
| DynamoDB | Query по PK, избегайте Scan |
| CDN | Для публичных blob — CDN для кэширования |
| Monitoring | Monitor storage metrics (RU/s, IOPS, throughput) |
| Security | Private 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 SQS | Azure Service Bus |
| Topics | Нет (только SNS) | Да (встроенные) |
| Sessions | Нет | Да |
| Dead-letter | Да (separate queue) | Да (встроенная) |
| Max message size | 256 KB | 256 KB / 1 MB |
| Visibility timeout | 0–12 hours | 30 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
| Тип | Ordering | Deduplication | Delivery |
| Service Bus Queue | No (except sessions) | No | At-least-once |
| Service Bus Queue + Sessions | FIFO within session | No | At-least-once |
| Service Bus Topic | No | No | At-least-once per subscription |
| SQS | No | No | At-least-once |
| Event Grid | No | No | At-least-once |
| Event Hubs | Within partition | No | At-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 Topic | Single consumer → Queue, Multiple → Topic |
| Ordering | Sessions для FIFO, Partition Key для Event Hubs |
| DLQ | Обрабатывать автоматически: Retry → Review → Drop |
| MaxDeliveryCount | Standard: 10, Premium: 14 |
| Concurrent Calls | 16 для queue, 8 для topic |
| Message Size | < 256 KB, для больших — Blob + reference |
| Batch Send | Группировать для throughput |
| Monitoring | Alert на DLQ size и delivery count |
| Idempotency | Всегда — at-least-once delivery |
| TTL | Set 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-Factor | Config в environment, stateless processes, structured logging |
| Sidecar | Логирование, observability, rate limiting |
| Ambassador | External API isolation, retry, auth |
| Strangler Fig | Поэтапная миграция, API Gateway routing |
| BFF | Separate BFF per client type, cookie auth |
| Microservices | Start with modular monolith, extract when needed |
| Deployment | Blue-green, canary, feature flags |
| Monitoring | Distributed 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
| Практика | Рекомендация |
| APM | Application Insights (Azure) / CloudWatch (AWS) |
| Tracing | OpenTelemetry — industry standard |
| Logging | Structured logging, send to centralized aggregator |
| Metrics | Custom business KPIs + infrastructure metrics |
| Alerts | Threshold + anomaly detection |
| Dashboards | Operational (real-time) + Executive (summary) |
| Correlation | TraceId across all services |
| Health Checks | /health, /health/ready, /health/live |
| Performance | P50, P95, P99 — не average |
| Cost | Retention 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
| Provider | Egress 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 | Только при реальной необходимости |
| Abstraction | Business logic, не managed features |
| Data residency | Validate region at runtime |
| Egress | CDN, compression, private links |
| DR | Multi-region в одном cloud → проще |
| RTO/RPO | Define SLAs before architecture |
| Cost | Monitor egress, use private endpoints |
| Compliance | Region-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 start | Premium/Flex plan, provisioned concurrency |
| Orchestration | Durable Functions для long-running workflows |
| State | External store (Cosmos DB, Redis), не в function |
| Memory | 1024 MB — оптимальный баланс cost/performance |
| Timeout | Set explicit timeout, default varies by plan |
| Concurrency | Dynamic concurrency enabled |
| AOT | Native AOT для минимального cold start |
| Warm-up | Timer trigger для поддержания warm |
| Batching | Process multiple items per invocation |
| Monitoring | Track 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
| Практика | Рекомендация |
| TCO | Calculate before migration, include hidden costs |
| Reserved | 1-year for predictable, 3-year for steady-state |
| Auto-scale | Set min/max, use cost-aware metrics |
| Spot | For batch, CI/CD, fault-tolerant workloads |
| FinOps | Inform → Optimize → Operate |
| Allocation | Chargeback by team, showback by service |
| Anomaly | Alert on >2σ deviation from average |
| Monitoring | Cost per request, per service, per team |
| Right-sizing | Regular 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 │ ││ │
│ │ └─────────────┘ └─────────────┘ └────────────────┘ ││ │
│ └──────────────────────────────────────────────────────────┘│ │
└─────────────────────────────────────────────────────────────────────┘
Критерии прохождения
Обязательные критерии
| # | Критерий | Статус |
| 1 | Zero hardcoded credentials в codebase (managed identity everywhere) | ☐ |
| 2 | All external dependencies use managed cloud services | ☐ |
| 3 | End-to-end distributed tracing across all cloud services | ☐ |
| 4 | Monthly cost report с per-service breakdown | ☐ |
| 5 | Disaster recovery plan tested с measured RTO/RPO | ☐ |
Дополнительные критерии
| # | Критерий | Статус |
| 6 | Topic-based notification system с subscription filtering | ☐ |
| 7 | Dead-letter queue handling для poison messages | ☐ |
| 8 | Serverless function с event-driven processing | ☐ |
| 9 | Auto-scaling с cost-aware thresholds | ☐ |
| 10 | Cost 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
| Параметр | Значение |
| RTO | 15 минут |
| RPO | 5 минут |
| Strategy | Active-Passive geo-redundant |
| Primary Region | East US 2 |
| Secondary Region | West Europe |
| DR Testing | Quarterly |
Cost Monitoring
| Metric | Alert Threshold | Action |
| Monthly budget | 80% | Notify team |
| Monthly budget | 100% | Alert + auto-scale down |
| Monthly budget | 120% | Emergency review |
| P95 Latency | > 2s | Investigate |
| 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