[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.
This commit is contained in:
Viktor Barzin 2026-02-28 14:14:20 +00:00
parent 098c0eb752
commit 6aa29e9f77
No known key found for this signature in database
GPG key ID: 0EB088298288D958
4 changed files with 312 additions and 5 deletions

Binary file not shown.

View file

@ -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
}

View file

@ -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,
]
}

View file

@ -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"
}
}
}