feat: добавить аддон technitium-dns — HA DNS Primary+Secondary с kube-vip LB и zone sync

- 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
This commit is contained in:
Sergey Antropoff
2026-04-26 17:58:28 +03:00
parent f6fc33a38b
commit f3dfe87d03
24 changed files with 1329 additions and 3 deletions

View File

@@ -58,7 +58,7 @@ DOCKER_RUN := docker run --rm -it \
addon-harbor addon-gitea addon-owncloud addon-nextcloud \ addon-harbor addon-gitea addon-owncloud addon-nextcloud \
addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \ addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \
addon-smtp-relay addon-vault addon-external-secrets \ 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-node remove-node \
add-etcd-node remove-etcd-node \ add-etcd-node remove-etcd-node \
etcd-backup etcd-restore etcd-list-snapshots \ 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" @printf "$(CYAN)Устанавливаю Yandex 360 DNS Controller...$(NC)\n"
$(DOCKER_RUN) addon yandex-dns-controller $(ARGS) $(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/<name>/playbook.yml # Generic цель — любой аддон из addons/<name>/playbook.yml
addon-%: _check_env _check_image addon-%: _check_env _check_image
@if [ ! -f "addons/$*/playbook.yml" ]; then \ @if [ ! -f "addons/$*/playbook.yml" ]; then \

View File

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

View File

@@ -0,0 +1,7 @@
---
- name: Install Technitium DNS HA
hosts: k3s_master[0]
gather_facts: false
become: true
roles:
- role: "{{ playbook_dir }}/role"

View File

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

View File

@@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 / <vault password>"
- " 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 %}"

View File

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

View File

@@ -9,8 +9,8 @@ addon_nfs_server: false # NFS сервер
addon_csi_nfs: false # CSI NFS Driver + StorageClass addon_csi_nfs: false # CSI NFS Driver + StorageClass
addon_ingress_nginx: true # ingress-nginx (Ingress controller) addon_ingress_nginx: true # ingress-nginx (Ingress controller)
addon_cert_manager: false # cert-manager (TLS через Let's Encrypt) addon_cert_manager: false # cert-manager (TLS через Let's Encrypt)
addon_metrics_server: true # metrics-server (kubectl top nodes/pods) addon_metrics_server: false # metrics-server (kubectl top nodes/pods)
addon_prometheus_stack: true # Prometheus + Grafana + Alertmanager addon_prometheus_stack: false # Prometheus + Grafana + Alertmanager
addon_istio: false # Istio service mesh + Kiali UI addon_istio: false # Istio service mesh + Kiali UI
addon_argocd: false # ArgoCD (GitOps) addon_argocd: false # ArgoCD (GitOps)
addon_longhorn: false # Longhorn (distributed block storage) 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_proxypass: false # External Services Ingress Proxy — проксировать внешние сервисы через ingress-nginx
addon_ingress_add_domains: false # Ingress-only — добавить домены к существующим сервисам кластера addon_ingress_add_domains: false # Ingress-only — добавить домены к существующим сервисам кластера
addon_yandex_dns_controller: false # Yandex 360 DNS controller — управление DNS через ConfigMap (safe mode) 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 Server ───────────────────────────────────────────────────────────────
nfs_exports: nfs_exports:
@@ -358,6 +359,28 @@ minio_api_ingress_host: "s3.example.com"
# netbird_exit_node_enabled: false # netbird_exit_node_enabled: false
# После установки — настрой маршруты в Management UI # После установки — настрой маршруты в 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 ──────────────────────────────────────────────────────────────
etcd_backup_dir: "{{ k3s_data_dir }}/server/db/snapshots" etcd_backup_dir: "{{ k3s_data_dir }}/server/db/snapshots"
etcd_backup_retention: 5 # сколько снимков хранить etcd_backup_retention: 5 # сколько снимков хранить

View File

@@ -132,3 +132,6 @@ vault_transmission_password: "changeme-transmission"
yandex_dns: yandex_dns:
org_id: "3312086" org_id: "3312086"
token: "y0_ЗАМЕНИ_НА_OAUTH_ТОКЕН" token: "y0_ЗАМЕНИ_НА_OAUTH_ТОКЕН"
# ── Technitium DNS HA ─────────────────────────────────────────────────────────
technitium_dns_admin_password: "ЗАМЕНИ_НААРОЛЬ" # минимум 8 символов

View File

@@ -319,3 +319,11 @@
when: addon_yandex_dns_controller | default(false) | bool when: addon_yandex_dns_controller | default(false) | bool
roles: roles:
- role: "{{ playbook_dir }}/../addons/yandex-dns-controller/role" - 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"