PowerDNS API ile Programatik DNS Yönetimi

DNS yönetimi, bir sistemin can damarlarından biri. Elle zone dosyası düzenlemek, her değişiklik için servis yeniden başlatmak… Bunlar geçmişte kalan acı anılar olabilir. PowerDNS’in sunduğu RESTful API sayesinde DNS kayıtlarınızı programatik olarak yönetebilir, otomasyon pipeline’larınıza entegre edebilir ve insan hatasını minimuma indirebilirsiniz. Bu yazıda PowerDNS API’sini gerçek dünya senaryolarıyla ele alacağız.

PowerDNS API Nedir ve Neden Kullanmalısınız?

PowerDNS Authoritative Server, 4.x sürümünden itibaren oldukça olgun bir REST API sunuyor. Bu API sayesinde zone oluşturma, kayıt ekleme/silme/güncelleme, zone transferi yönetimi ve çok daha fazlasını HTTP istekleriyle gerçekleştirebilirsiniz.

Neden manuel yönetim yerine API tercih edilmeli?

  • Otomasyon kolaylığı: Yeni bir sunucu açtığınızda DNS kaydını otomatik ekleyebilirsiniz
  • Hata azaltma: Elle düzenleme sırasında oluşan yazım hataları ortadan kalkar
  • CI/CD entegrasyonu: Deployment pipeline’larınıza DNS yönetimini dahil edebilirsiniz
  • Audit trail: Her değişiklik loglanabilir ve izlenebilir hale gelir
  • Hız: Yüzlerce kaydı saniyeler içinde güncelleyebilirsiniz

API’yi Etkinleştirmek

Önce PowerDNS Authoritative Server kurulu ve çalışıyor olması gerekiyor. API’yi etkinleştirmek için pdns.conf dosyasını düzenlemeniz gerekiyor.

# pdns.conf dosyasını aç
sudo nano /etc/powerdns/pdns.conf

Aşağıdaki satırları ekleyin veya uncomment edin:

# API'yi etkinleştir
api=yes
api-key=gizli-api-anahtariniz-buraya

# Web sunucusu ayarları
webserver=yes
webserver-address=127.0.0.1
webserver-port=8081
webserver-allow-from=127.0.0.1,192.168.1.0/24

Önemli: webserver-address için dikkatli olun. Production ortamında API’yi doğrudan internete açmayın. Bir reverse proxy veya firewall arkasında tutun.

Değişiklikleri uygulamak için servisi yeniden başlatın:

sudo systemctl restart pdns
sudo systemctl status pdns

API’nin çalışıp çalışmadığını test etmek için:

curl -s -H 'X-API-Key: gizli-api-anahtariniz-buraya' 
  http://localhost:8081/api/v1/servers/localhost | python3 -m json.tool

Başarılı bir yanıtta sunucu bilgilerini, backend türünü ve desteklenen zone sayısını göreceksiniz.

Temel API Konseptleri

PowerDNS API’sinde birkaç temel kavramı anlamak önemli:

  • Server: PowerDNS instance’ı. Genellikle localhost olarak referans edilir
  • Zone: DNS zone’u. Trailing dot ile biter: example.com.
  • RRset: Resource Record Set. Aynı isim ve tipe sahip kayıtların grubu
  • Canonicalization: Tüm FQDN’ler trailing dot ile yazılır

Bu kuralları çiğnerseniz API size hata döner ve saatlerce neden çalışmadığını sorgularsınız. Benden söylemesi.

Zone Yönetimi

Yeni Zone Oluşturma

İlk gerçek dünya senaryomuz: Yeni bir müşteri için zone oluşturma. Şirkette on yeni müşteri geldiğinde tek tek zone oluşturmak yerine bir script yazalım.

#!/bin/bash
# create_zone.sh - Yeni DNS zone oluşturur

PDNS_API_URL="http://localhost:8081/api/v1"
PDNS_API_KEY="gizli-api-anahtariniz-buraya"
DOMAIN="${1}"
NS1="ns1.example.com."
NS2="ns2.example.com."

if [ -z "$DOMAIN" ]; then
    echo "Kullanim: $0 <domain>"
    echo "Ornek: $0 yenimüşteri.com"
    exit 1
fi

# Trailing dot ekle
ZONE="${DOMAIN}."

curl -s -X POST 
  -H "X-API-Key: ${PDNS_API_KEY}" 
  -H "Content-Type: application/json" 
  "${PDNS_API_URL}/servers/localhost/zones" 
  -d "{
    "name": "${ZONE}",
    "kind": "Native",
    "nameservers": ["${NS1}", "${NS2}"],
    "soa_edit_api": "INCEPTION-INCREMENT"
  }" | python3 -m json.tool

echo ""
echo "Zone ${ZONE} olusturuldu!"

Bu scripti çalıştırın:

chmod +x create_zone.sh
./create_zone.sh yenimüşteri.com

soa_edit_api parametresini INCEPTION-INCREMENT olarak ayarlamak, her API değişikliğinde SOA serial’ının otomatik güncellenmesini sağlar. Bu hayat kurtarıcı bir detay.

Zone Listeleme ve Sorgulama

Mevcut zone’ları listelemek:

curl -s -H 'X-API-Key: gizli-api-anahtariniz-buraya' 
  http://localhost:8081/api/v1/servers/localhost/zones | 
  python3 -c "import sys,json; zones=json.load(sys.stdin); [print(z['name']) for z in zones]"

Belirli bir zone’un detaylarını almak:

curl -s -H 'X-API-Key: gizli-api-anahtariniz-buraya' 
  http://localhost:8081/api/v1/servers/localhost/zones/example.com. | 
  python3 -m json.tool

DNS Kayıtları Yönetimi

Kayıt Ekleme

Gerçek dünya senaryosu: Yeni bir web sunucusu deploy ettiniz ve A kaydını eklemeniz gerekiyor. Ayrıca www CNAME’ini de eklemek istiyorsunuz.

PowerDNS API’sinde kayıt yönetimi PATCH metodu ile yapılır. Kayıtları RRset olarak gönderirsiniz ve changetype ile işlem türünü belirtirsiniz.

#!/bin/bash
# add_record.sh - DNS kaydı ekler veya günceller

PDNS_API_URL="http://localhost:8081/api/v1"
PDNS_API_KEY="gizli-api-anahtariniz-buraya"
ZONE="example.com."
HOSTNAME="webserver01.example.com."
IP="192.168.1.100"
TTL=300

# A kaydı ekle
curl -s -X PATCH 
  -H "X-API-Key: ${PDNS_API_KEY}" 
  -H "Content-Type: application/json" 
  "${PDNS_API_URL}/servers/localhost/zones/${ZONE}" 
  -d "{
    "rrsets": [
      {
        "name": "${HOSTNAME}",
        "type": "A",
        "ttl": ${TTL},
        "changetype": "REPLACE",
        "records": [
          {
            "content": "${IP}",
            "disabled": false
          }
        ]
      }
    ]
  }"

echo "A kaydi eklendi: ${HOSTNAME} -> ${IP}"

# www CNAME ekle
curl -s -X PATCH 
  -H "X-API-Key: ${PDNS_API_KEY}" 
  -H "Content-Type: application/json" 
  "${PDNS_API_URL}/servers/localhost/zones/${ZONE}" 
  -d "{
    "rrsets": [
      {
        "name": "www.example.com.",
        "type": "CNAME",
        "ttl": ${TTL},
        "changetype": "REPLACE",
        "records": [
          {
            "content": "webserver01.example.com.",
            "disabled": false
          }
        ]
      }
    ]
  }"

echo "CNAME kaydi eklendi: www.example.com -> webserver01.example.com"

Önemli: changetype: REPLACE kullanırken dikkatli olun. Bu işlem mevcut kaydın tüm içeriğini siler ve yerini alır. Birden fazla IP ile round-robin yapmak istiyorsanız tüm IP’leri aynı anda göndermeniz gerekir.

Toplu Kayıt Ekleme

Gerçek hayatta sık karşılaşılan senaryo: Bir CSV dosyasından onlarca kaydı tek seferde yüklemek. Örneğin sunucu envanterinizi DNS’e aktarmak istiyorsunuz.

#!/bin/bash
# bulk_add_records.sh
# CSV formatı: hostname,ip,ttl

PDNS_API_URL="http://localhost:8081/api/v1"
PDNS_API_KEY="gizli-api-anahtariniz-buraya"
ZONE="example.com."
CSV_FILE="${1:-sunucular.csv}"

if [ ! -f "$CSV_FILE" ]; then
    echo "CSV dosyasi bulunamadi: $CSV_FILE"
    exit 1
fi

# JSON array oluştur
RRSETS="["
FIRST=true

while IFS=, read -r hostname ip ttl; do
    # Başlık satırını atla
    [[ "$hostname" == "hostname" ]] && continue
    # Boş satırları atla
    [ -z "$hostname" ] && continue

    if [ "$FIRST" = true ]; then
        FIRST=false
    else
        RRSETS+=","
    fi

    RRSETS+="{
        "name": "${hostname}.${ZONE%?}.",
        "type": "A",
        "ttl": ${ttl:-300},
        "changetype": "REPLACE",
        "records": [{"content": "${ip}", "disabled": false}]
    }"

    echo "Hazırlaniyor: ${hostname} -> ${ip}"
done < "$CSV_FILE"

RRSETS+="]"

# Tek API çağrısıyla tümünü gönder
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH 
  -H "X-API-Key: ${PDNS_API_KEY}" 
  -H "Content-Type: application/json" 
  "${PDNS_API_URL}/servers/localhost/zones/${ZONE}" 
  -d "{"rrsets": ${RRSETS}}")

if [ "$RESPONSE" = "204" ]; then
    echo "Tum kayitlar basariyla eklendi!"
else
    echo "Hata! HTTP kodu: $RESPONSE"
    exit 1
fi

PowerDNS API başarılı PATCH işlemlerinde 204 No Content döner. Bunu kontrol etmek iyi bir alışkanlık.

Kayıt Silme

# Tek bir kaydı sil
curl -s -X PATCH 
  -H "X-API-Key: gizli-api-anahtariniz-buraya" 
  -H "Content-Type: application/json" 
  http://localhost:8081/api/v1/servers/localhost/zones/example.com. 
  -d '{
    "rrsets": [
      {
        "name": "eskisunucu.example.com.",
        "type": "A",
        "changetype": "DELETE"
      }
    ]
  }'

Silme işleminde records alanını belirtmenize gerek yok. Sadece name, type ve changetype: DELETE yeterli.

Python ile Gelişmiş Yönetim

Bash scriptler basit işler için harika ama daha karmaşık senaryolar için Python tercih etmek mantıklı. requests kütüphanesiyle temiz bir PowerDNS client yazalım.

#!/usr/bin/env python3
# pdns_manager.py - PowerDNS API yönetim modülü

import requests
import json
import sys
from typing import List, Dict, Optional

class PowerDNSManager:
    def __init__(self, api_url: str, api_key: str):
        self.api_url = api_url.rstrip('/')
        self.session = requests.Session()
        self.session.headers.update({
            'X-API-Key': api_key,
            'Content-Type': 'application/json'
        })
        self.server = 'localhost'

    def _base_url(self) -> str:
        return f"{self.api_url}/servers/{self.server}/zones"

    def zone_exists(self, zone: str) -> bool:
        zone = self._ensure_dot(zone)
        r = self.session.get(f"{self._base_url()}/{zone}")
        return r.status_code == 200

    def create_zone(self, zone: str, nameservers: List[str], kind: str = 'Native') -> Dict:
        zone = self._ensure_dot(zone)
        ns_with_dots = [self._ensure_dot(ns) for ns in nameservers]

        payload = {
            'name': zone,
            'kind': kind,
            'nameservers': ns_with_dots,
            'soa_edit_api': 'INCEPTION-INCREMENT'
        }

        r = self.session.post(self._base_url(), json=payload)
        r.raise_for_status()
        print(f"[OK] Zone olusturuldu: {zone}")
        return r.json()

    def add_a_record(self, zone: str, hostname: str, ip: str, ttl: int = 300) -> bool:
        return self._update_record(zone, hostname, 'A', ip, ttl)

    def add_cname_record(self, zone: str, hostname: str, target: str, ttl: int = 300) -> bool:
        return self._update_record(zone, hostname, 'CNAME', self._ensure_dot(target), ttl)

    def add_mx_record(self, zone: str, hostname: str, priority: int, target: str, ttl: int = 3600) -> bool:
        content = f"{priority} {self._ensure_dot(target)}"
        return self._update_record(zone, hostname, 'MX', content, ttl)

    def add_txt_record(self, zone: str, hostname: str, content: str, ttl: int = 300) -> bool:
        txt_content = f'"{content}"'
        return self._update_record(zone, hostname, 'TXT', txt_content, ttl)

    def delete_record(self, zone: str, hostname: str, record_type: str) -> bool:
        zone = self._ensure_dot(zone)
        hostname = self._ensure_dot(hostname) if not hostname.endswith(zone) else hostname

        payload = {
            'rrsets': [{
                'name': hostname,
                'type': record_type,
                'changetype': 'DELETE'
            }]
        }

        r = self.session.patch(f"{self._base_url()}/{zone}", json=payload)
        if r.status_code == 204:
            print(f"[OK] Kayit silindi: {hostname} {record_type}")
            return True
        else:
            print(f"[HATA] Kayit silinemedi: {r.status_code} - {r.text}")
            return False

    def _update_record(self, zone: str, hostname: str, rtype: str, content: str, ttl: int) -> bool:
        zone = self._ensure_dot(zone)
        fqdn = self._ensure_dot(hostname) if '.' in hostname else f"{hostname}.{zone}"

        payload = {
            'rrsets': [{
                'name': fqdn,
                'type': rtype,
                'ttl': ttl,
                'changetype': 'REPLACE',
                'records': [{'content': content, 'disabled': False}]
            }]
        }

        r = self.session.patch(f"{self._base_url()}/{zone}", json=payload)
        if r.status_code == 204:
            print(f"[OK] {rtype} kaydi eklendi: {fqdn} -> {content}")
            return True
        else:
            print(f"[HATA] {r.status_code}: {r.text}")
            return False

    def _ensure_dot(self, name: str) -> str:
        return name if name.endswith('.') else f"{name}."


# Kullanim ornegi
if __name__ == '__main__':
    pdns = PowerDNSManager(
        api_url='http://localhost:8081/api/v1',
        api_key='gizli-api-anahtariniz-buraya'
    )

    # Yeni zone oluştur
    if not pdns.zone_exists('yenimüşteri.com'):
        pdns.create_zone('yenimüşteri.com', ['ns1.example.com', 'ns2.example.com'])

    # Kayıtları ekle
    pdns.add_a_record('yenimüşteri.com', 'yenimüşteri.com', '1.2.3.4')
    pdns.add_cname_record('yenimüşteri.com', 'www.yenimüşteri.com', 'yenimüşteri.com')
    pdns.add_mx_record('yenimüşteri.com', 'yenimüşteri.com', 10, 'mail.yenimüşteri.com')
    pdns.add_txt_record('yenimüşteri.com', 'yenimüşteri.com', 'v=spf1 mx ~all')

Otomatik Deployment Entegrasyonu

Gerçek dünya senaryosu: Ansible veya Terraform ile sunucu açtığınızda DNS kaydının otomatik oluşturulması. Aşağıda bir Ansible task örneği var:

# tasks/dns_registration.yml
---
- name: DNS A kaydi ekle
  uri:
    url: "http://pdns-server:8081/api/v1/servers/localhost/zones/{{ dns_zone }}."
    method: PATCH
    headers:
      X-API-Key: "{{ pdns_api_key }}"
      Content-Type: "application/json"
    body_format: json
    body:
      rrsets:
        - name: "{{ inventory_hostname }}.{{ dns_zone }}."
          type: "A"
          ttl: 300
          changetype: "REPLACE"
          records:
            - content: "{{ ansible_default_ipv4.address }}"
              disabled: false
    status_code: 204
  delegate_to: localhost

- name: PTR kaydi ekle (reverse DNS)
  uri:
    url: "http://pdns-server:8081/api/v1/servers/localhost/zones/{{ reverse_zone }}."
    method: PATCH
    headers:
      X-API-Key: "{{ pdns_api_key }}"
      Content-Type: "application/json"
    body_format: json
    body:
      rrsets:
        - name: "{{ ansible_default_ipv4.address.split('.')[-1] }}.{{ reverse_zone }}."
          type: "PTR"
          ttl: 300
          changetype: "REPLACE"
          records:
            - content: "{{ inventory_hostname }}.{{ dns_zone }}."
              disabled: false
    status_code: 204
  delegate_to: localhost
  when: manage_reverse_dns | default(false)

Zone Export ve Backup

DNS kayıtlarınızın yedeğini almak kritik bir operasyon. PowerDNS API bunu da kolaylaştırıyor:

#!/bin/bash
# backup_zones.sh - Tüm zone'ları yedekler

PDNS_API_URL="http://localhost:8081/api/v1"
PDNS_API_KEY="gizli-api-anahtariniz-buraya"
BACKUP_DIR="/var/backups/dns/$(date +%Y%m%d)"

mkdir -p "$BACKUP_DIR"

# Tüm zone'ları listele
ZONES=$(curl -s -H "X-API-Key: ${PDNS_API_KEY}" 
  "${PDNS_API_URL}/servers/localhost/zones" | 
  python3 -c "import sys,json; [print(z['name']) for z in json.load(sys.stdin)]")

echo "Zone yedekleme basladi: $(date)"

for ZONE in $ZONES; do
    FILENAME="${BACKUP_DIR}/${ZONE%?}.json"

    curl -s -H "X-API-Key: ${PDNS_API_KEY}" 
      "${PDNS_API_URL}/servers/localhost/zones/${ZONE}" 
      -o "$FILENAME"

    if [ $? -eq 0 ]; then
        echo "[OK] Yedeklendi: $ZONE -> $FILENAME"
    else
        echo "[HATA] Yedeklenemedi: $ZONE"
    fi
done

# AXFR export (bind formatında)
for ZONE in $ZONES; do
    FILENAME="${BACKUP_DIR}/${ZONE%?}.zone"

    curl -s -H "X-API-Key: ${PDNS_API_KEY}" 
      "${PDNS_API_URL}/servers/localhost/zones/${ZONE}/export" 
      -o "$FILENAME"

    echo "[OK] AXFR export: $ZONE"
done

echo "Yedekleme tamamlandi: $(date)"
echo "Konum: $BACKUP_DIR"

Bu scripti crontab’a ekleyin:

# Her gece 02:00'de DNS yedekleme
0 2 * * * /opt/scripts/backup_zones.sh >> /var/log/dns-backup.log 2>&1

Yaygın Hatalar ve Çözümleri

PowerDNS API kullanırken sık karşılaşılan sorunlar:

  • 422 Unprocessable Entity: Genellikle trailing dot eksikliğinden kaynaklanır. Tüm FQDN’lerin nokta ile bittiğinden emin olun
  • 404 Not Found: Zone adı yanlış veya zone mevcut değil. Zone adını kontrol edin
  • 409 Conflict: Zone zaten mevcut. Oluşturmadan önce kontrol edin
  • 400 Bad Request: JSON formatı hatalı veya zorunlu alan eksik. Payload’ı validate edin
  • MX kaydı hatası: MX içeriği "10 mail.example.com." formatında olmalı, sadece IP veya hostname yetmez
  • TXT kayıt tırnak işareti: TXT içerikleri çift tırnak içinde gönderilmeli: ""v=spf1 mx ~all""

Hata ayıklarken verbose mod kullanın:

curl -v -H 'X-API-Key: gizli-api-anahtariniz-buraya' 
  -H 'Content-Type: application/json' 
  http://localhost:8081/api/v1/servers/localhost/zones/example.com.

Güvenlik Önerileri

API güvenliği için dikkat edilmesi gerekenler:

  • API key yönetimi: API key’i environment variable veya secrets manager’da saklayın, asla kaynak koda yazmayın
  • Network izolasyonu: API portunu sadece gerekli IP’lere açın. webserver-allow-from parametresini kısıtlayın
  • HTTPS: Reverse proxy arkasında TLS sonlandırma yapın. Nginx veya Caddy kullanabilirsiniz
  • Rate limiting: Nginx’te limit_req_zone ile istek sayısını sınırlandırın
  • Monitoring: API erişim loglarını izleyin, anormal kayıt değişikliklerini alarm olarak ayarlayın
  • Farklı API key’leri: Read-only işlemler için ayrı, write işlemler için ayrı key kullanın

Sonuç

PowerDNS API, DNS yönetimini gerçekten başka bir seviyeye taşıyor. Bu yazıda anlattıklarımızı özetleyelim:

  • API etkinleştirme ve temel güvenlik konfigürasyonu
  • Zone oluşturma ve yönetimi
  • Tek ve toplu kayıt ekleme/silme işlemleri
  • Python ile yeniden kullanılabilir yönetim modülü
  • Ansible entegrasyonu ile otomatik DNS kaydı
  • Zone backup ve export stratejileri

Bunların hepsini bir araya getirince DNS artık manuel bir iş olmaktan çıkıyor. Yeni sunucu açıldığında DNS kaydı otomatik oluşuyor, sunucu kapatıldığında otomatik siliniyor, deployment pipeline’ınız DNS değişikliklerini kendi hallediyor.

Bir adım daha ileriye gitmek isteyenler için PowerDNS Recursor API’sini de incelemenizi öneririm. Authoritative Server API ile birlikte kullanıldığında cache flush ve query istatistikleri gibi operasyonları da programatik olarak yönetebilirsiniz. DNS artık korkutucu değil, sadece bir API çağrısı uzaklığında.

Yorum yapın