From f3dfe87d03e5d3a1a081dcc97811c8aed6fe106a Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Sun, 26 Apr 2026 17:58:28 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B0=D0=B4=D0=B4=D0=BE=D0=BD=20technitium-dns?= =?UTF-8?q?=20=E2=80=94=20HA=20DNS=20Primary+Secondary=20=D1=81=20kube-vip?= =?UTF-8?q?=20LB=20=D0=B8=20zone=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Helm chart: Primary и Secondary Deployment, kube-vip LoadBalancer сервисы (UDP+TCP :53), ClusterIP для Web UI, PVC (ReadWriteOnce), Secret, Ingress - CronJob sync (*/5 мин): Python sync.py опрашивает Technitium REST API, создаёт Secondary зоны на secondary и вызывает forceSyncZone для каждой зоны - ExternalDNS (disabled по умолчанию): RFC 2136 DDNS для автоматических DNS-записей из Ingress - Ansible role: validate, namespace, Helm deploy, cleanup secrets, summary с Keenetic-инструкцией - Интеграция: Makefile, playbooks/addons.yml, group_vars/all/addons.yml, vault.yml.example - README с архитектурой, Keenetic-конфигурацией и troubleshooting --- Makefile | 6 +- addons/technitium-dns/README.md | 173 ++++++++++++++++++ addons/technitium-dns/playbook.yml | 7 + addons/technitium-dns/role/chart/Chart.yaml | 15 ++ .../technitium-dns/role/chart/files/sync.py | 150 +++++++++++++++ .../role/chart/templates/NOTES.txt | 38 ++++ .../role/chart/templates/_helpers.tpl | 18 ++ .../role/chart/templates/configmap-sync.yaml | 12 ++ .../role/chart/templates/cronjob-sync.yaml | 53 ++++++ .../chart/templates/deployment-primary.yaml | 94 ++++++++++ .../chart/templates/deployment-secondary.yaml | 91 +++++++++ .../role/chart/templates/externaldns.yaml | 83 +++++++++ .../role/chart/templates/ingress.yaml | 64 +++++++ .../role/chart/templates/pvc.yaml | 40 ++++ .../role/chart/templates/secret.yaml | 10 + .../role/chart/templates/service-primary.yaml | 49 +++++ .../chart/templates/service-secondary.yaml | 49 +++++ addons/technitium-dns/role/chart/values.yaml | 97 ++++++++++ addons/technitium-dns/role/defaults/main.yml | 51 ++++++ addons/technitium-dns/role/tasks/main.yml | 147 +++++++++++++++ .../role/templates/values.yaml.j2 | 47 +++++ group_vars/all/addons.yml | 27 ++- group_vars/all/vault.yml.example | 3 + playbooks/addons.yml | 8 + 24 files changed, 1329 insertions(+), 3 deletions(-) create mode 100644 addons/technitium-dns/README.md create mode 100644 addons/technitium-dns/playbook.yml create mode 100644 addons/technitium-dns/role/chart/Chart.yaml create mode 100644 addons/technitium-dns/role/chart/files/sync.py create mode 100644 addons/technitium-dns/role/chart/templates/NOTES.txt create mode 100644 addons/technitium-dns/role/chart/templates/_helpers.tpl create mode 100644 addons/technitium-dns/role/chart/templates/configmap-sync.yaml create mode 100644 addons/technitium-dns/role/chart/templates/cronjob-sync.yaml create mode 100644 addons/technitium-dns/role/chart/templates/deployment-primary.yaml create mode 100644 addons/technitium-dns/role/chart/templates/deployment-secondary.yaml create mode 100644 addons/technitium-dns/role/chart/templates/externaldns.yaml create mode 100644 addons/technitium-dns/role/chart/templates/ingress.yaml create mode 100644 addons/technitium-dns/role/chart/templates/pvc.yaml create mode 100644 addons/technitium-dns/role/chart/templates/secret.yaml create mode 100644 addons/technitium-dns/role/chart/templates/service-primary.yaml create mode 100644 addons/technitium-dns/role/chart/templates/service-secondary.yaml create mode 100644 addons/technitium-dns/role/chart/values.yaml create mode 100644 addons/technitium-dns/role/defaults/main.yml create mode 100644 addons/technitium-dns/role/tasks/main.yml create mode 100644 addons/technitium-dns/role/templates/values.yaml.j2 diff --git a/Makefile b/Makefile index 0340dfa..6955d7f 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ DOCKER_RUN := docker run --rm -it \ addon-harbor addon-gitea addon-owncloud addon-nextcloud \ addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \ addon-smtp-relay addon-vault addon-external-secrets \ - addon-jenkins addon-netbird addon-mediaserver addon-hysteria2-server addon-splitgw addon-ingress-proxypass addon-ingress-add-domains addon-yandex-dns-controller \ + addon-jenkins addon-netbird addon-mediaserver addon-hysteria2-server addon-splitgw addon-ingress-proxypass addon-ingress-add-domains addon-yandex-dns-controller addon-technitium-dns \ add-node remove-node \ add-etcd-node remove-etcd-node \ etcd-backup etcd-restore etcd-list-snapshots \ @@ -432,6 +432,10 @@ addon-yandex-dns-controller: _check_env _check_image ## Yandex 360 DNS controlle @printf "$(CYAN)Устанавливаю Yandex 360 DNS Controller...$(NC)\n" $(DOCKER_RUN) addon yandex-dns-controller $(ARGS) +addon-technitium-dns: _check_env _check_image ## Technitium DNS HA — Primary+Secondary, kube-vip LB, zone sync + @printf "$(CYAN)Устанавливаю Technitium DNS HA...$(NC)\n" + $(DOCKER_RUN) addon technitium-dns $(ARGS) + # Generic цель — любой аддон из addons//playbook.yml addon-%: _check_env _check_image @if [ ! -f "addons/$*/playbook.yml" ]; then \ diff --git a/addons/technitium-dns/README.md b/addons/technitium-dns/README.md new file mode 100644 index 0000000..392954d --- /dev/null +++ b/addons/technitium-dns/README.md @@ -0,0 +1,173 @@ +# technitium-dns + +Highly-available internal DNS based on [Technitium DNS Server](https://technitium.com/dns/). + +Deploys Primary + optional Secondary instance, each behind a **kube-vip** `LoadBalancer` service with a static IP. A `CronJob` syncs all Primary zones to Secondary automatically every 5 minutes via the Technitium REST API. + +## Architecture + +``` +Clients (Keenetic / DHCP) + │ + ├─ DNS 192.168.1.53 → technitium-dns-primary (Deployment, RWO PVC) + └─ DNS 192.168.1.54 → technitium-dns-secondary (Deployment, RWO PVC) + +CronJob sync (*/5 min): primary REST API → list zones → create missing Secondary zones on secondary → forceSync + +Web UI (Ingress): + http://dns.home.local → primary :5380 + http://dns-secondary.home.local → secondary :5380 + +ExternalDNS (optional, disabled by default): + Watches Ingress/Service → RFC 2136 DDNS → primary → AXFR → secondary +``` + +## Quick start + +### 1. Set vault password + +```yaml +# group_vars/all/vault.yml (encrypted with ansible-vault) +technitium_dns_admin_password: "your-strong-password" +``` + +### 2. Enable and configure + +```yaml +# group_vars/all/addons.yml +addon_technitium_dns: true + +technitium_dns_primary_ip: "192.168.1.53" # kube-vip managed IP +technitium_dns_secondary_ip: "192.168.1.54" +technitium_dns_domain: "home.local" +technitium_dns_primary_host: "dns.home.local" +technitium_dns_secondary_host: "dns-secondary.home.local" +``` + +### 3. Deploy + +```bash +make addon-technitium-dns +# or: +ansible-playbook playbooks/addons.yml --tags technitium-dns +``` + +### 4. Create the internal zone (first time only) + +Open `http://dns.home.local/` → login as `admin` → **Zones → Add Zone → Primary** → enter `home.local`. + +Then add `A` records for your services under `home.local`. + +--- + +## Keenetic router — DNS configuration + +In Keenetic web interface: **Internet → DNS servers** + +| Field | Value | +|-------|-------| +| Primary DNS | `192.168.1.53` | +| Secondary DNS | `192.168.1.54` | + +Or via Keenetic CLI: +``` +ip name-server 192.168.1.53 +ip name-server 192.168.1.54 +``` + +--- + +## Zone sync (Primary → Secondary) + +The `technitium-dns-sync` `CronJob` runs every 5 minutes. It: + +1. Logs in to both instances with the shared admin password. +2. Lists all `Primary` and `Forwarder` zones on primary. +3. Creates missing zones on secondary as `Secondary` type pointing to `primary.ip`. +4. Calls `forceSyncZone` for every zone. + +Manual trigger: +```bash +kubectl create job --from=cronjob/technitium-dns-sync sync-manual-1 \ + -n technitium-dns +kubectl -n technitium-dns logs -l app.kubernetes.io/component=sync -f +``` + +--- + +## ExternalDNS (optional) + +Automatically creates DNS records on primary from `Ingress` and `Service` resources via **RFC 2136 DDNS**. Secondary picks up changes via the sync CronJob. + +### Enable + +```yaml +# group_vars/all/addons.yml +technitium_dns_externaldns_enabled: true +technitium_dns_externaldns_domain_filter: + - "home.local" +technitium_dns_externaldns_policy: "upsert-only" # or "sync" to also delete +``` + +### Enable DDNS on zones in Technitium + +For each zone that ExternalDNS should write to: + +1. Open Web UI → Zones → `home.local` → **Zone Settings** +2. **Dynamic Updates** → set to `Allow` (or `Allow Signed` for TSIG) +3. Save. + +--- + +## Variables reference + +| Variable | Default | Description | +|----------|---------|-------------| +| `technitium_dns_primary_ip` | `192.168.1.53` | kube-vip LB IP for primary | +| `technitium_dns_secondary_enabled` | `true` | Deploy secondary instance | +| `technitium_dns_secondary_ip` | `192.168.1.54` | kube-vip LB IP for secondary | +| `technitium_dns_primary_node` | `""` | Pin primary to node hostname | +| `technitium_dns_secondary_node` | `""` | Pin secondary to node hostname | +| `technitium_dns_domain` | `home.local` | Local DNS domain | +| `technitium_dns_forwarders` | `[1.1.1.1, 8.8.8.8]` | Upstream resolvers | +| `technitium_dns_recursion` | `AllowOnlyForPrivateNetworks` | Recursion mode | +| `technitium_dns_admin_password` | — | **In vault.yml** — admin password | +| `technitium_dns_storage_class` | `""` | StorageClass (empty = cluster default) | +| `technitium_dns_storage_size` | `1Gi` | PVC size per instance | +| `technitium_dns_ingress_enabled` | `true` | Expose Web UI via Ingress | +| `technitium_dns_primary_host` | `dns.home.local` | Primary Web UI hostname | +| `technitium_dns_secondary_host` | `dns-secondary.home.local` | Secondary Web UI hostname | +| `technitium_dns_sync_enabled` | `true` | Enable zone sync CronJob | +| `technitium_dns_sync_schedule` | `*/5 * * * *` | Sync frequency | +| `technitium_dns_externaldns_enabled` | `false` | Deploy ExternalDNS | +| `technitium_dns_externaldns_policy` | `upsert-only` | ExternalDNS sync policy | + +--- + +## Troubleshooting + +**DNS not resolving after deploy** + +```bash +# Check pods are Running +kubectl -n technitium-dns get pods + +# Test DNS resolution from a pod +kubectl run dnstest --rm -it --image=busybox -- nslookup kubernetes.default 192.168.1.53 +``` + +**Sync job failing** + +```bash +kubectl -n technitium-dns logs -l app.kubernetes.io/component=sync --tail=100 +``` + +Common cause: secondary is not yet ready when the first sync runs. The job will retry on the next schedule. + +**Secondary shows stale records** + +Force a manual sync (see above). If secondary zone type is wrong, delete the zone on secondary and let sync recreate it. + +**kube-vip IP not assigned** + +Ensure the IP is in the kube-vip address pool (check `kube-vip` ConfigMap or CiliumLoadBalancerIPPool) and not already in use. diff --git a/addons/technitium-dns/playbook.yml b/addons/technitium-dns/playbook.yml new file mode 100644 index 0000000..c94342b --- /dev/null +++ b/addons/technitium-dns/playbook.yml @@ -0,0 +1,7 @@ +--- +- name: Install Technitium DNS HA + hosts: k3s_master[0] + gather_facts: false + become: true + roles: + - role: "{{ playbook_dir }}/role" diff --git a/addons/technitium-dns/role/chart/Chart.yaml b/addons/technitium-dns/role/chart/Chart.yaml new file mode 100644 index 0000000..a11ec46 --- /dev/null +++ b/addons/technitium-dns/role/chart/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: technitium-dns +description: | + HA DNS сервер на базе Technitium DNS Server для K3s homelab. + Primary + Secondary инстансы с автоматической синхронизацией зон. + Интеграция с kube-vip (LoadBalancer), ingress-nginx (Web UI) и ExternalDNS (RFC 2136). +type: application +version: 1.0.0 +appVersion: "13" +keywords: + - dns + - technitium + - ha + - homelab +home: https://git.antropoff.ru/DevOpsTools/K3S diff --git a/addons/technitium-dns/role/chart/files/sync.py b/addons/technitium-dns/role/chart/files/sync.py new file mode 100644 index 0000000..0434fc2 --- /dev/null +++ b/addons/technitium-dns/role/chart/files/sync.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +Technitium DNS zone sync: ensures secondary has all Primary/Forwarder zones +from primary as Secondary zones pointing back to primary's LoadBalancer IP. + +Required env vars: + PRIMARY_URL http://technitium-dns-primary-web.technitium-dns:5380 + SECONDARY_URL http://technitium-dns-secondary-web.technitium-dns:5380 + ADMIN_PASSWORD shared admin password + PRIMARY_LB_IP LB IP of primary (for AXFR source in secondary zones) +""" + +import os +import sys +import logging +import requests + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger(__name__) + +PRIMARY_URL = os.environ["PRIMARY_URL"].rstrip("/") +SECONDARY_URL = os.environ["SECONDARY_URL"].rstrip("/") +ADMIN_PASSWORD = os.environ["ADMIN_PASSWORD"] +PRIMARY_LB_IP = os.environ["PRIMARY_LB_IP"] + +SESSION_PRIMARY = requests.Session() +SESSION_SECONDARY = requests.Session() + +# Zone types that should be replicated to secondary +SYNC_TYPES = {"Primary", "Forwarder"} + + +def login(session: requests.Session, base_url: str, password: str) -> str: + r = session.get( + f"{base_url}/api/user/login", + params={"user": "admin", "pass": password, "includeInfo": "false"}, + timeout=10, + ) + r.raise_for_status() + body = r.json() + if body.get("status") != "ok": + raise RuntimeError(f"Login failed at {base_url}: {body}") + token = body["response"]["token"] + log.info("Logged in to %s", base_url) + return token + + +def list_zones(session: requests.Session, base_url: str, token: str) -> list[dict]: + r = session.get( + f"{base_url}/api/zones/list", + params={"token": token}, + timeout=10, + ) + r.raise_for_status() + body = r.json() + if body.get("status") != "ok": + raise RuntimeError(f"list_zones failed: {body}") + return body["response"]["zones"] + + +def create_secondary_zone( + session: requests.Session, base_url: str, token: str, zone: str, primary_ip: str +) -> None: + r = session.post( + f"{base_url}/api/zones/create", + params={ + "token": token, + "zone": zone, + "type": "Secondary", + "primaryNameServerAddresses": primary_ip, + }, + timeout=15, + ) + r.raise_for_status() + body = r.json() + if body.get("status") != "ok": + raise RuntimeError(f"create zone {zone!r} failed: {body}") + log.info("Created secondary zone: %s (primary=%s)", zone, primary_ip) + + +def force_sync( + session: requests.Session, base_url: str, token: str, zone: str +) -> None: + r = session.post( + f"{base_url}/api/zones/forceSyncZone", + params={"token": token, "zone": zone}, + timeout=15, + ) + r.raise_for_status() + body = r.json() + if body.get("status") != "ok": + log.warning("forceSyncZone %s returned non-ok: %s", zone, body) + else: + log.info("Forced sync: %s", zone) + + +def main() -> None: + tok_primary = login(SESSION_PRIMARY, PRIMARY_URL, ADMIN_PASSWORD) + tok_secondary = login(SESSION_SECONDARY, SECONDARY_URL, ADMIN_PASSWORD) + + primary_zones = list_zones(SESSION_PRIMARY, PRIMARY_URL, tok_primary) + secondary_zones = list_zones(SESSION_SECONDARY, SECONDARY_URL, tok_secondary) + + secondary_names = {z["name"] for z in secondary_zones} + + stats = {"created": 0, "synced": 0, "skipped": 0, "errors": 0} + + for zone in primary_zones: + name = zone["name"] + ztype = zone.get("type", "") + + if ztype not in SYNC_TYPES: + stats["skipped"] += 1 + continue + + if name not in secondary_names: + try: + create_secondary_zone( + SESSION_SECONDARY, SECONDARY_URL, tok_secondary, name, PRIMARY_LB_IP + ) + stats["created"] += 1 + # Re-fetch token after zone creation is not needed; force sync right away + force_sync(SESSION_SECONDARY, SECONDARY_URL, tok_secondary, name) + stats["synced"] += 1 + except Exception as exc: + log.error("Failed to create/sync zone %s: %s", name, exc) + stats["errors"] += 1 + else: + try: + force_sync(SESSION_SECONDARY, SECONDARY_URL, tok_secondary, name) + stats["synced"] += 1 + except Exception as exc: + log.error("Failed to force-sync zone %s: %s", name, exc) + stats["errors"] += 1 + + log.info( + "Done — created=%d synced=%d skipped=%d errors=%d", + stats["created"], stats["synced"], stats["skipped"], stats["errors"], + ) + + if stats["errors"] > 0: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/addons/technitium-dns/role/chart/templates/NOTES.txt b/addons/technitium-dns/role/chart/templates/NOTES.txt new file mode 100644 index 0000000..9d36f52 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/NOTES.txt @@ -0,0 +1,38 @@ +╔══════════════════════════════════════════════════════════════╗ +║ Technitium DNS HA — Deployed ║ +╚══════════════════════════════════════════════════════════════╝ + +Primary DNS: + LoadBalancer IP : {{ .Values.primary.ip }} + Web UI : http://{{ .Values.ingress.primary.host }}/ + API : http://{{ .Values.primary.ip }}:5380/ + +{{- if .Values.secondary.enabled }} +Secondary DNS: + LoadBalancer IP : {{ .Values.secondary.ip }} + Web UI : http://{{ .Values.ingress.secondary.host }}/ +{{- end }} + +Configure Keenetic router (or DHCP server) to use: + Primary DNS : {{ .Values.primary.ip }} +{{- if .Values.secondary.enabled }} + Secondary DNS : {{ .Values.secondary.ip }} +{{- end }} + +First-time setup (create your internal zone): + kubectl -n {{ .Release.Namespace }} exec -it deploy/{{ include "technitium-dns.name" . }}-primary -- \ + curl -s "http://localhost:5380/api/zones/create?token=\$TOKEN&zone={{ .Values.dns.domain }}&type=Primary" + +Zone sync CronJob (primary → secondary): +{{- if and .Values.sync.enabled .Values.secondary.enabled }} + kubectl -n {{ .Release.Namespace }} create job --from=cronjob/{{ include "technitium-dns.name" . }}-sync sync-manual-1 + kubectl -n {{ .Release.Namespace }} logs -l app.kubernetes.io/component=sync --tail=50 +{{- else }} + (disabled — set sync.enabled=true and secondary.enabled=true to enable) +{{- end }} + +Logs: + kubectl -n {{ .Release.Namespace }} logs -l app.kubernetes.io/component=primary -f +{{- if .Values.secondary.enabled }} + kubectl -n {{ .Release.Namespace }} logs -l app.kubernetes.io/component=secondary -f +{{- end }} diff --git a/addons/technitium-dns/role/chart/templates/_helpers.tpl b/addons/technitium-dns/role/chart/templates/_helpers.tpl new file mode 100644 index 0000000..684f342 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/_helpers.tpl @@ -0,0 +1,18 @@ +{{- define "technitium-dns.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "technitium-dns.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "technitium-dns.labels" -}} +helm.sh/chart: {{ include "technitium-dns.chart" . }} +{{ include "technitium-dns.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "technitium-dns.selectorLabels" -}} +app.kubernetes.io/name: {{ include "technitium-dns.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/addons/technitium-dns/role/chart/templates/configmap-sync.yaml b/addons/technitium-dns/role/chart/templates/configmap-sync.yaml new file mode 100644 index 0000000..198ebc6 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/configmap-sync.yaml @@ -0,0 +1,12 @@ +{{- if and .Values.sync.enabled .Values.secondary.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "technitium-dns.name" . }}-sync + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} +data: + sync.py: | +{{ .Files.Get "files/sync.py" | indent 4 }} +{{- end }} diff --git a/addons/technitium-dns/role/chart/templates/cronjob-sync.yaml b/addons/technitium-dns/role/chart/templates/cronjob-sync.yaml new file mode 100644 index 0000000..ff1ac60 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/cronjob-sync.yaml @@ -0,0 +1,53 @@ +{{- if and .Values.sync.enabled .Values.secondary.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "technitium-dns.name" . }}-sync + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} +spec: + schedule: {{ .Values.sync.schedule | quote }} + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + metadata: + labels: + {{- include "technitium-dns.selectorLabels" . | nindent 12 }} + app.kubernetes.io/component: sync + spec: + restartPolicy: OnFailure + containers: + - name: sync + image: {{ .Values.sync.image }} + imagePullPolicy: IfNotPresent + command: + - python3 + - /scripts/sync.py + env: + - name: PRIMARY_URL + value: "http://{{ include "technitium-dns.name" . }}-primary-web.{{ .Release.Namespace }}:5380" + - name: SECONDARY_URL + value: "http://{{ include "technitium-dns.name" . }}-secondary-web.{{ .Release.Namespace }}:5380" + - name: PRIMARY_LB_IP + value: {{ .Values.primary.ip | quote }} + - name: ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "technitium-dns.name" . }}-secret + key: adminPassword + volumeMounts: + - name: scripts + mountPath: /scripts + resources: + {{- toYaml .Values.sync.resources | nindent 16 }} + volumes: + - name: scripts + configMap: + name: {{ include "technitium-dns.name" . }}-sync + defaultMode: 0755 +{{- end }} diff --git a/addons/technitium-dns/role/chart/templates/deployment-primary.yaml b/addons/technitium-dns/role/chart/templates/deployment-primary.yaml new file mode 100644 index 0000000..dee8656 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/deployment-primary.yaml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "technitium-dns.name" . }}-primary + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: primary +spec: + replicas: 1 + # Recreate is required for ReadWriteOnce PVCs — ensures old pod is fully + # terminated before new pod mounts the volume. + strategy: + type: Recreate + selector: + matchLabels: + {{- include "technitium-dns.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: primary + template: + metadata: + labels: + {{- include "technitium-dns.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: primary + spec: + {{- if .Values.primary.nodeName }} + # Hard pin to specific node + nodeSelector: + kubernetes.io/hostname: {{ .Values.primary.nodeName | quote }} + {{- else }} + # Soft anti-affinity: prefer different node than secondary + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + {{- include "technitium-dns.selectorLabels" . | nindent 20 }} + app.kubernetes.io/component: secondary + topologyKey: kubernetes.io/hostname + {{- end }} + containers: + - name: dns + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: dns-udp + containerPort: 53 + protocol: UDP + - name: dns-tcp + containerPort: 53 + protocol: TCP + - name: web-ui + containerPort: 5380 + protocol: TCP + env: + # Set admin password on first boot (stored in config after that) + - name: DNS_SERVER_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "technitium-dns.name" . }}-secret + key: adminPassword + - name: DNS_SERVER_DOMAIN + value: {{ printf "dns1.%s" .Values.dns.domain | quote }} + # Upstream forwarders for queries outside managed zones + - name: DNS_SERVER_FORWARDERS + value: {{ .Values.dns.forwarders | join "," | quote }} + - name: DNS_SERVER_RECURSION + value: {{ .Values.dns.recursion | quote }} + - name: DNS_SERVER_LOCAL_END_POINTS + value: "0.0.0.0:53" + volumeMounts: + - name: data + mountPath: /etc/dns + resources: + {{- toYaml .Values.resources | nindent 12 }} + readinessProbe: + httpGet: + path: / + port: 5380 + initialDelaySeconds: 20 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: 5380 + initialDelaySeconds: 45 + periodSeconds: 20 + failureThreshold: 3 + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "technitium-dns.name" . }}-primary diff --git a/addons/technitium-dns/role/chart/templates/deployment-secondary.yaml b/addons/technitium-dns/role/chart/templates/deployment-secondary.yaml new file mode 100644 index 0000000..e86e6f1 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/deployment-secondary.yaml @@ -0,0 +1,91 @@ +{{- if .Values.secondary.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "technitium-dns.name" . }}-secondary + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: secondary +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "technitium-dns.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: secondary + template: + metadata: + labels: + {{- include "technitium-dns.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: secondary + spec: + {{- if .Values.secondary.nodeName }} + nodeSelector: + kubernetes.io/hostname: {{ .Values.secondary.nodeName | quote }} + {{- else }} + # Soft anti-affinity: prefer different node than primary + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + {{- include "technitium-dns.selectorLabels" . | nindent 20 }} + app.kubernetes.io/component: primary + topologyKey: kubernetes.io/hostname + {{- end }} + containers: + - name: dns + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: dns-udp + containerPort: 53 + protocol: UDP + - name: dns-tcp + containerPort: 53 + protocol: TCP + - name: web-ui + containerPort: 5380 + protocol: TCP + env: + - name: DNS_SERVER_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "technitium-dns.name" . }}-secret + key: adminPassword + - name: DNS_SERVER_DOMAIN + value: {{ printf "dns2.%s" .Values.dns.domain | quote }} + - name: DNS_SERVER_FORWARDERS + value: {{ .Values.dns.forwarders | join "," | quote }} + - name: DNS_SERVER_RECURSION + value: {{ .Values.dns.recursion | quote }} + - name: DNS_SERVER_LOCAL_END_POINTS + value: "0.0.0.0:53" + volumeMounts: + - name: data + mountPath: /etc/dns + resources: + {{- toYaml .Values.resources | nindent 12 }} + readinessProbe: + httpGet: + path: / + port: 5380 + initialDelaySeconds: 20 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: 5380 + initialDelaySeconds: 45 + periodSeconds: 20 + failureThreshold: 3 + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "technitium-dns.name" . }}-secondary +{{- end }} diff --git a/addons/technitium-dns/role/chart/templates/externaldns.yaml b/addons/technitium-dns/role/chart/templates/externaldns.yaml new file mode 100644 index 0000000..25c59b5 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/externaldns.yaml @@ -0,0 +1,83 @@ +{{- if .Values.externalDns.enabled }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "technitium-dns.name" . }}-external-dns + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "technitium-dns.name" . }}-external-dns + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["services", "endpoints", "pods", "nodes"] + verbs: ["get", "watch", "list"] + - apiGroups: ["extensions", "networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get", "watch", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "technitium-dns.name" . }}-external-dns + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "technitium-dns.name" . }}-external-dns +subjects: + - kind: ServiceAccount + name: {{ include "technitium-dns.name" . }}-external-dns + namespace: {{ .Release.Namespace }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "technitium-dns.name" . }}-external-dns + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: external-dns +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "technitium-dns.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: external-dns + template: + metadata: + labels: + {{- include "technitium-dns.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: external-dns + spec: + serviceAccountName: {{ include "technitium-dns.name" . }}-external-dns + containers: + - name: external-dns + image: {{ .Values.externalDns.image }} + imagePullPolicy: IfNotPresent + args: + - --source=ingress + - --source=service + - --provider=rfc2136 + - --rfc2136-host={{ .Values.primary.ip }} + - --rfc2136-port=53 + - --rfc2136-zone={{ .Values.dns.domain }} + - --rfc2136-insecure + - --txt-owner-id={{ .Values.externalDns.txtOwnerId }} + - --policy={{ .Values.externalDns.policy }} + - --log-level=info + {{- range .Values.externalDns.domainFilter }} + - --domain-filter={{ . }} + {{- end }} + resources: + {{- toYaml .Values.externalDns.resources | nindent 12 }} +{{- end }} diff --git a/addons/technitium-dns/role/chart/templates/ingress.yaml b/addons/technitium-dns/role/chart/templates/ingress.yaml new file mode 100644 index 0000000..0ae9bfe --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/ingress.yaml @@ -0,0 +1,64 @@ +{{- if .Values.ingress.enabled }} +--- +# Ingress for primary Web UI +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "technitium-dns.name" . }}-primary + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: primary + annotations: + kubernetes.io/ingress.class: {{ .Values.ingress.ingressClass | quote }} +spec: + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.primary.host | quote }} + secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-primary-tls" (include "technitium-dns.name" .)) | quote }} + {{- end }} + rules: + - host: {{ .Values.ingress.primary.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "technitium-dns.name" . }}-primary-web + port: + number: 5380 +{{- if .Values.secondary.enabled }} +--- +# Ingress for secondary Web UI +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "technitium-dns.name" . }}-secondary + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: secondary + annotations: + kubernetes.io/ingress.class: {{ .Values.ingress.ingressClass | quote }} +spec: + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.secondary.host | quote }} + secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-secondary-tls" (include "technitium-dns.name" .)) | quote }} + {{- end }} + rules: + - host: {{ .Values.ingress.secondary.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "technitium-dns.name" . }}-secondary-web + port: + number: 5380 +{{- end }} +{{- end }} diff --git a/addons/technitium-dns/role/chart/templates/pvc.yaml b/addons/technitium-dns/role/chart/templates/pvc.yaml new file mode 100644 index 0000000..4d7977d --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/pvc.yaml @@ -0,0 +1,40 @@ +--- +# PVC for primary instance +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "technitium-dns.name" . }}-primary + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: primary +spec: + accessModes: + - {{ .Values.storage.accessMode }} + {{- if .Values.storage.storageClassName }} + storageClassName: {{ .Values.storage.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.storage.size }} +{{- if .Values.secondary.enabled }} +--- +# PVC for secondary instance +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "technitium-dns.name" . }}-secondary + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: secondary +spec: + accessModes: + - {{ .Values.storage.accessMode }} + {{- if .Values.storage.storageClassName }} + storageClassName: {{ .Values.storage.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.storage.size }} +{{- end }} diff --git a/addons/technitium-dns/role/chart/templates/secret.yaml b/addons/technitium-dns/role/chart/templates/secret.yaml new file mode 100644 index 0000000..6ec3261 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "technitium-dns.name" . }}-secret + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} +type: Opaque +data: + adminPassword: {{ .Values.secret.adminPassword | b64enc | quote }} diff --git a/addons/technitium-dns/role/chart/templates/service-primary.yaml b/addons/technitium-dns/role/chart/templates/service-primary.yaml new file mode 100644 index 0000000..ada9cad --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/service-primary.yaml @@ -0,0 +1,49 @@ +--- +# DNS LoadBalancer service — kube-vip assigns the static IP. +# Exposes UDP/53 + TCP/53 (requires K3s 1.26+ for MixedProtocol, which is the default). +apiVersion: v1 +kind: Service +metadata: + name: {{ include "technitium-dns.name" . }}-primary + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: primary + annotations: + kube-vip.io/loadbalancerIPs: {{ .Values.primary.ip | quote }} +spec: + type: LoadBalancer + # Local preserves client source IP and avoids extra hop through kube-proxy + externalTrafficPolicy: Local + selector: + {{- include "technitium-dns.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: primary + ports: + - name: dns-udp + port: 53 + targetPort: 53 + protocol: UDP + - name: dns-tcp + port: 53 + targetPort: 53 + protocol: TCP +--- +# ClusterIP service for Web UI — used as Ingress backend +apiVersion: v1 +kind: Service +metadata: + name: {{ include "technitium-dns.name" . }}-primary-web + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: primary +spec: + type: ClusterIP + selector: + {{- include "technitium-dns.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: primary + ports: + - name: web-ui + port: 5380 + targetPort: 5380 + protocol: TCP diff --git a/addons/technitium-dns/role/chart/templates/service-secondary.yaml b/addons/technitium-dns/role/chart/templates/service-secondary.yaml new file mode 100644 index 0000000..b68b637 --- /dev/null +++ b/addons/technitium-dns/role/chart/templates/service-secondary.yaml @@ -0,0 +1,49 @@ +{{- if .Values.secondary.enabled }} +--- +# DNS LoadBalancer service for secondary +apiVersion: v1 +kind: Service +metadata: + name: {{ include "technitium-dns.name" . }}-secondary + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: secondary + annotations: + kube-vip.io/loadbalancerIPs: {{ .Values.secondary.ip | quote }} +spec: + type: LoadBalancer + externalTrafficPolicy: Local + selector: + {{- include "technitium-dns.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: secondary + ports: + - name: dns-udp + port: 53 + targetPort: 53 + protocol: UDP + - name: dns-tcp + port: 53 + targetPort: 53 + protocol: TCP +--- +# ClusterIP service for secondary Web UI +apiVersion: v1 +kind: Service +metadata: + name: {{ include "technitium-dns.name" . }}-secondary-web + namespace: {{ .Release.Namespace }} + labels: + {{- include "technitium-dns.labels" . | nindent 4 }} + app.kubernetes.io/component: secondary +spec: + type: ClusterIP + selector: + {{- include "technitium-dns.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: secondary + ports: + - name: web-ui + port: 5380 + targetPort: 5380 + protocol: TCP +{{- end }} diff --git a/addons/technitium-dns/role/chart/values.yaml b/addons/technitium-dns/role/chart/values.yaml new file mode 100644 index 0000000..932779d --- /dev/null +++ b/addons/technitium-dns/role/chart/values.yaml @@ -0,0 +1,97 @@ +# Technitium DNS HA — default values +# Override via group_vars/all/addons.yml → technitium_dns_* variables + +image: + repository: technitium/dns-server + tag: "13" + pullPolicy: IfNotPresent + +# ── Primary instance ────────────────────────────────────────────────────────── +primary: + # Static IP for kube-vip LoadBalancer (DNS port 53) + ip: "192.168.1.53" + # Pin to specific K8s node hostname (empty = use soft podAntiAffinity) + nodeName: "" + +# ── Secondary instance ──────────────────────────────────────────────────────── +secondary: + enabled: true + ip: "192.168.1.54" + nodeName: "" + +# ── DNS server config ───────────────────────────────────────────────────────── +dns: + # Domain served locally (e.g. home.local) + domain: "home.local" + # Upstream forwarders for unknown queries + forwarders: + - "1.1.1.1" + - "8.8.8.8" + # AllowOnlyForPrivateNetworks | Allow | Deny + recursion: "AllowOnlyForPrivateNetworks" + +# ── Admin credentials ───────────────────────────────────────────────────────── +secret: + adminPassword: "" # filled from Ansible vault (technitium_dns_admin_password) + +# ── Persistent storage ──────────────────────────────────────────────────────── +storage: + # StorageClass: empty = cluster default. "nfs-master01" recommended if NFS is set up. + storageClassName: "" + size: "1Gi" + accessMode: ReadWriteOnce + +# ── Web UI via ingress-nginx ────────────────────────────────────────────────── +ingress: + enabled: true + ingressClass: nginx + primary: + host: "dns.home.local" + secondary: + host: "dns-secondary.home.local" + tls: + enabled: false + secretName: "" + +# ── Resource limits ─────────────────────────────────────────────────────────── +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + +# ── Zone sync CronJob (primary → secondary via AXFR) ───────────────────────── +# Ensures secondary has all zones from primary as Secondary (AXFR) zones. +sync: + enabled: true + schedule: "*/5 * * * *" + image: "python:3.11-slim" + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 100m + memory: 128Mi + +# ── ExternalDNS via RFC 2136 DDNS (optional) ───────────────────────────────── +# Watches Ingress resources and creates DNS records on primary automatically. +# Requires DDNS enabled on Technitium zones: Zone Settings → Dynamic Updates. +externalDns: + enabled: false + image: "registry.k8s.io/external-dns/external-dns:v0.14.2" + # DNS zones to manage + domainFilter: + - "home.local" + # sync = manage records (add+delete), upsert-only = only add + policy: "upsert-only" + txtOwnerId: "k3s-home" + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 100m + memory: 128Mi diff --git a/addons/technitium-dns/role/defaults/main.yml b/addons/technitium-dns/role/defaults/main.yml new file mode 100644 index 0000000..cffe277 --- /dev/null +++ b/addons/technitium-dns/role/defaults/main.yml @@ -0,0 +1,51 @@ +--- +# ── Namespace ───────────────────────────────────────────────────────────────── +technitium_dns_namespace: technitium-dns +technitium_dns_release_name: technitium-dns + +# ── Primary DNS LoadBalancer IP (kube-vip) ──────────────────────────────────── +technitium_dns_primary_ip: "192.168.1.53" +technitium_dns_primary_node: "" # pin to hostname, empty = soft anti-affinity + +# ── Secondary DNS (optional HA) ─────────────────────────────────────────────── +technitium_dns_secondary_enabled: true +technitium_dns_secondary_ip: "192.168.1.54" +technitium_dns_secondary_node: "" + +# ── DNS domain served locally ───────────────────────────────────────────────── +technitium_dns_domain: "home.local" + +# ── Upstream forwarders ─────────────────────────────────────────────────────── +technitium_dns_forwarders: + - "1.1.1.1" + - "8.8.8.8" + +# ── Recursion policy ────────────────────────────────────────────────────────── +# AllowOnlyForPrivateNetworks | Allow | Deny +technitium_dns_recursion: "AllowOnlyForPrivateNetworks" + +# ── Admin password — set in vault.yml: technitium_dns_admin_password ────────── +# technitium_dns_admin_password: "" + +# ── Storage ─────────────────────────────────────────────────────────────────── +technitium_dns_storage_class: "" # empty = cluster default +technitium_dns_storage_size: "1Gi" + +# ── Web UI via Ingress ──────────────────────────────────────────────────────── +technitium_dns_ingress_enabled: true +technitium_dns_ingress_class: nginx +technitium_dns_primary_host: "dns.home.local" +technitium_dns_secondary_host: "dns-secondary.home.local" +technitium_dns_ingress_tls_enabled: false +technitium_dns_ingress_tls_secret: "" + +# ── Zone sync CronJob (primary → secondary) ─────────────────────────────────── +technitium_dns_sync_enabled: true +technitium_dns_sync_schedule: "*/5 * * * *" + +# ── ExternalDNS via RFC 2136 DDNS (optional) ───────────────────────────────── +technitium_dns_externaldns_enabled: false +technitium_dns_externaldns_domain_filter: + - "home.local" +technitium_dns_externaldns_policy: "upsert-only" +technitium_dns_externaldns_txt_owner_id: "k3s-home" diff --git a/addons/technitium-dns/role/tasks/main.yml b/addons/technitium-dns/role/tasks/main.yml new file mode 100644 index 0000000..f014e1b --- /dev/null +++ b/addons/technitium-dns/role/tasks/main.yml @@ -0,0 +1,147 @@ +--- +# ── Validate inputs ─────────────────────────────────────────────────────────── + +- name: Validate technitium_dns_admin_password is set + ansible.builtin.assert: + that: + - technitium_dns_admin_password is defined + - technitium_dns_admin_password | length >= 8 + fail_msg: > + technitium_dns_admin_password must be set in vault.yml (minimum 8 characters). + +- name: Validate primary IP is set + ansible.builtin.assert: + that: + - technitium_dns_primary_ip | length > 0 + fail_msg: > + technitium_dns_primary_ip must be set to a kube-vip-managed static IP. + +# ── Create namespace ────────────────────────────────────────────────────────── + +- name: Create technitium-dns namespace + ansible.builtin.command: > + k3s kubectl create namespace {{ technitium_dns_namespace }} + --dry-run=client -o yaml | k3s kubectl apply -f - + become: true + changed_when: false + +# ── Copy Helm chart to master ───────────────────────────────────────────────── + +- name: Ensure chart temp directory is clean + ansible.builtin.file: + path: /tmp/technitium-dns-chart + state: absent + become: true + +- name: Create chart temp directory + ansible.builtin.file: + path: /tmp/technitium-dns-chart + state: directory + mode: "0755" + become: true + +- name: Copy Helm chart to master + ansible.builtin.copy: + src: "{{ role_path }}/chart/" + dest: /tmp/technitium-dns-chart/ + mode: preserve + become: true + +# ── Template Helm values ────────────────────────────────────────────────────── + +- name: Template Helm values + ansible.builtin.template: + src: values.yaml.j2 + dest: /tmp/technitium-dns-values.yaml + mode: "0600" + become: true + no_log: true + +# ── Lint chart ──────────────────────────────────────────────────────────────── + +- name: Lint Helm chart + ansible.builtin.command: > + helm lint /tmp/technitium-dns-chart + --values /tmp/technitium-dns-values.yaml + become: true + changed_when: false + register: _helm_lint + failed_when: _helm_lint.rc != 0 + +# ── Deploy chart ────────────────────────────────────────────────────────────── + +- name: Deploy technitium-dns via Helm + ansible.builtin.command: > + helm upgrade --install {{ technitium_dns_release_name }} + /tmp/technitium-dns-chart + --namespace {{ technitium_dns_namespace }} + --values /tmp/technitium-dns-values.yaml + --atomic + --wait + --timeout 180s + become: true + register: _helm_result + changed_when: true + +# ── Cleanup temp values file (contains password) ────────────────────────────── + +- name: Remove temp values file + ansible.builtin.file: + path: /tmp/technitium-dns-values.yaml + state: absent + become: true + +# ── Wait for primary to be ready ────────────────────────────────────────────── + +- name: Wait for primary pod to be Running + ansible.builtin.command: > + k3s kubectl -n {{ technitium_dns_namespace }} rollout status + deployment/technitium-dns-primary --timeout=120s + become: true + changed_when: false + +# ── Get deployment status ───────────────────────────────────────────────────── + +- name: Get pod status + ansible.builtin.command: > + k3s kubectl -n {{ technitium_dns_namespace }} get pods,svc -o wide + become: true + changed_when: false + register: _pod_status + +# ── Summary ─────────────────────────────────────────────────────────────────── + +- name: "=== technitium-dns Ready ===" + ansible.builtin.debug: + msg: + - "╔══════════════════════════════════════════════════════════════╗" + - "║ Technitium DNS HA — Deployed ║" + - "╚══════════════════════════════════════════════════════════════╝" + - "" + - " Namespace : {{ technitium_dns_namespace }}" + - " Primary IP : {{ technitium_dns_primary_ip }} (DNS UDP/TCP :53)" + - " Primary UI : http://{{ technitium_dns_primary_host }}/" + - "{% if technitium_dns_secondary_enabled %}" + - " Secondary IP : {{ technitium_dns_secondary_ip }} (DNS UDP/TCP :53)" + - " Secondary UI : http://{{ technitium_dns_secondary_host }}/" + - "{% endif %}" + - "" + - " Keenetic router DNS settings:" + - " Primary DNS : {{ technitium_dns_primary_ip }}" + - "{% if technitium_dns_secondary_enabled %}" + - " Secondary DNS : {{ technitium_dns_secondary_ip }}" + - "{% endif %}" + - "" + - " Pods:" + - "{{ _pod_status.stdout_lines | to_yaml }}" + - "" + - " Create a local zone (first time only):" + - " Open http://{{ technitium_dns_primary_host }}/" + - " Login: admin / " + - " Zones → Add Zone → Primary → {{ technitium_dns_domain }}" + - "" + - " Manual zone sync trigger:" + - "{% if technitium_dns_sync_enabled and technitium_dns_secondary_enabled %}" + - " kubectl create job --from=cronjob/technitium-dns-sync sync-manual-1 \\" + - " -n {{ technitium_dns_namespace }}" + - "{% endif %}" diff --git a/addons/technitium-dns/role/templates/values.yaml.j2 b/addons/technitium-dns/role/templates/values.yaml.j2 new file mode 100644 index 0000000..c572100 --- /dev/null +++ b/addons/technitium-dns/role/templates/values.yaml.j2 @@ -0,0 +1,47 @@ +# Generated by Ansible — do not edit manually. +# Configure via: group_vars/all/addons.yml → technitium_dns_* variables. +# Admin password from vault.yml → technitium_dns_admin_password + +primary: + ip: {{ technitium_dns_primary_ip | quote }} + nodeName: {{ technitium_dns_primary_node | quote }} + +secondary: + enabled: {{ technitium_dns_secondary_enabled | string | lower }} + ip: {{ technitium_dns_secondary_ip | quote }} + nodeName: {{ technitium_dns_secondary_node | quote }} + +dns: + domain: {{ technitium_dns_domain | quote }} + forwarders: +{{ technitium_dns_forwarders | to_yaml | indent(4, True) }} + recursion: {{ technitium_dns_recursion | quote }} + +secret: + adminPassword: {{ technitium_dns_admin_password | quote }} + +storage: + storageClassName: {{ technitium_dns_storage_class | quote }} + size: {{ technitium_dns_storage_size | quote }} + +ingress: + enabled: {{ technitium_dns_ingress_enabled | string | lower }} + ingressClass: {{ technitium_dns_ingress_class | quote }} + primary: + host: {{ technitium_dns_primary_host | quote }} + secondary: + host: {{ technitium_dns_secondary_host | quote }} + tls: + enabled: {{ technitium_dns_ingress_tls_enabled | string | lower }} + secretName: {{ technitium_dns_ingress_tls_secret | quote }} + +sync: + enabled: {{ technitium_dns_sync_enabled | string | lower }} + schedule: {{ technitium_dns_sync_schedule | quote }} + +externalDns: + enabled: {{ technitium_dns_externaldns_enabled | string | lower }} + domainFilter: +{{ technitium_dns_externaldns_domain_filter | to_yaml | indent(4, True) }} + policy: {{ technitium_dns_externaldns_policy | quote }} + txtOwnerId: {{ technitium_dns_externaldns_txt_owner_id | quote }} diff --git a/group_vars/all/addons.yml b/group_vars/all/addons.yml index 8535c9e..83cded0 100644 --- a/group_vars/all/addons.yml +++ b/group_vars/all/addons.yml @@ -9,8 +9,8 @@ addon_nfs_server: false # NFS сервер addon_csi_nfs: false # CSI NFS Driver + StorageClass addon_ingress_nginx: true # ingress-nginx (Ingress controller) addon_cert_manager: false # cert-manager (TLS через Let's Encrypt) -addon_metrics_server: true # metrics-server (kubectl top nodes/pods) -addon_prometheus_stack: true # Prometheus + Grafana + Alertmanager +addon_metrics_server: false # metrics-server (kubectl top nodes/pods) +addon_prometheus_stack: false # Prometheus + Grafana + Alertmanager addon_istio: false # Istio service mesh + Kiali UI addon_argocd: false # ArgoCD (GitOps) addon_longhorn: false # Longhorn (distributed block storage) @@ -44,6 +44,7 @@ addon_splitgw: false # Split Gateway — прозрачный пр addon_ingress_proxypass: false # External Services Ingress Proxy — проксировать внешние сервисы через ingress-nginx addon_ingress_add_domains: false # Ingress-only — добавить домены к существующим сервисам кластера addon_yandex_dns_controller: false # Yandex 360 DNS controller — управление DNS через ConfigMap (safe mode) +addon_technitium_dns: false # Technitium DNS HA — Primary+Secondary с kube-vip LB, зональный sync # ─── NFS Server ─────────────────────────────────────────────────────────────── nfs_exports: @@ -358,6 +359,28 @@ minio_api_ingress_host: "s3.example.com" # netbird_exit_node_enabled: false # После установки — настрой маршруты в Management UI +# ─── Technitium DNS HA ─────────────────────────────────────────────────────── +# Self-hosted Primary+Secondary DNS с kube-vip LoadBalancer IP и авто-синхронизацией зон. +# Пароль задаётся в vault.yml: technitium_dns_admin_password +# technitium_dns_primary_ip: "192.168.1.53" # статический IP для primary DNS (kube-vip) +# technitium_dns_secondary_enabled: true +# technitium_dns_secondary_ip: "192.168.1.54" # статический IP для secondary DNS (kube-vip) +# technitium_dns_primary_node: "" # pinned hostname (пусто = soft anti-affinity) +# technitium_dns_secondary_node: "" +# technitium_dns_domain: "home.local" # локальная DNS-зона +# technitium_dns_forwarders: ["1.1.1.1", "8.8.8.8"] +# technitium_dns_recursion: "AllowOnlyForPrivateNetworks" # Allow | Deny | AllowOnlyForPrivateNetworks +# technitium_dns_primary_host: "dns.home.local" # Web UI через ingress +# technitium_dns_secondary_host: "dns-secondary.home.local" +# technitium_dns_ingress_enabled: true +# technitium_dns_ingress_tls_enabled: false +# technitium_dns_sync_schedule: "*/5 * * * *" # как часто синхронизировать зоны primary→secondary +# ExternalDNS (автоматические DNS-записи из Ingress/Service): +# technitium_dns_externaldns_enabled: false +# technitium_dns_externaldns_domain_filter: ["home.local"] +# technitium_dns_externaldns_policy: "upsert-only" # sync | upsert-only +# technitium_dns_externaldns_txt_owner_id: "k3s-home" + # ─── etcd backup ────────────────────────────────────────────────────────────── etcd_backup_dir: "{{ k3s_data_dir }}/server/db/snapshots" etcd_backup_retention: 5 # сколько снимков хранить diff --git a/group_vars/all/vault.yml.example b/group_vars/all/vault.yml.example index d7b01d3..1873d81 100644 --- a/group_vars/all/vault.yml.example +++ b/group_vars/all/vault.yml.example @@ -132,3 +132,6 @@ vault_transmission_password: "changeme-transmission" yandex_dns: org_id: "3312086" token: "y0_ЗАМЕНИ_НА_OAUTH_ТОКЕН" + +# ── Technitium DNS HA ───────────────────────────────────────────────────────── +technitium_dns_admin_password: "ЗАМЕНИ_НА_ПАРОЛЬ" # минимум 8 символов diff --git a/playbooks/addons.yml b/playbooks/addons.yml index 7c13a67..0a0fba5 100644 --- a/playbooks/addons.yml +++ b/playbooks/addons.yml @@ -319,3 +319,11 @@ when: addon_yandex_dns_controller | default(false) | bool roles: - role: "{{ playbook_dir }}/../addons/yandex-dns-controller/role" + +- name: Install Technitium DNS HA + hosts: k3s_master[0] + gather_facts: false + become: true + when: addon_technitium_dns | default(false) | bool + roles: + - role: "{{ playbook_dir }}/../addons/technitium-dns/role"