This repository manages DNS zones at Hetzner in a fully declarative, Git‑driven way using Ansible.
  • Makefile 57.7%
  • Jinja 42.3%
Find a file
2026-02-08 23:40:23 +01:00
inventory init 2026-02-08 23:40:23 +01:00
tasks init 2026-02-08 23:40:23 +01:00
templates init 2026-02-08 23:40:23 +01:00
vars init 2026-02-08 23:40:23 +01:00
zones init 2026-02-08 23:40:23 +01:00
.gitignore init 2026-02-08 23:40:23 +01:00
makefile init 2026-02-08 23:40:23 +01:00
pb.yml init 2026-02-08 23:40:23 +01:00
README.md init 2026-02-08 23:40:23 +01:00
requirements.yml init 2026-02-08 23:40:23 +01:00

Hetzner DNS with Ansible (DNSasCode)

This repository manages DNS zones at Hetzner in a fully declarative, Gitdriven way using Ansible.

The goal is a robust DNSasCode workflow with:

  • clear separation of defaults and zonespecific 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
  • Providermanaged 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.dns collection
  • 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 into zones/

  • make apply
    Apply the desired state to Hetzner DNS

  • make all
    Run generate + apply

  • make check
    Apply stage in check mode with diff

  • make validate
    Syntax check only

Data Model

Defaults

vars/dns_defaults.yml contains zoneagnostic 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 zonespecific 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:

  • records is a single unified array
  • use prefix or prefixes
  • value is 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