Plan ve Apply: Terraform ile Güvenli Değişiklik Akışı

Altyapı değişikliklerini yönetmek, özellikle production ortamlarında, her sysadmin’in kabuslarından biri olabilir. Yanlış bir komut, beklenmedik bir silme operasyonu veya gözden kaçan bir bağımlılık; saatler süren kesintilere, veri kayıplarına ve müşteri şikayetlerine yol açabilir. Terraform’un plan ve apply iş akışı, tam da bu noktada hayat kurtarıcı oluyor. Bu yazıda, bu iki komutun neden sadece birer CLI komutu olmadığını, doğru kullanıldığında nasıl güvenli bir değişiklik kapısına dönüştüğünü gerçek dünya senaryolarıyla ele alacağız.

Terraform Plan ve Apply’ın Temeli

Terraform’u ilk kez kullananlar için kısa bir özet geçelim. Terraform, altyapınızı kod olarak tanımlamanızı sağlayan bir araç. Siz .tf dosyalarında “ne olmasını istiyorum” diye yazıyorsunuz, Terraform ise mevcut durumla karşılaştırarak “ne yapmalıyım” sorusunu cevaplayıp uyguluyor.

Bu süreç iki kritik adıma ayrılıyor:

  • terraform plan: Mevcut durum ile istenen durum arasındaki farkı hesaplar, yapılacak değişikliklerin bir önizlemesini sunar
  • terraform apply: Hesaplanan değişiklikleri gerçekten uygular

Basit görünüyor değil mi? Ama derinlere indiğinizde bu iş akışının nüansları, production güvenliğinin bel kemiğini oluşturuyor.

Plan Çıktısını Okumak: Semboller ve Anlamları

terraform plan çalıştırdığınızda karşınıza çıkan çıktıyı düzgün okuyabilmek çok önemli. Her satırın başındaki sembol size yapılacak işlemi anlatıyor.

terraform plan -out=tfplan

# Örnek çıktı:
# + create
# - destroy
# ~ update in-place
# -/+ destroy and then create (replacement)
# <= read

Burada en dikkat edilmesi gereken sembol -/+ yani “destroy and then create”. Bu, kaynağın silinip yeniden oluşturulacağı anlamına gelir. Örneğin bir EC2 instance’ının AMI’sini değiştirdiğinizde ya da bir RDS cluster’ının engine version’ını güncellemek istediğinizde bu sembolü görürsünüz. Production’da bu sembolü fark etmeden apply çalıştırmak, veri kaybına veya ciddi kesintiye yol açabilir.

Gerçek bir senaryo: Bir müşteri projesinde RDS instance’ının storage_type‘ını gp2‘den gp3‘e geçirmeye çalışıyorduk. Plan çıktısında ~ (update in-place) yerine -/+ gördük. Demek ki AWS bu değişikliği yerinde yapmıyor, instance’ı yeniden oluşturuyordu. Eğer planı dikkatlice okumadan apply çalıştırseydik, veritabanı birkaç dakika offline kalacaktı.

Plan Dosyasına Kaydetmek: Neden Şart?

terraform plan çıktısını doğrudan apply’a geçirmek yerine, planı bir dosyaya kaydedip o dosyayı apply’a vermek en iyi pratiklerden biri.

# Planı dosyaya kaydet
terraform plan -out=tfplan.binary

# Kaydedilen planı uygula
terraform apply tfplan.binary

Bu yaklaşımın neden kritik olduğunu şöyle açıklayayım: terraform plan ile terraform apply arasında geçen sürede altyapınız değişebilir. Başka bir ekip üyesi farklı bir değişiklik uygulamış olabilir, cloud provider’da geçici bir durum oluşmuş olabilir. Planı dosyaya kaydedip apply’a verdiğinizde, Terraform tam olarak incelediğiniz değişiklikleri uygular, ne fazla ne eksik.

CI/CD pipeline’larında bu yaklaşım neredeyse zorunlu. Önce plan çalışır, çıktı artifact olarak saklanır, sonra manuel onay alınır, ardından aynı plan dosyasıyla apply çalışır. Bu döngü, “review ettiğimiz şeyin uygulandığından emin olmak” garantisini sağlar.

# Plan dosyasını human-readable formata çevirmek için
terraform show tfplan.binary

# JSON formatına çevirmek için (CI araçlarında işlemek için)
terraform show -json tfplan.binary > tfplan.json

Detaylı Plan Analizi: -refresh ve -target Seçenekleri

Bazen tüm altyapıyı yeniden hesaplamak istemezsiniz ya da sadece belirli bir kaynağı değiştirmek istiyorsunuzdur.

# State'i refresh etmeden plan al (hızlı ama dikkatli kullanın)
terraform plan -refresh=false

# Sadece belirli bir kaynağı hedefle
terraform plan -target=aws_instance.web_server

# Birden fazla hedef
terraform plan 
  -target=aws_instance.web_server 
  -target=aws_security_group.web_sg

-target kullanımı hakkında önemli bir uyarı: Bu seçenek güçlüdür ama bağımlılıkları görmezden gelmenize yol açabilir. Production’da sadece acil müdahalelerde veya büyük altyapılarda belirli bir modülü izole etmek için kullanın. Günlük rutin değişikliklerde tüm altyapıya plan çalıştırmak daha güvenli.

-refresh=false ise state dosyasının cloud provider’a sorgu atmadan kullanılması anlamına gelir. Yüzlerce kaynağınız varsa ve sadece kod değişikliklerini kontrol etmek istiyorsanız zaman kazandırır, ancak state ile gerçek durum arasındaki farkları göremezsiniz.

Var Dosyaları ile Güvenli Parametre Yönetimi

Production ve staging ortamları için farklı değişken dosyaları kullanmak, hem güvenliği hem de tekrar kullanılabilirliği artırır.

# production.tfvars
# environment = "production"
# instance_type = "m5.xlarge"
# min_capacity = 3

# staging.tfvars  
# environment = "staging"
# instance_type = "t3.medium"
# min_capacity = 1

# Production için plan
terraform plan 
  -var-file="environments/production.tfvars" 
  -out=production.tfplan

# Staging için plan
terraform plan 
  -var-file="environments/staging.tfvars" 
  -out=staging.tfplan

Bu yapıyı CI/CD pipeline’ınızla birleştirdiğinizde, environment başına ayrı pipeline’lar oluşturabilir ve her ortam için ayrı onay süreçleri uygulayabilirsiniz.

Apply Aşamasında Güvenlik Önlemleri

Apply komutunun kendisi de çeşitli güvenlik mekanizmaları sunuyor.

# İnteraktif onay istemi (default davranış)
terraform apply tfplan.binary

# Otomatik onay (CI/CD'de kullanın, production'da dikkatli olun)
terraform apply -auto-approve tfplan.binary

# Paralel işlem sayısını sınırla (rate limiting için)
terraform apply -parallelism=5 tfplan.binary

-auto-approve seçeneği CI/CD pipeline’larında neredeyse zorunlu olsa da, production ortamları için bu komutun çalışması öncesinde pipeline seviyesinde manuel onay adımı eklemenizi öneririm. Sadece Terraform seviyesinde değil, pipeline seviyesinde de bir insan gözü olsun.

-parallelism seçeneği genellikle göz ardı edilir. Default değer 10’dur, yani Terraform aynı anda 10 işlemi paralel çalıştırır. Eğer cloud provider’ınızda API rate limit sorunları yaşıyorsanız bu değeri düşürmek problemi çözebilir. Özellikle büyük altyapılarda (50+ kaynak) yeni bir apply çalıştırırken bunu 5-10 arasında tutmak iyi bir pratik.

Destroy Planını Güvenli Yönetmek

terraform destroy komutunu direkt çalıştırmak yerine plan yaklaşımını burada da kullanabilirsiniz.

# Destroy planı oluştur ve dosyaya kaydet
terraform plan -destroy -out=destroy.tfplan

# Destroy planını incele
terraform show destroy.tfplan

# Onaylandıktan sonra uygula
terraform apply destroy.tfplan

Production’da kaynak silme operasyonlarında bu akışı neredeyse zorunlu tutuyorum. Bir keresinde bir microservice stack’ini kaldırırken bu yaklaşım sayesinde, silme planında beklenmedik şekilde bir RDS snapshot politikasının da hedeflenmediğini fark ettik. Plan dosyasını incelemeden direkt destroy çalıştırsaydık, uzun dönem backup politikamız da gidecekti.

Lifecycle Kuralları ile Kritik Kaynakları Korumak

Terraform kodu içinde de güvenlik katmanları oluşturabilirsiniz. lifecycle bloğu bu konuda güçlü araçlar sunuyor.

resource "aws_db_instance" "production_db" {
  identifier        = "prod-database"
  engine            = "postgres"
  instance_class    = "db.m5.large"
  allocated_storage = 100

  lifecycle {
    prevent_destroy = true
    
    # Bu attributelar değişirse kaynağı yeniden oluşturma, ignore et
    ignore_changes = [
      password,
      snapshot_identifier
    ]
    
    # Önce yeni kaynak oluştur, sonra eskisini sil
    create_before_destroy = true
  }
}

prevent_destroy: Bu özellik aktifken terraform destroy veya kaynağı kaldıran bir plan çalıştırırsanız Terraform hata verir. Production veritabanları, kritik S3 bucket’ları, load balancer’lar için bu seçeneği mutlaka aktif edin.

ignore_changes: Terraform dışında yönetilen değişiklikleri ignore etmek için kullanılır. Örneğin veritabanı şifreleri rotation policy ile otomatik değişiyorsa, Terraform’un bunu “drift” olarak algılamasını ve plan’da değişiklik göstermesini engellemek için kullanabilirsiniz.

create_before_destroy: Özellikle zero-downtime deployment’larda kritik. Eski kaynağı silmeden önce yeni kaynağı oluşturur, yük dengeleyici veya DNS geçişi yapıldıktan sonra eskisini siler.

CI/CD Pipeline Entegrasyonu: Gerçekçi Bir Örnek

GitLab CI üzerinde Terraform plan-apply iş akışının nasıl kurulacağını gösterelim.

# .gitlab-ci.yml
stages:
  - validate
  - plan
  - apply

variables:
  TF_ROOT: ${CI_PROJECT_DIR}/infrastructure
  TF_STATE_NAME: production

terraform:validate:
  stage: validate
  image: hashicorp/terraform:1.6.0
  script:
    - cd ${TF_ROOT}
    - terraform init -backend=false
    - terraform validate
    - terraform fmt -check -recursive
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

terraform:plan:
  stage: plan
  image: hashicorp/terraform:1.6.0
  script:
    - cd ${TF_ROOT}
    - terraform init
    - terraform plan 
        -var-file="environments/production.tfvars" 
        -out=tfplan.binary
    - terraform show -json tfplan.binary > tfplan.json
  artifacts:
    paths:
      - ${TF_ROOT}/tfplan.binary
      - ${TF_ROOT}/tfplan.json
    expire_in: 1 week
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

terraform:apply:
  stage: apply
  image: hashicorp/terraform:1.6.0
  script:
    - cd ${TF_ROOT}
    - terraform init
    - terraform apply tfplan.binary
  dependencies:
    - terraform:plan
  when: manual
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Bu pipeline’da dikkat edilmesi gereken nokta, apply stage’inin when: manual olarak ayarlanmış olması. Bu sayede plan otomatik çalışır, çıktıyı artifact olarak saklar, ama uygulamak için bir insan müdahalesi gerekir. Plan artifact’ı 1 hafta saklanır, bu da “geçen hafta hangi plan onaylandı ve ne değişti” sorusunu yanıtlamanızı kolaylaştırır.

Plan Çıktısını Programatik Olarak Analiz Etmek

Büyük ekiplerde, plan çıktısındaki tehlikeli operasyonları otomatik olarak tespit edip uyarı vermek isteyebilirsiniz. JSON formatındaki plan çıktısı bunu kolaylaştırıyor.

# Plan JSON çıktısını analiz eden basit bir script
#!/bin/bash

PLAN_FILE="tfplan.json"
DANGEROUS_ACTIONS=("delete" "replace")
WARNING_COUNT=0

echo "Plan analizi basliyor..."

for action in "${DANGEROUS_ACTIONS[@]}"; do
  COUNT=$(jq --arg action "$action" 
    '[.resource_changes[] | 
     select(.change.actions[] | 
     contains($action))] | length' 
    "$PLAN_FILE")
  
  if [ "$COUNT" -gt 0 ]; then
    echo "UYARI: $COUNT adet '$action' operasyonu tespit edildi!"
    
    jq --arg action "$action" 
      '.resource_changes[] | 
       select(.change.actions[] | contains($action)) | 
       .address' 
      "$PLAN_FILE"
    
    WARNING_COUNT=$((WARNING_COUNT + 1))
  fi
done

if [ "$WARNING_COUNT" -gt 0 ]; then
  echo ""
  echo "Tehlikeli operasyonlar tespit edildi. Lutfen plani dikkatlice inceleyin."
  exit 1
fi

echo "Plan temiz gorunuyor. Devam edebilirsiniz."
exit 0

Bu script, plan JSON’undaki tüm “delete” ve “replace” operasyonlarını tespit eder ve bulunursa exit code 1 ile çıkar. CI/CD pipeline’ınıza bu scripti ekleyerek tehlikeli operasyonları otomatik olarak flag’leyebilir, ekibinizi uyarabilirsiniz.

State Lock Mekanizması ve Çakışma Yönetimi

Birden fazla kişi veya pipeline aynı anda plan-apply çalıştırmaya çalıştığında ne olur? Terraform’un state locking mekanizması devreye girer.

# Apply sırasında başka bir işlem zaten lock almışsa:
# Error: Error acquiring the state lock
# 
# Lock bilgisini görme
terraform force-unlock LOCK_ID

# Plan sırasında mevcut lock'ları listeleme (S3 backend için)
aws s3api list-objects 
  --bucket terraform-state-bucket 
  --prefix locks/

DynamoDB ile S3 backend kullanıyorsanız lock bilgisi otomatik olarak yönetilir. Eğer bir pipeline yarıda kesilir ve lock release edilmezse, terraform force-unlock komutuyla manuel olarak kaldırabilirsiniz. Ancak bunu yapmadan önce gerçekten başka bir işlemin devam etmediğinden emin olun, aksi halde state dosyasında corruption yaşanabilir.

Production ortamlarında şu kuralı uyguluyorum: Tek seferde tek bir apply. Pipeline’larınızı buna göre dizayn edin, eş zamanlı apply’ları pipeline seviyesinde de kilitlemek için araçların “resource lock” özelliklerini kullanın (GitLab’da environments, GitHub Actions’ta concurrency groups).

Drift Tespiti ve Düzenli Plan Çalıştırma

Altyapı drift’i, cloud console’dan veya başka araçlarla yapılan değişikliklerin Terraform state’iyle uyumsuz hale gelmesi durumu. Bunu tespit etmek için düzenli aralıklarla plan çalıştırmak iyi bir pratik.

#!/bin/bash
# drift-detection.sh - Cron ile her gece çalıştırın

ENVIRONMENTS=("production" "staging" "development")
DRIFT_DETECTED=false

for ENV in "${ENVIRONMENTS[@]}"; do
  echo "[$ENV] Drift kontrolu basliyor..."
  
  cd "infrastructure/environments/$ENV"
  
  terraform init -no-color > /dev/null 2>&1
  
  # Refresh yaparak mevcut durumu al
  PLAN_OUTPUT=$(terraform plan 
    -detailed-exitcode 
    -no-color 2>&1)
  
  EXIT_CODE=$?
  
  # Exit code 2 = değişiklik var, 0 = değişiklik yok, 1 = hata
  case $EXIT_CODE in
    0)
      echo "[$ENV] Drift tespit edilmedi. Temiz."
      ;;
    2)
      echo "[$ENV] DRIFT TESPIT EDILDI!"
      echo "$PLAN_OUTPUT" | grep -E "^[~+-]"
      DRIFT_DETECTED=true
      # Slack notification veya email gönder
      ;;
    1)
      echo "[$ENV] HATA: Plan calistirilamadi"
      ;;
  esac
done

if [ "$DRIFT_DETECTED" = true ]; then
  # Ops ekibine bildirim gönder
  echo "Drift bildirimi gonderildi"
  exit 1
fi

-detailed-exitcode seçeneği bu kullanım için kritik. Normal terraform plan her durumda exit code 0 döner. Bu seçenekle değişiklik varsa 2, yoksa 0, hata varsa 1 döner. Bu sayede script’inizde durumu programatik olarak kontrol edebilirsiniz.

Production Öncesi Son Kontrol Listesi

Yıllarca production altyapısı yönetirken oluşturduğum mental kontrol listesini paylaşayım. Her apply öncesi bu soruları soruyorum kendime:

  • Destroy sayısı: Plan çıktısında kaç tane destroy operasyonu var? Beklediğimden fazla mı?
  • Replace operasyonları: -/+ sembolü olan kaynaklar var mı? Downtime yaratır mı?
  • Kritik kaynaklar: Veritabanı, load balancer, DNS kaydı gibi kritik kaynaklar etkileniyor mu?
  • Bağımlılık sırası: Bağımlı kaynakların destroy/create sırası mantıklı mı?
  • State lock: Başka bir apply devam ediyor mu?
  • Rollback planı: Bir şeyler ters giderse nasıl geri döneceğim?
  • Maintenance window: Bu değişiklik için doğru zaman mı?
# Hızlı özet için plan istatistiklerini çıkar
terraform show -json tfplan.binary | jq '
  .resource_changes | 
  group_by(.change.actions[]) | 
  map({
    action: .[0].change.actions[0], 
    count: length,
    resources: map(.address)
  })'

Bu komut size “kaç create, kaç update, kaç destroy var ve hangi kaynaklar” şeklinde temiz bir özet verir. Büyük plan’larda önce bu özeti bakıp sonra detaylara inmek zaman kazandırır.

Sonuç

Terraform’un plan-apply iş akışı, altyapı değişikliklerini kör uçuştan görüş mesafeli uçuşa çeviriyor. Ancak bu araçların sunduğu güvenlik mekanizmalarını aktif olarak kullanmak gerekiyor. Planı dosyaya kaydetmeden apply çalıştırmak, lifecycle kurallarını ihmal etmek, CI/CD’de manuel onay adımını atlamak, bunların hepsi güvenlik ağınızda açık bırakmak demek.

Özetlemek gerekirse en önemli pratikler şunlar: Her zaman planı dosyaya kaydedin ve o dosyayla apply çalıştırın. -/+ ve - sembollerine ekstra dikkat edin. Kritik kaynaklar için prevent_destroy kullanın. CI/CD pipeline’larında plan ve apply’ı ayrı stage’lere ayırın ve apply’a manuel onay ekleyin. Düzenli drift detection çalıştırın. Plan çıktısını JSON formatına çevirip programatik analize tabi tutun.

Bu iş akışlarını oturttuğunuzda, altyapı değişiklikleri artık “bakalım ne olacak” macerası olmaktan çıkıp, öngörülebilir ve geri alınabilir operasyonlara dönüşüyor. Ve bir sysadmin olarak en değerli şeyin ne olduğunu hepimiz biliyoruz: gece rahat uyumak.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir