DNS Kaydı Yönetimi: Terraform ile Otomatik DNS Yapılandırması
DNS yönetimi, sysadmin hayatının en sıkıcı ama bir o kadar da kritik parçalarından biri. Yanlış bir A kaydı, production ortamında saatlerce kesintiye yol açabilir. Elle yapılan değişiklikler, “kim bu kaydı ekledi?” sorusunu beraberinde getirir. Terraform ile DNS yönetimini otomatize ettiğinizde ise bu kaos bir anda düzene giriyor. Her değişiklik versiyon kontrol altında, her kayıt kod olarak tanımlı, her deployment tekrarlanabilir. Bu yazıda gerçek dünya senaryolarıyla Terraform ile DNS yönetimini nasıl yapacağınızı adım adım anlatacağım.
Neden DNS Yönetimini Terraform ile Yapmalısınız?
Çoğu ekip DNS kayıtlarını hâlâ web arayüzünden, bazen de direkt zone dosyalarını düzenleyerek yönetiyor. Bu yaklaşımın sorunları zamanla birikir. Bir developer production’da yanlış bir CNAME ekler, kimse fark etmez. Bir yıl sonra o kaydın ne işe yaradığını kimse bilmez. Rollback yapmak istediğinizde eski halin ne olduğunu hatırlayamazsınız.
Terraform bu problemlerin hepsini çözer:
- State yönetimi: Mevcut DNS durumu Terraform state dosyasında tutulur
- Versiyon kontrolü: Her değişiklik Git’te görünür, kim ne zaman ne değiştirdi?
- Plan/Apply döngüsü:
terraform planile değişikliği production’a uygulamadan önce görürsünüz - Modüler yapı: Benzer kayıtları modüllerle tekrar kullanabilirsiniz
- CI/CD entegrasyonu: Pull request açıldığında otomatik plan, merge edilince otomatik apply
Terraform DNS Provider Seçimi
Terraform’da DNS yönetimi için kullanabileceğiniz birçok provider var. Hangi DNS sağlayıcısını kullandığınıza göre seçim yapmanız gerekiyor.
En yaygın seçenekler:
- hashicorp/dns: RFC 2136 destekleyen herhangi bir DNS sunucusuyla çalışır (BIND, Infoblox vb.)
- cloudflare/cloudflare: Cloudflare DNS için
- aws/aws: Route 53 için
- digitalocean/digitalocean: DigitalOcean DNS için
- google/google: Cloud DNS için
Bu yazıda hem Cloudflare hem de AWS Route 53 örneklerini göstereceğim, çünkü bunlar kurumsal ve startup ortamlarında en sık karşılaşılan senaryolar.
Proje Yapısını Kurmak
İyi bir DNS Terraform projesi için dosya yapısını baştan doğru kurmak şart. Monorepo içinde DNS için ayrı bir modül oluşturmanızı öneririm.
mkdir -p terraform/dns/{modules/dns-records,environments/{prod,staging,dev}}
cd terraform/dns
# Temel dosya yapısı
tree .
# .
# ├── modules/
# │ └── dns-records/
# │ ├── main.tf
# │ ├── variables.tf
# │ └── outputs.tf
# ├── environments/
# │ ├── prod/
# │ │ ├── main.tf
# │ │ ├── variables.tf
# │ │ └── terraform.tfvars
# │ └── staging/
# │ ├── main.tf
# │ └── terraform.tfvars
# └── README.md
Bu yapıyla her ortamın kendi state dosyası olur ve production değişikliklerini staging’den bağımsız yönetebilirsiniz.
Cloudflare DNS Yönetimi
Cloudflare, özellikle CDN ve güvenlik özellikleriyle birlikte DNS yönetimi için popüler bir seçim. Provider konfigürasyonuyla başlayalım.
# versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
backend "s3" {
bucket = "sirket-terraform-state"
key = "dns/prod/terraform.tfstate"
region = "eu-west-1"
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
Credentials için asla hardcode kullanmayın. API token’ı environment variable olarak geçin:
# .env dosyası veya CI/CD secret olarak tanımlayın
export TF_VAR_cloudflare_api_token="your-api-token-here"
# Daha güvenli alternatif: Vault veya AWS Secrets Manager'dan çekme
export TF_VAR_cloudflare_api_token=$(aws secretsmanager get-secret-value
--secret-id cloudflare/api-token
--query SecretString
--output text)
Şimdi gerçek bir şirket senaryosu düşünelim. sirketim.com domain’iniz var ve şu kayıtları yönetmeniz gerekiyor:
# modules/dns-records/main.tf
# Zone verisini data source ile çekiyoruz
data "cloudflare_zone" "main" {
name = var.zone_name
}
# A Kaydı - Ana web sitesi
resource "cloudflare_record" "root" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = var.web_server_ip
type = "A"
ttl = 1 # 1 = Cloudflare proxy aktifken otomatik
proxied = true
comment = "Ana web sitesi - Load balancer IP"
}
# WWW redirect
resource "cloudflare_record" "www" {
zone_id = data.cloudflare_zone.main.id
name = "www"
value = var.zone_name
type = "CNAME"
ttl = 1
proxied = true
comment = "WWW -> root redirect"
}
# API subdomain - proxy olmadan direkt
resource "cloudflare_record" "api" {
zone_id = data.cloudflare_zone.main.id
name = "api"
value = var.api_server_ip
type = "A"
ttl = 120
proxied = false
comment = "API sunucusu - ${var.environment} ortami"
}
# Mail kayıtları
resource "cloudflare_record" "mx_primary" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "aspmx.l.google.com"
type = "MX"
ttl = 3600
priority = 1
comment = "Google Workspace birincil MX"
}
resource "cloudflare_record" "mx_secondary" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "alt1.aspmx.l.google.com"
type = "MX"
ttl = 3600
priority = 5
comment = "Google Workspace ikincil MX"
}
# SPF kaydı
resource "cloudflare_record" "spf" {
zone_id = data.cloudflare_zone.main.id
name = "@"
value = "v=spf1 include:_spf.google.com include:sendgrid.net ~all"
type = "TXT"
ttl = 3600
comment = "SPF - Google Workspace + SendGrid"
}
# DMARC kaydı
resource "cloudflare_record" "dmarc" {
zone_id = data.cloudflare_zone.main.id
name = "_dmarc"
value = "v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100"
type = "TXT"
ttl = 3600
comment = "DMARC politikasi"
}
AWS Route 53 ile DNS Yönetimi
AWS altyapısı kullanıyorsanız Route 53, Terraform ile çok iyi entegre oluyor. Özellikle ECS, EKS veya ALB gibi servislerle birlikte kullanıldığında dinamik DNS yönetimi hayat kurtarır.
# Route 53 hosted zone
resource "aws_route53_zone" "primary" {
name = "sirketim.com"
comment = "Ana production zone - Terraform ile yonetiliyor"
tags = {
Environment = "production"
ManagedBy = "terraform"
Team = "platform"
}
}
# ALB için alias kaydı
resource "aws_route53_record" "app" {
zone_id = aws_route53_zone.primary.zone_id
name = "app.sirketim.com"
type = "A"
alias {
name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
evaluate_target_health = true
}
}
# Health check ile failover konfigürasyonu
resource "aws_route53_health_check" "primary" {
fqdn = "api.sirketim.com"
port = 443
type = "HTTPS"
resource_path = "/health"
failure_threshold = 3
request_interval = 30
tags = {
Name = "api-primary-health-check"
}
}
# Failover primary kaydı
resource "aws_route53_record" "api_primary" {
zone_id = aws_route53_zone.primary.zone_id
name = "api.sirketim.com"
type = "A"
ttl = 60
failover_routing_policy {
type = "PRIMARY"
}
set_identifier = "primary"
health_check_id = aws_route53_health_check.primary.id
records = [var.api_primary_ip]
}
# Failover secondary kaydı
resource "aws_route53_record" "api_secondary" {
zone_id = aws_route53_zone.primary.zone_id
name = "api.sirketim.com"
type = "A"
ttl = 60
failover_routing_policy {
type = "SECONDARY"
}
set_identifier = "secondary"
records = [var.api_secondary_ip]
}
Dinamik DNS Kayıtları: for_each ile Çoklu Kayıt Yönetimi
Gerçek hayatta onlarca mikroservis subdomain’i yönetmeniz gerekebilir. Her biri için ayrı resource yazmak yerine for_each kullanın.
# variables.tf
variable "microservices" {
description = "Mikroservis DNS kayitlari"
type = map(object({
ip = string
ttl = number
proxied = bool
comment = string
}))
default = {}
}
# terraform.tfvars - production ortami
microservices = {
"auth" = {
ip = "10.0.1.10"
ttl = 120
proxied = false
comment = "Auth servisi - internal"
}
"payments" = {
ip = "10.0.2.20"
ttl = 60
proxied = false
comment = "Odeme servisi - PCI scope"
}
"notifications" = {
ip = "10.0.3.30"
ttl = 300
proxied = true
comment = "Bildirim servisi"
}
"dashboard" = {
ip = "10.0.4.40"
ttl = 120
proxied = true
comment = "Admin dashboard"
}
}
# main.tf
resource "cloudflare_record" "microservices" {
for_each = var.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
comment = each.value.comment
}
Bu yaklaşımla yeni bir mikroservis eklemek sadece tfvars dosyasına bir satır eklemek anlamına geliyor. Değişiklik Git’e gidiyor, PR açılıyor, terraform plan output PR’da görünüyor, onaylanınca apply ediliyor.
CI/CD Pipeline Entegrasyonu
DNS değişikliklerini manuel apply etmek risk taşır. GitHub Actions ile tam otomasyona geçelim.
# .github/workflows/terraform-dns.yml
name: DNS Terraform
on:
push:
branches:
- main
paths:
- 'terraform/dns/**'
pull_request:
branches:
- main
paths:
- 'terraform/dns/**'
env:
TF_VERSION: "1.6.0"
WORKING_DIR: terraform/dns/environments/prod
jobs:
terraform-plan:
name: Terraform Plan
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
working-directory: ${{ env.WORKING_DIR }}
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Plan
working-directory: ${{ env.WORKING_DIR }}
run: terraform plan -no-color -out=tfplan
env:
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Plan Ciktisini PR'a Yaz
uses: actions/github-script@v7
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
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
working-directory: ${{ env.WORKING_DIR }}
run: terraform init
- name: Terraform Apply
working-directory: ${{ env.WORKING_DIR }}
run: terraform apply -auto-approve
env:
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Mevcut DNS Kayıtlarını Terraform’a Import Etmek
Çoğu zaman mevcut bir DNS yönetimini Terraform’a taşımanız gerekir. Bu süreç biraz sabır ister ama doğru yapıldığında sorunsuz geçer.
# Önce resource'ları tanımlayın, sonra import edin
# Cloudflare için zone ID ve record ID'ye ihtiyacınız var
# Zone ID'yi bulun
curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=sirketim.com"
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
-H "Content-Type: application/json" | jq '.result[0].id'
# Zone içindeki kayıtları listeleyin
curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records"
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | jq '.result[] | {id, name, type, content}'
# Import komutu - format: zone_id/record_id
terraform import cloudflare_record.root abc123zone/def456record
# Terraform 1.5+ ile import block kullanabilirsiniz
# main.tf içine ekleyin:
import {
to = cloudflare_record.root
id = "abc123zone/def456record"
}
# Plan çalıştırın, farklılıkları görün
terraform plan
# Sorun yoksa apply edin, import bloğunu silin
terraform apply
Büyük zone’lar için bu işlemi otomatize eden bir script yazabilirsiniz:
#!/bin/bash
# dns-import.sh - Cloudflare zone'unu Terraform'a import eder
ZONE_ID=$1
API_TOKEN=$2
if [ -z "$ZONE_ID" ] || [ -z "$API_TOKEN" ]; then
echo "Kullanim: $0 <zone_id> <api_token>"
exit 1
fi
# Tum kayıtları cek
RECORDS=$(curl -s -X GET
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?per_page=100"
-H "Authorization: Bearer $API_TOKEN"
-H "Content-Type: application/json")
# Her kayıt icin import komutu olustur
echo "$RECORDS" | jq -r '.result[] | "(.name)_(.type) (.id)"' | while read -r name_type record_id; do
resource_name=$(echo "$name_type" | tr '.' '_' | tr '@' 'root' | tr '[:upper:]' '[:lower:]')
echo "terraform import cloudflare_record.${resource_name} ${ZONE_ID}/${record_id}"
done
Gizli Bilgileri Yönetmek: DKIM ve Hassas TXT Kayıtları
DKIM public key gibi uzun TXT kayıtlarını yönetmek Terraform’da biraz dikkat ister. Bazı DNS sağlayıcıları uzun TXT kayıtlarını otomatik bölerken bazıları bölmez.
# DKIM kaydı - Google Workspace
resource "cloudflare_record" "dkim_google" {
zone_id = data.cloudflare_zone.main.id
name = "google._domainkey"
type = "TXT"
ttl = 3600
# Uzun DKIM key'i variables.tf'ten alıyoruz
value = var.dkim_google_key
comment = "Google Workspace DKIM - ${formatdate("YYYY-MM", timestamp())} rotasyonu"
lifecycle {
# DKIM rotasyonu sırasında eski kaydı silmeden önce yenisini oluştur
create_before_destroy = true
}
}
# DKIM key'i Terraform variable olarak tanımlayın
# terraform.tfvars yerine environment variable veya Vault'tan çekin
# TF_VAR_dkim_google_key="v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
# variables.tf
variable "dkim_google_key" {
description = "Google Workspace DKIM public key"
type = string
sensitive = true # Plan ciktisinda gizle
}
TTL Stratejisi ve Deployment Öncesi Hazırlık
Deployment yaparken DNS TTL yönetimi kritik. Bunu Terraform lifecycle kurallarıyla otomatize edebilirsiniz.
# Deployment oncesi TTL'i dusur (blue-green veya cutover icin)
locals {
# Deployment modunda mı? CI değişkeniyle kontrol
deployment_mode = var.deployment_mode
# Deployment modunda TTL 60 saniye, normal modda 300 saniye
web_ttl = local.deployment_mode ? 60 : 300
}
resource "cloudflare_record" "web_primary" {
zone_id = data.cloudflare_zone.main.id
name = "www"
value = var.deployment_mode ? var.new_server_ip : var.current_server_ip
type = "A"
ttl = local.web_ttl
proxied = false
comment = "Web sunucusu - deployment_mode: ${var.deployment_mode}"
}
# variables.tf
variable "deployment_mode" {
description = "Deployment modu - TTL'i dusurur ve yeni IP'ye gecisi hazirlar"
type = bool
default = false
}
variable "new_server_ip" {
description = "Yeni sunucu IP'si - deployment sirasinda kullanilir"
type = string
default = ""
}
Deployment sürecinde şu adımları izleyebilirsiniz:
- 1. Adım:
deployment_mode=trueile apply yapın, TTL 60’a düşer - 2. Adım: Mevcut TTL süresi kadar bekleyin (eski TTL ne kadarda ise)
- 3. Adım: Yeni sunucuyu hazırlayın ve testlerini yapın
- 4. Adım: Yeni IP ile apply yapın, cutover gerçekleşir
- 5. Adım: Her şey yolundaysa
deployment_mode=falseile TTL’i normale döndürün
Terraform State Güvenliği
DNS state dosyası hassas bilgiler içerebilir. S3 backend ile güvenli saklama:
# State bucket oluşturma - sadece bir kez çalıştırın
aws s3api create-bucket
--bucket sirket-terraform-dns-state
--region eu-west-1
--create-bucket-configuration LocationConstraint=eu-west-1
# Versiyonlamayı aktifleştirin
aws s3api put-bucket-versioning
--bucket sirket-terraform-dns-state
--versioning-configuration Status=Enabled
# Şifrelemeyi aktifleştirin
aws s3api put-bucket-encryption
--bucket sirket-terraform-dns-state
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms"
}
}]
}'
# DynamoDB tablosu ile state lock
aws dynamodb create-table
--table-name terraform-dns-locks
--attribute-definitions AttributeName=LockID,AttributeType=S
--key-schema AttributeName=LockID,KeyType=HASH
--billing-mode PAY_PER_REQUEST
--region eu-west-1
# backend.tf
terraform {
backend "s3" {
bucket = "sirket-terraform-dns-state"
key = "dns/production/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-dns-locks"
kms_key_id = "arn:aws:kms:eu-west-1:123456789:key/abc-def"
}
}
Monitoring ve Alerting
DNS değişikliklerini izlemek, sorunları erken fark etmek için önemli. Terraform ile monitoring kaynaklarını da yönetebilirsiniz.
# Cloudflare Notifications - DNS değişiklik bildirimi
resource "cloudflare_notification_policy" "dns_changes" {
account_id = var.cloudflare_account_id
name = "DNS Degisiklik Bildirimi"
description = "DNS kayıtları degistiginde bildirim gonder"
enabled = true
alert_type = "dns_record_changed"
email_integration {
id = cloudflare_notification_policy_webhooks.ops_team.id
}
filters {
zones = [data.cloudflare_zone.main.id]
}
}
# Route 53 Health Check alarm
resource "aws_cloudwatch_metric_alarm" "dns_health" {
alarm_name = "route53-api-health-check"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "HealthCheckStatus"
namespace = "AWS/Route53"
period = 60
statistic = "Minimum"
threshold = 1
alarm_description = "API endpoint DNS health check basarisiz"
dimensions = {
HealthCheckId = aws_route53_health_check.primary.id
}
alarm_actions = [aws_sns_topic.ops_alerts.arn]
ok_actions = [aws_sns_topic.ops_alerts.arn]
}
Yaygın Hatalar ve Çözümleri
DNS Terraform yönetiminde sık karşılaşılan sorunlar ve çözümleri:
- Duplicate kayıt hatası: Aynı isim ve tip için birden fazla kayıt tanımlamak.
for_eachile manage edin ve unique key’ler kullanın - TTL propagation beklememek: Plan/apply sonrası eski TTL süresi dolmadan testi yapmak. Her zaman mevcut TTL kadar bekleyin
- State drift: Web arayüzünden elle yapılan değişiklikler state ile uyumsuzluk yaratır.
terraform refreshile sync edin, sonrasında elle değişiklik yapılmasını IAM policy ile engelleyin - Provider version lock unutmak:
~> 4.0gibi versiyon constraint’i olmadan kullanmak breaking change riskidir. Her zamanrequired_providersiçinde versiyon belirtin - Sensitive data loglama: API token veya DKIM key’in plan output’unda görünmesi.
sensitive = truekullanın
Sonuç
Terraform ile DNS yönetimi başlangıçta biraz kurulum gerektiriyor ama getirisi çok hızlı görünüyor. Artık “kim bu kaydı ekledi?” sorusu Git blame’e soruluyor. “Neden bu CNAME var?” sorusunun cevabı comment alanında yazıyor. Bir yanlış değişiklikten sonra rollback için Git’te bir commit geri gidip apply ediyorsunuz.
En kritik nokta şu: DNS değişiklikleri küçük görünse de etkisi büyük olabiliyor. Bir MX kaydı yanlış yazıldığında email’ler uçup gidiyor, bir A kaydı yanlış IP’ye işaret ettiğinde production site çöküyor. Terraform’un plan adımı bu riskleri minimize ediyor. “Apply etmeden önce ne değişecek?” sorusunun cevabını görmek, özellikle ekip büyüdükçe ve ortam karmaşıklaştıkça inanılmaz değer taşıyor.
Kademeli geçişi öneririm. Önce staging ortamından başlayın, workflow’u oturttuktan sonra production’a geçin. Mevcut kayıtları import etmeye çalışırken acele etmeyin, her kaydı tek tek doğrulayın. Ve mutlaka S3 veya benzeri bir remote backend kullanın, local state ile production DNS yönetmek potansiyel bir felaket.
