12 факторів: архітектурний компас для хмарних застосунків на .NET та Azure
Концепція «Дванадцяти факторів» — це маніфест для створення сучасних, масштабованих та надійних хмарних застосунків. Вона пропонує набір найкращих практик, які допомагають уникнути поширених помилок та побудувати стійку архітектуру. Розглянемо кожен фактор детально з технічними прикладами на базі .NET та хмарної платформи Microsoft Azure.
1. Codebase: Одна кодова база — безліч розгортань
Кожен мікросервіс або застосунок повинен мати єдину, централізовану кодову базу, яка відстежується у системі контролю версій (наприклад, Git). З цієї кодової бази можуть створюватись численні розгортання (releases) для різних середовищ: dev, staging, production.
Це фундаментальний принцип, що забезпечує прозорість та керованість проєкту. Якщо у вас два застосунки виконують схожі функції, але мають різні кодові бази — це два різних застосунки. Якщо ж вони ділять спільний код, його слід винести в окрему бібліотеку та підключати як залежність.
Практика в Azure та .NET:
- Джерело: Єдиний репозиторій в Azure Repos або GitHub.
- CI/CD: Azure Pipelines використовує цей репозиторій як єдине джерело правди для збірки та розгортання артефактів у різні середовища (Dev App Service, Staging Slot, Production App Service).
# Приклад структури репозиторію /product-service ├── .git ├── src/ │ ├── ProductService.Api/ │ └── ProductService.Core/ ├── tests/ ├── azure-pipelines.yml └── README.md
2. Dependencies: Явне декларування та ізоляція залежностей
Забудьте фразу «У мене на машині працює». Застосунок ніколи не повинен покладатися на неявно існуючі системні пакети. Усі залежності, як-от бібліотеки .NET (NuGet) або системні утиліти, мають бути чітко задекларовані.
Практика в Azure та .NET:
- Декларація: Всі NuGet-пакети чітко визначені у файлі
.csprojвашого проєкту. Це гарантує, що будь-який розробник або система збірки встановить однаковий набір залежностей. - Ізоляція: Контейнеризація за допомогою Docker — ідеальний спосіб ізолювати середовище виконання. Dockerfile чітко описує базовий образ ОС та всі необхідні системні компоненти, що унеможливлює розбіжності між середовищами.
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Azure.Identity" Version="1.11.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> </ItemGroup> </Project>Dockerfile забезпечує, що середовище для запуску буде ідентичним як локально, так і в Azure Container Apps або Azure Kubernetes Service (AKS).
3. Config: Зберігання конфігурації в середовищі
Конфігурація — це все, що може відрізнятися між розгортаннями (dev, staging, prod). Це можуть бути рядки підключення до баз даних, ключі API, налаштування портів. Конфігурація повинна бути повністю відокремлена від коду.
Зберігання конфігурації в коді (appsettings.json, що комітиться в репозиторій) є антипатерном, оскільки це створює загрозу безпеці та ускладнює керування.
Практика в Azure та .NET:
- Джерело: Використовуйте Azure App Configuration або Azure Key Vault для централізованого та безпечного зберігання конфігурацій.
- Доступ: .NET застосунки інтегруються з цими сервісами через провайдери конфігурації. У коді ви працюєте з інтерфейсом
IConfiguration, а джерело даних підключається на етапі запуску.
// Program.cs в ASP.NET Core
var builder = WebApplication.CreateBuilder(args);
// Завантаження конфігурації з Azure App Configuration
var connectionString = builder.Configuration.GetConnectionString("AppConfig");
builder.Configuration.AddAzureAppConfiguration(connectionString);
// ... далі в коді
var dbConnection = builder.Configuration.GetConnectionString("DatabaseConnection");Будь-який зовнішній сервіс, до якого підключається ваш застосунок — база даних (Azure SQL), кеш (Azure Cache for Redis), черга повідомлень (Azure Service Bus), файлове сховище (Azure Blob Storage) — має розглядатися як підключений ресурс (attached resource).
Код не повинен містити жорстких прив’язок до конкретної реалізації. Наприклад, для локальної розробки ви можете використовувати локальний SQL Server або навіть in-memory базу, а в Azure — Azure SQL Database. Перемикання має відбуватися лише через зміну конфігурації (рядка підключення).
Практика в Azure та .NET:
- Абстракція: Використовуйте абстракції, як-от
DbContextв Entity Framework Core. Ваша бізнес-логіка працює зDbContext, не знаючи, чи під ним PostgreSQL, SQL Server, чи Cosmos DB. - Підключення: Рядки підключення та інші креданшали зберігаються в Azure Key Vault та підключаються до застосунку через Managed Identities. Це дозволяє вашому App Service автентифікуватися в Azure SQL без збереження паролів у конфігурації.
// Program.cs
builder.Services.AddDbContext<ProductDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
5. Build, Release, Run: Суворе розділення етапів
Цей фактор вимагає чіткого розмежування трьох етапів життєвого циклу застосунку:
- Build (Збірка): Етап, де вихідний код перетворюється на виконуваний артефакт (наприклад, набір
DLL-файлів або Docker-образ). - Release (Реліз): На цьому етапі артефакт збірки поєднується з конфігурацією для конкретного середовища (staging, production). Результат — імутабельний (незмінний) реліз. Кожен реліз повинен мати унікальний ідентифікатор.
- Run (Виконання): Запуск релізу в цільовому середовищі. На цьому етапі не можна вносити жодних змін у код.
Практика в Azure та .NET:
- Azure Pipelines ідеально моделює цей процес:
- Build Pipeline: Компілює .NET проєкт (
dotnet build), запускає тести (dotnet test) та публікує артефакт (dotnet publish). - Release Pipeline: Бере цей артефакт, завантажує відповідні конфігурації з Azure App Configuration або змінних пайплайну та розгортає його в Azure App Service. Кожен запуск Release Pipeline створює іменований реліз.
- Build Pipeline: Компілює .NET проєкт (
Цей підхід гарантує, що якщо реліз v1.1.5 працює в staging, то той самий артефакт з іншою конфігурацією буде розгорнутий у production.
6. Processes: Запуск застосунку як одного або кількох stateless-процесів
Застосунок має виконуватися як процес без збереження стану (stateless). Будь-які дані, які потрібно зберігати (стан сесії, завантажені файли), повинні бути винесені у зовнішнє сховище (stateful backing service), наприклад, базу даних або кеш.
Це дозволяє легко масштабувати застосунок горизонтально, просто додаючи нові екземпляри процесу. Кожен запит користувача може бути оброблений будь-яким із цих екземплярів.
Практика в Azure та .NET:
- Горизонтальне масштабування: В Azure App Service Plan ви можете налаштувати Scale Out, збільшуючи кількість інстансів, на яких працює ваш застосунок. Якщо ваш сервіс stateless, це працює без жодних змін у коді.
- Кешування та сесії: Замість зберігання сесій в пам’яті процесу (
In-Memory), використовуйте розподілений кеш.
// Program.cs - налаштування розподіленого кешу для сесій
builder.Services.AddStackExchangeRedisCache(options => {
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "SampleInstance";
});
builder.Services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
Таким чином, стан сесії зберігається в Azure Cache for Redis, і будь-який інстанс вашого сервісу може його прочитати.
7. Port Binding: Експорт сервісів через прив’язку до портів
Веб-застосунок повинен бути самодостатнім і експортувати свої сервіси через HTTP-порт. Він не повинен покладатися на зовнішній веб-сервер (наприклад, IIS), який буде інжектитись у процес під час виконання.
Практика в Azure та .NET:
- Kestrel: Сучасні ASP.NET Core застосунки за замовчуванням використовують вбудований веб-сервер Kestrel. Він запускається разом із процесом застосунку і слухає певний порт, що повністю відповідає цьому фактору.
- Контейнеризація: При запуску в Docker, ви явно вказуєте, який порт контейнера потрібно відкрити (наприклад,
EXPOSE 8080), і платформа (Azure Container Apps, AKS) вже маршрутизує трафік на цей порт.
# Dockerfile FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app COPY --from=build-env /app/publish . # Kestrel буде слухати порт 8080 всередині контейнера ENV ASPNETCORE_URLS=http://+:8080 EXPOSE 8080 ENTRYPOINT ["dotnet", "ProductService.Api.dll"]
8. Concurrency: Масштабування через процеси
Навантаження на застосунок може зростати. Замість того, щоб робити один великий процес ще більшим (вертикальне масштабування), архітектура має дозволяти масштабуватися горизонтально, додаючи більше процесів.
Цей фактор тісно пов’язаний із шостим (stateless processes). Процеси мають бути спроєктовані так, щоб вони могли працювати паралельно і незалежно один від одного.
Практика в Azure та .NET:
- Azure App Service: Налаштування Scale Out (горизонтальне масштабування) дозволяє автоматично додавати або видаляти інстанси застосунку на основі навантаження (наприклад, завантаження CPU або кількість запитів).
- Azure Kubernetes Service (AKS): Використовує Horizontal Pod Autoscaler (HPA), який автоматично масштабує кількість подів (екземплярів вашого сервісу) залежно від метрик.
- Azure Functions: Серверлесс-модель, де платформа автоматично масштабує кількість інстансів вашої функції від нуля до сотень залежно від кількості подій (HTTP-запитів, повідомлень у черзі).
9. Disposability: Швидкий запуск та graceful shutdown
Процеси вашого застосунку повинні бути «одноразовими»: їх можна швидко запустити та швидко зупинити. Це критично для еластичного масштабування, швидкого розгортання та відновлення після збоїв.
- Швидкий старт: Мінімізуйте час запуску.
- Graceful Shutdown (витончене завершення): Процес повинен вміти коректно завершувати роботу. Коли платформа надсилає сигнал про зупинку (наприклад,
SIGTERM), процес повинен завершити поточні запити, звільнити ресурси (закрити з’єднання з базою) і лише потім зупинитися.
Практика в Azure та .NET:
- .NET Generic Host: Сучасні .NET застосунки використовують
IHost, який підтримує graceful shutdown. Він слухає системні сигнали (SIGTERM,SIGINT) і надає час для завершення фонових завдань.
// IHostedService для коректного завершення
public class MyLongRunningService : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken) { /* ... */ }
public Task StopAsync(CancellationToken cancellationToken)
{
// Логіка для коректного звільнення ресурсів
// cancellationToken буде активовано, коли застосунок отримує сигнал зупинки
return Task.CompletedTask;
}
}
Azure: Коли Azure App Service або AKS зупиняє інстанс, вони спочатку надсилають сигнал SIGTERM, чекають певний час (grace period), і лише потім, якщо процес не завершився, надсилають SIGKILL для примусового завершення.
10. Dev/Prod Parity: Максимальна наближеність середовищ
Чим більша різниця між середовищем розробки (dev) та продуктивним (prod), тим більша ймовірність несподіваних помилок. Потрібно прагнути до того, щоб ці середовища були максимально схожими за:
- Часом: Розробник не повинен чекати тижні, щоб побачити свій код у продакшені.
- Персоналом: Код пише той самий розробник, який потім його підтримує.
- Інструментами: Використовуйте однакові версії ОС, баз даних, кешів тощо.
Практика в Azure та .NET:
- Контейнери (Docker): Це найкращий інструмент для досягнення паритету. Розробник запускає той самий Docker-контейнер локально (через Docker Desktop), який потім буде розгорнуто в Azure Container Apps або AKS.
- Інфраструктура як код (IaC): Використовуйте Bicep або Terraform для опису вашої Azure-інфраструктури у вигляді коду. Це гарантує, що інфраструктура для
dev,stagingтаprodстворюється за однаковими шаблонами, відрізняючись лише параметрами (наприклад, потужністю ресурсів). - Azure DevOps: Єдиний процес CI/CD для всіх середовищ гарантує однаковий спосіб збірки та розгортання.
11. Logs: Розгляд логів як потоку подій
Застосунок не повинен займатися зберіганням або ротацією логів. Замість цього він має просто писати всі свої логи (події) у стандартний потік виводу (stdout). Платформа виконання повинна взяти на себе відповідальність за збір, агрегацію та обробку цих потоків.
Практика в Azure та .NET:
- Логування: Використовуйте стандартний механізм логування
Microsoft.Extensions.Logging(ILogger), який за замовчуванням пише в консоль.
public class ProductsController : ControllerBase
{
private readonly ILogger<ProductsController> _logger;
public ProductsController(ILogger<ProductsController> logger) { _logger = logger; }
[HttpGet]
public IActionResult Get()
{
_logger.LogInformation("An INFO request to get all products was made.");
// ...
}
}
Агрегація: Azure Monitor та Application Insights автоматично збирають stdout з Azure App Service, AKS та інших сервісів. Вони дозволяють централізовано шукати, аналізувати та створювати сповіщення на основі цих логів за допомогою мови запитів KQL (Kusto Query Language). Це дає змогу швидко діагностувати проблеми в розподіленій системі.
12. Admin Processes: Запуск адміністративних завдань як одноразових процесів
Адміністративні чи управлінські задачі, як-от міграції баз даних, виконання скриптів чи обчислення звітів, повинні виконуватися як окремі, одноразові процеси. Вони мають запускатися в тому ж середовищі, що й основний застосунок, і використовувати ту саму кодову базу та конфігурацію.
Практика в Azure та .NET:
- Міграції EF Core: Замість того, щоб запускати міграції вручну, їх можна виконувати як частину вашого CI/CD пайплайну.
# Команда, що запускається в Azure Pipelines перед розгортанням нової версії dotnet ef database update
Фонові задачі: Для періодичних або тривалих завдань використовуйте Azure Functions (з таймерним тригером) або Azure WebJobs. Вони існують окремо від вашого основного веб-процесу, але можуть використовувати спільний код (через спільні бібліотеки) та конфігурацію, що дозволяє їм підключатися до тих самих баз даних та сервісів. Це ізолює адміністративні задачі від основного потоку обробки запитів.
Підсумок: 12 Факторів як Blueprint Сучасної Розробки
Дотримання методології «Дванадцяти факторів» — це не просто слідування правилам, а стратегічний підхід до створення надійних, масштабованих та легких у підтримці хмарних застосунків. Як було показано на прикладах з .NET та Azure, ці принципи ідеально лягають на сучасний стек технологій Microsoft.
Ключові висновки:
- Ізоляція та Автономність: Кожен сервіс є самодостатнім завдяки єдиній кодовій базі, явним залежностям та відокремленій конфігурації. Це спрощує розробку, тестування та розгортання.
- Імутабельність та Відтворюваність: Чітке розділення етапів
Build-Release-Runта використання контейнеризації (Docker) гарантують, що артефакт, який пройшов тестування, буде ідентично працювати в продуктивному середовищі. - Масштабованість та Стійкість: Проєктування
stateless-процесів та горизонтальне масштабування (Concurrency) дозволяють системі легко витримувати змінні навантаження. Швидкий запуск та коректне завершення (Disposability) роблять її стійкою до збоїв. - Прозорість та Керованість: Централізоване логування (
stdout+ Azure Monitor) та виконання адміністративних завдань в ізольованих процесах (Azure Functions, WebJobs) забезпечують повний контроль над системою та швидку діагностику проблем.
По суті, 12 факторів пропонують універсальну мову та набір практик, які дозволяють командам розробників створювати застосунки, що повною мірою використовують переваги хмарних платформ, таких як Azure, мінімізуючи при цьому операційні складнощі та ризики.

3 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів