Terraform ile Cloudflare DNS Yönetimi
DNS kayıtlarını elle yönetmek, özellikle onlarca domain ve yüzlerce kayıt söz konusu olduğunda, tam anlamıyla bir kabus haline gelebilir. Bir kaydı yanlış giriyorsun, production ortamı çöküyor, hangisini değiştirdiğini hatırlamıyorsun ve gece 2’de Cloudflare panelinde kayıt avı yapıyorsun. Terraform ile Cloudflare DNS yönetimini öğrendikten sonra bu tablonun tamamen değiştiğini söyleyebilirim. Bütün DNS yapınız kod haline geliyor, versiyon kontrolüne giriyor ve değişiklikler denetlenebilir hale geliyor.
Neden Terraform ile DNS Yönetimi?
Cloudflare’in web arayüzü güzel ve kullanışlı, bunu kabul ediyorum. Ama birden fazla kişinin aynı account üzerinde çalıştığı, onlarca domain’in olduğu ve sık değişiklik yapıldığı ortamlarda manuel yönetim kaçınılmaz olarak hatalara yol açıyor.
Terraform yaklaşımının getirdikleri:
- Versiyon kontrolü: Her DNS değişikliği Git commit’i oluyor, kim ne zaman ne değiştirdi görülüyor
- Tekrarlanabilirlik: Aynı yapıyı farklı environment’lara (staging, production) kolayca klonlayabiliyorsun
- Drift tespiti:
terraform planile mevcut durum ile kodunuz arasındaki farkı görüyorsunuz - Ekip çalışması: Pull request süreçleriyle DNS değişiklikleri code review’dan geçiyor
- Felaket kurtarma: DNS yapınızı sıfırdan dakikalar içinde yeniden oluşturabiliyorsunuz
Ön Gereksinimler ve Kurulum
Başlamadan önce birkaç şeyi hazır etmeniz gerekiyor. Terraform kurulu olmalı (en az 1.0 versiyonu), bir Cloudflare hesabı ve API token’ı lazım.
Cloudflare API token oluşturmak için Cloudflare dashboard’una girin, sağ üst köşedeki profil menüsünden API Tokens sayfasına gidin. “Create Token” butonuna tıklayın ve “Edit zone DNS” template’ini seçin. Zone permissions kısmında DNS:Edit yetkisini vermeniz yeterli.
Token’ı aldıktan sonra güvenli bir yerde saklayın. Bu token’ı kod içine yazmamanız kritik önemde. Environment variable veya secrets manager kullanacağız.
Proje Yapısını Oluşturalım
İyi bir Terraform projesi için düzenli bir dizin yapısı şart. Ben genellikle şu yapıyı kullanıyorum:
mkdir -p cloudflare-dns/{modules/dns-zone,environments/{staging,production}}
cd cloudflare-dns
# Temel dosyaları oluştur
touch main.tf variables.tf outputs.tf providers.tf terraform.tfvars
Dizin yapısı şu şekilde görünmeli:
cloudflare-dns/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
├── modules/
│ └── dns-zone/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
├── staging/
└── production/
Provider Konfigürasyonu
Önce Cloudflare provider’ını tanımlayalım. providers.tf dosyasına şunları yazıyoruz:
terraform {
required_version = ">= 1.0"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
# Remote state için S3 backend (opsiyonel ama önerilen)
backend "s3" {
bucket = "my-terraform-state"
key = "cloudflare-dns/terraform.tfstate"
region = "eu-central-1"
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
variables.tf dosyasına değişkenleri ekleyelim:
variable "cloudflare_api_token" {
description = "Cloudflare API Token"
type = string
sensitive = true
}
variable "cloudflare_account_id" {
description = "Cloudflare Account ID"
type = string
}
variable "domain" {
description = "Ana domain adı"
type = string
default = "example.com"
}
terraform.tfvars dosyasında ise değerleri tanımlıyoruz. Bu dosyayı kesinlikle .gitignore‘a eklemeyi unutmayın:
cloudflare_account_id = "your_account_id_here"
domain = "example.com"
# cloudflare_api_token değerini environment variable olarak set edin
# export TF_VAR_cloudflare_api_token="your_token_here"
Temel DNS Kayıtları Yönetimi
Şimdi asıl işe gelelim. main.tf dosyasına zone ve temel kayıtları ekleyelim:
# Mevcut zone'u data source olarak çek
data "cloudflare_zone" "main" {
name = var.domain
}
# A Kaydı - Ana domain
resource "cloudflare_record" "root" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "203.0.113.10"
type = "A"
ttl = 1 # 1 = otomatik (proxied kayıtlar için)
proxied = true
}
# www subdomain
resource "cloudflare_record" "www" {
zone_id = data.cloudflare_zone.main.id
name = "www"
value = "203.0.113.10"
type = "A"
ttl = 1
proxied = true
}
# API subdomain - proxied değil, direkt bağlantı
resource "cloudflare_record" "api" {
zone_id = data.cloudflare_zone.main.id
name = "api"
value = "203.0.113.20"
type = "A"
ttl = 300
proxied = false
}
# MX Kayıtları - Google Workspace için
resource "cloudflare_record" "mx_1" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "aspmx.l.google.com"
type = "MX"
ttl = 3600
priority = 1
}
resource "cloudflare_record" "mx_2" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "alt1.aspmx.l.google.com"
type = "MX"
ttl = 3600
priority = 5
}
# SPF Kaydı
resource "cloudflare_record" "spf" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "v=spf1 include:_spf.google.com ~all"
type = "TXT"
ttl = 3600
}
# DMARC Kaydı
resource "cloudflare_record" "dmarc" {
zone_id = data.cloudflare_zone.main.id
name = "_dmarc"
value = "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
type = "TXT"
ttl = 3600
}
Dinamik Kayıtlar ve Döngüler
Gerçek dünyada aynı pattern’i tekrarlamak zorunda kalırsınız. Diyelim ki birden fazla microservice subdomain’i eklemeniz gerekiyor. Bunu Terraform’un for_each özelliğiyle çok temiz şekilde halledebilirsiniz:
# Microservice tanımları
locals {
microservices = {
"auth" = {
ip = "10.0.1.10"
proxied = false
ttl = 300
}
"payments" = {
ip = "10.0.1.11"
proxied = false
ttl = 300
}
"notifications" = {
ip = "10.0.1.12"
proxied = true
ttl = 1
}
"admin" = {
ip = "10.0.1.13"
proxied = false
ttl = 120
}
}
}
resource "cloudflare_record" "microservices" {
for_each = local.microservices
zone_id = data.cloudflare_zone.main.id
name = each.key
value = each.value.ip
type = "A"
ttl = each.value.ttl
proxied = each.value.proxied
}
Bu yaklaşım yeni bir microservice eklemenizi son derece kolaylaştırıyor. Sadece locals bloğuna yeni bir entry ekliyorsunuz, terraform apply diyorsunuz ve kayıt oluşuyor.
Cloudflare Page Rules ve Firewall Kuralları
DNS yönetiminin ötesine geçip Cloudflare’in diğer özelliklerini de Terraform ile yönetebilirsiniz. Bu, altyapınızın gerçek anlamda “as code” olması demek:
# HTTP'den HTTPS'e yönlendirme - Page Rule
resource "cloudflare_page_rule" "force_https" {
zone_id = data.cloudflare_zone.main.id
target = "http://${var.domain}/*"
priority = 1
actions {
always_use_https = true
}
}
# www olmadan ana domain'e yönlendirme
resource "cloudflare_page_rule" "www_redirect" {
zone_id = data.cloudflare_zone.main.id
target = "www.${var.domain}/*"
priority = 2
actions {
forwarding_url {
url = "https://${var.domain}/$1"
status_code = 301
}
}
}
# Bot koruması için Firewall Rule
resource "cloudflare_filter" "block_bad_bots" {
zone_id = data.cloudflare_zone.main.id
description = "Kötü botları engelle"
expression = "(cf.client.bot) and not (cf.verified_bot_category in {"Search Engine Crawlers" "Monitoring & Analytics"})"
}
resource "cloudflare_firewall_rule" "block_bad_bots" {
zone_id = data.cloudflare_zone.main.id
description = "Kötü bot engelleme kuralı"
filter_id = cloudflare_filter.block_bad_bots.id
action = "block"
priority = 1
}
Zone Ayarları ve SSL Konfigürasyonu
Cloudflare zone’un genel ayarlarını da Terraform ile yönetebilirsiniz. Bu, yeni bir zone oluştururken standart ayarları otomatik uygulamanıza olanak tanır:
# SSL modunu Full (Strict) olarak ayarla
resource "cloudflare_zone_settings_override" "main" {
zone_id = data.cloudflare_zone.main.id
settings {
ssl = "strict"
always_use_https = "on"
min_tls_version = "1.2"
tls_1_3 = "zrt"
automatic_https_rewrites = "on"
opportunistic_encryption = "on"
browser_cache_ttl = 14400
security_level = "medium"
# Saldırı altında mod için (normalde kapalı tutun)
under_attack = "off"
# HTTP/2 ve HTTP/3
http2 = "on"
http3 = "on"
# Minification
minify {
css = "on"
js = "on"
html = "on"
}
}
}
Modüler Yapı: Birden Fazla Domain Yönetimi
Birden fazla domain yönetiyorsanız, modüler yapı hayat kurtarıcı. Önce modules/dns-zone/main.tf dosyasını oluşturalım:
# modules/dns-zone/variables.tf
variable "domain" {
description = "Zone domain adı"
type = string
}
variable "a_records" {
description = "A kayıtları listesi"
type = map(object({
value = string
proxied = bool
ttl = number
}))
default = {}
}
variable "cname_records" {
description = "CNAME kayıtları listesi"
type = map(object({
value = string
proxied = bool
ttl = number
}))
default = {}
}
variable "mx_records" {
description = "MX kayıtları"
type = list(object({
value = string
priority = number
ttl = number
}))
default = []
}
variable "txt_records" {
description = "TXT kayıtları"
type = map(object({
name = string
value = string
ttl = number
}))
default = {}
}
Modülün ana dosyası modules/dns-zone/main.tf:
data "cloudflare_zone" "zone" {
name = var.domain
}
resource "cloudflare_record" "a_records" {
for_each = var.a_records
zone_id = data.cloudflare_zone.zone.id
name = each.key
value = each.value.value
type = "A"
ttl = each.value.proxied ? 1 : each.value.ttl
proxied = each.value.proxied
}
resource "cloudflare_record" "cname_records" {
for_each = var.cname_records
zone_id = data.cloudflare_zone.zone.id
name = each.key
value = each.value.value
type = "CNAME"
ttl = each.value.proxied ? 1 : each.value.ttl
proxied = each.value.proxied
}
resource "cloudflare_record" "mx_records" {
count = length(var.mx_records)
zone_id = data.cloudflare_zone.zone.id
name = "@"
value = var.mx_records[count.index].value
type = "MX"
ttl = var.mx_records[count.index].ttl
priority = var.mx_records[count.index].priority
proxied = false
}
resource "cloudflare_record" "txt_records" {
for_each = var.txt_records
zone_id = data.cloudflare_zone.zone.id
name = each.value.name
value = each.value.value
type = "TXT"
ttl = each.value.ttl
proxied = false
}
output "zone_id" {
value = data.cloudflare_zone.zone.id
}
Modülü ana main.tf‘te kullanmak şöyle oluyor:
module "example_com" {
source = "./modules/dns-zone"
domain = "example.com"
a_records = {
"@" = {
value = "203.0.113.10"
proxied = true
ttl = 1
}
"www" = {
value = "203.0.113.10"
proxied = true
ttl = 1
}
"api" = {
value = "203.0.113.20"
proxied = false
ttl = 300
}
}
cname_records = {
"mail" = {
value = "ghs.googlehosted.com"
proxied = false
ttl = 3600
}
}
mx_records = [
{
value = "aspmx.l.google.com"
priority = 1
ttl = 3600
},
{
value = "alt1.aspmx.l.google.com"
priority = 5
ttl = 3600
}
]
txt_records = {
"spf" = {
name = "@"
value = "v=spf1 include:_spf.google.com ~all"
ttl = 3600
}
"dmarc" = {
name = "_dmarc"
value = "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
ttl = 3600
}
}
}
CI/CD Pipeline ile Otomatik Deploy
Gerçek güç, Terraform’u CI/CD pipeline’ına entegre ettiğinizde ortaya çıkıyor. GitHub Actions örneği:
# .github/workflows/terraform-dns.yml
name: Terraform DNS Deploy
on:
push:
branches: [main]
paths:
- '**.tf'
- '**.tfvars'
pull_request:
branches: [main]
env:
TF_VERSION: "1.6.0"
jobs:
terraform-plan:
name: Terraform Plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Terraform Setup
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -out=tfplan
env:
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TF_VAR_cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Plan Sonucunu Comment Olarak Yaz
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
const output = `#### Terraform Plan Sonucu
```
${{ steps.plan.outputs.stdout }}
````;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
terraform-apply:
name: Terraform Apply
runs-on: ubuntu-latest
needs: terraform-plan
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Terraform Setup
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Apply
run: terraform apply -auto-approve
env:
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TF_VAR_cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
Mevcut DNS Kayıtlarını Import Etmek
Eğer zaten Cloudflare’de DNS kayıtlarınız varsa ve bunları Terraform yönetimine almak istiyorsanız, import komutunu kullanmanız gerekiyor. Bu biraz zahmetli ama bir kez yapıldıktan sonra her şey otomasyona giriyor:
# Önce mevcut kayıtları listele
# Cloudflare dashboard'dan Record ID'leri öğrenebilirsiniz
# Ya da Cloudflare API'yi kullanın:
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, name, type, content}'
# Terraform import komutu - her kayıt için ayrı ayrı yapmanız gerekiyor
terraform import cloudflare_record.root ZONE_ID/RECORD_ID
terraform import cloudflare_record.www ZONE_ID/RECORD_ID
terraform import cloudflare_record.api ZONE_ID/RECORD_ID
# Terraform 1.5+ ile import blokları kullanabilirsiniz (önerilen yeni yöntem)
# main.tf içine ekleyin:
import {
to = cloudflare_record.root
id = "ZONE_ID/RECORD_ID"
}
Import işleminden sonra terraform plan çalıştırdığınızda hiçbir değişiklik görmemeniz gerekiyor. Değişiklik görürseniz, yazdığınız kaynak tanımının mevcut durumla eşleşmediği anlamına geliyor, düzeltmeniz lazım.
Yaygın Hatalar ve Çözümleri
Terraform ile Cloudflare çalışırken sık karşılaşılan birkaç durumu paylaşayım:
- Proxied kayıtta TTL çakışması:
proxied = trueolan kayıtlarda TTL değeri her zaman1(otomatik) olmalı. Farklı bir değer girseniz bile Cloudflare onu 1 olarak set ediyor ve Terraform sürekli “değişiklik var” gösteriyor. Çözüm: proxied kayıtlardattl = 1kullanın.
- MX kayıtlarında proxied: MX kayıtları hiçbir zaman proxied olamaz,
proxied = falsezorunlu. Aynı durum TXT ve SRV kayıtları için de geçerli.
- State dosyasının kaybolması: Remote backend kullanmıyorsanız ve
terraform.tfstatedosyasını kaybederseniz büyük sorun yaşarsınız. S3 + DynamoDB backend kurulumu biraz zaman alıyor ama kesinlikle değer.
- API rate limiting: Çok sayıda kayıt oluştururken Cloudflare API rate limit’ine takılabilirsiniz.
parallelismdeğerini düşürmek işe yarıyor:terraform apply -parallelism=5
Outputs Tanımlamak
outputs.tf dosyasına zone ID ve record bilgilerini eklemek, diğer Terraform modülleriyle entegrasyon için çok kullanışlı:
output "zone_id" {
description = "Cloudflare Zone ID"
value = data.cloudflare_zone.main.id
}
output "nameservers" {
description = "Zone nameserver'ları"
value = data.cloudflare_zone.main.name_servers
}
output "root_record_id" {
description = "Ana domain A kaydı ID'si"
value = cloudflare_record.root.id
}
Sonuç
Terraform ile Cloudflare DNS yönetimi, başlangıçta biraz yatırım gerektiriyor. Import işlemleri, state yönetimi, CI/CD entegrasyonu… Bunları kurmak zaman alıyor. Ama bir kez oturttuğunuzda geri dönmek istemiyorsunuz.
Şu an yönettiğim ortamda 15’ten fazla domain, 300’ü aşkın DNS kaydı ve onlarca Cloudflare kuralı tamamen Terraform ile yönetiliyor. Bir kayıt değişikliği yapmam gerektiğinde PR açıyorum, ekip arkadaşım terraform plan çıktısını görüp onaylıyor, merge ediyorum ve GitHub Actions otomatik apply ediyor. Gece yarısı Cloudflare panelinde kayıt avı artık geçmişte kaldı.
Başlamak için büyük bir altyapıya ihtiyacınız yok. Tek bir domain, tek bir main.tf dosyası ve birkaç kayıtla başlayın. Yapının nasıl işlediğini anladıkça kademeli olarak genişletin. DNS gibi kritik bir altyapı bileşenini kod olarak yönetmek, sysadmin hayatınızın kalitesini ciddi ölçüde artırıyor.
