# AdGuard Home — household DNS with ad blocking and local nook.family resolution resource "kubernetes_namespace" "dns" { metadata { name = "dns" labels = { managed-by = "terraform" tier = "infrastructure" } } } resource "kubernetes_persistent_volume_claim" "adguard_config" { wait_until_bound = false metadata { name = "adguard-config" namespace = kubernetes_namespace.dns.metadata[0].name } spec { access_modes = ["ReadWriteOnce"] resources { requests = { storage = "1Gi" } } } } resource "kubernetes_persistent_volume_claim" "adguard_data" { wait_until_bound = false metadata { name = "adguard-data" namespace = kubernetes_namespace.dns.metadata[0].name } spec { access_modes = ["ReadWriteOnce"] resources { requests = { storage = "5Gi" } } } } resource "kubernetes_deployment" "adguard" { metadata { name = "adguard" namespace = kubernetes_namespace.dns.metadata[0].name labels = { app = "adguard" } } spec { replicas = 1 strategy { type = "Recreate" } selector { match_labels = { app = "adguard" } } template { metadata { labels = { app = "adguard" } } spec { # hostNetwork gives AdGuard direct access to port 53 on the host IP host_network = true dns_policy = "ClusterFirstWithHostNet" # Ensure bind_hosts is always 0.0.0.0 regardless of what was written to the PVC init_container { name = "fix-bind-hosts" image = "python:3.12-alpine" command = ["python3", "-c"] args = [<<-EOT import re, os config = "/opt/adguardhome/conf/AdGuardHome.yaml" if os.path.exists(config): with open(config) as f: content = f.read() # Replace any bind_hosts list with 0.0.0.0 content = re.sub( r'(bind_hosts:\n)((?:[ \t]*- .*\n)*)', 'bind_hosts:\n - 0.0.0.0\n', content ) with open(config, "w") as f: f.write(content) print("bind_hosts set to 0.0.0.0") else: print("No config yet — AdGuard will create it on first run") EOT ] volume_mount { name = "config" mount_path = "/opt/adguardhome/conf" } } container { name = "adguard" image = "adguard/adguardhome:latest" port { name = "dns-tcp" container_port = 53 protocol = "TCP" } port { name = "dns-udp" container_port = 53 protocol = "UDP" } port { name = "http" container_port = 3000 protocol = "TCP" } volume_mount { name = "config" mount_path = "/opt/adguardhome/conf" } volume_mount { name = "data" mount_path = "/opt/adguardhome/work" } resources { requests = { memory = "128Mi" cpu = "100m" } limits = { memory = "512Mi" cpu = "500m" } } } volume { name = "config" persistent_volume_claim { claim_name = kubernetes_persistent_volume_claim.adguard_config.metadata[0].name } } volume { name = "data" persistent_volume_claim { claim_name = kubernetes_persistent_volume_claim.adguard_data.metadata[0].name } } } } } } # UI accessible via Traefik ingress (port 3000 → ingress) resource "kubernetes_service" "adguard_ui" { metadata { name = "adguard-ui" namespace = kubernetes_namespace.dns.metadata[0].name } spec { selector = { app = "adguard" } port { name = "http" port = 3000 target_port = 3000 } type = "ClusterIP" } } resource "kubernetes_ingress_v1" "adguard" { metadata { name = "adguard" namespace = kubernetes_namespace.dns.metadata[0].name annotations = { "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" "cert-manager.io/cluster-issuer" = "letsencrypt-prod" } } spec { tls { hosts = ["dns.${var.domain_suffix}"] secret_name = "adguard-tls" } rule { host = "dns.${var.domain_suffix}" http { path { path = "/" path_type = "Prefix" backend { service { name = kubernetes_service.adguard_ui.metadata[0].name port { number = 3000 } } } } } } } depends_on = [helm_release.cert_manager] } # Host-level bootstrap notes (run bootstrap.sh once on new Legion installs): # - systemd-resolved disabled (conflicts with AdGuard on port 53) # - k3s config.yaml includes tls-san for LAN IP # - /etc/resolv.conf set to 1.1.1.1 fallback # See: servers/legion/bootstrap.sh