Gerçek Incident'lar 1: Global Yayılan Config — Cloudflare 2025, Google Cloud 2025 ve CrowdStrike 2024
Cloudflare, Google Cloud ve CrowdStrike'ın büyük kesintilerinde tetikleyici kod değil, saniyeler içinde bütün dünyaya yayılan bir config ya da veri değişikliğiydi; ortak ders, kendi config'ini de input gibi doğrulamak ve aşamalı dağıtmaktır. Üç vakayı resmî postmortem'lerinden özetliyor, aynı hatayı Bookstore'un switches.json dosyasında üretiyor (geçersiz değerde her checkout 500, bozuk dosyada crash loop) ve bir doğrulama aracı ile last-known-good okuyucuyla düzeltiyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 ve Prometheus 3.16.0 (promtool dahil) ile denenmiştir. Gerçek incident'larla ilgili bilgiler yalnız şirketlerin resmî postmortem'lerinden; o sistemlerde hiçbir şey çalıştırılmadı.
Önce Tahmin Et
Serinin altıncı yazısında Bookstore.Api'ye (Bookstore, sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) incident sırasında kullanılacak iki kol ekledik: ödeme timeout'u ve checkout'u kapatan kill switch. İkisi de bütün instance'ların yanında duran küçük bir switches.json dosyasından, restart olmadan okunuyor. Gece yarısı birisi bu dosyayı bütün instance'lara gönderiyor ama dosyada bir hata var. Üç ayrı durumu tahmin et: (1) dosya geçerli JSON ama TimeoutSeconds değeri 0; (2) değer "6s" gibi sayı olmayan bir string; (3) dosya yarıda kesilmiş, JSON bozuk. Çalışan instance'lar her durumda ne yapar? Tam o sırada yeni açılan bir instance (autoscale, deploy, crash sonrası restart) ne yapar? Cevapları yazının ortasında ölçtük.
Ortak Pattern: Kod Değil, Config
Bu ve bir sonraki yazı, büyük şirketlerin kendi yayımladıkları postmortem'lerden ders çıkarıyor. Bu yazıdaki üç vakanın ortak noktası, arızayı tetikleyenin bir kod deploy'u değil, config ya da metadata olması: bir izin değişikliğinin ürettiği bir dosya, bir veritabanına eklenen bir policy kaydı, bir content update. Üçünde de değişiklik saniyeler ya da dakikalar içinde bütün dünyaya yayıldı ve üçünde de kodun kendisi için var olan aşamalı rollout, test ve doğrulama adımları bu tür veriye uygulanmıyordu.
Kod deploy'unu canary ile yüzde yüzde açıyoruz, ama config, feature dosyası ya da policy kaydı çoğu zaman "sadece veri" sayılıp tek seferde her yere gidiyor. Oysa çalışan bir binary için bu veri, kodu farklı bir yoldan çalıştıran bir input. Aşağıdaki anlatımlar yalnız şirketlerin resmî postmortem'lerinden, kendi cümlelerimizle.
Cloudflare, 18 Kasım 2025
Saat 11:05'te (UTC) Cloudflare, ClickHouse veritabanı cluster'ında bir erişim yetkisi değişikliği yaptı: kullanıcıların zaten dolaylı olarak erişebildiği r0 veritabanındaki tablolar açıkça görünür hâle geldi. Değişiklik cluster'a kademeli olarak yayılıyordu. Bot Management ürününün feature dosyasını üreten sorgu system.columns tablosunu veritabanı adıyla filtrelemiyordu; değişiklikten sonra aynı kolonlar ikinci kez, r0 veritabanından da geldi ve dosya iki katına çıktı.
Bu dosya birkaç dakikada bir yeniden üretilip bütün ağa dağıtılıyordu. Proxy'deki Bot Management module'ü performans için belleği önceden ayırıyor ve 200 feature'lık bir sınır koyuyordu; normal kullanım 60 civarıydı. Yeni dosya 200'ü aştı. Yeni proxy FL2'de (Rust ile yazılmış, geçişi süren yeni proxy) bu, sınır aşıldığında dönen hatanın unwrap() ile açılması yüzünden panic ve HTTP 5xx demekti. Eski proxy FL'de (Cloudflare'in önceki proxy'si) ise her isteğin bot skoru 0 oldu ve bot engelleyen müşteriler gerçek kullanıcıları da engelledi.
Arızayı uzatan şey dalgalanmasıydı: dosya her beş dakikada bir, kademeli güncellenen cluster'da üretiliyordu ve sorgunun hangi düğüme denk geldiğine göre iyi ya da kötü çıkıyordu. Ağ çöküp kendine geliyordu; bu ekibe önce büyük bir DDoS saldırısını düşündürdü. Tamamen Cloudflare dışında barındırılan status page'in tesadüfen aynı anda çökmesi de şüpheyi güçlendirdi. 13:05'te Workers KV (Cloudflare'in veri saklama servisi) ve Access için proxy'yi atlayan bir yol açıldı. 14:24'te yeni dosya üretimi ve dağıtımı durduruldu, bilinen iyi bir dosya dağıtım kuyruğuna kondu ve proxy restart edildi; 14:30'da ana trafik büyük ölçüde normaldi. Bütün etki 17:06'da bitti. Cloudflare yazısında bunu 2019'dan beri yaşadıkları en kötü kesinti olarak tanımlıyor.
Şirketin açıkladığı düzeltmeler: kendi ürettiği config dosyalarını kullanıcıdan gelen input gibi doğrulamak, feature'lar için daha fazla global kill switch, hata raporlarının sistem kaynaklarını tüketmesini engellemek ve proxy module'lerinin hata durumlarını gözden geçirmek.
Google Cloud, 12 Haziran 2025
Google Cloud'un API isteklerinin yetki, policy ve quota kontrolünü yapan Service Control binary'sine 29 Mayıs'ta yeni bir quota policy kontrolü eklendi. Kod bölge bölge, aşamalı olarak dağıtıldı; ama yeni kod yolu yalnız belirli bir policy değişikliği geldiğinde çalışıyordu ve rollout sırasında hiç çalışmadı. Kod feature flag ile korunmuyordu ve hata yönetimi eksikti.
12 Haziran'da 17:45 UTC civarında (Google raporunda saatler Pasifik saatiyle: 10:45 PDT (ABD'nin batı kıyısındaki yaz saati)) Spanner tablolarına yeni bir policy kaydı eklendi. Kayıtta istenmeden boş bırakılmış alanlar vardı. Policy verisi tasarım gereği saniyeler içinde bütün bölgelere kopyalandı, her bölgedeki Service Control boş alanı okuyan yeni yola girdi ve null pointer yüzünden çöktü; binary'ler crash loop'a girdi ve dış API istekleri 503 aldı. Google'ın raporu durumu tek cümlede özetliyor: "If this had been flag protected, the issue would have been caught in staging." (Google Cloud incident raporu, 2025).
Tespit hızlıydı: SRE (Site Reliability Engineering; güvenilirliği yazılım mühendisliğiyle yönetme disiplini) ekibi 10 dakikada sebebi bulup yeni kod yolunu kapatan "red button"u hazırladı, 40 dakika içinde de bütün bölgelere dağıttı. Ama büyük bölgelerden us-central1'in toparlanması 2 saat 40 dakikaya kadar sürdü: aynı anda yeniden başlayan Service Control task'ları, randomized exponential backoff (bir sonraki denemeden önceki beklemeyi katlayarak ve rastgele dağıtarak uzatma) olmadığı için Spanner tablosuna sürü hâlinde yüklendi ve onu aşırı yükledi. Etki toplamda yaklaşık 3 saat sürdü. İlk müşteri duyurusu da yaklaşık bir saat gecikti, çünkü duyuruların yayımlandığı Cloud Service Health de etkilenmişti.
Google'ın açıkladığı düzeltmeler: Service Control'ü module'lere ayırıp hata durumunda fail-open (isteği reddetmek yerine geçirmek) davranışı, global kopyalanan verinin aşamalı ve doğrulanarak yayılması, kritik binary'lerdeki her değişikliğin varsayılanı kapalı bir feature flag arkasında gelmesi, backoff denetimi ve iletişim altyapısının Google Cloud çöktüğünde de ayakta kalması.
CrowdStrike, 19 Temmuz 2024
CrowdStrike'ın Falcon sensörü, sensör sürümünden bağımsız olarak "Rapid Response Content" adlı içerik güncellemeleri alıyor. Bu içerik, sensördeki bir Content Interpreter tarafından önceden tanımlı Template Type'lara göre yorumlanıyor. Şubat 2024'te sensör 7.11 ile gelen IPC (process'ler arası iletişim) Template Type'ı 21 input alanı tanımlıyordu, ama sensörün bu tipi çağıran kodu yalnız 20 değer veriyordu. Uyuşmazlık aylarca fark edilmedi, çünkü testlerde ve o güne kadarki içeriklerde 21. alan joker karakterle (her şeyle eşleşen) kullanılmıştı.
19 Temmuz 04:09 UTC'de yayımlanan iki yeni Template Instance'tan biri 21. alanda joker olmayan bir kriter kullandı. Content Validator'daki bir mantık hatası bu içeriği geçirdi; sensör 21. değeri okumaya çalışınca input array'inin sonunun ötesine okudu (out-of-bounds read) ve Windows çöktü. CrowdStrike'ın RCA (Root Cause Analysis; kök neden analizi) raporu bunu "This parameter count mismatch evaded multiple layers of build validation and testing" diye anlatıyor (CrowdStrike, Channel File 291 RCA, Ağustos 2024). İçerik 05:27 UTC'de, 78 dakika sonra geri çekildi; o aralıkta çevrimiçi olan Windows makineler çöktü. Microsoft etkilenen cihaz sayısını yaklaşık 8,5 milyon olarak tahmin etti. Toparlanma ise günler sürdü: RCA'ya göre Windows sensörlerinin yaklaşık yüzde 99'u ancak 29 Temmuz'da yeniden çevrimiçiydi.
CrowdStrike'ın açıkladığı düzeltmeler: alan sayısının derleme anında, array sınırlarının çalışma anında kontrolü, her alan için joker olmayan değerlerle test, Content Validator'a yeni kontroller ve en önemlisi Rapid Response Content için aşamalı dağıtım: önce canary, sonra bekleme süreli halkalar (ring); müşterilere de içeriğin nereye ve ne zaman gideceğini seçme imkânı.
Üç vakayı yan yana koyunca ortak pattern görünüyor:
| Vaka | Tetikleyici | Nasıl yayıldı | Etkiyi uzatan | Açıklanan ana düzeltme |
|---|---|---|---|---|
| Cloudflare, 18 Kasım 2025 | izin değişikliğiyle iki katına çıkan feature dosyası | dakikalar içinde bütün ağa dağıtılan dosya | dalgalanma, DDoS şüphesi, çöken status page | kendi config'ini de input gibi doğrulamak, global kill switch'ler |
| Google Cloud, 12 Haziran 2025 | boş alanlı bir quota policy kaydı | saniyeler içinde bütün bölgelere kopyalanan veri | backoff'suz restart'ların yarattığı yük, gecikmeli duyuru | varsayılanı kapalı feature flag, aşamalı yayılım, fail-open |
| CrowdStrike, 19 Temmuz 2024 | 21. alanı joker olmayan bir content update | çevrimiçi bütün Windows sensörlerine giden içerik | geri çekmeden sonra günlerce süren toparlanma | derleme ve çalışma anı kontrolleri, canary ve halkalarla dağıtım |
Bookstore'da Aynı Hata: switches.json
Üç vakanın küçük bir kopyası bizim laboratuvarımızda da var. Serinin altıncı yazısında Bookstore.Api'ye eklediğimiz switches.json, bütün instance'lara aynı anda giden ve çalışma anında yeniden okunan bir config dosyası. Önce Tahmin Et'teki üç durumu ayrı bir API instance'ında denedik: checkout endpoint'ine, var olmayan bir sepet ID'siyle istek attık (geçerli config'de cevap 404; kill switch açıkken 503) ve dosyayı her adımda değiştirip 3 saniye bekledik:
# Her adımda switches.json değişti; 3 saniye sonra checkout'a istek (var olmayan sepet: geçerli config'de 404)
{ "Payments": { "TimeoutSeconds": 3, "CheckoutEnabled": true } } → 404
{ "Payments": { "TimeoutSeconds": 0, "CheckoutEnabled": true } } → 500, 500
{ "Payments": { "TimeoutSeconds": 3, "CheckoutEnabled": true } } → 404
{ "Payments": { "TimeoutSeconds": "6s", "CheckoutEnabled": true } } → 500
{ "Payments": { "TimeoutSeconds": 6, "CheckoutEnabled": false } } → 503
{ "Payments": { "TimeoutSeconds": 6, "CheckoutEnabled": fal → 503
# son adımda API hiçbir log satırı yazmadı
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'PaymentsOptions' members: 'TimeoutSeconds' with the error: 'The field TimeoutSeconds must be between 1 and 30.'.
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: Failed to convert configuration value '6s' at 'Payments:TimeoutSeconds' to type 'System.Int32'.
Sonuçlar üç ayrı davranış gösteriyor. Değer geçersiz olduğunda (0 ya da "6s") dosya değişir değişmez her checkout 500 aldı. IOptionsMonitor.CurrentValue, yeniden bağlama ya da validation başarısız olunca her çağrıda exception fırlatıyor; dosya bütün instance'lara aynı anda gittiği için tek bir hatalı değer hepsinde checkout'u aynı anda kırar. Cloudflare'in 200 sınırını aşan dosyası da bunu yaptı. Dosya yarıda kesildiğinde ise çalışan instance hiçbir şey yapmadı: log yok, metric yok, kill switch açık kalmaya devam etti. Bu da iyi değil: instance'ların hangi değerle çalıştığını artık dosyaya bakarak bilemezsin. Aynı bozuk dosyayla yeni bir instance başlattığımızda ise process açılırken çöktü:
# Aynı bozuk switches.json ile yeni bir instance
dotnet Bookstore.Api.dll
Unhandled exception. System.IO.InvalidDataException: Failed to load configuration from file '/app/switches.json'.
---> System.FormatException: Could not parse the JSON file.
---> System.Text.Json.JsonReaderException: 'fal' is an invalid JSON literal. Expected the literal 'false'. LineNumber: 0 | BytePositionInLine: 59.
...
echo $?
134
Çıkış kodu 134. Kubernetes'te bu bir crash loop demek: deploy, autoscale ya da bir node değişimiyle başlayan her yeni pod aynı dosyayı okuyup çöker. Çalışan instance'lar sessizce eski değerle devam ederken yenileri açılamıyor; yük arttığında ölçeklenemeyen ve yavaş yavaş küçülen bir sistem.
Config'i Input Gibi Doğrulamak
Cloudflare'in ilk düzeltmesi, kendi ürettiği config dosyalarını kullanıcıdan gelen input gibi doğrulamaktı. Bookstore'da bunu iki yerde yapıyoruz: dosya gönderilmeden önce ve instance'ın içinde. İlki, pipeline'da çalışan küçük bir kontrol: tools/switches-check/switches-check.cs dosyayı boyut sınırıyla okuyor, tekrarlanan key'leri reddeden ayarla parse ediyor (.NET 10'daki AllowDuplicateProperties), bilinmeyen key'leri hata sayıyor ve her değerin tipini ve aralığını kontrol ediyor:
const int MaxBytes = 4096;
string path = args.Length > 0 ? args[0] : "switches.json";
List<string> problems = [];
byte[] bytes = File.ReadAllBytes(path);
if (bytes.Length > MaxBytes)
{
problems.Add($"file is {bytes.Length} bytes, the limit is {MaxBytes}");
}
try
{
using JsonDocument doc = JsonDocument.Parse(bytes, new JsonDocumentOptions { AllowDuplicateProperties = false });
CheckObject(doc.RootElement, "", new()
{
["Payments"] = payments => CheckObject(payments, "Payments", new()
{
["TimeoutSeconds"] = v => CheckInt(v, "Payments:TimeoutSeconds", 1, 30),
["CheckoutEnabled"] = v => CheckBool(v, "Payments:CheckoutEnabled")
})
});
}
catch (JsonException ex)
{
problems.Add($"not valid JSON: {ex.Message}");
}
...
void CheckObject(JsonElement element, string name, Dictionary<string, Action<JsonElement>> known)
{
if (element.ValueKind != JsonValueKind.Object)
{
problems.Add($"'{name}' must be an object");
return;
}
foreach (JsonProperty property in element.EnumerateObject())
{
// Bilinmeyen key hatadır: yoksa bir yazım hatası ("CheckoutEnable") sessizce yok sayılır.
if (known.TryGetValue(property.Name, out Action<JsonElement>? check))
{
check(property.Value);
}
else
{
problems.Add($"unknown key '{(name.Length > 0 ? name + ":" : "")}{property.Name}'");
}
}
}
Bilinmeyen key'in hata olması önemli: CheckoutEnable gibi bir yazım hatası .NET configuration'ında sessizce yok sayılır ve kill switch hiç açılmaz. Dört dosyayla çalıştırdığımızda:
dotnet run tools/switches-check/switches-check.cs -- switches.json
OK switches.json
dotnet run tools/switches-check/switches-check.cs -- switches-typo.json
ERROR switches-typo.json: 'Payments:TimeoutSeconds' must be a whole number, got "6s"
ERROR switches-typo.json: unknown key 'Payments:CheckoutEnable'
2 problem(s)
dotnet run tools/switches-check/switches-check.cs -- switches-duplicate.json
ERROR switches-duplicate.json: not valid JSON: Duplicate property 'TimeoutSeconds' encountered during deserialization.
1 problem(s)
dotnet run tools/switches-check/switches-check.cs -- switches-truncated.json
ERROR switches-truncated.json: not valid JSON: 'fal' is an invalid JSON literal. Expected the literal 'false'. LineNumber: 0 | BytePositionInLine: 59.
1 problem(s)
Hatalı dosyalarda çıkış kodu 1; dosyayı instance'lara gönderen pipeline burada duruyor. Tekrarlanan key'i reddetmek de bilinçli: Cloudflare'in dosyası, aynı kolonların ikinci kez gelmesiyle büyümüştü.
Pipeline'daki kontrol tek başına yetmez; dosya elle de değişebilir. İkinci savunma instance'ın içinde. LastKnownGood<T>, IOptionsMonitor'dan okurken bağlama ya da validation hatası olursa son geçerli değeri döndürüyor, hatayı bir kez log'luyor ve bir metric artırıyor:
// Çalışma anında değişebilen options'ı okur; bir değişiklik bağlanamaz ya da doğrulanamazsa son geçerli değeri tutar.
// Bu olmadan IOptionsMonitor.CurrentValue, dosya düzeltilene kadar her çağrıda exception fırlatır.
public sealed class LastKnownGood<T>(IOptionsMonitor<T> monitor, ILogger<LastKnownGood<T>> logger) where T : class
{
private T current = monitor.CurrentValue; // açılışta doğrulandı (ValidateOnStart)
private string? lastError;
public T Value
{
get
{
try
{
current = monitor.CurrentValue;
lastError = null;
}
catch (Exception ex) when (ex is OptionsValidationException or InvalidOperationException)
{
// Her istekte değil, her farklı sorun için bir kez log'la ve say.
if (ex.Message != lastError)
{
lastError = ex.Message;
SwitchesFile.Rejected.Add(1, new KeyValuePair<string, object?>("reason", "invalid"));
logger.LogError(ex, "Rejected new {Options} values; keeping the last valid ones", typeof(T).Name);
}
}
return current;
}
}
}
Bozuk JSON'un davranışı ise uygulamanın hangi aşamada olduğuna bağlı karar istiyor. Çalışırken bozuk bir dosya zaten görmezden geliniyordu (eski değerler kalıyordu); bunu koruyup üstüne bir metric ekliyoruz. Açılırken ise crash loop yerine dosyayı yok sayıp appsettings değerleriyle açılmayı seçiyoruz:
public static WebApplicationBuilder AddSwitchesFile(this WebApplicationBuilder builder)
{
string path = builder.Configuration["SwitchesFile"] ?? "switches.json";
builder.Configuration.AddJsonFile(source =>
{
source.Path = path;
source.Optional = true;
source.ReloadOnChange = true;
source.OnLoadException = context =>
{
Rejected.Add(1, new KeyValuePair<string, object?>("reason", "parse"));
// Açılırken: crash loop yerine appsettings değerleriyle devam et (fail-open).
// Çalışırken: yok sayma; yeniden yükleme düşer ve son değerler geçerli kalır.
if (!started)
{
context.Ignore = true;
Console.Error.WriteLine($"switches.json ignored at startup: {context.Exception.GetBaseException().Message}");
}
};
source.ResolveFileProvider();
});
return builder;
}
Program.cs'te builder.AddSwitchesFile() çağrılıyor ve uygulama açıldıktan sonra app.Lifetime.ApplicationStarted.Register(SwitchesFile.MarkStarted) ile "artık çalışıyoruz" bayrağı kalkıyor. Ödeme client'ı ve OrderService artık IOptionsMonitor<PaymentsOptions> yerine LastKnownGood<PaymentsOptions> alıyor. Önceki deneyi aynı sırayla tekrarladık:
{ "Payments": { "TimeoutSeconds": 3, "CheckoutEnabled": true } } → 404
{ "Payments": { "TimeoutSeconds": 0, "CheckoutEnabled": true } } → 404
{ "Payments": { "TimeoutSeconds": "6s", "CheckoutEnabled": true } } → 404
{ "Payments": { "TimeoutSeconds": 6, "CheckoutEnabled": false } } → 503
{ "Payments": { "TimeoutSeconds": 6, "CheckoutEnabled": fal → 503
# aynı bozuk dosyayla yeni bir instance başlattık; açıldı ve checkout'a cevap verdi:
new instance → 404
Geçersiz değerler artık checkout'u kırmıyor; instance son geçerli değerle çalışıyor ve hatayı bir kez log'luyor:
fail: Bookstore.Configuration.LastKnownGood[0]
Rejected new PaymentsOptions values; keeping the last valid ones
Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'PaymentsOptions' members: 'TimeoutSeconds' with the error: 'The field TimeoutSeconds must be between 1 and 30.'.
fail: Bookstore.Configuration.LastKnownGood[0]
Rejected new PaymentsOptions values; keeping the last valid ones
System.InvalidOperationException: Failed to convert configuration value '6s' at 'Payments:TimeoutSeconds' to type 'System.Int32'.
Bozuk dosyayla açılan yeni instance da artık çökmüyor. Prometheus'ta da sayaç görünüyor ve deploy/prometheus/rules/config-alerts.yaml'daki SwitchesFileRejected alert'i buna bakıp bir ticket açıyor (promtool testiyle birlikte repoda):
bookstore_switches_rejected_total
{__name__="bookstore_switches_rejected_total", job="bookstore-api", reason="invalid", ...} 2
{__name__="bookstore_switches_rejected_total", job="bookstore-api", reason="parse", ...} 1
# Bir instance'ın reddettiği switches.json değişiklikleri (bozuk JSON, yanlış tip, aralık dışı değer).
# Instance son geçerli değerlerle devam eder, bir şey kırılmaz; ama birinin az önce çektiği kol uygulanmamıştır.
groups:
- name: bookstore-config
rules:
- alert: SwitchesFileRejected
expr: sum by (reason) (increase(bookstore_switches_rejected_total{job="bookstore-api"}[10m])) > 0
labels:
severity: ticket
annotations:
summary: "An instance rejected a switches.json change ({{ $labels.reason }}); it still runs with the previous values"
runbook_url: https://git.example.com/bookstore/blob/main/docs/incidents/levers.md
Sayaçta açılıştaki ret yok: OnLoadException çalıştığında OpenTelemetry'nin metric pipeline'ı henüz kurulmamış oluyor. Bu yüzden açılıştaki ret ayrıca stderr'e yazılıyor (switches.json ignored at startup: 'fal' is an invalid JSON literal…); izlemek için log'a bakmak gerekiyor.
Fail-Open mı, Fail-Closed mı?
Yeni instance'ın bozuk dosyayla açılması bir karar: fail-open (hata durumunda varsayılan, açık davranışla devam etmek) seçtik. Bedeli deneyde görünüyor: eski instance kill switch'i açık tutarken (503), yeni instance checkout'u kabul ediyor (404, yani sepet aranıyor). Aynı anda iki farklı davranış. Bu dosya için kabul edilebilir, çünkü varsayılanlar normal çalışma değerleri ve en kötü sonuç, kill switch'in yeni instance'larda kapanması; alert de bunu bildiriyor.
Ama bu her config için doğru değil. Yetkilendirme kuralları, rate limit sınırları ya da ödeme tutarı kontrolü gibi güvenlikle ilgili bir config'te bozuk dosya karşısında "her şeye izin ver" demek, bir arızayı bir güvenlik açığına çevirir; orada fail-closed (hata durumunda reddetmek) doğru. Kararı her config dosyası için önceden, yazılı olarak ver; incident sırasında değil. Bookstore bunu docs/incidents/levers.md'ye yazdı.
İkinci ders aşamalı dağıtım. Google global kopyalanan verinin bölge bölge ve doğrulanarak yayılmasını, CrowdStrike içerik güncellemeleri için canary ve halkalar kullanmayı açıkladı. Bookstore ölçeğinde karşılığı basit: switches.json önce tek bir instance'a gidiyor, bir dakika checkout status code'larına bakılıyor, sonra diğerlerine. Kodun canary'si için kurduğun düzen (Deployment Stratejileri serisinin canary release yazısı) config için de kullanılabilir; ConfigMap değişikliklerinin pod'lara nasıl yansıdığını Kubernetes serisinin "ConfigMap ve Secret ile .NET Configuration" yazısı anlatıyor.
Üçüncü ders sınırların davranışı: Cloudflare'in 200 feature sınırı yanlış değildi, sorun sınır aşılınca proxy'nin panic ile durmasıydı. Sınır aşıldığında yeni girdiyi reddedip eskisiyle devam etmek ve bunu yüksek sesle bildirmek gerekir; LastKnownGood'un yaptığı da bu. Dördüncüsü iletişim altyapısı: Cloudflare'de çöken status page saldırı şüphesini büyüttü, Google'da ilk duyuru bir saat gecikti. Bookstore'un status page'i ve incident kanalı Bookstore'un altyapısına bağlı olmamalı.
Üretimde Dikkat
Instance'ların hangi config'le çalıştığını görünür yap. Bozuk bir değişiklik reddedildiğinde instance'lar farklı değerlerle çalışabilir; metric ya da health endpoint'i üzerinden etkin değerlerin bir özetini (örneğin bir hash) yayınlamak, hangi instance'ın geride kaldığını gösterir.
Kill switch'lerin kendisi de config. Cloudflare'in "daha fazla global kill switch" düzeltmesiyle çelişmiyor: kill switch de doğrulanmalı ve bozuk olduğunda ne olacağı önceden kararlaştırılmalı.
Sık Düşülen Tuzaklar
Config'i "veri" sayıp test etmemek. Kod yolunu değiştiren her girdi, kod kadar test ister. CrowdStrike vakasında testler 21. alan için hep joker kullanmıştı; gerçek bir değer geldiğinde hiç denenmemiş bir yol çalıştı.
Varsayılanı bilmemek. IOptionsMonitor'ın geçersiz değerde her çağrıda exception fırlattığını, bozuk JSON'u ise çalışırken sessizce yok sayıp açılışta çöktüğünü dokümandan değil, deneyerek gördük. Kullandığın configuration kütüphanesinin bu üç durumda ne yaptığını ölçerek öğren.
Retry'ı backoff'suz kurmak. Google'da us-central1'in geç toparlanması, aynı anda yeniden başlayan task'ların ortak veritabanına yüklenmesindendi. Restart ve retry, rastgele dağıtılmış ve katlanarak artan beklemeyle yapılmalı.
Küçük Bir Deneme
★☆☆ switches-check.cs'e bir kural ekle: CheckoutEnabled false ise dosyada bir Reason alanı zorunlu olsun. Uygulamanın bu alanı bilmediğine dikkat et; .NET configuration bilinmeyen key'i yok sayar, ama kontrol aracı artık onu bilmeli.
★★☆ LastKnownGood<T>'a etkin değerlerin bir hash'ini metric olarak yayınlayan bir özellik ekle (ör. bookstore_switches_info{hash="…"}) ve iki instance'ı farklı dosyalarla çalıştırıp Prometheus'ta farkı göster.
★★★ Bu yazıdaki üç vakadan birinin postmortem'ini baştan sona oku ve serinin yedinci yazısındaki dört sınıfa (prevent, detect, mitigate, process) göre şirketin açıkladığı düzeltmeleri sınıflandır. Hangi sınıf eksik?
Bu yazıdaki üç kesintide tetikleyici bir veri değişikliğiydi. Bir sonraki yazıda başka bir aileye bakıyoruz: AWS'in (Amazon'un bulut platformu) Ekim 2025, Facebook'un Ekim 2021, GitLab'ın Ocak 2017 ve Knight Capital'ın Ağustos 2012 vakalarında otomasyonun kendi arızaları, gizli bağımlılıklar ve kurtarma yolunun da bozulan şeye bağlı olması; Bookstore için de bir restore drill.