Mert Özen Her Satırda Daha İleri
DevOps Kubernetes — #07

ConfigMap ve Secret ile .NET Configuration: Environment Variable, Volume ve Değişikliklerin Yansıması

Mert Özen 8 Kas 2026 10 dk 4 görüntülenme
ConfigMap ve Secret ile .NET Configuration: Environment Variable, Volume ve Değişikliklerin Yansıması

ConfigMap ve Secret, .NET configuration'ına environment variable (ConnectionStrings__Default) ya da volume'daki dosya olarak girer; env hiç, subPath mount'u hiç güncellenmez, klasör mount'u 10–80 saniyede güncellenir. kind'daki ölçümde .NET 10'un varsayılan watcher'ı ConfigMap değişikliğini görmedi, polling watcher gördü. Provider sırası tuzağını, rollout tetiklemeyi, immutable ConfigMap'i, base64'ün şifreleme olmadığını ve Angular config.json'u gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler kind 0.33 ile Kubernetes 1.37, kubectl 1.37, .NET 10 (ASP.NET Core 10.0.14) ve Angular 22 ile denenmiştir.

Önce Tahmin Et

Bookstore API'sinin (Application Programming Interface) mağaza adı bir ConfigMap'te duruyor; ConfigMap pod'a volume olarak mount edilmiş ve uygulama dosyayı reloadOnChange: true ile okuyor. ConfigMap'teki adı "Bookstore Ankara"dan "Bookstore Izmir"e çeviriyorsun. /api/info yeni adı ne zaman döner: hemen mi, bir dakika içinde mi, hiç mi? Aynı dosyayı subPath ile mount eden bir pod'da ne olur? Son soru: aynı ayar hem environment variable'da hem bu dosyada varsa hangisi kazanır?

ConfigMap'ten .NET Configuration'a İki Yol

ConfigMap, Kubernetes'te key/value biçiminde gizli olmayan configuration tutmanın standart yolu. Bir pod onu iki şekilde tüketebilir: environment variable olarak ya da bir volume'daki dosyalar olarak. ASP.NET Core'un configuration sistemi ikisini de doğal olarak okur; mekanizmanın ayrıntısı ASP.NET Core Web API serisinin Configuration ve Options Pattern yazısında. Kubernetes tarafında bilmen gereken tek kural çift alt çizgi: environment variable adlarında : kullanılamadığı için .NET __'yi hiyerarşi ayıracı sayar. ConnectionStrings__Default, ConnectionStrings:Default anahtarına karşılık gelir.

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
  namespace: bookstore
data:
  appsettings.json: |
    {
      "Bookstore": {
        "StoreName": "Bookstore Ankara",
        "DiscountPercent": 10
      }
    }

Deployment'ta ikisini birlikte kullanıyoruz: connection string bir Secret'tan environment variable olarak, mağaza ayarları ConfigMap'ten dosya olarak geliyor. Karşılaştırma için mağaza adını bir environment variable'la da veriyorum:

      containers:
        - name: api
          image: bookstore-api:1.0.0
          env:
            - name: ConnectionStrings__Default
              valueFrom:
                secretKeyRef:
                  name: api-db
                  key: connection-string
            - name: Bookstore__StoreName
              value: "Bookstore (from env)"
          volumeMounts:
            - name: config
              mountPath: /app/config
              readOnly: true
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              memory: 384Mi
      volumes:
        - name: config
          configMap:
            name: api-config

Resource ayarlarını dokuzuncu yazıda açacağız; burada olmalarının nedeni yazının sonundaki "Üretimde Dikkat" bölümünde. Uygulama tarafında tek satır var: mount edilen dosyayı configuration'a eklemek.

var builder = WebApplication.CreateBuilder(args);

// ConfigMap'ten mount edilen opsiyonel configuration klasörü
builder.Configuration.AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: true);

builder.Services.Configure<StoreOptions>(builder.Configuration.GetSection("Bookstore"));

/api/info endpoint'i değeri IOptionsMonitor<StoreOptions>.CurrentValue ile okuyor; yani configuration yeniden yüklenirse yeni değeri görür. IOptions<T> kullansaydık değer uygulama ömrü boyunca sabit kalırdı.

Provider Sırası: Kim Kazanır?

$ kubectl exec client -- wget -qO- http://api/api/info
{"pod":"api-55dfb49c7d-7d767","version":"1.0.0","environment":"Production","storeName":"Bookstore Ankara","discountPercent":10,"database":"postgres"}

Environment variable "Bookstore (from env)" dediği hâlde dosyadaki "Bookstore Ankara" kazandı. Baştaki son sorunun cevabı provider sırasında: WebApplication.CreateBuilder varsayılan provider'ları (appsettings.json, appsettings.{Environment}.json, environment variable'lar, command line) ekler; bizim AddJsonFile çağrımız onlardan sonra geldiği için listenin sonunda durur ve sonra eklenen provider önceki değeri ezer. Bu çoğu zaman istemediğin bir şey: Kubernetes'te environment variable'ı genellikle "son söz" olarak kullanırsın. İki çözüm var. Ya mount edilen dosyayı environment variable'lardan önce ekleyecek şekilde provider listesini düzenlersin, ya da bir ayarı yalnız tek bir kaynaktan verirsin. Bookstore'da ikincisini seçiyoruz: mağaza ayarları dosyada, bağlantı bilgileri ve environment'a özgü küçük değerler environment variable'da.

Volume Mount'un İçi: ..data Symlink'i ve subPath

API image'ı chiseled olduğu için içinde ls yok; aynı ConfigMap'i bir busybox pod'una iki şekilde mount ettim: bütün klasör olarak /config'e ve yalnız bir dosya olarak subPath ile /single'a.

$ kubectl exec cm-viewer -- ls -la /config /single
/config:
drwxrwxrwx    3 root     root          4096 Nov  8 04:21 .
drwxr-xr-x    1 root     root          4096 Nov  8 04:22 ..
drwxr-xr-x    2 root     root          4096 Nov  8 04:21 ..2026_11_08_04_21_56.280075211
lrwxrwxrwx    1 root     root            31 Nov  8 04:21 ..data -> ..2026_11_08_04_21_56.280075211
lrwxrwxrwx    1 root     root            23 Nov  8 04:21 appsettings.json -> ..data/appsettings.json

/single:
-rw-r--r--    1 root     root            88 Nov  8 04:21 appsettings.json

Klasör mount'unda appsettings.json gerçek bir dosya değil: ..data/appsettings.json'a giden bir symlink, ..data da zaman damgalı bir klasöre giden ikinci bir symlink. kubelet ConfigMap değişince yeni zaman damgalı bir klasör yazar ve ..data symlink'ini tek bir atomik işlemle yeni klasöre çevirir. Uygulama hiçbir zaman yarım yazılmış bir dosya görmez. subPath mount'u ise dosyanın o anki kopyasını doğrudan bağlar; bu atomik değişim mekanizmasının dışında kalır.

Değişiklik Pod'a Ne Zaman Yansır?

ConfigMap'i "Bookstore Izmir" yapıp her 3 saniyede bir hem busybox pod'undaki iki dosyayı hem de iki API pod'unun /api/info yanıtını kaydettim:

apply at 04:25:18
04:25:19 volume/subPath=Bookstore Izmir/Bookstore Ankara/ api= Bookstore Ankara Bookstore Ankara
# 3 dakika boyunca aynı satır
04:28:29 volume/subPath=Bookstore Izmir/Bookstore Ankara/ api= Bookstore Ankara Bookstore Ankara
$ kubectl debug api-687f7ddc54-mfmgj --image=busybox:1.37 --target=api --profile=general -q -- cat /proc/1/root/app/config/appsettings.json
{
  "Bookstore": {
    "StoreName": "Bookstore Izmir",
    "DiscountPercent": 10
  }
}

Üç ayrı sonuç var. Klasör mount'u güncellendi. subPath mount'u hiç güncellenmedi; Kubernetes dokümantasyonu da bunu açıkça söylüyor. Asıl sürpriz uygulamada: API pod'unun içindeki dosya da yeni değeri taşıyor (bunu shell'siz pod'a eklediğim ephemeral debug container'ıyla okudum; ayrıntısı 16. yazıda), ama reloadOnChange: true olan uygulama üç dakika sonra bile eski adı dönüyor. .NET'in varsayılan file watcher'ı Linux'ta inotify kullanıyor ve kubelet'in symlink değiştirerek yaptığı güncellemeyi bu kurulumda fark etmedi.

Aynı denemeyi DOTNET_USE_POLLING_FILE_WATCHER=true environment variable'ıyla tekrarladım. Bu ayar file provider'ı olay beklemek yerine dosyayı periyodik olarak (dört saniyede bir) kontrol etmeye geçirir:

apply at 04:28:54
04:29:04 volume/subPath=Bookstore Izmir/Bookstore Ankara/ api= Bookstore Izmir Bookstore Bursa
04:29:59 volume/subPath=Bookstore Bursa/Bookstore Ankara/ api= Bookstore Izmir Bookstore Bursa
04:30:15 volume/subPath=Bookstore Bursa/Bookstore Ankara/ api= Bookstore Bursa Bookstore Bursa

Bu sefer iki API pod'u da yeni adı ("Bookstore Bursa") gördü: biri 10, diğeri 81 saniye sonra. Aradaki fark uygulamadan değil kubelet'ten geliyor: busybox pod'u ile aynı node'daki API pod'unun dosyası ancak 04:29:59 civarında güncellendi. Dokümantasyona göre gecikme "kubelet sync period + cache propagation delay" kadar olabilir; varsayılan ayarlarla bu yaklaşık bir dakika mertebesinde. Angular tarafında da aynısını gördük, aşağıda.

Environment variable'lar ise hiç güncellenmez: process başlarken okunur, ConfigMap değişse de pod yeniden başlayana kadar eski değer kalır. Tablo hâlinde:

Tüketim şekliConfigMap değişince.NET tarafında
Environment variable (env, envFrom)GüncellenmezPod restart gerekir
Klasör volume mountYaklaşık 10–80 sn içindeVarsayılan watcher görmedi; polling watcher ile görüldü
subPath mountHiç güncellenmezPod restart gerekir

Değişince Rollout Tetiklemek

Canlı reload cazip görünse de production'da çoğu ekip tersini tercih eder: configuration değişince pod'ları kontrollü bir rollout ile yenilemek. Nedeni basit: yeni configuration bozuksa, rolling update ve readiness probe'ları hatayı ilk pod'da yakalar ve eski pod'lar trafiği taşımaya devam eder. Canlı reload'da ise bozuk değer bütün pod'lara yaklaşık aynı anda ulaşır ve geri dönüş yolu yoktur.

En basit yol kubectl rollout restart:

$ kubectl rollout restart deploy/api
deployment.apps/api restarted
$ kubectl get deploy api -o jsonpath='{.spec.template.metadata.annotations}'
{"kubectl.kubernetes.io/restartedAt":"2026-11-08T07:33:32+03:00"}

Komutun yaptığı tek şey pod template'ine bir annotation eklemek; template değiştiği için yeni bir ReplicaSet ve normal bir rolling update başlar. Bunu otomatikleştirmenin iki yaygın yolu var. Birincisi, ConfigMap içeriğinin hash'ini pod template'inde bir annotation'a yazmak (Helm chart'larında checksum/config olarak bilinen yaklaşım): içerik değişince annotation, dolayısıyla template değişir. İkincisi, ConfigMap'in adına içeriğin hash'ini eklemek (örneğin api-config-7g2k9m4b5c) ve Deployment'ın bu ada referans vermesi: her değişiklik yeni bir ConfigMap ve yeni bir referans demek. Kustomize'ın configMapGenerator'ı bunu kendiliğinden yapıyor; 14. yazıda göreceğiz.

İkinci yaklaşım immutable ConfigMap'lerle çok iyi eşleşir. 1.21'den beri stable olan immutable: true, ConfigMap'in içeriğinin bir daha değiştirilemeyeceğini söyler; kubelet de onu izlemeyi bırakır, bu da çok sayıda ConfigMap'in olduğu cluster'larda API server'ın yükünü azaltır:

$ kubectl apply -f immutable.yaml
configmap/api-config-v1 created
$ sed 's/Ankara/Izmir/' immutable.yaml | kubectl apply -f -
The ConfigMap "api-config-v1" is invalid: data: Forbidden: field is immutable when `immutable` is set

Değiştirmek için yeni adla yeni bir ConfigMap yaratırsın; eski ReplicaSet'ler eski ConfigMap'e referans vermeye devam ettiği için rollout undo configuration'ı da geri alır.

Secret: base64 Şifreleme Değildir

Secret, ConfigMap'in hassas veri için olan kardeşi; tüketim şekilleri (env, volume) aynı. Ama "secret" adı yanıltıcı olabilir:

$ kubectl create secret generic demo-secret --from-literal=password='not-a-real-password' --dry-run=client -o yaml
apiVersion: v1
data:
  password: bm90LWEtcmVhbC1wYXNzd29yZA==
kind: Secret
metadata:
  name: demo-secret
$ echo bm90LWEtcmVhbC1wYXNzd29yZA== | base64 -d
not-a-real-password

base64 bir encoding, şifreleme değil; yalnız binary veriyi YAML'a (YAML Ain't Markup Language) sığdırmaya yarar. Kubernetes dokümantasyonu açıkça uyarıyor: Secret'lar varsayılan olarak etcd'de şifrelenmemiş tutulur; API erişimi olan herkes, etcd'ye erişimi olan herkes ve bir namespace'te pod yaratabilen herkes (pod'a Secret'ı mount ederek) o namespace'teki Secret'ları okuyabilir. Secret'ı gerçekten korumak için birkaç katman gerekir: RBAC (Role-Based Access Control) ile Secret'lara get/list yetkisini daraltmak, API server'da encryption at rest'i açmak (managed cluster'larda genellikle bir KMS (Key Management Service) ile), Secret YAML'larını Git'e koymamak ve mümkünse değerleri cluster dışındaki bir secret store'dan (Azure Key Vault, AWS (Amazon Web Services) Secrets Manager, HashiCorp Vault) senkronize etmek. Son konu ilerideki Configuration ve Secret Management serisinin işi; External Secrets Operator ve CSI (Container Storage Interface) Secret Store sürücüsü orada.

.NET tarafında küçük ama önemli bir alışkanlık: connection string'i loglamamak. IConfiguration'ı bütünüyle dump eden debug endpoint'leri ve "startup'ta bütün ayarları yazdır" kodları Secret'ı log sistemine taşır; log'lar da genellikle Secret'lardan çok daha geniş bir kitleye açıktır.

Angular Runtime config.json'u ConfigMap'ten Sunmak

Angular uygulaması build sırasında derlenir; environment.ts'e yazılan bir API adresi image'a gömülür ve her environment için ayrı image gerekir. Bookstore'da bunun yerine runtime configuration kullanıyoruz: uygulama açılırken /config/config.json'u fetch ile okuyor ve değerleri bir InjectionToken ile dağıtıyor. Dosyanın kendisi ConfigMap'ten geliyor:

apiVersion: v1
kind: ConfigMap
metadata:
  name: web-config
  namespace: bookstore
data:
  config.json: |
    {
      "apiBaseUrl": "",
      "storeName": "Bookstore"
    }

Web Deployment'ı onu /usr/share/nginx/html/config klasörüne mount ediyor ve nginx bu dosya için Cache-Control: no-store header'ı dönüyor; tarayıcı eski configuration'ı cache'lemesin. Klasör mount'u olduğu için güncelleme pod'u yeniden başlatmadan yansıdı:

$ kubectl create configmap web-config --from-literal=config.json='{"apiBaseUrl": "", "storeName": "Bookstore Izmir"}' --dry-run=client -o yaml | kubectl apply -f -
configmap/web-config configured
applied 04:32:21
04:32:21 {  "apiBaseUrl": "",  "storeName": "Bookstore"}
04:33:32 {"apiBaseUrl": "", "storeName": "Bookstore Izmir"}

71 saniye. Aynı image artık her environment'ta farklı bir config.json ile çalışabilir. Bu dosyaya hiçbir zaman secret koyma: tarayıcıya giden her şey herkese açıktır.

Üretimde Dikkat

Configuration değişikliklerini deploy gibi ele al: Git'te tut, review'dan geçir, hash'li ad ya da checksum annotation'ı ile rollout tetikle. Canlı reload'u yalnız gerçekten anında değişmesi gereken ve bozulursa zarar vermeyecek ayarlar (log seviyesi, feature flag) için düşün; onu kullanacaksan DOTNET_USE_POLLING_FILE_WATCHER'ı ayarla ve kendi cluster'ında ölç.

Bu yazının deneyleri sırasında yük altındaki lokal cluster'da API pod'ları OOMKilled ile yeniden başladı; o an pod'larda hiç resource request yoktu. Node'un belleği daraldığında kernel önce request'siz (BestEffort) pod'ları öldürür. Pod'lara request eklemem bu yüzdendi; konuyu dokuzuncu yazıda ayrıntılı ölçeceğiz.

Secret'lara erişimi namespace ve RBAC düzeyinde sınırla, encryption at rest'i açık olduğunu doğrula ve uygulamanın Secret değerlerini loglamadığından emin ol.

Sık Düşülen Tuzaklar

subPath ile mount edip güncelleme beklemek. Tek bir dosyayı mevcut bir klasörün içine koymak için subPath pratik (örneğin /app/appsettings.Production.json), ama o dosya ConfigMap değişince hiç güncellenmez. Ayrı bir klasöre mount et ve dosyayı oradan ekle.

Klasör mount'uyla uygulamanın klasörünü ezmek. ConfigMap'i /app'e mount edersen image'daki bütün dosyalar (DLL'ler dahil) görünmez olur ve uygulama başlamaz. Hep ayrı, boş bir klasör kullan.

Tek alt çizgi kullanmak. ConnectionStrings_Default .NET için hiyerarşik bir anahtar değil; sessizce yok sayılır ve uygulama "connection string bulunamadı" hatası verir. İki alt çizgi.

Secret'ı ConfigMap'e koymak. ConfigMap'ler genellikle daha geniş bir okuma yetkisiyle açılır ve kubectl describe çıktısında değerler görünür. Hassas her değer Secret'a.

Provider sırasını unutmak. Yukarıda gördüğün gibi sonradan eklenen bir JSON (JavaScript Object Notation) dosyası environment variable'ı ezebilir; "ayarı env'den değiştirdim ama değişmedi" şikâyetinin klasik nedeni.

Küçük Bir Deneme

★☆☆ api-config'i subPath ile /app/config/appsettings.json'a mount eden bir Deployment yaz, ConfigMap'i değiştir ve /api/info'nun eski değeri döndüğünü gör. Sonra kubectl rollout restart ile yeni değeri al.

★★☆ Program.cs'te mount edilen dosyayı environment variable provider'ından önce ekleyecek şekilde provider listesini düzenle (ipucu: builder.Configuration.Sources bir liste). Aynı Deployment'la environment variable'ın artık kazandığını doğrula.

★★★ ConfigMap içeriğinin SHA (Secure Hash Algorithm) 256 hash'ini hesaplayıp pod template'ine checksum/config annotation'ı olarak yazan küçük bir bash script'i hazırla. ConfigMap'i değiştirip script'i çalıştırdığında yeni bir ReplicaSet doğduğunu, değiştirmeden çalıştırdığında doğmadığını göster.

Uygulama artık doğru configuration ile çalışıyor. Ama Kubernetes onun gerçekten sağlıklı olup olmadığını nereden biliyor? Bir sonraki yazıda startup, liveness ve readiness probe'larını ASP.NET Core health check'leriyle bağlıyor ve en pahalı hatalardan birini, liveness'a veritabanı kontrolü koymayı, gerçek bir zincirleme restart ile görüyoruz.