Terraform ile Cloudflare DNS Yönetimi

Cloudflare DNS kayıtlarını elle yönetmek başlangıçta masum görünür. Bir kayıt eklersin, bir tanesini silersin, işler yolunda gider. Ama zamanla onlarca subdomain, birden fazla domain ve farklı servisler için DNS yapılandırmaları birikiyor. Bir gün “bu A kaydını ben mi ekledim, ne zaman ekledim, neden ekledim?” sorusunu sormaya başlıyorsun. İşte tam bu noktada Terraform devreye giriyor.

Infrastructure as Code yaklaşımı DNS yönetimini kökten değiştiriyor. Cloudflare’in harika bir API’ı var ve Terraform’un resmi Cloudflare provider’ı bu API’ı tam anlamıyla kullanıyor. DNS kayıtlarını kod olarak yönetmek; versiyon kontrolü, ekip işbirliği, audit trail ve hata yapma riskini minimize etme gibi avantajlar sunuyor.

Neden Terraform ile DNS Yönetimi?

Manuel DNS yönetiminin acısını çekmiş biri olarak şunu söyleyebilirim: Cloudflare paneline girip “şu MX kaydını kim silmiş?” diye bakarken yaşadığın çaresizlik, seni IaC’a iten en güçlü motivasyon oluyor.

Terraform ile DNS yönetmenin somut faydaları şunlar:

  • Versiyon kontrolü: Tüm DNS değişiklikleri Git geçmişinde kayıtlı. Kim ne değiştirmiş, ne zaman değiştirmiş, neden değiştirmiş.
  • Code review süreci: Kritik bir DNS değişikliği yapmadan önce ekip arkadaşlarının gözünden geçirmesi.
  • Tekrarlanabilirlik: Aynı DNS yapısını farklı domainler için kolayca kopyalayabilirsin.
  • Drift detection: Birisi panelden elle bir şey değiştirdiyse terraform plan bunu hemen yakalar.
  • Toplu değişiklikler: 50 subdomain için TTL değerini tek seferde değiştirmek artık birkaç satır kod meselesi.

Ortam Hazırlığı

Başlamadan önce birkaç şeye ihtiyacın var. Terraform kurulu olmalı, Cloudflare hesabın ve yönetmek istediğin domain olmalı.

Cloudflare API Token Oluşturma

Eski yöntem olan Global API Key kullanmak yerine scoped API token kullanmanı şiddetle tavsiye ederim. Cloudflare dashboard’una gir, sağ üstten profiline tıkla, “API Tokens” sekmesine geç.

“Create Token” butonuna bas ve “Edit zone DNS” template’ini seç. Zone’unu seç, gerekirse IP restriction ekle. Token’ı oluşturduktan sonra bir yere kaydet, bir daha göremeyeceksin.

Proje Yapısı

Küçük projeler için bile iyi bir dosya yapısı kurmak önemli. Ben genellikle şunu kullanıyorum:

cloudflare-dns/
├── main.tf
├── variables.tf
├── terraform.tfvars
├── outputs.tf
└── records/
    ├── example-com.tf
    └── otherdomain-com.tf

Bu yapı, her domaini ayrı dosyada tutmanı sağlıyor. 10 domain yönettiğinde ne kadar mantıklı olduğunu anlıyorsun.

Temel Yapılandırma

Provider Kurulumu

main.tf dosyasına başlıyoruz:

terraform {
  required_version = ">= 1.0"
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }

  # Opsiyonel: Remote state için
  # backend "s3" {
  #   bucket = "terraform-state-bucket"
  #   key    = "cloudflare-dns/terraform.tfstate"
  #   region = "eu-central-1"
  # }
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

Değişken Tanımları

variables.tf dosyası:

variable "cloudflare_api_token" {
  description = "Cloudflare API Token"
  type        = string
  sensitive   = true
}

variable "cloudflare_zone_id" {
  description = "Cloudflare Zone ID for primary domain"
  type        = string
}

variable "domain" {
  description = "Primary domain name"
  type        = string
  default     = "example.com"
}

terraform.tfvars dosyası (bunu asla Git’e commit etme!):

cloudflare_api_token = "your-api-token-here"
cloudflare_zone_id   = "your-zone-id-here"
domain               = "example.com"

.gitignore dosyana şunları ekle:

# Terraform değişken dosyaları
*.tfvars
*.tfvars.json

# Terraform state dosyaları
*.tfstate
*.tfstate.backup
.terraform/
.terraform.lock.hcl

Zone ID’yi Cloudflare dashboard’unun sağ alt köşesinden bulabilirsin, domain seçili durumdayken “Overview” sayfasında görünüyor.

DNS Kayıtları Oluşturma

A Kayıtları

Web sunucun için temel A kayıtları:

# Ana domain A kaydı
resource "cloudflare_record" "root_a" {
  zone_id = var.cloudflare_zone_id
  name    = "@"
  value   = "1.2.3.4"
  type    = "A"
  ttl     = 1  # 1 = Auto (proxied kayıtlarda)
  proxied = true
}

# WWW subdomain
resource "cloudflare_record" "www_a" {
  zone_id = var.cloudflare_zone_id
  name    = "www"
  value   = "1.2.3.4"
  type    = "A"
  ttl     = 1
  proxied = true
}

# API subdomain - CDN bypass, direkt sunucuya
resource "cloudflare_record" "api_a" {
  zone_id = var.cloudflare_zone_id
  name    = "api"
  value   = "1.2.3.4"
  type    = "A"
  ttl     = 300
  proxied = false  # API için proxy genellikle sorun çıkarır
}

proxied = true kullandığında TTL otomatik olarak 1 (Auto) yapılıyor, Cloudflare bunu yönetiyor. proxied = false olduğunda istediğin TTL değerini verebilirsin.

CNAME Kayıtları

# Mail servisi için CNAME
resource "cloudflare_record" "mail_cname" {
  zone_id = var.cloudflare_zone_id
  name    = "mail"
  value   = "mail.protonmail.ch"
  type    = "CNAME"
  ttl     = 3600
  proxied = false
}

# Staging ortamı
resource "cloudflare_record" "staging_cname" {
  zone_id = var.cloudflare_zone_id
  name    = "staging"
  value   = "staging-server.example.com"
  type    = "CNAME"
  ttl     = 300
  proxied = false
}

MX Kayıtları – Mail Yapılandırması

MX kayıtları biraz farklı, öncelik değeri gerekiyor:

# Google Workspace MX kayıtları
resource "cloudflare_record" "mx_1" {
  zone_id  = var.cloudflare_zone_id
  name     = "@"
  value    = "aspmx.l.google.com"
  type     = "MX"
  ttl      = 3600
  priority = 1
}

resource "cloudflare_record" "mx_2" {
  zone_id  = var.cloudflare_zone_id
  name     = "@"
  value    = "alt1.aspmx.l.google.com"
  type     = "MX"
  ttl      = 3600
  priority = 5
}

resource "cloudflare_record" "mx_3" {
  zone_id  = var.cloudflare_zone_id
  name     = "@"
  value    = "alt2.aspmx.l.google.com"
  type     = "MX"
  ttl      = 3600
  priority = 5
}

TXT Kayıtları – SPF, DKIM, DMARC

E-posta güvenliği için bu kayıtlar kritik:

# SPF kaydı
resource "cloudflare_record" "spf" {
  zone_id = var.cloudflare_zone_id
  name    = "@"
  value   = "v=spf1 include:_spf.google.com include:sendgrid.net ~all"
  type    = "TXT"
  ttl     = 3600
}

# DMARC kaydı
resource "cloudflare_record" "dmarc" {
  zone_id = var.cloudflare_zone_id
  name    = "_dmarc"
  value   = "v=DMARC1; p=quarantine; rua=mailto:[email protected]; ruf=mailto:[email protected]; fo=1"
  type    = "TXT"
  ttl     = 3600
}

# Domain doğrulama (Google Search Console, vb.)
resource "cloudflare_record" "google_verification" {
  zone_id = var.cloudflare_zone_id
  name    = "@"
  value   = "google-site-verification=xxxxxxxxxxxxxxxxxxxxx"
  type    = "TXT"
  ttl     = 3600
}

Gerçek Dünya Senaryoları

Senaryo 1: SaaS Uygulaması için Çoklu Ortam DNS

Birçok sysadmin’in karşılaştığı durum: production, staging ve dev ortamları için tutarlı DNS yapısı kurmak.

# Ortam bazlı locals kullanımı
locals {
  environments = {
    production = {
      ip      = "1.2.3.4"
      proxied = true
      ttl     = 1
    }
    staging = {
      ip      = "1.2.3.5"
      proxied = false
      ttl     = 300
    }
    dev = {
      ip      = "1.2.3.6"
      proxied = false
      ttl     = 60
    }
  }
}

resource "cloudflare_record" "app_environments" {
  for_each = local.environments

  zone_id = var.cloudflare_zone_id
  name    = each.key == "production" ? "app" : "app-${each.key}"
  value   = each.value.ip
  type    = "A"
  ttl     = each.value.ttl
  proxied = each.value.proxied
}

Bu yapıyla production için app.example.com, staging için app-staging.example.com, dev için app-dev.example.com oluşturuluyor. Yeni bir ortam eklemek sadece locals bloğuna bir satır eklemek anlamına geliyor.

Senaryo 2: Mevcut DNS Kayıtlarını Import Etmek

Eğer Cloudflare’i zaten kullanıyorsun ve Terraform’a geçiş yapıyorsun, mevcut kayıtları import etmen gerekiyor. Bu süreç biraz zahmetli ama bir kere yapınca rahat ediyorsun.

# Önce resource bloğunu oluştur
resource "cloudflare_record" "existing_a" {
  zone_id = var.cloudflare_zone_id
  name    = "www"
  value   = "1.2.3.4"
  type    = "A"
  proxied = true
  ttl     = 1
}

# Sonra import et
# Format: terraform import cloudflare_record.kayit_adi ZONE_ID/RECORD_ID
# Cloudflare API ile mevcut kayıtları listele
curl -X GET "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" 
     -H "Authorization: Bearer YOUR_API_TOKEN" 
     -H "Content-Type: application/json" | jq '.result[] | {id: .id, name: .name, type: .type}'

# Sonra her kayıt için import yap
terraform import cloudflare_record.www_a ZONE_ID/RECORD_ID

Cloudflare’in çok fazla kaydı varsa tüm kayıtları tek tek import etmek yerine cf-terraforming aracını kullanabilirsin. Cloudflare’in resmi aracı mevcut konfigürasyonu otomatik Terraform koduna çeviriyor:

# cf-terraforming kurulumu
go install github.com/cloudflare/cf-terraforming/cmd/cf-terraforming@latest

# Mevcut DNS kayıtlarını Terraform koduna çevir
cf-terraforming generate 
  --token YOUR_API_TOKEN 
  --zone ZONE_ID 
  cloudflare_record

# Import komutlarını oluştur
cf-terraforming import 
  --token YOUR_API_TOKEN 
  --zone ZONE_ID 
  cloudflare_record

Senaryo 3: Wildcard ve Özel Subdomainler

Microservice mimarisi olan bir projede her servis için ayrı subdomain lazım. Bunu for_each ile kolayca yönetebilirsin:

locals {
  services = {
    auth     = "10.0.1.10"
    payments = "10.0.1.11"
    storage  = "10.0.1.12"
    admin    = "10.0.1.13"
    metrics  = "10.0.1.14"
  }
}

resource "cloudflare_record" "internal_services" {
  for_each = local.services

  zone_id = var.cloudflare_zone_id
  name    = "internal-${each.key}"
  value   = each.value
  type    = "A"
  ttl     = 120
  proxied = false

  comment = "Internal service - ${each.key}"
}

Yeni bir servis eklemek için locals bloğuna sadece bir satır ekliyorsun. terraform plan ile önce ne değişeceğini görüyor, terraform apply ile uyguluyorsun.

Cloudflare Page Rules ve Redirect Yönetimi

DNS kayıtlarının ötesinde Terraform ile Cloudflare redirect kurallarını da yönetebilirsin:

# HTTP -> HTTPS yönlendirmesi
resource "cloudflare_page_rule" "https_redirect" {
  zone_id  = var.cloudflare_zone_id
  target   = "http://example.com/*"
  priority = 1

  actions {
    always_use_https = true
  }
}

# WWW -> Non-WWW yönlendirmesi
resource "cloudflare_page_rule" "www_redirect" {
  zone_id  = var.cloudflare_zone_id
  target   = "www.example.com/*"
  priority = 2

  actions {
    forwarding_url {
      url         = "https://example.com/$1"
      status_code = 301
    }
  }
}

State Yönetimi ve Ekip Çalışması

Tek başına çalışıyorsan local state yeterli olabilir ama bir ekiple çalışıyorsan remote state şart. S3 + DynamoDB kombinasyonu Terraform state kilitleme için en yaygın yöntem:

terraform {
  backend "s3" {
    bucket         = "sirket-terraform-state"
    key            = "cloudflare-dns/terraform.tfstate"
    region         = "eu-central-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}

Cloudflare R2 de state backend olarak kullanılabilir. R2, S3 uyumlu API sunduğu için S3 backend yapılandırması küçük değişikliklerle çalışıyor.

Güvenlik İpuçları

API token’ı doğrudan terraform.tfvars içinde tutmak yerine ortam değişkeni kullanmak daha güvenli:

# Environment variable olarak set et
export TF_VAR_cloudflare_api_token="your-token-here"

# Sonra normal şekilde çalıştır
terraform plan
terraform apply

CI/CD pipeline’ında (GitHub Actions, GitLab CI) bu değerleri secret olarak tanımlayıp environment variable olarak inject edebilirsin.

Ayrıca Terraform’un sensitive = true özelliğini kullanan değişkenler için değerler log çıktısında maskeleniyor, ama yine de state dosyasında plain text olarak saklanıyor. State dosyasını şifreli olarak saklamak bu yüzden önemli.

Yaygın Hatalar ve Çözümleri

Terraform ile Cloudflare DNS yönetirken sık karşılaşılan sorunlar:

  • “record already exists” hatası: Cloudflare’de zaten olan bir kaydı Terraform’a eklemek istiyorsun ama import etmedin. Çözüm: terraform import komutunu kullan.
  • TTL çakışması: proxied = true olan kayıtlar için TTL 1 (Auto) olmak zorunda. Farklı bir değer girersen hata alırsın.
  • “zone_id not found” hatası: Zone ID’yi yanlış yazmış olabilirsin ya da API token’ın o zone’a erişim iznine sahip değil.
  • Rate limiting: Çok fazla kayıt eklerken Cloudflare API rate limit’e takılabilirsin. terraform apply -parallelism=1 ile paralelliği düşür.
  • CNAME flattening: Root domain (@) için CNAME ekleyemezsin, Cloudflare bunu A kaydına çeviriyor. Terraform’da bunu direkt A kaydı olarak tanımla.

Plan ve Apply İş Akışı

Gerçek bir değişiklik yapmadan önce daima plan çalıştır:

# Değişiklikleri önizle
terraform plan

# Belirli bir resource'u hedefle
terraform plan -target=cloudflare_record.api_a

# Değişiklikleri uygula
terraform apply

# Onay sormadan uygula (CI/CD için)
terraform apply -auto-approve

# Belirli bir resource'u destroy et
terraform destroy -target=cloudflare_record.old_staging_cname

terraform plan çıktısını dikkatle oku. + yeni kayıt eklenecek, - kayıt silinecek, ~ kayıt güncellenecek anlamına geliyor. DNS değişikliklerinde silme işlemine özellikle dikkat et.

Outputs Kullanımı

# outputs.tf
output "nameservers" {
  description = "Cloudflare nameservers for the zone"
  value       = cloudflare_record.root_a.hostname
}

output "www_record_id" {
  description = "ID of WWW A record"
  value       = cloudflare_record.www_a.id
}

Bu outputlar özellikle başka Terraform modülleriyle entegrasyon yaparken işe yarıyor.

Sonuç

Terraform ile Cloudflare DNS yönetimine geçmek başlangıçta biraz zahmetli görünüyor, özellikle mevcut kayıtları import etme aşaması. Ama bir kere kurduğunda geri dönmek istemiyorsun.

En büyük kazanım Git geçmişi oluyor. Üç ay önce yapılan bir DNS değişikliğinin neden yapıldığını commit mesajından anlayabilmek, incident sırasında çok değerli bir bilgi. Ekip büyüdükçe code review süreci de kritik önem kazanıyor, kritik DNS değişikliklerinin başka bir gözden geçirilmesi büyük kazalar önlüyor.

Pratik tavsiyem: Küçük başla. Önce tek bir domainin A ve CNAME kayıtlarını Terraform’a taşı. Import sürecini öğren, plan ve apply döngüsüne alış. Sonra MX ve TXT kayıtlarını ekle. Zamanla tüm Cloudflare yapılandırmanı, Page Rules ve Firewall kurallarını dahil ederek genişlet.

Infrastructure as Code sadece sunucular için değil. DNS gibi kritik ve sessizce hayat kurtaran altyapının da kod olarak yönetilmesi, olgun bir sysadmin operasyonunun işareti.

Bir yanıt yazın

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