Mert Özen Her Satırda Daha İleri
DevOps Configuration ve Secret Management — #04

Azure App Configuration: Merkezi Configuration, Label'larla Environment'lar, Sentinel Key ile Yenileme ve Key Vault Reference'ları

Mert Özen 14 Kas 2026 10 dk 11 görüntülenme
Azure App Configuration: Merkezi Configuration, Label'larla Environment'lar, Sentinel Key ile Yenileme ve Key Vault Reference'ları

Azure App Configuration, birden çok servisin paylaştığı ve deploy'suz değişmesi gereken gizli olmayan ayarlar içindir; secret'lar Key Vault'ta kalır, store'da yalnızca reference durur. Resmi emulator üzerinde label'larla environment ayırmayı, sentinel key değişmeden ayarın yansımadığını, çözülemeyen Key Vault reference'ının açılışı durdurduğunu ve 1 saniyelik refresh alt sınırını gerçek çıktılarla görüyor, değişikliği pull request ve pipeline ile yönetiyoruz.

Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.402), Microsoft.Azure.AppConfiguration.AspNetCore 8.6.1 ve Azure App Configuration emulator'ü 1.2.0 ile Docker 29.2 üzerinde denenmiştir. Azure'da bir App Configuration store'u oluşturulmadı; az komutları Microsoft Learn'deki örneklere göre yazıldı ve çalıştırılmadı.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) staging'de ücretsiz kargo eşiğini Azure App Configuration'dan okuyor. Refresh aralığı beş saniye. Eşiği 800'den 950'ye çeviriyorsun ve bir dakika bekliyorsun. API hangi değeri döner? Bir de şu: eşik değerini, sitedeki bütün instance'lar her istekte yeni değeri görsün diye refresh aralığını 200 milisaniyeye indirirsen ne olur?

App Configuration Ne Zaman Gerekir, Ne Zaman Gereksizdir?

İkinci yazıda environment'a özgü ayarları environment variable ile verdik. Bu yöntem tek bir uygulama için iyi çalışır, ama iki sınırı var. Birincisi, değişiklik restart ya da yeni bir rollout ister: App Service app setting'i değişince uygulamayı yeniden başlatır, Kubernetes'te environment variable değişikliği yeni pod demektir. İkincisi, ortak ayarlar kopyalanır: Bookstore'da kargo eşiğini API, Ordering worker'ı ve kampanya hesaplayan job aynı anda biliyorsa, aynı değer üç yerde durur ve er geç biri unutulur.

Azure App Configuration bu iki sorunu çözen merkezi bir key-value store. Değerler tek yerde durur, uygulamalar onları configuration provider ile okur ve deploy olmadan yenileyebilir. Microsoft'un FAQ (Frequently Asked Questions; sık sorulan sorular) sayfası App Configuration ile Key Vault'u birbirinin tamamlayıcısı olarak konumluyor: secret'lar Key Vault'ta kalır, App Configuration'da yalnızca onlara referans durur.

Gereksiz olduğu durum da net: tek bir uygulaman varsa, ayarlar ayda bir değişiyorsa ve değişiklik zaten deploy ile birlikte geliyorsa, App Configuration yalnızca bir bağımlılık daha ekler. Startup'ta erişilmesi gereken bir servis daha, izlenecek bir kota daha, yetki verilecek bir kaynak daha. Bookstore'da kullanma nedenimiz, kurumsal sipariş ve kampanya akışlarında birden çok servisin aynı iş kurallarını okuması ve bu kuralları iş biriminin deploy beklemeden değiştirmek istemesi.

Key-Value, Label ve Environment'lar

App Configuration'da her kayıt bir key, bir value ve isteğe bağlı bir label'dan oluşur. Key'ler genellikle : ile hiyerarşik yazılır. Label aynı key'in farklı sürümlerini ayırmanın yolu; en yaygın kullanımı environment. Bookstore'un staging ve production'ı için ayarları repo'da bir dosyada tutuyoruz:

{
  "items": [
    { "key": "Bookstore:Shipping:FreeShippingThreshold", "value": "500", "label": null, "content_type": null, "tags": {} },
    { "key": "Bookstore:Shipping:StandardFee", "value": "49.90", "label": null, "content_type": null, "tags": {} },
    { "key": "Bookstore:Shipping:FreeShippingThreshold", "value": "800", "label": "Staging", "content_type": null, "tags": {} },
    { "key": "Bookstore:Sentinel", "value": "1", "label": "Staging", "content_type": null, "tags": {} },
    { "key": "Bookstore:Sentinel", "value": "1", "label": "Production", "content_type": null, "tags": {} }
  ]
}

Label'sız kayıtlar her environment'ın ortak varsayılanı; Staging label'ı yalnızca farklı olanı override ediyor. Bu dosya Azure CLI'ın appconfig/kvset import biçiminde, yani pipeline'da tek komutla store'a yüklenebiliyor. Label ile environment ayırmanın bir sınırı var: erişim store seviyesinde verilir. Microsoft'un FAQ'su, environment'lar arasında yetki ayrımı gerekiyorsa her environment için ayrı store önerir; label'lar ayrımın gerekmediği durumlar için. Bookstore'da production'ın kendi store'u var, dev ve staging tek store'u label'larla paylaşıyor.

Snapshot ise bir anda seçilen key-value'ların değiştirilemez bir kopyası. Bir release'i belirli bir snapshot'a bağlarsan, store'da sonradan yapılan değişiklik o release'i etkilemez; geri dönüş de eski snapshot'ı seçmek kadar kolay olur. Sık değişen iş kuralları için dinamik refresh, release ile birlikte sabitlenmesi gereken ayarlar için snapshot iyi bir ayrım.

Provider'ı Bağlamak: Select, Label ve TrimKeyPrefix

Bookstore'un App Configuration bağlantısı ayrı bir extension method'da:

public static IConfigurationBuilder AddBookstoreAppConfiguration(
    this IConfigurationBuilder builder,
    IConfiguration configuration,
    IHostEnvironment environment,
    TokenCredential credential)
{
    var label = environment.EnvironmentName;
    var refreshInterval = configuration.GetValue("AppConfig:RefreshInterval", TimeSpan.FromSeconds(30));

    return builder.AddAzureAppConfiguration(options =>
    {
        // Azure'da endpoint + managed identity; yerel emulator'de connection string
        if (configuration["AppConfig:ConnectionString"] is { Length: > 0 } connectionString)
        {
            options.Connect(connectionString);
        }
        else
        {
            options.Connect(new Uri(configuration["AppConfig:Endpoint"]!), credential);
        }

        options
            // Önce label'sız ortak değerler, sonra environment'ın label'ı: sonraki kazanır
            .Select(KeyPrefix + "*", LabelFilter.Null)
            .Select(KeyPrefix + "*", label)
            .TrimKeyPrefix(KeyPrefix)
            // Yalnız sentinel izlenir; değişince bütün ayarlar yeniden yüklenir
            .ConfigureRefresh(refresh => refresh
                .Register(SentinelKey, label, refreshAll: true)
                .SetRefreshInterval(refreshInterval))
            // Key Vault reference'ları uygulamanın kendi kimliğiyle çözülür
            .ConfigureKeyVault(keyVault => keyVault.SetCredential(credential));
    });
}

Select çağrılarının sırası layering'in kendisi: aynı key için sonra seçilen kazanır. TrimKeyPrefix, Bookstore:Shipping:FreeShippingThreshold'u Shipping:FreeShippingThreshold'a çevirir; böylece ikinci yazıdaki ShippingOptions ve validator'ları hiç değişmeden çalışır. Aynı store'u başka uygulamalar da kullanıyorsa her uygulamanın kendi önekini seçmesi, onların ayarlarını yüklemesini engeller.

Credential için üçüncü yazıdaki kodu AzureCredentials adlı ayrı bir class'a taşıdım; App Configuration ve Key Vault artık aynı credential instance'ını kullanıyor. Azure'da uygulamanın managed identity'sine store üzerinde "App Configuration Data Reader" rolü verilir. Program.cs'te provider, environment variable'lardan sonra ve Key Vault'tan önce ekleniyor:

var credential = AzureCredentials.Create(builder.Environment, builder.Configuration);

// Gizli olmayan, merkezde yönetilen ayarlar: App Configuration
var useAppConfiguration = AppConfigurationExtensions.IsEnabled(builder.Configuration);
if (useAppConfiguration)
{
    builder.Configuration.AddBookstoreAppConfiguration(builder.Configuration, builder.Environment, credential);
    builder.Services.AddAzureAppConfiguration();
}
var app = builder.Build();

if (useAppConfiguration)
{
    // Refresh'i request'ler tetikler; middleware pipeline'ın başında olmalı
    app.UseAzureAppConfiguration();
}

Emulator ile Yerelde Çalıştırmak

Microsoft'un App Configuration için resmi, açık kaynak bir emulator'ü var. Anonymous authentication ve HMAC (Hash-based Message Authentication Code; paylaşılan bir key ile isteği imzalama) ile access key destekliyor. Uygulamanın emulator'e bağlanması için çalışma klasöründe ürettiğim bir key ile HMAC'ı açtım; anonymous erişim yalnızca seed script'inin yazabilmesi için açık:

$ docker run -d --name secretseries-appconfig -p 8483:8483 \
    -e Tenant__AnonymousAuthEnabled=true -e Authentication__Anonymous__AnonymousUserRole=Owner \
    -e Tenant__HmacSha256Enabled=true \
    -e Tenant__AccessKeys__0__Id=bookstore-local -e Tenant__AccessKeys__0__Name=Primary \
    -e "Tenant__AccessKeys__0__Secret=$ACCESS_KEY" -e Tenant__AccessKeys__0__ReadOnly=true \
    mcr.microsoft.com/azure-app-configuration/app-configuration-emulator:1.2.0

$ python3 deploy/appconfig/seed-emulator.py deploy/appconfig/bookstore.kvset.json
Bookstore:Shipping:FreeShippingThreshold None 500
Bookstore:Shipping:StandardFee None 49.90
Bookstore:Shipping:FreeShippingThreshold Staging 800
Bookstore:Sentinel Staging 1
Bookstore:Sentinel Production 1

API'yi staging olarak, emulator'ün connection string'iyle başlattım. Bu connection string bir secret; yalnızca yerel emulator için var. Azure'da uygulama endpoint ve managed identity ile bağlanır ve store'da access key ile kimlik doğrulamayı kapatmak iyi bir varsayılandır:

$ export ASPNETCORE_ENVIRONMENT=Staging
$ export AppConfig__ConnectionString="Endpoint=http://localhost:8483;Id=bookstore-local;Secret=$ACCESS_KEY"
$ export AppConfig__RefreshInterval=00:00:05
$ dotnet Bookstore.Api.dll --urls http://localhost:5094 &
$ curl -s localhost:5094/api/info
{"environment":"Staging","freeShippingThreshold":800,"jwtKeyFingerprint":"5c69bb74","secrets":{"ConnectionStrings:Bookstore":false,"Jwt:SigningKey":true,"Email:ApiKey":true,"Payments:Webhook:Secret":false}}

800, staging label'ından geldi; appsettings.Staging.json'daki 750'yi ezdi, çünkü App Configuration provider'ı dosyalardan ve environment variable'lardan sonra ekleniyor.

Sentinel Key ile Deploy'suz Yenileme

Baştaki ilk sorunun cevabı: hiçbir şey değişmez. Provider yalnızca Register ile kaydedilen key'i izliyor, o da sentinel. Eşiği değiştirip bekledim, sonra sentinel'in değerini artırdım:

09:25:00 "freeShippingThreshold":800
PUT Bookstore:Shipping:FreeShippingThreshold Staging 950
09:25:04 "freeShippingThreshold":800
09:25:09 "freeShippingThreshold":800
09:25:13 "freeShippingThreshold":800
PUT Bookstore:Sentinel Staging 2
09:25:17 "freeShippingThreshold":800
09:25:21 "freeShippingThreshold":950
info: Microsoft.Extensions.Configuration.AzureAppConfiguration.Refresh[0]
      Setting updated. Key:'Bookstore:Sentinel'

Sentinel değişince refreshAll: true bütün seçili ayarları yeniden yükledi ve sonraki istek yeni değeri gördü. Bu davranış bir özellik: birbirine bağlı birkaç ayarı (eşik ve kargo ücreti gibi) tek tek değiştirirken uygulama yarım bir durum görmez. Bütün değişiklikleri yazar, en son sentinel'i artırırsın; değişiklik tek seferde uygulanır.

Refresh'in nasıl tetiklendiğini bilmek önemli. Microsoft Learn'deki dinamik configuration sayfasına göre UseAzureAppConfiguration middleware'i her istekte çalışır ama store'a ancak refresh aralığı dolduysa gider; kontrol asenkron yapılır, isteği bekletmez. İstek gelmeyen bir uygulama hiç refresh etmez; tetikleyen istek de genellikle eski değeri görür. Yukarıda sentinel'den sonraki ilk isteğin hâlâ 800 dönmesinin nedeni bu iki kural: ya refresh aralığı henüz dolmamıştı ya da kontrolü tetikleyen istek eski değeri gördü. Background job gibi HTTP isteği almayan bir süreçte refresh'i IConfigurationRefresherProvider üzerinden kendin tetiklemen gerekir. Değeri okuyan kodun IOptionsMonitor ya da IOptionsSnapshot kullanması da şart; ikinci yazıda gördüğümüz gibi IOptions ilk değeri saklar.

Provider'ın yeni sürümlerinde sentinel'e alternatif olarak RegisterAll() da var: seçili bütün key-value'ları izler ve herhangi biri değişince yeniden yükler. Sentinel'siz daha az disiplin ister, ama her değişiklik hemen uygulanır; birbirine bağlı ayarları atomik değiştirme imkânını kaybedersin.

Key Vault Reference'ları

App Configuration'a secret koymak yerine, Key Vault'taki secret'a işaret eden bir reference koyarsın. Reference, özel bir content type ile saklanan küçük bir JSON:

{
    "key": "Bookstore:Email:ApiKey",
    "label": "Staging",
    "content_type": "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8",
    "value": "{\"uri\":\"https://localhost:8443/secrets/BookstoreApi-Email--ApiKey\"}",
    ...
}

Kritik nokta şu: reference'ı App Configuration çözmez, uygulamanın içindeki provider çözer. Provider reference'ı görünce ConfigureKeyVault'ta verilen credential ile Key Vault'a gider. Yani App Configuration'ı okuyabilen birinin secret'ı da okuyabileceği varsayımı yanlış; secret'ı okumak için ayrıca Key Vault'ta Secrets User rolü gerekir. Uygulamanın identity'sinin iki rolü de olmalı: store'da Data Reader, vault'ta Secrets User.

Reference'ın çözülemediği durumu da denedim: emulator'e, ulaşılamayan bir vault adresini gösteren bir reference koydum ve API'yi yeniden başlattım:

Unhandled exception. Microsoft.Extensions.Configuration.AzureAppConfiguration.KeyVaultReferenceException: Key vault error. ErrorCode:'' Key:'Bookstore:Email:ApiKey' Label:'Staging' Etag:'T18-JcEXeEcvIDC-dK6OOg' SecretIdentifier:'https://localhost:8443/secrets/BookstoreApi-Email--ApiKey'
 ---> System.AggregateException: Retry failed after 7 tries. ... (Connection refused (localhost:8443)) ...

Uygulama yaklaşık 50 saniye retry yaptıktan sonra açılmadı. Üçüncü yazıda gördüğümüz App Service reference'ının aksine burada değer sessizce reference metni olarak kalmıyor; provider hata fırlatıyor. Refresh sırasında aynı hata olursa uygulama kapanmaz: provider uyarı log'lar ve cache'teki değerle devam eder.

Feature flag'ler de App Configuration'da ayrı bir key ön ekiyle saklanır ve aynı provider ile yüklenir; değerlendirme tarafı Deployment Stratejileri serisinin Feature Flag'ler yazısında anlatıldı. Depolama açısından fark tek: flag'ler sentinel'e bağlı değildir, kendi refresh'leri vardır.

Üretimde Dikkat

Kotayı hesapla. Microsoft'un FAQ'suna göre Standard tier store saatte 30.000 isteğe kadar izin verir; Free tier günde 1.000 istekle sınırlı, Premium'da kota yok. Aynı sayfadaki hesap örneğine göre sentinel kontrolleri gibi sık tekrarlanan ve değişmeyen isteklerin çoğu saatlik kotaya sayılmıyor; asıl kotayı yiyen, bir değişiklikten sonra bütün instance'ların bütün ayarları yeniden yüklemesi. 50 instance ve 1.000 ayar için bir sentinel değişikliği 500 istek demek.

Refresh aralığını kısaltarak "her istekte güncel" bir sistem kuramazsın. Provider bunu açıkça engelliyor; aralığı 200 milisaniye verince uygulama açılmadı:

Unhandled exception. System.ArgumentOutOfRangeException: The refresh interval cannot be less than 1000 milliseconds. (Parameter 'refreshInterval')

Varsayılan 30 saniye çoğu iş kuralı için yeterli; değişikliğin yansıma süresi bu aralık artı bir istek kadardır.

Değişikliği de kod gibi yönet. Portal'dan yapılan bir değişiklik audit log'da görünür ama review'dan geçmez. Bookstore'da ayarlar repo'daki bookstore.kvset.json'da; değişiklik bir pull request ile gelir, pipeline onu store'a yükler ve en son sentinel'i artırır. Azure'da bu adım şöyle görünür (çalıştırılmadı):

# Pipeline'da: ayarları dosyadan yükle, sonra sentinel'i artır
az appconfig kv import --endpoint https://bookstore-appconfig-staging.azconfig.io --auth-mode login \
  --source file --path deploy/appconfig/bookstore.kvset.json --format json --profile appconfig/kvset --yes
az appconfig kv set --endpoint https://bookstore-appconfig-staging.azconfig.io --auth-mode login \
  --key Bookstore:Sentinel --label Staging --value "$BUILD_ID" --yes

Validation'ı da pipeline'a koy: ikinci yazıda gördüğümüz gibi reload ile gelen geçersiz bir değer uygulamayı kapatmaz ama options'ı okuyan her isteği bozar. Dosyayı store'a yüklemeden önce aynı [OptionsValidator] kurallarını çalıştıran küçük bir test, kötü bir değerin production'a ulaşmasını engeller. Revision geçmişi de bir güvence: FAQ'ya göre Standard ve Premium tier'da 30 gün, Free ve Developer'da 7 gün tutuluyor.

Sık Düşülen Tuzaklar

Tek uygulama için App Configuration kurmak ve startup'a gereksiz bir bağımlılık eklemek. App Configuration'a secret'ın kendisini koymak; reference koy. Sentinel'i güncellemeyi unutmak ve "değişiklik yansımadı" diye saatler kaybetmek. Refresh'i tetikleyen middleware'i pipeline'da başka bir middleware'in arkasına koymak; istek ona hiç ulaşmayabilir. Background job'ların HTTP isteği almadığı için refresh etmediğini unutmak. Değeri IOptions ile okumak. Label'ı güvenlik sınırı sanmak; aynı store'a erişen herkes bütün label'ları okur. Production ayarını portal'dan elle değiştirmek ve bir sonraki pipeline import'unun onu geri almasına şaşırmak.

Küçük Bir Deneme

★☆☆ App Configuration emulator'ünü Docker'da çalıştır, bir key'i label'sız ve Staging label'ıyla iki kez yaz. Uygulamayı önce Production, sonra Staging environment'ında açıp hangi değeri okuduğunu karşılaştır.

★★☆ Sentinel ile RegisterAll()'ı karşılaştır: iki birbirine bağlı ayarı (eşik ve ücret) farklı zamanlarda değiştir ve her iki yöntemde uygulamanın arada tutarsız bir durum görüp görmediğini log'lardan izle.

★★★ Ayarları repo'daki bir dosyadan store'a yükleyen ve en son sentinel'i artıran bir script yaz. Script'e, yüklemeden önce değerleri options validator'larıyla kontrol eden bir adım ekle ve geçersiz bir değerle pipeline'ın durduğunu göster.

Azure tarafında secret'lar Key Vault'ta, ayarlar App Configuration'da. Ama her ekip Azure'da değil ve her secret statik değil. Bir sonraki yazıda bulut bağımsız bir secret server olan HashiCorp Vault ve onun açık kaynak fork'u OpenBao ile Bookstore'un SQL Server'ı için her açılışta yeniden üretilen kısa ömürlü login'leri kuracağız.