Tek Artifact, Çok Environment: .NET'te Configuration Layering, Environment Variable'lar ve Angular'da Runtime Config
Tek artifact'i çok environment'ta çalıştırmanın yolu, artifact'e yalnızca ortak varsayılanları koyup environment'a özgü değerleri deploy anında environment variable ya da runtime config dosyasıyla vermektir. .NET 10'da provider sırasını ölçüyor (BOOKSTORE_ öneki command line'ı ezer, ASPNETCORE_ öneki appsettings'e yenilir), ValidateOnStart ve [OptionsValidator] ile fail fast kuruyor, Angular'da provideAppInitializer ve nginx entrypoint'iyle config.json'u runtime'da üretiyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.402), Docker 29.2, Angular 22.2 (TypeScript 6.0, Vitest 5) ve nginx 1.30 ile denenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'sinde ücretsiz kargo eşiği appsettings.json'da 500 TL. Aynı ayarı dört yerden daha veriyorsun: appsettings.Staging.json'da 750, Shipping__FreeShippingThreshold environment variable'ında 1000, --Shipping:FreeShippingThreshold=1200 command line argümanında ve Program.cs'te AddEnvironmentVariables(prefix: "BOOKSTORE_") ile eklenmiş bir provider üzerinden BOOKSTORE_Shipping__FreeShippingThreshold=1300. Uygulama Staging'de açılınca eşik kaç olur? Bir de şunu tahmin et: ASPNETCORE_Shipping__FreeShippingThreshold=1400 verirsen ve başka hiçbir override yoksa sonuç 1400 mü olur, 500 mü?
Tek Artifact, Çok Environment
CI/CD Temelleri serisinin Build Once, Deploy Many yazısında kuralı koymuştuk: aynı build çıktısı dev, staging ve production'a olduğu gibi gider; environment'lar arasında değişen tek şey config'tir. Bu kuralın configuration tarafındaki karşılığı şu: artifact'in içinde yalnızca her yerde geçerli varsayılanlar olur, environment'a özgü değerler ve secret'lar deploy anında dışarıdan gelir. Bookstore API'sinin Docker image'ı bir kez build ediliyor; staging ile production arasındaki fark yalnızca o image'a verilen environment variable'lar ve ileride göreceğimiz secret store erişimi.
.NET'te bunu mümkün kılan mekanizma configuration layering: birden çok provider sırayla eklenir ve aynı key'e sonra eklenen provider'ın değeri kazanır. Provider sırasının temeli ve User Secrets ASP.NET Core Web API serisinin Configuration ve Options Pattern yazısında anlatıldı; burada sıranın operasyondaki sonuçlarına ve yanlış config'le uygulamanın hiç açılmamasına bakıyoruz. Bu yazıda Bookstore'un hem API'sini hem Angular istemcisini aynı ilkeye göre düzenleyeceğiz.
Layering'i Ölçmek: Aynı Ayar, Beş Kaynak
Baştaki senaryoyu katman katman çalıştırdım. Her satırda bir öncekine bir kaynak ekleniyor; çıktı /api/info'dan:
# ASPNETCORE_ENVIRONMENT yok
{"environment":"Production","freeShippingThreshold":500,...}
# + ASPNETCORE_ENVIRONMENT=Staging
{"environment":"Staging","freeShippingThreshold":750,...}
# + Shipping__FreeShippingThreshold=1000
{"environment":"Staging","freeShippingThreshold":1000,...}
# + --Shipping:FreeShippingThreshold=1200
{"environment":"Staging","freeShippingThreshold":1200,...}
# + BOOKSTORE_Shipping__FreeShippingThreshold=1300
{"environment":"Staging","freeShippingThreshold":1300,...}
# yalnız ASPNETCORE_Shipping__FreeShippingThreshold=1400
{"environment":"Production","freeShippingThreshold":500,...}
İlk dört satır beklenen sıra: appsettings.json, appsettings.{Environment}.json, environment variable, command line. Beşinci satır çoğu kişiyi şaşırtıyor: prefix'li provider command line'ı bile ezdi. Son satır ise ters yönde şaşırtıyor: ASPNETCORE_ önekiyle verilen değer appsettings.json'a yenildi. İkisinin de nedeni provider listesinde. WebApplication.CreateBuilder'ın kurduğu listeyi yazdırınca durum netleşiyor (dosya tabanlı örnek uygulamanın kendi settings dosyalarını listeden çıkardım):
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables(prefix: "BOOKSTORE_");
var i = 0;
foreach (var p in ((IConfigurationRoot)builder.Configuration).Providers)
Console.WriteLine($"{i++}: {p}");
0: MemoryConfigurationProvider
1: EnvironmentVariablesConfigurationProvider Prefix: 'ASPNETCORE_'
2: MemoryConfigurationProvider
3: EnvironmentVariablesConfigurationProvider Prefix: 'DOTNET_'
4: CommandLineConfigurationProvider
5: JsonConfigurationProvider for 'appsettings.json' (Optional)
6: JsonConfigurationProvider for 'appsettings.Production.json' (Optional)
9: EnvironmentVariablesConfigurationProvider
10: CommandLineConfigurationProvider
11: Microsoft.Extensions.Configuration.ChainedConfigurationProvider
12: EnvironmentVariablesConfigurationProvider Prefix: 'BOOKSTORE_'
ASPNETCORE_ ve DOTNET_ önekli provider'lar listenin başında; bunlar host'un kendi ayarları (environment adı, URL'ler, content root) için var ve appsettings dosyalarının altında kalıyor. Uygulama ayarını bu önekle verirsen dosyadaki değer kazanır. Program.cs'te sonradan eklediğimiz BOOKSTORE_ provider'ı ise listenin en sonunda, yani her şeyin üstünde. Prefix'li provider'ın faydası gerçek: aynı host'ta çalışan başka uygulamaların değişkenleri senin configuration'ına karışmaz. Ama onu eklediysen "son söz" olduğunu bilmen ve ekip içinde tek bir yöntemde anlaşman gerekir. Bookstore'da kuralımız: uygulama ayarları önekiyle, host ayarları ASPNETCORE_ ile.
Bir ayarın nereden geldiğini görmek için GetDebugView() en hızlı araç, ama ilk yazıda gördüğümüz gibi her değeri düz metin basar. .NET 7'den beri bir overload'u her değeri bir fonksiyondan geçirmene izin veriyor; bunu bir allow-list ile kullanınca güvenli bir teşhis endpoint'i elde ediyorsun:
public static class ConfigDebug
{
// Yalnız bu önekler değeriyle gösterilir; geri kalan her şey maskelenir.
private static readonly string[] VisiblePrefixes = ["Shipping:", "Jwt:Issuer", "Email:FromAddress"];
public static string SafeDebugView(IConfigurationRoot root) =>
root.GetDebugView(context =>
VisiblePrefixes.Any(p => context.Path.StartsWith(p, StringComparison.OrdinalIgnoreCase))
? context.Value ?? ""
: "***");
}
if (app.Environment.IsDevelopment())
{
// Hangi değer hangi provider'dan geldi; secret'lar maskeli
app.MapGet("/debug/config", (IConfiguration config) =>
ConfigDebug.SafeDebugView((IConfigurationRoot)config));
}
Email:
ApiKey=*** (EnvironmentVariablesConfigurationProvider)
FromAddress=orders@bookstore.example.com (JsonConfigurationProvider for 'appsettings.json' (Optional))
...
Jwt:
Issuer=https://bookstore.example.com (JsonConfigurationProvider for 'appsettings.json' (Optional))
SigningKey=*** (EnvironmentVariablesConfigurationProvider)
...
Shipping:
FreeShippingThreshold=1000 (EnvironmentVariablesConfigurationProvider)
StandardFee=49.90 (JsonConfigurationProvider for 'appsettings.json' (Optional))
Değer gizli ama kaynağı görünür: "SigningKey environment variable'dan gelmiş" bilgisi çoğu config hatasını çözmeye yeter. Endpoint yalnızca Development'ta map'leniyor; maskeleme allow-list ile çalıştığı için yeni eklenen bir secret varsayılan olarak gizli kalır. Tersi, yani deny-list ile "Password içerenleri gizle" yaklaşımı, ilk farklı adlandırılmış secret'ta sızdırır.
Container, systemd ve App Service'te Environment Variable
Environment'a özgü değerlerin çoğu deploy hedefinde environment variable olarak verilir; yalnızca taşıma biçimi platforma göre değişir. Docker'da tek tek -e yerine bir env file kullanmak, aynı image'ı farklı dosyalarla çalıştırmayı kolaylaştırır:
# staging.env (repo'ya girmez; deploy sırasında üretilir)
ASPNETCORE_ENVIRONMENT=Staging
Shipping__FreeShippingThreshold=900
Jwt__SigningKey=...
Email__ApiKey=...
$ docker run -d --name secretseries-api -p 18082:8080 --env-file staging.env secretseries-api:02
$ curl -s localhost:18082/api/info
{"environment":"Staging","freeShippingThreshold":900,"secrets":{"ConnectionStrings:Bookstore":false,"Jwt:SigningKey":true,"Email:ApiKey":true,"Payments:Webhook:Secret":false}}
Env file'ın içinde secret görüyorsan doğru: bu yazının konusu layering, secret'ı bu dosyadan çıkarıp bir secret store'a taşımak sonraki yazıların işi. Linux sunucuda systemd ile çalışan bir uygulamada aynı rolü EnvironmentFile= satırı oynar; Web Server'lar ve Hosting serisinin systemd yazısında kurduğumuz unit dosyası bu yöntemi kullanıyordu. Azure App Service'te ise app settings, uygulamaya environment variable olarak verilir; Microsoft Learn'deki App Service configuration sayfasına göre Linux'ta ayar adında yalnızca harf, rakam, nokta ve alt çizgi kullanılabildiği için : yerine __ yazılmalı. App setting değişince App Service uygulamayı yeniden başlatır; yani environment variable ile gelen her değer, değişince restart demektir.
Ayırıcı konusunda tek bir alışkanlık edin: her yerde __. : içeren bir değişken adı Docker'dan geçer ve .NET onu okur, ama shell'de tanımlanamaz:
$ docker run ... -e "Shipping:FreeShippingThreshold=950" secretseries-api:02
$ curl -s localhost:18082/api/info
{"environment":"Staging","freeShippingThreshold":950,"secret...
$ bash -c 'export Shipping:FreeShippingThreshold=950'
bash: line 0: export: `Shipping:FreeShippingThreshold=950': not a valid identifier
Aynı ayarın bir platformda çalışıp diğerinde sessizce yok sayılması, environment taşırken en çok zaman kaybettiren hatalardan biri.
Fail Fast: ValidateOnStart ve Source-Generated Validator
Layering'in en tehlikeli yanı eksik bir değerin hata vermemesi. JwtOptions.SigningKey hiçbir provider'dan gelmezse binder sessizce boş string bırakır ve sorun ilk login denemesinde, belki gece yarısı ortaya çıkar. Doğrusu, eksik ya da hatalı config ile uygulamanın hiç ayağa kalkmaması. Bookstore'un options class'larına DataAnnotations attribute'ları ekledim:
public sealed class JwtOptions
{
public const string SectionName = "Jwt";
[Required, Url]
public string Issuer { get; set; } = "";
// HMAC-SHA256 için en az 256 bit: base64'te 44 karakter
[Required, MinLength(44)]
public string SigningKey { get; set; } = "";
}
Doğrulamayı reflection yerine compile time'da üretilen kodla yapmak için [OptionsValidator] attribute'u var; .NET 8 ile geldi, trimming ve Native AOT (uygulamanın önceden native koda derlenmesi) ile de uyumlu. Gövdesi boş bir partial class yazıyorsun, source generator geri kalanını üretiyor:
[OptionsValidator]
public sealed partial class ShippingOptionsValidator : IValidateOptions<ShippingOptions>;
[OptionsValidator]
public sealed partial class JwtOptionsValidator : IValidateOptions<JwtOptions>;
[OptionsValidator]
public sealed partial class EmailOptionsValidator : IValidateOptions<EmailOptions>;
public static IServiceCollection AddBookstoreOptions(this IServiceCollection services)
{
services.AddOptions<ShippingOptions>().BindConfiguration(ShippingOptions.SectionName).ValidateOnStart();
services.AddOptions<JwtOptions>().BindConfiguration(JwtOptions.SectionName).ValidateOnStart();
services.AddOptions<EmailOptions>().BindConfiguration(EmailOptions.SectionName).ValidateOnStart();
services.AddOptions<PaymentWebhookOptions>().BindConfiguration(PaymentWebhookOptions.SectionName);
// Reflection yerine source generator'ın ürettiği validator'lar
services.AddSingleton<IValidateOptions<ShippingOptions>, ShippingOptionsValidator>();
services.AddSingleton<IValidateOptions<JwtOptions>, JwtOptionsValidator>();
services.AddSingleton<IValidateOptions<EmailOptions>, EmailOptionsValidator>();
return services;
}
ValidateOnStart(), doğrulamayı ilk kullanım yerine host başlarken çalıştırır. Secret'lar olmadan başlattığımda uygulama port açmadan çıktı:
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
Hosting failed to start
System.AggregateException: One or more errors occurred. (SigningKey: The JwtOptions.SigningKey field is required.) (ApiKey: The EmailOptions.ApiKey field is required.)
---> Microsoft.Extensions.Options.OptionsValidationException: SigningKey: The JwtOptions.SigningKey field is required.
İki hata birlikte raporlanıyor; operasyon ekibi tek denemede eksiklerin hepsini görüyor. İkinci deneme iki klasik hatayı birden içeriyordu: kısa bir key ve boş bırakılmış bir environment variable (Email__FromAddress=):
Unhandled exception. System.AggregateException: One or more errors occurred. (SigningKey: The field JwtOptions.SigningKey must be a string or array type with a minimum length of '44'.) (FromAddress: The EmailOptions.FromAddress field is required.)
İkincisi önemli: boş string "ayarlanmamış" demek değil. appsettings.json'da FromAddress dolu olduğu hâlde boş environment variable onu ezdi ve değer boş string oldu. [Required] boş string'i varsayılan olarak reddettiği için yakalandı; validation olmasaydı uygulama boş gönderen adresiyle e-posta göndermeye çalışacaktı. Kubernetes'te ve CI'da "değişkeni tanımla ama değerini sonra doldur" alışkanlığı tam olarak bu sonucu verir.
Container'da ve Kubernetes'te fail fast'in ikinci faydası da var: açılmayan bir pod rollout'u durdurur. Eski sürüm trafiği karşılamaya devam eder, yeni sürüm hatalı config ile production'a çıkamaz.
IOptions, IOptionsSnapshot, IOptionsMonitor ve Reload
Configuration değiştiğinde uygulamanın ne göreceği, değeri hangi interface ile okuduğuna bağlı. Üçünü aynı endpoint'te yan yana koydum:
// Üç options interface'inin aynı anda ne gördüğü
app.MapGet("/api/shipping/threshold", (
IOptions<ShippingOptions> options,
IOptionsSnapshot<ShippingOptions> snapshot,
IOptionsMonitor<ShippingOptions> monitor) => new
{
Options = options.Value.FreeShippingThreshold,
Snapshot = snapshot.Value.FreeShippingThreshold,
Monitor = monitor.CurrentValue.FreeShippingThreshold
});
Uygulama çalışırken yayımlanan klasördeki appsettings.json'da eşiği 500'den 650'ye çevirdim (reloadOnChange varsayılan provider'larda açık):
$ curl -s localhost:5090/api/shipping/threshold
{"options":500,"snapshot":500,"monitor":500}
# appsettings.json: 500 → 650
$ curl -s localhost:5090/api/shipping/threshold
{"options":500,"snapshot":650,"monitor":650}
IOptions<T> singleton'dır ve ilk okunduğu değeri uygulama ömrü boyunca tutar. IOptionsSnapshot<T> scoped'dır, her request'te yeniden hesaplanır. IOptionsMonitor<T> singleton'dır ama değişiklik bildirimini dinler; singleton service'lerde ve background job'larda doğru seçim odur. Bu ayrım rotation konusunda kritik: yedinci yazıda IOptions ile okunan eski bir secret'ın rotation'dan sonra nasıl canlı kaldığını göreceğiz.
Peki dosyaya geçersiz bir değer yazılırsa? Eşiği -5 yaptım:
$ curl -s -w " %{http_code}" localhost:5090/api/shipping/threshold
500
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Microsoft.Extensions.Options.OptionsValidationException: FreeShippingThreshold: The field ShippingOptions.FreeShippingThreshold must be between 0 and 100000.
Aynı şeyi yalnızca IOptionsMonitor kullanan /api/info'da da denedim; o da HTTP 500 döndü. ValidateOnStart yalnızca açılışı korur. Reload'da gelen hatalı değer uygulamayı kapatmaz, ama o options'ı okuyan her request'i hataya düşürür. Runtime'da değişebilen config'i (dosya, ConfigMap, App Configuration) değiştirmeden önce bir doğrulama adımından geçirmek bu yüzden şart; dördüncü yazıda App Configuration'daki değişiklikleri pull request ile yönetirken buna döneceğiz.
Aynı tipten birden çok ayar seti gerekiyorsa named options kullanılır. Bookstore'da iki kargo firmasının ayarları aynı class'a, farklı isimlerle bağlanıyor:
// Aynı tip, iki isim: her kargo firması kendi section'ından bağlanır
builder.Services.Configure<CarrierOptions>("Standard", builder.Configuration.GetSection("Shipping:Carriers:Standard"));
builder.Services.Configure<CarrierOptions>("Express", builder.Configuration.GetSection("Shipping:Carriers:Express"));
var app = builder.Build();
var carriers = app.Services.GetRequiredService<IOptionsMonitor<CarrierOptions>>();
foreach (var name in new[] { "Standard", "Express" })
{
var carrier = carriers.Get(name);
Console.WriteLine($"{name}: {carrier.BaseUrl}, {carrier.DeliveryDays} days");
}
Standard: https://standard-carrier.example.com, 3 days
Express: https://express-carrier.example.com, 1 days
Named options IOptionsSnapshot.Get(name) ve IOptionsMonitor.Get(name) ile okunur; IOptions<T> yalnızca isimsiz varsayılanı verir.
Angular: environment.ts Build-Time'dır
Angular'ın environment.ts ve fileReplacements mekanizması build sırasında çalışır: her environment için ayrı bir build demektir. Bu, staging'de test ettiğin bundle'ın production'a giden bundle olmaması, yani "build once, deploy many" ilkesinin bozulması anlamına geliyor. Çözüm, gizli olmayan config'i runtime'da bir JSON dosyasından okumak. Angular 19'dan beri bunun için APP_INITIALIZER token'ı yerine provideAppInitializer kullanılıyor; Angular 22'nin type tanımlarında eski token @deprecated olarak işaretli.
// Bu dosyaya giren her değer tarayıcıda herkese açıktır: secret yok.
export interface AppConfig {
environment: string;
apiBaseUrl: string;
freeShippingThreshold: number;
}
@Injectable({ providedIn: 'root' })
export class AppConfigService {
private readonly http = inject(HttpClient);
private readonly config = signal<AppConfig | null>(null);
get value(): AppConfig {
const config = this.config();
if (!config) {
throw new Error('App config is not loaded yet.');
}
return config;
}
async load(): Promise<void> {
const config = await firstValueFrom(this.http.get<AppConfig>('/config.json'));
if (!config.apiBaseUrl) {
throw new Error('config.json has no apiBaseUrl.');
}
this.config.set(config);
}
}
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideHttpClient(withFetch()),
// Uygulama config.json yüklenmeden açılmaz
provideAppInitializer(() => inject(AppConfigService).load()),
provideRouter(routes),
],
};
Initializer'ın döndürdüğü promise tamamlanmadan uygulama bootstrap olmaz; dosya yoksa ya da apiBaseUrl eksikse uygulama açılmaz. Bu, API'deki ValidateOnStart'ın frontend karşılığı. Service için yazdığım üç Vitest testi (dosyayı yükleme, eksik alanı reddetme, yüklenmeden okumayı engelleme) ve component testi geçiyor:
$ npx ng test --watch=false
Test Files 2 passed (2)
Tests 4 passed (4)
Geliştirme için public/config.json'da yerel değerler duruyor. Asıl soru, container image'ında bu dosyanın her environment'ta nasıl farklı olacağı.
config.json'u Container Açılırken Üretmek
Resmi nginx image'ı açılırken /docker-entrypoint.d/ klasöründeki script'leri sırayla çalıştırır. Image'a bir template ve bu template'i environment variable'larla dolduran küçük bir script koyuyoruz:
{
"environment": "${APP_ENVIRONMENT}",
"apiBaseUrl": "${API_BASE_URL}",
"freeShippingThreshold": ${FREE_SHIPPING_THRESHOLD}
}
#!/bin/sh
set -eu
# Eksik değişkenle container hiç açılmasın (fail fast)
: "${APP_ENVIRONMENT:?APP_ENVIRONMENT is required}"
: "${API_BASE_URL:?API_BASE_URL is required}"
: "${FREE_SHIPPING_THRESHOLD:?FREE_SHIPPING_THRESHOLD is required}"
envsubst '${APP_ENVIRONMENT} ${API_BASE_URL} ${FREE_SHIPPING_THRESHOLD}' \
< /etc/bookstore/config.template.json \
> /usr/share/nginx/html/config.json
echo "runtime config written for ${APP_ENVIRONMENT}"
FROM node:24-alpine AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npx ng build
FROM nginx:1.30-alpine
COPY docker/default.conf /etc/nginx/conf.d/default.conf
COPY docker/config.template.json /etc/bookstore/config.template.json
COPY docker/40-runtime-config.sh /docker-entrypoint.d/40-runtime-config.sh
COPY --from=build /src/dist/bookstore-web/browser /usr/share/nginx/html
# Image'daki geliştirme config'i silinir; config.json yalnız runtime'da üretilir
RUN rm /usr/share/nginx/html/config.json
envsubst'e değişken listesini açıkça vermek önemli; vermezsen template'teki başka $ işaretleri de boş string'e döner. nginx tarafında config.json için Cache-Control: no-store ekledim, yoksa tarayıcı eski environment'ın config'ini cache'ten okuyabilir. Aynı image'ı iki farklı environment'la çalıştırdım ve sayfayı headless Chrome ile render ettim:
$ docker run -d --name secretseries-web -p 18090:80 -e APP_ENVIRONMENT=Staging \
-e API_BASE_URL=https://api.staging.bookstore.example.com -e FREE_SHIPPING_THRESHOLD=750 secretseries-web:02
$ docker logs secretseries-web 2>&1 | grep runtime
/docker-entrypoint.sh: Launching /docker-entrypoint.d/40-runtime-config.sh
runtime config written for Staging
$ curl -s -D - localhost:18090/config.json | grep -i cache-control
Cache-Control: no-store
# render edilen sayfa
<header><h1>Bookstore</h1><p id="env">Staging · API: https://api.staging.bookstore.example.com</p><p>Free shipping over 750 TRY</p></header>
# aynı image, production değerleriyle
<p id="env">Production · API: https://api.bookstore.example.com</p>
Değişken eksik olduğunda container nginx'i başlatmadan çıktı:
$ docker run --name secretseries-web -e APP_ENVIRONMENT=Staging secretseries-web:02
/docker-entrypoint.d/40-runtime-config.sh: line 6: API_BASE_URL: API_BASE_URL is required
$ docker inspect secretseries-web -f '{{.State.Status}} {{.State.ExitCode}}'
exited 2
Kubernetes'te aynı dosyayı bir ConfigMap'ten mount etmek de mümkün; Kubernetes serisinin ConfigMap ve Secret yazısında o yolu gösterdik. Entrypoint yöntemi Docker Compose, App Service ve düz docker run gibi ConfigMap olmayan ortamlarda da çalıştığı için Bookstore'da varsayılan bu.
Üretimde Dikkat
Her deploy'un hangi config ile çalıştığı izlenebilir olmalı: environment variable'ları elle değil, deploy pipeline'ı ya da IaC (Infrastructure as Code; altyapının kodla tanımlanması) ile verilen bir dosyadan ayarla ki "staging'de neden 900?" sorusunun cevabı bir commit'te dursun. Startup'ta bir kez, değerleri değil kaynakları log'lamak (maskeli debug view'un bir özeti) olay anında çok zaman kazandırır. Validation kurallarını gerçek sınırlara göre yaz: [Range] ile kargo eşiğinin 0 ile 100.000 arasında olması, yanlışlıkla bir sıfır eklenmiş değeri production'a çıkmadan yakalar. App Service'te app setting değişikliğinin restart tetiklediğini, Kubernetes'te environment variable değişikliğinin yeni bir rollout gerektirdiğini hesaba kat; sık değişen değerler için dördüncü yazıdaki App Configuration daha uygun.
Sık Düşülen Tuzaklar
Environment variable adında : kullanmak: Docker'da çalışır, shell'de ve App Service'te çalışmaz; her yerde __. Uygulama ayarlarını ASPNETCORE_ önekiyle vermek: appsettings dosyalarına yenilir. Prefix'li bir provider ekleyip command line'ın hâlâ kazandığını sanmak. Boş string'i "ayarlanmamış" sanmak: boş environment variable dosyadaki dolu değeri ezer. appsettings.Production.json'u secret'larla repo'ya koymak; bu dosya ancak gizli olmayan production varsayılanları için kullanılabilir. Angular'da her environment için ayrı build almak ve environment.ts'e environment'a özgü URL koymak. config.json'u cache'lenebilir bırakmak. Son olarak, reload edilen config'in doğrulanmadığını unutmak: ValidateOnStart yalnızca açılışı korur.
Küçük Bir Deneme
★☆☆ Kendi API'nde provider listesini yazdır ve bir ayarı sırasıyla appsettings, environment variable ve command line ile ver. Sonra prefix'li bir provider ekleyip sıranın nasıl değiştiğini gözle.
★★☆ Options class'larından birine DataAnnotations ve [OptionsValidator] ekle, ValidateOnStart()'ı aç. Uygulamayı eksik, boş ve sınır dışı değerlerle başlatıp hata mesajlarını karşılaştır. Ardından çalışan uygulamada appsettings.json'a geçersiz bir değer yaz ve hangi endpoint'lerin bozulduğunu not et.
★★★ Angular uygulamanda environment.ts'i yalnızca build-time ayarlara indir, geri kalanı provideAppInitializer ile config.json'dan oku. Image'ı bir kez build et ve iki farklı environment ile çalıştırıp aynı image ID'sinin iki farklı config ile açıldığını doğrula.
Gizli olmayan ayarlar artık environment'lar arasında doğru taşınıyor; env file'da bekleyen secret'lar ise hâlâ yanlış yerde. Bir sonraki yazıda bu secret'ları Azure Key Vault'a taşıyacak, uygulamanın onlara parola kullanmadan managed identity ile ulaşmasını sağlayacağız.