Unbound Cluster ile Yüksek Erişilebilir DNS Kurulumu
Prodüksiyonda tek bir DNS sunucusuyla çalışmak, sabah 3’te telefon beklemek demektir. Bunu zor yoldan öğrendim. Küçük bir yapıda bile DNS çöktüğünde her şey durur: web uygulamaları bağlanamaz, servisler birbirini bulamaz, izleme sistemleri kör olur. Unbound tabanlı yüksek erişilebilir bir DNS kümesi kurmak, hem performans hem de dayanıklılık açısından ciddi bir fark yaratıyor. Bu yazıda gerçek bir production ortamına uygun, iki node’lu Unbound küme kurulumunu adım adım ele alacağız.
Mimari Genel Bakış
Kuracağımız yapı şu bileşenlerden oluşuyor:
- dns1 (192.168.10.11): Birincil Unbound DNS sunucusu
- dns2 (192.168.10.12): İkincil Unbound DNS sunucusu
- VIP (192.168.10.10): Keepalived tarafından yönetilen sanal IP adresi
- Keepalived: VRRP protokolü üzerinden failover yönetimi
- Unbound: Her iki node’da bağımsız çalışan resolver
İstemciler her zaman VIP adresine (192.168.10.10) bağlanır. Birincil sunucu düştüğünde Keepalived VIP’i ikincil sunucuya taşır. Bu geçiş genellikle 2-3 saniye içinde tamamlanır ve istemciler çoğunlukla bunu fark etmez.
Neden Unbound? BIND’a kıyasla daha az kaynak tüketiyor, konfigürasyonu daha anlaşılır ve özellikle recursive resolver rolü için optimize edilmiş. Küçük ve orta ölçekli altyapılarda BIND’ın otoriter DNS özelliklerine genellikle ihtiyaç duyulmaz. Sadece iç ağ çözümleme ve önbellekleme yapılacaksa Unbound çok daha temiz bir seçim.
Sunucu Hazırlığı
Her iki sunucuda da başlangıç hazırlıklarını yapalım. Ubuntu 22.04 üzerinde çalışıyoruz ama CentOS/RHEL için paket isimleri dışında büyük fark yok.
# Her iki sunucuda çalıştır
apt update && apt upgrade -y
apt install -y unbound unbound-host keepalived
# Unbound servisini şimdilik durdur, önce konfigürasyon yapalım
systemctl stop unbound
systemctl disable systemd-resolved
# systemd-resolved'un 53 portunu bloke etmemesi için
sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf
systemctl restart systemd-resolved
systemd-resolved meselesi Ubuntu’da klasik bir tuzak. Eğer bunu devre dışı bırakmazsanız Unbound 53 portunu bind edemez ve servis başlamaz. Yıllar önce bunu ilk kurulumda atladığımda neden çalışmadığını anlamak için saatler harcadım.
Unbound Ana Konfigürasyonu
Her iki sunucu için temel Unbound konfigürasyonunu oluşturalım. Bu konfigürasyon her iki node’da aynı olacak, yalnızca interface direktiflerinde node’a özgü IP adresleri kullanılacak.
dns1 için /etc/unbound/unbound.conf:
server:
# Dinleme arayüzleri - hem VIP hem de node IP'si
interface: 0.0.0.0
port: 53
# Erişim kontrolü
access-control: 127.0.0.0/8 allow
access-control: 192.168.10.0/24 allow
access-control: 10.0.0.0/8 allow
access-control: 0.0.0.0/0 refuse
# Performans ayarları
num-threads: 4
msg-cache-size: 128m
rrset-cache-size: 256m
cache-min-ttl: 300
cache-max-ttl: 86400
# Gizlilik ve güvenlik
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: yes
qname-minimisation: yes
# DNSSEC doğrulama
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Log ayarları
verbosity: 1
log-queries: no
logfile: "/var/log/unbound/unbound.log"
# Root hints
root-hints: "/etc/unbound/root.hints"
# Prefetch - popüler kayıtları önceden yenile
prefetch: yes
prefetch-key: yes
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
Root hints dosyasını çekmeyi unutmayın. Bu dosya olmadan Unbound recursive resolution yapamaz:
curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.root
# Root key oluştur
unbound-anchor -a /var/lib/unbound/root.key
İç Alan Adı Çözümlemesi
Gerçek dünyada her zaman iç alan adlarınız olur. Unbound’da bunları ayrı zone dosyaları olarak tanımlıyoruz. /etc/unbound/conf.d/ dizinini kullanmak konfigürasyonu düzenli tutar.
# /etc/unbound/conf.d/internal-zones.conf
# Forward zone - iç alan adları için
auth-zone:
name: "internal.example.com"
zonefile: "/etc/unbound/zones/internal.example.com.zone"
# Reverse zone
auth-zone:
name: "10.168.192.in-addr.arpa"
zonefile: "/etc/unbound/zones/192.168.10.rev"
Zone dosyasını oluşturalım:
mkdir -p /etc/unbound/zones
cat > /etc/unbound/zones/internal.example.com.zone << 'EOF'
$ORIGIN internal.example.com.
$TTL 300
@ IN SOA dns1.internal.example.com. admin.example.com. (
2024010101 ; Serial
3600 ; Refresh
900 ; Retry
604800 ; Expire
300 ) ; Minimum TTL
; Name servers
@ IN NS dns1.internal.example.com.
@ IN NS dns2.internal.example.com.
; DNS sunucuları
dns1 IN A 192.168.10.11
dns2 IN A 192.168.10.12
dns IN A 192.168.10.10
; Uygulama sunucuları
web1 IN A 192.168.10.21
web2 IN A 192.168.10.22
db1 IN A 192.168.10.31
db2 IN A 192.168.10.32
app1 IN A 192.168.10.41
; Load balancer VIP'leri
web IN A 192.168.10.20
db IN A 192.168.10.30
EOF
Ana konfigürasyona include direktifini ekleyelim:
echo 'include: "/etc/unbound/conf.d/*.conf"' >> /etc/unbound/unbound.conf
Keepalived Konfigürasyonu
Keepalived kurulumu kritik nokta. VRRP’yi doğru yapılandırmazsanız split-brain durumuna düşersiniz: her iki node da VIP’i sahiplenmeye çalışır ve istemciler tutarsız yanıtlar alır.
dns1 için /etc/keepalived/keepalived.conf (MASTER):
global_defs {
router_id DNS_CLUSTER
script_user root
enable_script_security
}
vrrp_script check_unbound {
script "/usr/local/bin/check-unbound.sh"
interval 2
weight -30
fall 3
rise 2
}
vrrp_instance DNS_VIP {
state MASTER
interface eth0
virtual_router_id 51
priority 110
advert_int 1
authentication {
auth_type PASS
auth_pass dns_cluster_2024
}
virtual_ipaddress {
192.168.10.10/24 dev eth0
}
track_script {
check_unbound
}
notify_master "/usr/local/bin/notify-master.sh"
notify_backup "/usr/local/bin/notify-backup.sh"
}
dns2 için /etc/keepalived/keepalived.conf (BACKUP):
Aynı konfigürasyon, yalnızca iki satır farklı:
state BACKUPpriority 100
Unbound sağlık kontrolü scripti:
cat > /usr/local/bin/check-unbound.sh << 'EOF'
#!/bin/bash
# Unbound'un gerçekten DNS sorgusu yanıtlayıp yanıtlamadığını test et
result=$(dig +short +time=2 +tries=1 @127.0.0.1 google.com A 2>/dev/null)
if [ -z "$result" ]; then
logger -t keepalived "Unbound health check FAILED - no response"
exit 1
fi
# unbound-control ile servis durumunu da kontrol et
if ! unbound-control status > /dev/null 2>&1; then
logger -t keepalived "Unbound health check FAILED - control unreachable"
exit 1
fi
exit 0
EOF
chmod +x /usr/local/bin/check-unbound.sh
Bu sağlık kontrolü ikili bir yaklaşım kullanıyor: hem gerçek bir DNS sorgusu atıyor hem de unbound-control ile servise erişilebilirliği doğruluyor. Sadece process varlığını kontrol etmek yetmez; Unbound bazen çalışıyor görünüp sorgu yanıtlamayabiliyor, özellikle hafıza baskısı altında.
Failover Notify Scriptleri
Failover anında ne olduğunu loglamak ve gerekirse ek aksiyonlar almak için notify scriptleri kullanıyoruz:
cat > /usr/local/bin/notify-master.sh << 'EOF'
#!/bin/bash
LOGFILE="/var/log/keepalived-transitions.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "$TIMESTAMP - Bu node MASTER oldu (VIP: 192.168.10.10)" >> $LOGFILE
# Unbound cache'i temizle - yeni master temiz başlasın
unbound-control flush_zone .
# İsteğe bağlı: Slack veya webhook bildirimi
# curl -s -X POST -H 'Content-type: application/json'
# --data '{"text":"DNS MASTER failover: dns1 -> aktif"}'
# https://hooks.slack.com/services/xxx/yyy/zzz
logger -t keepalived "TRANSITION: Bu node DNS MASTER oldu"
EOF
cat > /usr/local/bin/notify-backup.sh << 'EOF'
#!/bin/bash
LOGFILE="/var/log/keepalived-transitions.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "$TIMESTAMP - Bu node BACKUP moduna geçti" >> $LOGFILE
logger -t keepalived "TRANSITION: Bu node DNS BACKUP moduna geçti"
EOF
chmod +x /usr/local/bin/notify-master.sh /usr/local/bin/notify-backup.sh
Firewall Kuralları
Keepalived’ın VRRP trafiği için IP protokol 112’yi açmanız gerekiyor. Bunu unutursanız node’lar birbirini göremez ve her ikisi de MASTER olmaya çalışır.
# UFW kullanıyorsanız
ufw allow from 192.168.10.11 to any proto 112
ufw allow from 192.168.10.12 to any proto 112
ufw allow 53/tcp
ufw allow 53/udp
ufw allow 8953/tcp # Unbound control (sadece güvenli kaynaklardan)
# iptables kullanıyorsanız
iptables -A INPUT -p 112 -j ACCEPT
iptables -A OUTPUT -p 112 -j ACCEPT
iptables -A INPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p tcp --dport 53 -j ACCEPT
Servisleri Başlatma ve Doğrulama
Her şeyi sırayla ayağa kaldıralım:
# Her iki node'da Unbound'u başlat
systemctl enable unbound
systemctl start unbound
# Unbound'un doğru çalıştığını doğrula
unbound-control status
dig @127.0.0.1 google.com
dig @127.0.0.1 internal.example.com. dns1
# Keepalived'ı başlat
systemctl enable keepalived
systemctl start keepalived
# VIP'in dns1'de olduğunu doğrula
ip addr show dev eth0 | grep 192.168.10.10
# dns2'den kontrol - VIP burada olmamalı
ip addr show dev eth0 | grep 192.168.10.10
Failover testini elle yapın. Bu adımı atlama. Production’a almadan önce failover’ın gerçekten çalıştığını görmeniz lazım:
# dns1'de - Keepalived'ı durdur ve dns2'nin VIP'i alıp almadığını gör
systemctl stop keepalived
# dns2'de kontrol et
ip addr show dev eth0 # 192.168.10.10 burada görünmeli
# dns2'den VIP üzerinden sorgu yap
dig @192.168.10.10 google.com
# dns1'i geri getir
systemctl start keepalived
# Priority 110 olduğu için dns1 VIP'i geri alacak
İzleme ve Gözlemlenebilirlik
Kümeyi kurduktan sonra izlemeye almadan bırakmak olmaz. Unbound istatistiklerini Prometheus’a göndermek için küçük bir script işe yarıyor:
cat > /usr/local/bin/unbound-stats-exporter.sh << 'EOF'
#!/bin/bash
# Unbound istatistiklerini logla - cron ile her dakika çalıştır
STATS=$(unbound-control stats_noreset)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
TOTAL_QUERIES=$(echo "$STATS" | grep "total.num.queries=" | cut -d= -f2)
CACHE_HITS=$(echo "$STATS" | grep "total.num.cachehits=" | cut -d= -f2)
CACHE_MISS=$(echo "$STATS" | grep "total.num.cachemiss=" | cut -d= -f2)
RECURSION=$(echo "$STATS" | grep "total.num.recursivereplies=" | cut -d= -f2)
echo "$TIMESTAMP queries=$TOTAL_QUERIES hits=$CACHE_HITS miss=$CACHE_MISS recursive=$RECURSION"
>> /var/log/unbound/stats.log
# Cache hit oranı düşükse uyar
if [ -n "$TOTAL_QUERIES" ] && [ "$TOTAL_QUERIES" -gt 100 ]; then
HIT_RATE=$(echo "scale=2; $CACHE_HITS * 100 / $TOTAL_QUERIES" | bc)
if (( $(echo "$HIT_RATE < 60" | bc -l) )); then
logger -t unbound-monitor "UYARI: Cache hit oranı düşük: %$HIT_RATE"
fi
fi
EOF
chmod +x /usr/local/bin/unbound-stats-exporter.sh
echo "* * * * * root /usr/local/bin/unbound-stats-exporter.sh" > /etc/cron.d/unbound-stats
Zabbix veya Nagios kullanıyorsanız harici DNS kontrolü de ekleyin. Sadece sunucunun kendi loopback’inden değil, dışarıdan VIP adresine atılan sorguları izlemek daha gerçekçi bir resim veriyor.
Yaygın Sorunlar ve Çözümleri
Split-brain durumu: Her iki node da VIP’e sahip görünüyorsa önce firewall kurallarını kontrol edin. VRRP paketleri geçemiyor olabilir. tcpdump -i eth0 proto 112 komutu ile VRRP trafiğini izleyin.
Unbound başlamıyor: Çoğunlukla port çakışması. ss -tlunp | grep :53 ile hangi process 53’ü tuttuğuna bakın. systemd-resolved sık suçludur.
Cache tutarsızlıkları: Her iki node bağımsız cache tutar. Failover sonrası yeni master’da cache boş olabilir ve kısa süreliğine performans düşüşü yaşanabilir. Notify scriptindeki flush_zone bunu temizlemez, tersine kasıtlı olarak temiz başlatır. Eğer cache sürekliliği kritikse, Unbound’un remote-control API’si üzerinden cache dump/restore yapan özel bir script yazılabilir ama çoğu durumda bu gerekli olmuyor.
Yüksek latency: unbound-control stats çıktısındaki histogram.* değerlerine bakın. num-threads değerini CPU çekirdek sayısına eşitleyin ve msg-cache-size ile rrset-cache-size değerlerini sunucunun RAM’ine göre artırın.
Zone Senkronizasyonu
İki node arasında zone dosyalarını senkronize tutmak için basit bir rsync + inotify çözümü kullanabilirsiniz. Daha kurumsal bir yaklaşım için Git reposu ve CI/CD pipeline da kurulabilir.
# dns1'de - zone değişikliklerini dns2'ye push et
cat > /usr/local/bin/sync-zones.sh << 'EOF'
#!/bin/bash
ZONES_DIR="/etc/unbound/zones"
REMOTE_HOST="192.168.10.12"
REMOTE_USER="root"
rsync -av --delete
-e "ssh -i /root/.ssh/dns-sync-key -o StrictHostKeyChecking=yes"
"$ZONES_DIR/"
"${REMOTE_USER}@${REMOTE_HOST}:${ZONES_DIR}/"
if [ $? -eq 0 ]; then
# Uzak sunucuda Unbound konfigürasyonunu yeniden yükle
ssh -i /root/.ssh/dns-sync-key
"${REMOTE_USER}@${REMOTE_HOST}"
"unbound-control reload"
logger -t dns-sync "Zone senkronizasyonu başarılı"
else
logger -t dns-sync "HATA: Zone senkronizasyonu başarısız"
fi
EOF
chmod +x /usr/local/bin/sync-zones.sh
SSH key’i parola olmadan çalışacak şekilde oluşturup yalnızca bu komutla kısıtlayın:
ssh-keygen -t ed25519 -f /root/.ssh/dns-sync-key -N ""
# dns2'de authorized_keys'e kısıtlı olarak ekle:
# command="rsync --server -av --delete . /etc/unbound/zones/" ssh-ed25519 AAAA...
Sonuç
Bu kurulumla elde ettiğiniz şey: istemcilerin tek bir IP adresine bağlandığı, arka planda iki bağımsız Unbound instance’ının çalıştığı ve herhangi biri düştüğünde otomatik olarak devir teslim yapan bir DNS altyapısı. Tek bir node arızasına karşı tam korumalı, önbellekleme ile hızlı ve iç alan adı desteğiyle eksiksiz.
Buradan sonraki adımlar altyapınıza göre değişebilir. Çok bölgeli bir yapınız varsa her bölgeye bir çift node koymak ve Anycast ile yönlendirmek düşünülebilir. Yoğun trafik altında num-threads ve önbellek boyutlarını profiling yaparak ayarlamak önemli. DNSSEC imzalama gerekiyorsa ayrı otoriter sunucular devreye girmeli; Unbound’u resolver rolünde tutmak daha sağlıklı.
DNS kümesinin aylarca kimse fark etmeden çalışması en iyi başarı göstergesi. İyi kurulmuş altyapı sessizdir.
