ADR-0049: Configuration, Secrets И Options Pattern
Статус
Принято.
Контекст
Pin сервисы имеют множество конфигурируемых параметров: connection strings, JWT secrets, provider credentials, feature flags, rate limits, retry policies, timeouts. Без явного стандарта возникают:
- secrets в git репозитории (appsettings.json с паролями);
IConfiguration["key"]inline без типизации;- отсутствие валидации конфигурации при старте — сервис падает в runtime;
- разные сервисы используют разные env var naming conventions;
- provider credentials передаются через
HttpContextили hardcoded в коде.
Решение
IOptions Pattern Everywhere
Вся конфигурация читается через strongly-typed IOptions<T> — никогда через IConfiguration["key"] в production коде.
// Options class с DataAnnotations
public sealed class JwtTokenOptions
{
public const string SectionName = "JwtToken";
[Required, MinLength(32)]
public required string SigningKey { get; init; }
[Required]
public required string Issuer { get; init; }
[Required, MinLength(1)]
public required string[] Audiences { get; init; }
[Range(1, 1440)]
public int AccessTokenTtlMinutes { get; init; } = 15;
}
// Регистрация с валидацией при старте
builder.Services
.AddOptions<JwtTokenOptions>()
.BindConfiguration(JwtTokenOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart(); // падает при старте, не в runtime
ValidateOnStart — Обязательно
Каждый IOptions<T> регистрируется с .ValidateOnStart(). Сервис не запускается с невалидной конфигурацией — лучше fail fast при старте, чем необъяснимые NullReferenceException в production.
Secrets — Vault И k8s Secrets
| Среда | Способ |
|---|---|
| Local development | dotnet user-secrets — не в git |
| CI/CD | GitHub Actions / GitLab CI secrets |
| Staging / Production | HashiCorp Vault или k8s Secrets + External Secrets Operator |
Правило: никакой credential не попадает в appsettings.json, appsettings.Production.json или в git.
Secrets попадают в environment variables или secrets volume. IOptions<T> binding читает их из env vars через SECTION__KEY convention.
Environment Variable Naming
# Convention: SECTION__KEY (double underscore as separator)
JWT_TOKEN__SIGNING_KEY=...
JWT_TOKEN__ISSUER=...
PAYMENT_PROVIDER__API_KEY=...
PAYMENT_PROVIDER__WEBHOOK_SECRET=...
POSTGRES__CONNECTION_STRING=...
REDIS__CONNECTION_STRING=...
Feature Flags
Feature flags хранятся в PlatformSettings (PostgreSQL) и читаются через IFeatureFlagService. Нет hardcoded feature flags в appsettings.json — это не source of truth для runtime behaviour.
Provider Credentials
Каждый external provider имеет свой Options class:
public sealed class SomeProviderOptions
{
public const string SectionName = "SomeProvider";
[Required]
public required string ApiKey { get; init; }
[Required]
public required string WebhookSecret { get; init; }
[Required]
public required Uri BaseUrl { get; init; }
[Range(1, 30)]
public int TimeoutSeconds { get; init; } = 10;
}
Credentials никогда не передаются в HTTP query string или URL path. Только через header или request body по HTTPS.
Configuration Sections Standard
| Section | Использование |
|---|---|
JwtToken | JWT signing/validation |
Postgres | PostgreSQL connection |
Redis | Redis connection |
Kafka | Kafka bootstrap, SASL credentials |
Vault | Vault URL, AppRole credentials |
<ProviderName> | Конкретный external provider |
RateLimit | Rate limit параметры |
Resilience | Retry, timeout, circuit breaker параметры |
FeatureManagement | Feature flags (только non-sensitive) |
Локальная Разработка
# Использовать dotnet user-secrets для локальных secrets
dotnet user-secrets set "Postgres:ConnectionString" "Host=localhost;..."
dotnet user-secrets set "JwtToken:SigningKey" "local-dev-signing-key-..."
docker-compose.yml для локальной разработки может содержать публичные/non-sensitive dev credentials для PostgreSQL, Redis, Kafka. Production credentials никогда не попадают в docker-compose.
Последствия
- Сервис с невалидной конфигурацией не запустится — обнаруживается в deployment, не в production трафике.
- Secrets в git невозможны: CI pipeline scanning обнаруживает credentials.
- Все параметры типизированы и задокументированы в Options классе.
- Смена credential не требует изменения кода — только обновление secrets в Vault/k8s.