- Makefile 57.7%
- Jinja 42.3%
| inventory | ||
| tasks | ||
| templates | ||
| vars | ||
| zones | ||
| .gitignore | ||
| makefile | ||
| pb.yml | ||
| README.md | ||
| requirements.yml | ||
Hetzner DNS with Ansible (DNS‑as‑Code)
This repository manages DNS zones at Hetzner in a fully declarative, Git‑driven way using Ansible.
The goal is a robust DNS‑as‑Code workflow with:
- clear separation of defaults and zone‑specific overrides
- reproducible desired state
- clean, reviewable Git diffs
- safe usage of
prune: true
The implementation is based on the community.dns collection and the Hetzner DNS API.
Core Principles
- One zone = one YAML artifact
- Defaults describe standard records
- Zones explicitly override defaults
- Provider‑managed records are not owned (NS / SOA are ignored)
- Explicit records instead of wildcards
- Strict DNS semantics (A ≠ AAAA, MX requires priority, etc.)
Requirements
- Ansible
community.dnscollection- Hetzner DNS API token
ansible-galaxy collection install -r requirements.yml
Repository Structure
.
├─ pb.yml
├─ Makefile
├─ inventory/
│ └─ hosts.yml
├─ vars/
│ ├─ dns_defaults.yml
│ └─ zones.yml
├─ tasks/
│ ├─ generate.yml
│ └─ deploy.yml
├─ templates/
│ └─ zone.yml.j2
└─ zones/
└─ *.yml
Workflow (Makefile)
The entire workflow is driven by stage tags.
make getzones
make generate
git diff zones/
make apply
Available Targets
-
make getzones
Export existing zones from Hetzner (if implemented) -
make generate
Generate normalized zone files intozones/ -
make apply
Apply the desired state to Hetzner DNS -
make all
Rungenerate+apply -
make check
Apply stage in check mode with diff -
make validate
Syntax check only
Data Model
Defaults
vars/dns_defaults.yml contains zone‑agnostic standard records
(e.g. mail infrastructure, CAA, autodiscover).
---
dns_defaults:
records:
- type: MX
prefix: ""
value:
- 10 mail.rollenspiel.network.
- type: TXT
prefix: mta-sts
value:
- v=STSv1; id=default
...
Zones
vars/zones.yml defines only deviations and zone‑specific records.
---
dns_zones:
- name: rollenspiel.network
records:
- type: TXT
prefix: mta-sts
value:
- v=STSv1; id=202502081520
- type: A
prefixes:
- ""
- www
- api
value:
- 142.132.206.141
...
Rules:
recordsis a single unified array- use
prefixorprefixes valueis always a list(type, prefix)must be unique per zone- zone records override defaults with the same
(type, prefix)
Generated Zone Files
The directory zones/*.yml contains the fully expanded desired state:
---
zone:
name: rollenspiel.network
record_sets:
- prefix: ""
type: NS
ignore: true
- prefix: ""
type: SOA
ignore: true
- prefix: ""
type: MX
value:
- 10 mail.rollenspiel.network.
...
Properties:
- fully explicit
- no duplicate records
- compatible with
community.dns.hetzner_dns_record_sets - safe with
prune: true
Template
templates/zone.yml.j2:
- merges defaults and zone overrides
- prevents duplicate
(type, prefix) - normalizes
prefix: null→"" - automatically splits IPv4 / IPv6 into A / AAAA
- produces clean YAML without whitespace noise
Apply Stage
tasks/deploy.yml:
- loads exactly one zone file per iteration
- applies it immediately
- output always shows the current zone name
- no global variable leakage
Why NS / SOA Are Ignored
- they are owned by the provider
- they may change without notice
- they must not be deleted when using
prune: true
Therefore:
- prefix: ""
type: NS
ignore: true
- prefix: ""
type: SOA
ignore: true