
Monolitik Terraform State Bölme: IaC Plan Süresini 25 Dakikadan 45 Saniyeye Düşürmek

Giriş
Cuma akşamı saat 19:30. Production ortamındaki bir mikroserviste memory leak tespit edildi. Fix hazır, imaj tag’i güncellendi ve CI/CD pipeline’ı tetiklendi. Ancak deployment’ın gerçekleşmesi için Atlantis (Terraform Pull Request Automation) üzerinden terraform plan adımının tamamlanması gerekiyor. Tam 25 dakika 14 saniye. Sadece bir Kubernetes Deployment manifest’indeki imaj tag’ini değiştirmek için tüm AWS altyapısının (VPC, RDS, EKS, Redis, IAM) state refresh işleminden geçmesini bekliyoruz.
Bu senaryo, tek bir terraform.tfstate dosyasının 4800’den fazla kaynağı barındırdığı, 14.2 MB boyutuna ulaştığı monolitik altyapı günlerimizden bir kesit. IaC (Infrastructure as Code) süreçlerimizde yaşadığımız bu darboğaz, sadece bekleme sürelerini artırmakla kalmıyor, aynı zamanda blast radius (etki alanı) riskini de maksimize ediyordu. Bir IAM rolü eklerken yanlışlıkla RDS instance’ını silme veya modifiye etme riski, pipeline bekleme sürelerinden daha kritik bir tehditti.
Bu vaka çalışmasında, tek parça halindeki Terraform mimarimizi mantıksal katmanlara (Foundation, Data, Compute, Application) bölerek plan sürelerini %97 oranında (25 dakikadan 45 saniyeye) nasıl düşürdüğümüzü, geçiş operasyonundaki state taşıma (migration) adımlarını ve mimari kararlarda karşılaştığımız trade-off’ları teknik detaylarıyla inceleyeceğiz.
İçindekiler
- Problemin Anatomisi: API Rate Limitleri ve State Lock
- Mimari Karar: Katmanlı State Tasarımı (Layered State)
- Uygulama: Canlı Altyapıda State Taşıma (State Migration)
- Trade-off Analizi:
terraform_remote_statevs Data Sources - Sonuçlar ve Metrikler
- Pratik Öneriler / Production Notları
- Sık Sorulan Sorular
- Sonuç
Problemin Anatomisi: API Rate Limitleri ve State Lock
Terraform, varsayılan davranış olarak bir plan veya apply işlemi öncesinde state dosyasındaki kaynakların mevcut durumunu cloud sağlayıcısından sorgulayarak doğrular (Refresh fazı). Monolitik yapımızda 4800 kaynak bulunuyordu; bunların 1200’ü Security Group kuralları, 800’ü IAM policy’leri, 50 civarı RDS, ElastiCache ve OpenSearch cluster’larıydı.
AWS Throttling ve Exponential Backoff
terraform plan komutu çalıştığında, Terraform eşzamanlı olarak (bizde performans için -parallelism=50 olarak ayarlıydı) AWS API’lerine Describe istekleri atıyordu. Saniyeler içinde binlerce DescribeInstances, DescribeNetworkInterfaces ve GetRole isteği AWS API limitlerine (Throttling) takılıyordu. Terraform, Rate exceeded HTTP 429 hataları aldığında exponential backoff (katlanarak artan bekleme süresi) algoritmasını devreye sokuyor, istekleri yavaşlatıyor ve normalde 3-4 dakikada bitebilecek refresh işlemi 25 dakikalara kadar uzuyordu.
State Lock Kuyrukları
SRE ekibi, Backend ekibi ve Data ekibi aynı state dosyası üzerinde çalıştığı için DynamoDB üzerindeki LockID mekanizması tam bir darboğaz yaratmıştı. Data ekibi yeni bir S3 bucket oluşturduğunda state kitleniyor (lock), Backend ekibinin acil bir ECS task tanımı güncellemesi kuyrukta lock’un çözülmesini 30 dakika bekliyordu.
Mimari Karar: Katmanlı State Tasarımı (Layered State)
State dosyasını bölmek teoride basit görünse de, kaynaklar arası bağımlılıklar (örneğin RDS instance’ının VPC Subnet ID’lerine ihtiyaç duyması) sebebiyle ciddi bir sınır çizme (boundary) kararı gerektirir. Üç farklı state bölme stratejisi değerlendirdik:
- Ortam Bazlı (Environment-based): Prod, Staging, Dev. Zaten uygulanıyordu ancak sadece Prod ortamının kendisi bile 4800 kaynağa sahipti.
- Takım Bazlı (Team-based): Data, Backend, Frontend. Kaynakların ortak kullanımı nedeniyle çok fazla dairesel bağımlılık (circular dependency) yarattı.
- Bileşen ve Değişim Frekansı Bazlı (Lifecycle-based): Kaynakların ne sıklıkla değiştiğine göre ayrıştırılması.
Kararımız 3. seçenek oldu. Altyapıyı değişim frekansına (Rate of Change) göre 4 katmana ayırdık:
| Katman (Layer) | İçerik | Değişim Frekansı | Plan Süresi Hedefi |
|---|---|---|---|
| L1 – Foundation | VPC, Subnets, Transit Gateway, Route53 Zones | Yılda 1-2 kez | < 30 saniye |
| L2 – Data | RDS, ElastiCache, MSK, S3 Buckets, KMS Keys | Ayda 1-2 kez | < 2 dakika |
| L3 – Compute | EKS Clusters, ECS Clusters, ASG, Base IAM Roles | Haftada 1-2 kez | < 3 dakika |
| L4 – Application | ALB Rules, Target Groups, App-specific IAM, Deployments | Günde 10+ kez | < 45 saniye |
Uygulama: Canlı Altyapıda State Taşıma (State Migration)
Production ortamında çalışan 4800 kaynağı silip (destroy) baştan yaratmak kesinlikle masada değildi. Kaynakları mevcut monolitik state dosyasından yeni oluşturduğumuz L1, L2, L3 ve L4 state dosyalarına sıfır kesinti (zero-downtime) ile taşımamız gerekiyordu.
Adım 1: Kodun Ayrıştırılması (Refactoring)
Öncelikle tek bir dizinde duran .tf dosyalarını, yeni oluşturduğumuz repository’lere veya katman klasörlerine taşıdık. Ancak unutulmamalıdır ki, dosyaları taşımak sadece HCL kodunu taşımaktır, Terraform state’in yeni konumdan haberi yoktur.
Adım 2: State Taşıma Script’i (State Move)
Terraform’un state mv komutunu kullanarak JSON objelerini eski yapıdan koparıp yenisine dikmemiz gerekiyordu. 4800 kaynak için bu işlemi manuel yapmak felakete davetiye çıkaracağı için lokalde çalışan bir bash betiği kullandık.
#!/bin/bash
# migrate_l1_foundation.sh
# Monolitik state'ten L1-Foundation state'ine Network kaynaklarını taşır
SOURCE_STATE="monolith.tfstate"
DEST_STATE="l1_foundation.tfstate"
# Mevcut monolitik state'i lokal'e çek (S3'ten okur)
terraform state pull > $SOURCE_STATE
# L1 state'i (boş olarak initialize edilmiş) lokal'e çek
cd ../l1-foundation
terraform state pull > $DEST_STATE
cd ../monolith
# Taşınacak kaynakların listesi (Hedefli)
TARGETS=(
"module.vpc.aws_vpc.main"
"module.vpc.aws_subnet.private"
"module.vpc.aws_subnet.public"
"module.vpc.aws_nat_gateway.main"
)
for TARGET in "${TARGETS[@]}"; do
echo "$TARGET taşınıyor..."
# state mv komutu lokal dosyalar üzerinde çalıştırılır
terraform state mv -state=$SOURCE_STATE -state-out=../l1-foundation/$DEST_STATE "$TARGET" "$TARGET"
done
# Güncellenmiş (kaynakları silinmiş) monolith state'i S3'e geri gönder
echo "Monolith state güncelleniyor..."
terraform state push $SOURCE_STATE
# Yeni L1 state'ini (kaynaklar eklenmiş) S3'e gönder
cd ../l1-foundation
echo "L1 Foundation state güncelleniyor..."
terraform state push $DEST_STATE
Production Uyarısı: Bu işlemi yaparken Atlantis CI/CD süreçlerini durdurduk. Operasyonu gerçekleştiren admin IAM rolü dışında, tüm CD rollerinin AWS yetkilerini geçici olarak
ReadOnlyAccess‘e çektik. Böylece state taşıma sırasında yanlışlıkla tetiklenen bir pipeline’ın, taşınan kaynakları “silinmiş” zannedipdestroytetiklemesini engelledik.
Trade-off Analizi: terraform_remote_state vs Data Sources
State’leri böldükten sonra L2 (Data) katmanındaki RDS instance’ının, L1 (Foundation) katmanındaki VPC ve Subnet ID’lerine ihtiyacı oldu. Çapraz state referansları (cross-state references) için önümüzde iki ana seçenek vardı ve bu karar mimarimizin esnekliğini belirleyecekti.
Seçenek A: terraform_remote_state Veri Kaynağı
Bir state dosyasının, diğer state dosyasının çıktılarını (outputs) okuması mantığına dayanır.
data "terraform_remote_state" "foundation" {
backend = "s3"
config = {
bucket = "company-tf-state-prod"
key = "foundation/terraform.tfstate"
region = "eu-central-1"
}
}
resource "aws_db_subnet_group" "main" {
name = "main"
subnet_ids = data.terraform_remote_state.foundation.outputs.private_subnet_ids
}
Avantajı: Kurulumu maliyetsizdir. Sadece hedeflenen state’in S3 yolunu göstermek yeterlidir.
Dezavantajı (Trade-off): Katı bağımlılık (Tight coupling) yaratır. L1 state’inin yapısı değiştiğinde (örneğin output değişkeninin adı değiştiğinde), L2 state’i plan aşamasında hata verir. Daha büyük problem güvenliktir: L2 katmanını yöneten IAM rolünün, L1 state’indeki hassas verileri de okuma (S3 GetObject) yetkisine sahip olması gerekir.
Seçenek B: AWS SSM Parameter Store ve Native Data Sources (Seçtiğimiz Yöntem)
L1 katmanı, altyapının kilit kimliklerini (ID’lerini) AWS SSM Parameter Store’a yazar. L2 ve L3 katmanları ise native AWS data source’ları kullanarak bu parametreleri çeker.
# L1 Katmanı - Terraform ile SSM'e VPC ID yazılır
resource "aws_ssm_parameter" "vpc_id" {
name = "/infrastructure/l1/vpc_id"
type = "String"
value = module.vpc.vpc_id
}
# L2 Katmanı - SSM'den sadece ilgili veri okunur
data "aws_ssm_parameter" "vpc_id" {
name = "/infrastructure/l1/vpc_id"
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_ssm_parameter.vpc_id.value]
}
filter {
name = "tag:Tier"
values = ["Private"]
}
}
Neden Seçtik? Mükemmel bir gevşek bağlılık (Loose coupling) sağlar. L2 katmanı, L1 state dosyasına hiçbir şekilde temas etmez, hatta S3 bucket lokasyonunu bilmez. SSM Parameter Store üzerinden ince taneli (fine-grained) IAM yetkilendirmesi yaparak Data ekibinin sadece /infrastructure/l1/* parametrelerini okumasını, geri kalan Foundation state şifrelerine erişememesini sağladık.
Sonuçlar ve Metrikler
Monolitik yapıyı 4 farklı yapıya böldükten sonra elde ettiğimiz ölçülebilir metrikler şu şekildedir:
| Metrik | Öncesi (Monolitik) | Sonrası (Katmanlı L4 App) | Değişim Oranı |
|---|---|---|---|
Ortalama terraform plan Süresi |
25m 14s | 42s | %97 düşüş |
Ortalama terraform apply Süresi |
32m 40s | 1m 15s | %96 düşüş |
| State Dosyası Boyutu | 14.2 MB | 120 KB (L4 için) | %99 düşüş |
| Haftalık State Lock Bekleme Süresi | ~38 saat (Ekip geneli) | ~1.5 saat | %96 düşüş |
| API Throttling Hata Sayısı (Aylık) | 140+ | 0 | %100 düşüş |
Sayıların ötesindeki en büyük kazanım, geliştirici deneyimi (Developer Experience) oldu. Geliştiriciler artık basit bir Target Group değişikliği için 25 dakika beklemek zorunda kalmadığından bağlam değişimi (context switching) maliyetleri minimize edildi.
Pratik Öneriler / Production Notları
Bu çapta bir yeniden yapılandırma sürecine girecek sistem mühendisleri için sahada ödediğimiz bedellerden çıkardığımız dersler:
- S3 Versiyonlamayı Asla Kapatmayın: State migration sırasında hata yapma olasılığınız matematiksell olarak çok yüksektir. S3 bucket versiyonlaması aktif değilse,
terraform state pushile yanlış bir JSON ezdiğinizde tüm state geçmişiniz yok olur. Operasyon öncesi mevcut bucket’ın AWS CLI ile snapshot’ını alın. - Targeted Plan Kullanmayın: Geçici bir performans çözümü olarak
terraform plan -target=module.appkullanmayı düşünebilirsiniz, ancak bu kesinlikle bir anti-pattern’dir. Hedefli planlar, resource bağımlılık ağacını atladığı için state drift’e (kaymasına) neden olur. Kalıcı çözüm konfigürasyonu fiziksel olarak bölmektir. - Drift Detection Kurun: Katmanlı yapıya geçildiğinde, konsol üzerinden manuel yapılan değişiklikleri (ClickOps) tespit etmek zorlaşır. L1, L2 ve L3 katmanları için her gece saat 03:00’te otomatik
terraform plan -detailed-exitcodeçalıştıran bir pipeline kurduk. Exit code2(Değişiklik var) döndüğünde, SRE ekibine Slack üzerinden P2 alert gönderiliyor. - Strict Module Pinning: Katmanlar bölündüğünde ortak kullanılan Terraform modüllerinin versiyonlarını kesin olarak kilitleyin (örn:
version = "3.1.0"). Pinned edilmeyen (örn:~> 3.1) modüller, aynı gün L2 ve L3 katmanları çalıştığında farklı modül versiyonlarının inmesine yol açarak katmanlar arası tutarsızlığa neden olur.
Sık Sorulan Sorular
Terragrunt Neden Kullanmadınız?
Terragrunt’ın dependency blokları cross-state referanslarını harika çözüyor ve DRY (Don’t Repeat Yourself) prensibini dayatıyor. Ancak ekibimizin kullandığı Atlantis pipeline yapısı, tflint, tfsec ve Terraform LS (Language Server) gibi araçlar HCL standardında çalıştığı için Terragrunt sarmalayıcısı (wrapper) ile tam uyumlu entegre edilemedi. CI/CD araç zincirimizi kırmamak adına Terraform’un native veri kaynaklarını (Data Sources) ve AWS SSM’i tercih ettik. Sıfırdan bir mimari kuruyor olsaydık Terragrunt değerlendirilebilirdi.
L1’de yapılan bir değişiklik, bağımlı L4 kaynaklarını nasıl güncelliyor?
SSM mantığı kullandığımız için, L1’de örneğin yeni bir subnet eklendiğinde SSM parametresi güncellenir ancak L4 pipeline’ı otomatik olarak bunu algılamaz. Bağımlı katmanları haberdar etmek için CI/CD süreçlerimizde Downstream Trigger mekanizması kurduk. L1 pipeline’ı başarıyla apply edildiğinde, GitHub Actions repository_dispatch eventi üzerinden L2 ve L3 pipeline’larını sadece plan modunda tetikliyoruz. Bu planların sonuçları gözden geçirilmek üzere SRE onayına (Manual Approval) düşüyor.
State bölerken “Orphaned Resources” (Sahipsiz Kaynaklar) sorununu nasıl çözdünüz?
terraform state mv komutu çalışırken bir kaynağı eski state’ten silip yeni state’e ekler. Eğer süreç network kesintisi ile yarıda kalırsa, kaynak eski state’ten silinmiş ama yeni state’e yazılamamış olabilir (Orphaned). Bu riske girmemek için taşıma işlemlerini remote’da değil, tamamen lokaldeki JSON dosyaları üzerinde izole olarak yaptık. terraform plan ile iki lokal state’in de AWS tarafında hiçbir değişiklik yapmayacağını (No changes) doğruladıktan sonra terraform state push ile güvenle S3’e yükledik.
Terraform Workspace’ler state bölmek için alternatif olabilir miydi?
Hayır. Terraform Workspace’leri, aynı konfigürasyon kodunu farklı ortamlar (prod, staging, dev) için izole çalıştırmak üzere tasarlanmıştır. Monolitik bir yapıdaki kaynak sayısını veya API request yükünü yatayda azaltmaz. Workspace başına yine 4800 kaynağın refresh edilmesi gerekecekti, bu da asıl problemimizi (Throttling) çözmeyecekti.
Sonuç
Monolitik bir Terraform state dosyasını parçalamak, sadece .tf dosyalarını farklı klasörlere taşımak değildir; bu işlem kaynak yaşam döngüleri (lifecycle), takım otonomisi ve CI/CD güvenlik sınırlarının yeniden inşa edildiği kapsamlı bir altyapı refactoring sürecidir. Bağımlılıkları katmanlı yapıya bölerek ve AWS SSM gibi native state paylaştırma çözümlerini araya sokarak, bir zamanlar 25 dakikayı aşan pipeline’ları 45 saniyeye indirmeyi başardık.
Eğer sizin de terraform plan süreleriniz 5 dakikayı geçmeye başladıysa ve state lock kuyrukları deployment süreçlerinizi yavaşlatıyorsa, mevcut altyapınızı “Değişim Frekansı” (Rate of Change) metodolojisiyle analiz etmeye bugün başlayın. En az değişen Foundation (Network) katmanını, sürekli değişen Application kodunuzdan koparmak, atacağınız ilk ve en kritik adım olacaktır.
Bunları da beğenebilirsiniz

React Nedir? Neden Kullanmalıyız, Nasıl başlarız, Neler Yapabiliriz?
Ön uç çerçeveleri ve kitaplıkları, web geliştirme sürecinin önemli bir yönüdür. Yüksek performanslı duyarlı web siteleri ve web tabanlı uygulamalar oluşturmak için kitaplıkları kullanmak zorunlu…

