diff --git a/secrets/nfs_directories.txt b/secrets/nfs_directories.txt index 6103be30..f8a4de43 100644 Binary files a/secrets/nfs_directories.txt and b/secrets/nfs_directories.txt differ diff --git a/stacks/platform/main.tf b/stacks/platform/main.tf index 9867649a..2f149b30 100644 --- a/stacks/platform/main.tf +++ b/stacks/platform/main.tf @@ -174,6 +174,8 @@ module "technitium" { mysql_host = var.mysql_host homepage_token = var.homepage_credentials["technitium"]["token"] technitium_db_password = var.technitium_db_password + technitium_username = var.technitium_username + technitium_password = var.technitium_password tier = local.tiers.core } diff --git a/stacks/platform/modules/technitium/ha.tf b/stacks/platform/modules/technitium/ha.tf new file mode 100644 index 00000000..b6db1c17 --- /dev/null +++ b/stacks/platform/modules/technitium/ha.tf @@ -0,0 +1,272 @@ +# ============================================================================= +# Technitium DNS — High Availability (Primary-Secondary) +# ============================================================================= +# +# Secondary DNS instance replicates zones from primary via AXFR. +# Both pods share the `dns-server=true` label so the DNS LoadBalancer +# in main.tf routes queries to whichever pod is healthy. + +# Primary-only service for zone transfers (AXFR) and API access +resource "kubernetes_service" "technitium_primary" { + metadata { + name = "technitium-primary" + namespace = kubernetes_namespace.technitium.metadata[0].name + labels = { + "app" = "technitium" + } + } + + spec { + selector = { + app = "technitium" + } + port { + name = "dns-tcp" + port = 53 + protocol = "TCP" + } + port { + name = "dns-udp" + port = 53 + protocol = "UDP" + } + port { + name = "api" + port = 5380 + protocol = "TCP" + } + } +} + +# Secondary DNS deployment — zone-transfer replica +resource "kubernetes_deployment" "technitium_secondary" { + metadata { + name = "technitium-secondary" + namespace = kubernetes_namespace.technitium.metadata[0].name + labels = { + app = "technitium-secondary" + tier = var.tier + } + } + spec { + replicas = 1 + strategy { + type = "RollingUpdate" + rolling_update { + max_unavailable = "0" + max_surge = "1" + } + } + selector { + match_labels = { + app = "technitium-secondary" + } + } + template { + metadata { + labels = { + app = "technitium-secondary" + "dns-server" = "true" + } + } + spec { + affinity { + 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" + name = "technitium" + env { + name = "DNS_SERVER_ADMIN_PASSWORD" + value = var.technitium_password + } + env { + name = "DNS_SERVER_ENABLE_BLOCKING" + value = "true" + } + resources { + requests = { + cpu = "100m" + memory = "128Mi" + } + limits = { + cpu = "500m" + memory = "512Mi" + } + } + port { + container_port = 5380 + } + port { + container_port = 53 + } + 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" + } + } + volume { + name = "nfs-config" + nfs { + path = "/mnt/main/technitium-secondary" + server = var.nfs_server + } + } + dns_config { + option { + name = "ndots" + value = "2" + } + } + } + } + } +} + +# Secondary web service — internal only, used by setup Job +resource "kubernetes_service" "technitium_secondary_web" { + metadata { + name = "technitium-secondary-web" + namespace = kubernetes_namespace.technitium.metadata[0].name + labels = { + "app" = "technitium-secondary" + } + } + + spec { + selector = { + app = "technitium-secondary" + } + port { + name = "api" + port = 5380 + protocol = "TCP" + } + } +} + +# PodDisruptionBudget — keep at least 1 DNS pod running during voluntary disruptions +resource "kubernetes_pod_disruption_budget_v1" "technitium_dns" { + metadata { + name = "technitium-dns" + namespace = kubernetes_namespace.technitium.metadata[0].name + } + spec { + min_available = "1" + selector { + match_labels = { + "dns-server" = "true" + } + } + } +} + +# Setup Job — configures secondary zones via Technitium REST API +resource "kubernetes_job" "technitium_secondary_setup" { + metadata { + name = "technitium-secondary-setup" + namespace = kubernetes_namespace.technitium.metadata[0].name + } + spec { + backoff_limit = 5 + template { + metadata {} + spec { + restart_policy = "OnFailure" + container { + name = "setup" + image = "curlimages/curl:latest" + command = ["/bin/sh", "-c", <<-SCRIPT + set -e + PRIMARY="http://technitium-primary.technitium.svc.cluster.local:5380" + SECONDARY="http://technitium-secondary-web.technitium.svc.cluster.local:5380" + + # Wait for both to be ready + until curl -sf "$PRIMARY/api/user/login?user=$TECH_USER&pass=$TECH_PASS" -o /tmp/p.json; do echo "Waiting for primary..."; sleep 5; done + until curl -sf "$SECONDARY/api/user/login?user=$TECH_USER&pass=$TECH_PASS" -o /tmp/s.json; do echo "Waiting for secondary..."; sleep 5; done + P_TOKEN=$(cat /tmp/p.json | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') + S_TOKEN=$(cat /tmp/s.json | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') + + # Get zones from primary (split JSON into lines so sed can match each zone) + curl -sf "$PRIMARY/api/zones/list?token=$P_TOKEN" | tr ',' '\n' | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' > /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" } } }