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 planbunu 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 importkomutunu 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=1ile 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.
