Cloud'da Sağlam Mimari: Well-Architected Framework, Yedekleme ve DR, Lock-in ve Bir Sonraki Adım IaC
Bulutta sağlam mimari, Well-Architected'ın beş pillar'ını iş yüküne düzenli olarak sormak, RPO ve RTO'yu yazılı hedefe çevirmek ve yedeği geri yükleyerek test etmektir. Bookstore'u beş pillar'a göre gözden geçiriyor, zone redundancy ve failover group'un neyi kurtarıp neyi kurtarmadığını, gerçek bir restore tatbikatını, lock-in'e bilinçli yaklaşımı, landing zone'u, IaC'ye geçişi ve seri sonu checklist'ini veriyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.404), Microsoft.Data.SqlClient 7.1.2, bir SQL Server 2025 container'ı, Azure CLI 2.92.0 ve ShellCheck 0.11.0 ile denenmiştir. Restore tatbikatı yerel SQL Server'da gerçekten çalıştırıldı; Azure SQL için yazılan script yalnız ShellCheck ve az ... --help ile kontrol edildi. Servis bilgileri Microsoft Learn'den ve Amazon'un dokümantasyonundan alındı (14 Kasım 2026).
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) production veritabanı, Cuma akşamı çalışan hatalı bir migration yüzünden bozuldu: sipariş satırlarının fiyat kolonu sıfırlandı. 8. yazıda point-in-time restore'u 14 gün olarak ayarlamıştık. Soru basit: Bookstore kaç dakikada eski hâline döner ve kaç dakikalık sipariş kaybolur? Bu iki sayıyı bilmiyorsan, yazının konusu olan iki hedefin, RTO (Recovery Time Objective; kesintiden sonra hizmetin en geç ne zaman geri gelmesi gerektiği) ve RPO'nun (Recovery Point Objective; kabul edilebilir en büyük veri kaybı) Bookstore için henüz tanımlanmadığı anlamına gelir. İkinci, daha küçük soru: AWS'nin (Amazon Web Services; Amazon'un sunduğu bulut platformu) Well-Architected Framework'ünde kaç pillar var, Azure'unkinde kaç?
Well-Architected Framework: Beş Pillar
Microsoft'un Azure Well-Architected Framework'ü, bir iş yükünü beş açıdan gözden geçirmek için yazılmış bir rehber. Her pillar bir soru sorar. Reliability: sistem arızaya dayanıyor, beklenen sürede geri geliyor ve SLA (Service Level Agreement; taahhüt edilen erişilebilirlik) hedefini tutuyor mu? Security: veri ve kimlikler korunuyor, saldırının etkisi sınırlanıyor mu? Cost Optimization: harcanan para, alınan değere uygun mu? Operational Excellence: deploy, izleme ve olay müdahalesi tekrarlanabilir bir süreç mi? Performance Efficiency: sistem yük altında yeterince hızlı ve ölçeklenebiliyor mu? AWS'nin çerçevesi aynı beş pillar'a Aralık 2021'de eklenen Sustainability'yi (iş yükünün çevresel etkisi) koyarak altıya çıkıyor; tahmin sorusunun ikinci cevabı bu.
Bu bir checklist değil, bir konuşma biçimi. Azure'da Well-Architected Review adlı bir değerlendirme aracı var, Azure Advisor da önerilerini aynı beş pillar'a göre puanlıyor. Biz bu seriyi kendi checklist'imize çeviriyoruz. Aşağıdaki tablo her pillar'ın bu seride nereye karşılık geldiğini ve AWS tarafındaki adını veriyor; AWS bu seride her yazıda karşılaştırma için kullanıldı.
| Azure pillar | Bookstore'da bu seride | AWS karşılığı |
|---|---|---|
| Reliability | Composite SLA (1), health check (5), retry (8), yedek ve restore (8, 11) | Reliability |
| Security | Managed identity ve rol atamaları (3), kısa ömürlü imzalı adresler (7), Entra-only SQL (8), private endpoint (9) | Security |
| Cost Optimization | Compute seçimi (4), access tier (7), serverless ve ücretsiz teklif (8), budget ve tag (10) | Cost Optimization |
| Operational Excellence | İsimlendirme ve tag (2), script'le kurulum (2-10), restore tatbikatı (11) | Operational Excellence |
| Performance Efficiency | Cold start ölçümü (4), scale kuralları (5), kapakların cache'lenmesi (7) | Performance Efficiency |
| Karşılığı yok | Bölge ve kaynak boyutu seçiminin enerji etkisi | Sustainability |
Reliability: RPO, RTO ve Zone'lar
Learn iki hedefi şöyle tanımlıyor: RTO, bir olaydan sonra uygulamanın en fazla ne kadar süre erişilemez kalabileceği; RPO, en fazla ne kadarlık veri kaybının kabul edilebileceği. Bunlar teknik değil iş kararıdır: Selin'e "kampanya gecesi bir saat sipariş alamamak kabul edilebilir mi?" diye sorarsın, cevap mimariyi belirler.
Azure SQL için Learn'deki güncel tablo seçenekleri sıralıyor. Zone-redundant yapılandırmada bir zone kaybı 30 saniyenin altında, veri kaybı olmadan atlatılıyor. Failover group ya da geo-replication ile region kaybında RTO 60 saniyenin altında, RPO sıfır ya da biraz üzeri. Yalnız yedeğe güvenen geo-restore'da ikisi de "dakikalar ya da saatler". Tahmin sorusunun cevabı da buradan: bozuk migration bir region arızası değil, mantıksal bir hata. Failover group hatayı ikincil kopyaya da taşır; kurtaran point-in-time restore'dur. Log yedekleri yaklaşık 10 dakikada bir alındığı için migration'dan hemen önceki ana dönebilirsin, ama migration'dan sonra gelen siparişleri elle taşıman gerekir. Ne kadar süreceği ise veritabanının boyutuna bağlı; bunu ölçmenin tek yolu denemek.
Uygulama katmanında zone redundancy her serviste farklı. Learn'e göre App Service için Premium v2, v3 ya da v4 plan ve en az iki instance gerekiyor; Container Apps'te environment VNet içinde olmalı ve zone redundancy ancak environment oluşturulurken açılabiliyor; storage için ZRS (veriyi zone'lara eşzamanlı kopyalayan redundancy) seçersin. West Europe'ta üç availability zone var ve region'ın eşi North Europe. Bookstore için gerçekçi hedef: production'da zone redundancy (birkaç saniyelik kesinti, veri kaybı yok) ve region kaybı için yedekten dönüş (saatler). İkinci bir region'ı sürekli hazır tutmak, compute ve veritabanı maliyetini neredeyse ikiye katlar; 1. yazıdaki composite SLA hesabı da bu maliyetin karşılığında kazanılan dakikaları gösteriyordu.
Yedeği Geri Yükleyerek Test Etmek
Learn'ün disaster recovery rehberi, geri yüklemeleri düzenli olarak denemeyi ve tatbikat yapmayı söylüyor. Yedeğin var olması, geri yüklenebildiğinin kanıtı değil. Kendi yönettiğin bir SQL Server (VM ya da container) için küçük bir tatbikat aracı yazdık: veritabanını yedekliyor, yeni bir adla geri yüklüyor, tablo tablo satır sayılarını karşılaştırıyor ve süreyi yazıyor:
// Kendi çalıştırdığın bir SQL Server (VM ya da container) için restore tatbikatı: BookstoreDb'yi yedekle,
// yeni bir adla geri yükle, kaynakla karşılaştır ve süreyi yaz. Azure SQL için: infra/cli/11-restore-drill.sh.
// Çalıştır: dotnet run tools/cloud/restore-drill.cs (BOOKSTORE_DB = kaynak veritabanının connection string'i)
#:package Microsoft.Data.SqlClient
using System.Diagnostics;
using Microsoft.Data.SqlClient;
SqlConnectionStringBuilder source = new(Environment.GetEnvironmentVariable("BOOKSTORE_DB")
?? throw new InvalidOperationException("Set BOOKSTORE_DB to the source connection string."));
string database = source.InitialCatalog;
string drill = $"{database}_RestoreDrill";
string backupFile = $"/var/opt/mssql/data/{database}_drill.bak"; // SQL Server makinesindeki bir yol
SqlConnectionStringBuilder master = new(source.ConnectionString) { InitialCatalog = "master" };
await using SqlConnection server = new(master.ConnectionString);
await server.OpenAsync();
Stopwatch backup = Stopwatch.StartNew();
await ExecAsync($"BACKUP DATABASE [{database}] TO DISK = N'{backupFile}' WITH INIT, COPY_ONLY, CHECKSUM");
backup.Stop();
// Geri yüklenen kopyanın, asıl dosyaların yanında kendi dosya adları olmalı
List<string> moves = [];
await using (SqlCommand files = new($"RESTORE FILELISTONLY FROM DISK = N'{backupFile}'", server))
await using (SqlDataReader reader = await files.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
string logical = reader.GetString(0);
string physical = reader.GetString(1);
string target = Path.Combine(Path.GetDirectoryName(physical)!, $"{drill}_{logical}{Path.GetExtension(physical)}");
moves.Add($"MOVE N'{logical}' TO N'{target}'");
}
}
Stopwatch restore = Stopwatch.StartNew();
await ExecAsync($"RESTORE DATABASE [{drill}] FROM DISK = N'{backupFile}' WITH {string.Join(", ", moves)}, REPLACE, CHECKSUM");
restore.Stop();
const string CountsSql = """
SELECT s.name + '.' + t.name, SUM(p.rows)
FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
GROUP BY s.name, t.name ORDER BY 1
""";
Dictionary<string, long> expected = await CountsAsync(database);
Dictionary<string, long> actual = await CountsAsync(drill);
int mismatches = expected.Count(e => !actual.TryGetValue(e.Key, out long rows) || rows != e.Value);
Console.WriteLine($"Backup {database,-24} {backup.Elapsed.TotalSeconds,6:0.00} s");
Console.WriteLine($"Restore {drill,-24} {restore.Elapsed.TotalSeconds,6:0.00} s");
Console.WriteLine($"Tables compared: {expected.Count}, rows: {expected.Values.Sum()}, mismatches: {mismatches}");
Console.WriteLine(mismatches == 0 ? "Drill passed: the backup restores and matches the source." : "Drill FAILED.");
await ExecAsync($"ALTER DATABASE [{drill}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{drill}]");
return mismatches == 0 ? 0 : 1;
async Task ExecAsync(string sql)
{
await using SqlCommand command = new(sql, server) { CommandTimeout = 600 };
await command.ExecuteNonQueryAsync();
}
async Task<Dictionary<string, long>> CountsAsync(string db)
{
await server.ChangeDatabaseAsync(db);
Dictionary<string, long> counts = [];
await using (SqlCommand command = new(CountsSql, server))
await using (SqlDataReader reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
counts[reader.GetString(0)] = reader.GetInt64(1);
}
}
await server.ChangeDatabaseAsync("master");
return counts;
}
Araç, Entra ID gerekmediği için ek paket istemeyen Microsoft.Data.SqlClient 7.1.2'yi kullanıyor; sürüm depodaki Directory.Packages.props'ta. Yerel SQL Server container'ında, 8. yazının BookstoreDb'sine karşı:
$ BOOKSTORE_DB="Server=localhost,1433;Database=BookstoreDb;..." dotnet run tools/cloud/restore-drill.cs
Backup BookstoreDb 0.05 s
Restore BookstoreDb_RestoreDrill 0.20 s
Tables compared: 10, rows: 64, mismatches: 0
Drill passed: the backup restores and matches the source.
Yirmi kitaplık bir veritabanı için süre anlamsız derecede küçük; değerli olan sürecin kendisi. Aynı araç gerçek boyuttaki bir veritabanında çalıştığında, RTO'nun veri katmanındaki payını tahminle değil ölçümle bilirsin. Azure SQL Database'te BACKUP komutu yok; orada tatbikat, point-in-time restore ile yeni bir veritabanı açmak:
#!/usr/bin/env bash
# Azure SQL'de restore tatbikatı: BookstoreDb'yi point-in-time restore ile yeni bir veritabanına aç, süreyi ölç, sonra sil.
# GNU date kullanır (Cloud Shell, WSL, Git Bash, GitHub-hosted runner'lar).
set -euo pipefail
: "${AZURE_SUBSCRIPTION_ID:?Set AZURE_SUBSCRIPTION_ID to the target subscription}"
ENVIRONMENT="${1:?Usage: 11-restore-drill.sh <dev|staging|prod>}"
RG="rg-bookstore-${ENVIRONMENT}-weu"
SQL_SERVER="sql-bookstore-${ENVIRONMENT}-weu"
DRILL="BookstoreDb-drill-$(date -u +%Y%m%d)"
POINT_IN_TIME=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)
az account set --subscription "$AZURE_SUBSCRIPTION_ID"
# Restore her zaman yeni bir veritabanı oluşturur; asıl veritabanına dokunulmaz
STARTED=$(date +%s)
az sql db restore --resource-group "$RG" --server "$SQL_SERVER" --name BookstoreDb \
--dest-name "$DRILL" --time "$POINT_IN_TIME"
echo "Restored ${POINT_IN_TIME} into ${DRILL} in $(( $(date +%s) - STARTED )) s"
# Silmeden önce kopyayı kaynakla karşılaştır (satır sayıları, son sipariş), ör. tools/cloud/restore-drill.cs'teki sorgularla
read -r -p "Delete ${DRILL}? [y/N] " ANSWER
if [[ "$ANSWER" == "y" ]]; then
az sql db delete --resource-group "$RG" --server "$SQL_SERVER" --name "$DRILL" --yes
fi
Bu tatbikatı ayda bir, takvimde bir göreve bağla ve ölçülen süreyi bir yere yaz. Süre her ay büyüyorsa, veritabanı büyüdüğü için RTO hedefin sessizce aşılıyor demektir.
Lock-in: Ne Kadarı Gerçek Bir Sorun?
Bir bulut sağlayıcısına bağımlılık (lock-in) her managed servisin bedeli. Bu seride Bookstore'a eklediğimiz her parçanın taşınabilirliği farklı. Container image'ı her bulutta ve kendi sunucunda çalışır. SQL Server ve PostgreSQL şeması, bir bacpac ya da dump ile taşınır. OpenTelemetry ile toplanan log, metric ve trace herhangi bir backend'e gönderilebilir; Learn'e göre Azure Monitor da OpenTelemetry'yi destekliyor ve proje Mayıs 2026'da CNCF'de (Cloud Native Computing Foundation; Kubernetes gibi açık kaynak projelerin vakfı) graduated seviyesine çıktı. Öte yandan Functions trigger'ları, Event Grid aboneliği, managed identity ve Entra ID entegrasyonu Azure'a özgü; başka buluta geçmek bu parçaları yeniden yazmak demek.
Çoğu ekip için doğru cevap multi-cloud değil. Uygulamayı aynı anda iki bulutta çalıştırmak, her iki tarafın en düşük ortak paydasına inmek ve iki kat operasyon yükü demek. Daha gerçekçi olan, lock-in'i bilinçli seçmek: iş mantığını Bookstore.Api'de tutup sağlayıcıya özgü parçaları ince adaptörlerde toplamak (6. yazıdaki küçük resim üretimi function'dan ayrı bir class'ta duruyor) ve kısa bir çıkış planı yazmak: "Azure'dan çıkarsak neyi yeniden yazarız, ne kadar sürer?" Bu soruya bir sayfalık cevap, multi-cloud mimarisinden çok daha ucuz bir sigorta.
Landing Zone ve Bir Sonraki Adım: IaC
Bu serideki her şeyi portal ve az CLI script'leriyle kurduk. Script'ler belgeliyor ama sorunları var: aynı script'i iki kez çalıştırmak her komutta aynı sonucu vermez, birinin portal'dan yaptığı değişiklik script'e yansımaz ve "şu an Azure'da ne var" sorusunun cevabı hiçbir dosyada yazmaz. Infrastructure as Code bu üç sorunu çözer: istenen durum dosyada yazar, araç farkı bulur ve yalnız farkı uygular. Infrastructure as Code: Terraform serisi, Bookstore'un resource group'undan başlayarak bu serideki kaynakları Terraform ile yeniden kuruyor; ilk adım için "Infrastructure as Code Nedir? Terraform, OpenTofu, Bicep, Pulumi ve Ne Zaman Hangisi" yazısına bak.
Kuruluş büyüdükçe bir katman daha gerekiyor: landing zone. Cloud Adoption Framework'e göre landing zone iki parçadan oluşur: identity, ağ, policy ve izleme gibi ortak altyapıyı taşıyan platform landing zone ve her iş yükünün kendi subscription'larında çalıştığı workload landing zone'ları. Bookstore tek ekipli bir ürünken 2. yazıdaki subscription ve tag düzeni yeter; beşinci ekip geldiğinde ortak ağı, policy'leri ve log toplamayı bir platform ekibine vermek gerekir. İzleme tarafında ise bu seri bilerek yüzeyde kaldı: Observability serisi OpenTelemetry kurulumunu, metric'leri, trace'leri ve log backend'lerini ayrıntılı anlatıyor.
Seri Sonu Checklist'i
Bookstore'u buluta alırken bu seriden çıkan liste. Her madde bir yazıya karşılık geliyor ve her birinin cevabı "evet" olmadan production'a çıkmamak gerekiyor:
- Identity: insanlar gruplarla, uygulamalar managed identity ile erişiyor; kimsede kalıcı Owner yok, veri rolleri en dar scope'ta (3).
- Network: veritabanı ve storage private endpoint arkasında, public erişim bilinçli olarak açık ya da kapalı, private DNS zone'lar VNet'e link'li (9).
- Secret: kodda ve app setting'te account key, parola ya da connection string secret'ı yok; shared key kapalı, SAS (süresi ve yetkisi sınırlı erişim adresi) kısa ömürlü ve user delegation ile imzalı (3, 7, 8).
- Maliyet: her kaynak tag'li, budget ve bildirimleri kurulu, tahmin aracı güncel, geçici ortamların bitiş tarihi var (2, 10).
- Yedek: RPO ve RTO yazılı, restore en az bir kez denenmiş ve süresi ölçülmüş, soft delete açık (7, 8, 11).
- İzleme: health check'ler bağlı, poison queue ve bütçe alarmları birine gidiyor, log maliyeti kontrol altında (5, 6, 10).
Üretimde Dikkat
Hedefleri yaz. "Hızlı geri döneriz" bir hedef değil; "RTO 1 saat, RPO 15 dakika" bir hedef. Yazılı hedef olmadan hangi yapılandırmanın yeterli olduğuna karar veremezsin.
Mimari gözden geçirmeyi tekrarla. Well-Architected Review'u bir kez yapıp bırakma; büyük bir değişiklikten sonra ya da yılda iki kez Advisor'ın beş pillar'daki puanına ve önerilerine bak.
Chaos testini küçük başlat. Azure Chaos Studio hâlâ mevcut ve yeni modeli preview'da; ama ilk adım için bir dev ortamında SQL'i durdurup uygulamanın davranışını izlemek bile, 8. yazıda gördüğümüz gibi, kâğıt üzerinde görünmeyen şeyleri ortaya çıkarır.
Sık Düşülen Tuzaklar
Failover group'u yedek sanmak. Replikasyon hatayı da kopyalar: silinen tablo iki region'da birden silinir. Mantıksal hatalara karşı koruyan şey point-in-time restore ve long-term retention.
Zone redundancy'yi tek yerde açmak. Veritabanı zone-redundant ama App Service tek instance'lıysa, zincirin en zayıf halkası yine uygulama.
Lock-in korkusuyla managed servisten kaçmak. Kendi yönettiğin bir PostgreSQL VM'i taşınabilir ama yedeği, patch'i ve failover'ı senin işin; çoğu ekip için bu iş, lock-in'den daha pahalı.
IaC'yi her şey bittikten sonraya bırakmak. Kaynak sayısı arttıkça portal'da yapılmış değişiklikleri koda geri taşımak zorlaşır; ilk production ortamından önce başlamak en ucuzu.
Küçük Bir Deneme
★☆☆ Bookstore için RTO ve RPO hedeflerini bir paragrafta yaz ve hangi Azure SQL yapılandırmasının (yedek, zone redundancy, failover group) bu hedefi karşıladığını gerekçesiyle seç.
★★☆ restore-drill.cs'e son siparişin zamanını (MAX(PlacedAtUtc)) karşılaştıran bir kontrol ekle. Point-in-time restore'dan sonra bu değer, hangi veri kaybının yaşandığını nasıl gösterir?
★★★ Bookstore için bir sayfalık çıkış planı yaz: bu serideki her Azure parçasının (App Service, Container Apps, Functions, Blob Storage, Azure SQL, Entra ID, Front Door) AWS'deki karşılığını, taşınması gereken kodu ve tahmini süreyi bir tabloya koy.
Cloud Temelleri serisi burada bitiyor. Buradan sonraki doğal adım, bu serideki kaynakları kod olarak tanımlamak: Infrastructure as Code: Terraform serisi aynı Bookstore'u Terraform ile Azure'a kuruyor, izleme tarafını ise Observability serisi tamamlıyor.