From 6aa29e9f77a1177b5f80e2abaa5415bf42944789 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 28 Feb 2026 14:14:20 +0000 Subject: [PATCH] [ci skip] technitium: add primary-secondary DNS HA with AXFR zone replication Secondary instance on a separate node replicates all zones from primary via zone transfer. LoadBalancer routes DNS queries to both pods. PDB ensures at least 1 DNS pod survives voluntary disruptions. Setup job automates zone transfer enablement and secondary zone creation via Technitium REST API. --- secrets/nfs_directories.txt | Bin 1721 -> 1754 bytes stacks/platform/main.tf | 2 + stacks/platform/modules/technitium/ha.tf | 272 +++++++++++++++++++++ stacks/platform/modules/technitium/main.tf | 43 +++- 4 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 stacks/platform/modules/technitium/ha.tf diff --git a/secrets/nfs_directories.txt b/secrets/nfs_directories.txt index 6103be30020c3c85e6768f196575fce45d6adba7..f8a4de4387a487434fe8e4d910de5fbefc5b153a 100644 GIT binary patch literal 1754 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 /tmp/zones.txt + echo "Found zones:"; cat /tmp/zones.txt + + # Enable zone transfers on primary for each zone + while read -r zone; do + echo "Enabling zone transfer for: $zone" + curl -sf "$PRIMARY/api/zones/options/set?token=$P_TOKEN&zone=$zone&zoneTransfer=Allow" || true + done < /tmp/zones.txt + + # Create secondary zones on secondary instance (ignore "already exists" errors) + while read -r zone; do + echo "Creating secondary zone: $zone" + curl -sf "$SECONDARY/api/zones/create?token=$S_TOKEN&zone=$zone&type=Secondary&primaryNameServerAddresses=$PRIMARY_IP" || true + done < /tmp/zones.txt + + # Force resync all secondary zones to pull latest data + while read -r zone; do + echo "Resyncing: $zone" + curl -sf "$SECONDARY/api/zones/resync?token=$S_TOKEN&zone=$zone" || true + done < /tmp/zones.txt + + echo "Secondary zone setup complete" + SCRIPT + ] + env { + name = "TECH_USER" + value = var.technitium_username + } + env { + name = "TECH_PASS" + value = var.technitium_password + } + env { + name = "PRIMARY_IP" + value = kubernetes_service.technitium_primary.spec[0].cluster_ip + } + } + dns_config { + option { + name = "ndots" + value = "2" + } + } + } + } + } + + depends_on = [ + kubernetes_deployment.technitium, + kubernetes_deployment.technitium_secondary, + kubernetes_service.technitium_primary, + kubernetes_service.technitium_secondary_web, + ] +} diff --git a/stacks/platform/modules/technitium/main.tf b/stacks/platform/modules/technitium/main.tf index a03bd691..cfa2487c 100644 --- a/stacks/platform/modules/technitium/main.tf +++ b/stacks/platform/modules/technitium/main.tf @@ -4,6 +4,8 @@ variable "homepage_token" {} variable "technitium_db_password" {} variable "nfs_server" { type = string } variable "mysql_host" { type = string } +variable "technitium_username" { type = string } +variable "technitium_password" { type = string } resource "kubernetes_namespace" "technitium" { metadata { @@ -91,7 +93,11 @@ resource "kubernetes_deployment" "technitium" { } spec { strategy { - type = "Recreate" + type = "RollingUpdate" + rolling_update { + max_unavailable = "0" + max_surge = "1" + } } # replicas = 1 selector { @@ -107,12 +113,13 @@ resource "kubernetes_deployment" "technitium" { "diun.include_tags" = "latest" } labels = { - app = "technitium" + app = "technitium" + "dns-server" = "true" } } spec { - # Prefer nodes running Traefik for network locality affinity { + # Prefer nodes running Traefik for network locality pod_affinity { preferred_during_scheduling_ignored_during_execution { weight = 100 @@ -128,6 +135,19 @@ resource "kubernetes_deployment" "technitium" { } } } + # Spread DNS pods across nodes for HA + pod_anti_affinity { + required_during_scheduling_ignored_during_execution { + label_selector { + match_expressions { + key = "dns-server" + operator = "In" + values = ["true"] + } + } + topology_key = "kubernetes.io/hostname" + } + } } container { image = "technitium/dns-server:latest" @@ -151,6 +171,20 @@ resource "kubernetes_deployment" "technitium" { port { container_port = 80 } + liveness_probe { + tcp_socket { + port = 53 + } + initial_delay_seconds = 10 + period_seconds = 10 + } + readiness_probe { + tcp_socket { + port = 53 + } + initial_delay_seconds = 5 + period_seconds = 5 + } volume_mount { mount_path = "/etc/dns" name = "nfs-config" @@ -184,7 +218,6 @@ resource "kubernetes_deployment" "technitium" { } } - resource "kubernetes_service" "technitium-web" { metadata { name = "technitium-web" @@ -234,7 +267,7 @@ resource "kubernetes_service" "technitium-dns" { } external_traffic_policy = "Local" selector = { - app = "technitium" + "dns-server" = "true" } } }