This repository manages DNS zones at Hetzner in a fully declarative, Git‑driven way using Ansible.
  • Shell 52.5%
  • Makefile 36.2%
  • Jinja 11.3%
Find a file
Tealk eabc54c837
All checks were successful
Check playbook / Check playbook syntax (push) Successful in 21s
update[vars]: add wall to rollenspiel.social
2026-03-14 12:33:11 +01:00
.forgejo/workflows ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
.gch ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
.helper ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
inventory init 2026-02-08 23:40:23 +01:00
tasks ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
templates init 2026-02-08 23:40:23 +01:00
vars update[vars]: add wall to rollenspiel.social 2026-03-14 12:33:11 +01:00
.ansible-lint ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
.gitignore ignore genereated zone files 2026-02-10 12:15:19 +01:00
.yamllint ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
LICENSE ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
makefile ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
pb.yml ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +01:00
README.md ref[pb]: standardize file structure and content layout 2026-03-03 18:37:09 +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

License

This Ansible configuration is licensed under the GNU Affero General Public License v3.0 (AGPLv3).

What this means:

  • You can use, modify, and distribute this software freely
  • Internal use within your organization has no restrictions
  • ⚠️ If you modify and distribute over a network, you must share your changes under AGPL

This ensures improvements benefit the entire community.

See the LICENSE file for full details.

AGPL FAQ

Q: Can we use this internally without releasing changes?

A: Yes! Only modifications distributed over a network require source code release. Internal use within containers/VMs is unrestricted.

Q: What if we use it in Docker containers or virtual machines?

A: Completely fine. AGPL only applies to modifications accessed or distributed over a network.

Q: Can we add proprietary roles?

A: Yes, as long as they don't network-interact with the AGPL core.

Q: Is this compatible with other licenses?

A: Check FSF License List for compatibility. Generally compatible with: GPLv3, AGPLv3.