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 plan ile 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=true ile 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=false ile 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_each ile 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 refresh ile sync edin, sonrasında elle değişiklik yapılmasını IAM policy ile engelleyin
  • Provider version lock unutmak: ~> 4.0 gibi versiyon constraint’i olmadan kullanmak breaking change riskidir. Her zaman required_providers içinde versiyon belirtin
  • Sensitive data loglama: API token veya DKIM key’in plan output’unda görünmesi. sensitive = true kullanı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.

Bir yanıt yazın

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