netcup-DNS mit Ansible pflegen: Einträge per Playbook statt im CCP
Teil 14 der Reihe netcup Spezial: DNS-Einträge bei netcup mit Ansible und community.general.netcup_dns pflegen, mit Ansible Vault, Trockenlauf, solo und state absent. Getestet.
Geprüft am 04.11.2026 · für ansible-core 2.21.6, community.general 14.0.0, nc-dnsapi 0.1.5, Ubuntu 24.04.5
WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Wer mehrere Domains bei netcup betreibt, klickt DNS-Einträge irgendwann nicht mehr einzeln im CCP zusammen. Mit Ansible beschreiben Sie die Einträge als Liste in einer Datei, und ein Playbook bringt die Zone bei netcup auf genau diesen Stand. Dafür gibt es das Modul community.general.netcup_dns, das die DNS-API von netcup anspricht. Diese Anleitung zeigt Schritt für Schritt den Weg vom API-Schlüssel über verschlüsselte Zugangsdaten bis zum Playbook, das Einträge anlegt, ersetzt und wieder löscht.
Dies ist Teil 14 der Reihe „netcup Spezial“.
Als Beispiel dient ein netcup-VPS mit Ubuntu als Ansible-Rechner. Die Domain liegt im klassischen netcup-DNS mit den Nameservern root-dns, second-dns und third-dns.netcup.net und heißt im Artikel example.com.
Domains, die netcup bereits auf das neue CloudDNS umgestellt hat, konnten wir nicht testen, weil keine unserer Domains schon umgestellt war. Laut netcup-Hilfe ist die neue netcup-API für CloudDNS-Domains gedacht. Wer noch nicht migriert ist, kann die bisherige API weiter nutzen. Der netcup-Support schrieb uns am 04.11.2026 außerdem, dass Domains eines Kundenkontos nur auf CloudDNS umgestellt werden können, wenn dort keine Legacy-API-Keys mehr existieren. Wer den Weg aus dieser Anleitung nutzt, bleibt also vorerst beim klassischen DNS.
Voraussetzungen
- Eine Domain bei netcup, die die netcup-Nameserver nutzt
- Zugang zum Customer Control Panel (CCP)
- Ein Linux-Rechner mit Python 3.12 oder neuer, im Test Ubuntu 24.04. ansible-core 2.21 verlangt laut Paketangabe mindestens Python 3.12. Es kann ein Server oder Ihr Arbeitsrechner sein, Ansible läuft hier nur lokal
- root-Rechte. Alle Befehle und Pfade in dieser Anleitung gehen von root aus
- Etwa 30 Minuten
Schritt 1: API-Schlüssel im CCP anlegen
Melden Sie sich im CCP an und öffnen Sie Stammdaten, dann den Reiter API. Die Seite hat zwei Bereiche. Oben stehen die API-Keys für die neue REST-API, darunter die Legacy-API-Keys. Das Ansible-Modul braucht einen Legacy-API-Key und das zugehörige API-Passwort.

Klicken Sie unter Legacy-API-Keys auf API-Key erstellen und danach auf API-Passwort neu erstellen. Notieren Sie beide Werte und Ihre Kundennummer direkt in einem Passwortmanager. Beachten Sie den gelben Hinweis: Die Legacy-API werde „in Zukunft nicht mehr verfügbar sein“. Ein Datum nennt netcup dort nicht.
Ein Key gilt für alle Domains Ihres Kontos, nicht nur für eine. Wer ihn hat, kann jede Ihrer DNS-Zonen ändern. Behandeln Sie ihn deshalb wie ein Passwort.
Verifizieren: Unter Legacy-API-Keys steht ein Key, und Sie haben API-Key, API-Passwort und Kundennummer notiert.
Schritt 2: Ansible und das netcup-Modul installieren
Installieren Sie Ansible in einer eigenen Python-Umgebung. So kommt die Bibliothek nc-dnsapi, die das Modul braucht, sauber in dieselbe Umgebung:
apt-get update
apt-get install -y python3-venv
python3 -m venv /opt/ansible
/opt/ansible/bin/pip install ansible-core nc_dnsapi
/opt/ansible/bin/ansible-galaxy collection install community.general
export PATH=/opt/ansible/bin:$PATH
Die letzte Zeile gilt nur für die aktuelle Sitzung. Tragen Sie sie in ~/.bashrc ein, wenn Sie Ansible öfter nutzen.
Verifizieren: ansible-galaxy collection list community.general zeigt die Collection, bei uns community.general 14.0.0, und ansible-doc community.general.netcup_dns zeigt die Hilfe des Moduls.
Schritt 3: Projektordner anlegen
Legen Sie einen Ordner für das DNS-Projekt an und wechseln Sie hinein. Ansible liest die ansible.cfg nur aus dem aktuellen Ordner, alle weiteren Befehle laufen deshalb von hier aus:
mkdir -p /root/netcup-dns/group_vars/all
cd /root/netcup-dns
Weil die Konfiguration gleich auf eine Datei mit dem Vault-Passwort verweist, legen Sie diese jetzt schon an. Sonst bricht jeder Ansible-Befehl mit „The vault password file /root/.vault-pass was not found“ ab:
(umask 077; openssl rand -base64 32 > ~/.vault-pass)
Die Klammer sorgt dafür, dass die Datei von Anfang an nur für root lesbar ist. Sichern Sie das Vault-Passwort zusätzlich im Passwortmanager, ohne es lassen sich die Zugangsdaten später nicht mehr öffnen. Dann folgen zwei kleine Dateien. Die ansible.cfg:
[defaults]
inventory = inventory.ini
vault_password_file = ~/.vault-pass
callback_result_format = yaml
Die inventory.ini enthält nur den lokalen Rechner. Das Modul spricht die netcup-API direkt an, ein Zielserver per SSH ist nicht nötig. Wichtig ist der Python-Pfad, denn nur dort ist nc-dnsapi installiert:
localhost ansible_connection=local ansible_python_interpreter=/opt/ansible/bin/python
Verifizieren: ansible localhost -m ping antwortet mit ping: pong, und ls -l ~/.vault-pass zeigt -rw-------.
Schritt 4: Zugangsdaten mit Ansible Vault verschlüsseln
API-Key und Passwort gehören nicht im Klartext in das Projekt. Ansible Vault verschlüsselt sie mit dem Passwort aus Schritt 3. Ohne Angabe öffnet ansible-vault den Editor vi, mit EDITOR=nano bekommen Sie den einfacheren nano:
EDITOR=nano ansible-vault create group_vars/all/vault.yml
Im Editor tragen Sie Ihre Werte ein:
netcup_customer_id: 12345
netcup_api_key: "IHR-API-KEY"
netcup_api_password: "IHR-API-PASSWORT"
Nach dem Speichern ist die Datei verschlüsselt. Ändern können Sie sie später mit ansible-vault edit group_vars/all/vault.yml.
Verifizieren: head -1 group_vars/all/vault.yml zeigt $ANSIBLE_VAULT;1.1;AES256.
Schritt 5: Die DNS-Einträge als Liste beschreiben
Die Einträge stehen als Daten in group_vars/all/vars.yml. Jede Zeile ist ein Eintrag mit Name, Typ und Wert. Für MX kommt die Priorität dazu:
netcup_domain: example.com
dns_records:
- { name: web, type: A, value: 203.0.113.10 }
- { name: web, type: AAAA, value: "2001:db8::10" }
- { name: www, type: CNAME, value: web.example.com }
- { name: lab, type: TXT, value: "verwaltet-mit-ansible" }
- { name: lab, type: MX, value: mail.example.com, priority: 10 }
- { name: "@", type: CAA, value: '0 issue "letsencrypt.org"' }
Das @ steht für die Domain selbst. Setzen Sie es in Anführungszeichen, ebenso IPv6-Adressen und Werte mit Sonderzeichen.
Verifizieren: ansible localhost -m debug -a "var=dns_records" gibt die Liste ohne Fehler aus.
Schritt 6: Das Playbook schreiben
Das Playbook dns.yml geht die Liste durch. Die Zugangsdaten und die Domain stehen einmal unter module_defaults, damit sie nicht in jeder Aufgabe wiederholt werden müssen:
- name: DNS-Einträge bei netcup pflegen
hosts: localhost
gather_facts: false
module_defaults:
community.general.netcup_dns:
customer_id: "{{ netcup_customer_id }}"
api_key: "{{ netcup_api_key }}"
api_password: "{{ netcup_api_password }}"
domain: "{{ netcup_domain }}"
timeout: 30
tasks:
- name: Einträge setzen
community.general.netcup_dns:
record: "{{ item.name }}"
type: "{{ item.type }}"
value: "{{ item.value }}"
priority: "{{ item.priority | default(omit) }}"
solo: "{{ item.solo | default(false) }}"
state: "{{ item.state | default('present') }}"
loop: "{{ dns_records }}"
loop_control:
label: "{{ item.name }} {{ item.type }} {{ item.value }}"
Der Standardwert für timeout liegt bei 5 Sekunden. Wir haben ihn auf 30 erhöht, damit eine langsame Antwort der API nicht gleich zum Abbruch führt. loop_control sorgt dafür, dass in der Ausgabe nur Name, Typ und Wert je Eintrag stehen.
Verifizieren: ansible-playbook dns.yml --syntax-check meldet keinen Fehler.
Schritt 7: Trockenlauf mit --check und --diff
Bevor Sie etwas ändern, zeigt ein Trockenlauf, was passieren würde:
ansible-playbook dns.yml --check --diff
Für jeden Eintrag zeigt Ansible einen Diff der Zone. Ein Plus markiert, was neu hinzukäme. Gekürzt sah das bei uns so aus:
+ - id: null
+ name: web
+ priority: null
+ type: A
+ value: 203.0.113.10
changed: [localhost] => (item=web A 203.0.113.10)
Der Diff zeigt als Kontext auch bestehende Einträge der Zone, bei uns zum Beispiel den DMARC-Eintrag mit seiner Berichtsadresse. Mit -v gibt das Modul zusätzlich die komplette Zone im Ergebnis aus. Geben Sie solche Ausgaben deshalb nicht ungeprüft weiter. Geschrieben hat der Trockenlauf nichts: Der folgende echte Lauf musste alle sechs Einträge noch anlegen.
Verifizieren: Jeder geplante Eintrag erscheint mit changed, und im CCP hat sich nichts geändert.
Schritt 8: Anwenden und ein zweites Mal laufen lassen
Jetzt ohne --check:
ansible-playbook dns.yml
changed: [localhost] => (item=web A 203.0.113.10)
changed: [localhost] => (item=web AAAA 2001:db8::10)
changed: [localhost] => (item=www CNAME web.example.com)
changed: [localhost] => (item=lab TXT verwaltet-mit-ansible)
changed: [localhost] => (item=lab MX mail.example.com)
changed: [localhost] => (item=@ CAA 0 issue "letsencrypt.org")
Der Lauf dauerte 5 Sekunden. Starten Sie das Playbook gleich noch einmal. Jetzt muss jeder Eintrag ok melden, denn es gibt nichts mehr zu tun:
localhost : ok=1 changed=0 unreachable=0 failed=0
Die Zahlen in dieser Zeile zählen Aufgaben, nicht Einträge. Das Playbook hat nur eine Aufgabe, die Schleife über die Einträge steckt darin. Diese Eigenschaft heißt idempotent. Sie können das Playbook beliebig oft laufen lassen, etwa täglich per Cron oder aus einer Pipeline, ohne dass sich etwas verändert, solange die Liste gleich bleibt.
Verifizieren: Der erste Lauf meldet changed, der zweite changed=0.
Schritt 9: Im CCP, im API-Log und per dig prüfen
Öffnen Sie im CCP unter Domains Ihre Domain und den Reiter DNS. Die neuen Einträge stehen dort neben den alten. Einträge, die nicht in Ihrer Liste stehen, bleiben unangetastet:

Unter Domains finden Sie oben rechts das API Log. Es zeigt jede schreibende Anfrage mit Zeit, Status und Key:

Jeder Eintrag ist eine eigene Anfrage updateDnsRecords. Der zweite, idempotente Lauf erzeugte keinen Eintrag im Log, weil das Modul vorher liest und nur schreibt, wenn sich etwas ändert. Die Zeile mit dem Status „Gestartet“ fällt zeitlich in unsere Fehlertests und stammt vermutlich vom CNAME auf @, den netcup ablehnte. Sie blieb ohne Rückmeldung stehen.
Ob die Einträge auch nach außen sichtbar sind, prüfen Sie direkt am netcup-Nameserver:
dig +short A web.example.com @root-dns.netcup.net
dig +short SOA example.com @root-dns.netcup.net
203.0.113.10
root-dns.netcup.net. dnsadmin.netcup.net. 2026110416 3600 3600 604800 60
netcup veröffentlicht nicht sofort. Der erste echte Lauf schrieb bis 08:59:47 Uhr. Um 08:59:57 Uhr lieferte der Nameserver noch nichts und zeigte die alte Seriennummer, ab 09:01:04 Uhr die neue Adresse. Im Test dauerte es also gut eine Minute. Öffentliche Resolver können länger brauchen. Bei uns lieferte 9.9.9.9 um 09:01:26 Uhr noch keine Antwort.
Verifizieren: dig am netcup-Nameserver liefert für jeden Eintrag den Wert aus Ihrer Liste, und die Seriennummer im SOA ist gestiegen.
Schritt 10: Werte ändern und Einträge löschen
Hier liegt die wichtigste Falle. Das Modul sucht einen Eintrag anhand von Name, Typ und Wert. Wir haben in der Liste nur den Wert von web von 203.0.113.10 auf 203.0.113.11 geändert. Das Modul fand den alten Eintrag nicht und legte einen zweiten an:
A-Einträge web: ['203.0.113.11', '203.0.113.10']
Soll ein Wert den alten ersetzen, setzen Sie solo: true. Dann löscht das Modul alle anderen Einträge mit gleichem Namen und Typ:
- { name: web, type: A, value: 203.0.113.11, solo: true }
Nach diesem Lauf war nur noch 203.0.113.11 übrig. Nutzen Sie solo gezielt für Einträge, die es nur einmal geben darf, etwa A und AAAA eines Hosts. Bei MX oder TXT mit mehreren gewollten Werten würde es die übrigen löschen.
Bei MX gehört die Priorität mit zum Vergleich, und hier hilft auch solo nicht. Wir haben einen MX mit Priorität 10 angelegt und dann denselben Wert mit Priorität 20 und solo: true gesetzt. Danach gab es beide Einträge, mit 10 und mit 20, denn solo löscht nur Einträge mit anderem Wert. Ändern Sie eine MX-Priorität deshalb in zwei Schritten: den alten Eintrag mit state: absent entfernen und den neuen anlegen.
Zum Löschen ergänzen Sie den Eintrag um state: absent:
- { name: lab, type: TXT, value: "verwaltet-mit-ansible", state: absent }
- { name: lab, type: MX, value: mail.example.com, priority: 10, state: absent }
Der nächste Lauf meldete für beide changed und entfernte sie, der übernächste changed=0. Löschen Sie die Zeilen erst danach aus der Liste. Fehlt ein Eintrag einfach in der Liste, lässt das Modul ihn in der Zone stehen.
Verifizieren: Der zweite Lauf nach dem Löschen meldet changed=0. Bei uns lieferte dig für die beiden gelöschten Einträge gut elf Minuten später keine Antwort mehr.
Schritt 11: Zugangsdaten bleiben aus der Ausgabe
Im Quelltext des Moduls sind API-Key und API-Passwort als geheim markiert (no_log). Wir haben das geprüft: Wir haben das Playbook mit der ausführlichsten Ausgabe -vvv und mit --diff laufen lassen und beide Ausgaben nach den echten Werten durchsucht. Die Ausgabe mit -vvv hatte 493 Zeilen. Weder Key noch Passwort kamen in einer der beiden Ausgaben vor.
Gefährlich bleiben Ihre eigenen Aufrufe. Ein debug auf die Variable gibt den Wert sehr wohl aus. Nutzen Sie solche Befehle nur zur Fehlersuche und nie in einem Log, das andere lesen.
So prüfen Sie es selbst, ohne dass der Key in der Befehlshistorie landet. read -rs liest ihn unsichtbar ein:
read -rs KEY
ansible-playbook dns.yml -vvv | grep -cF "$KEY"
unset KEY
Verifizieren: Der Befehl gibt 0 aus.
Typische Fehler
Alle Meldungen haben wir im Test absichtlich ausgelöst.
| Meldung | Ursache und Lösung |
|---|---|
record type MX required the 'priority' argument | MX ohne Priorität. priority: 10 ergänzen. Priorität 0 wertet das Modul ebenfalls als fehlend. |
Invalid record type: SSHFP | Das Modul nennt SSHFP, braucht dafür aber nc-dnsapi ab 0.1.6. Auf PyPI war am 04.11.2026 nur 0.1.5 verfügbar. SSHFP-Einträge vorerst im CCP pflegen. |
Host for CNAME entry is incorrect. (4013) | CNAME auf @. Für die Domain selbst ist kein CNAME erlaubt, A und AAAA nehmen. |
The API Password does not match ... (4010) | Falsches API-Passwort oder ein neu erstelltes Passwort, das noch nicht im Vault steht. Mit ansible-vault edit korrigieren. |
Domain not found. (5028) | Domain gehört nicht zu Ihrem Konto oder ist falsch geschrieben. netcup_domain prüfen. |
Failed to import the required Python library (nc-dnsapi) | Ansible nutzt das falsche Python. ansible_python_interpreter auf die Umgebung aus Schritt 2 setzen. |
The vault password file /root/.vault-pass was not found | Vault-Passwortdatei fehlt. Datei anlegen oder Pfad in der ansible.cfg anpassen. |
| Zwei Einträge statt einem | Wert geändert ohne solo: true, siehe Schritt 10. |
Häufige Fragen
Funktioniert das auch mit CloudDNS?
Das konnten wir nicht testen, weil keine unserer Domains schon auf CloudDNS umgestellt war. netcup stellt Bestandsdomains nach eigener Aussage schrittweise und automatisch um. Für CloudDNS-Domains beschreibt die netcup-Hilfe die neue netcup-API mit eigenem API-Key. Prüfen Sie nach der Umstellung mit --check, ob das Modul Ihre Zone noch lesen kann.
Verwaltet Ansible damit die ganze Zone?
Nein. Das Modul arbeitet je Eintrag. Was nicht in Ihrer Liste steht, bleibt unverändert. Wer die Zone vollständig aus der Datei erzeugen will, muss alle Einträge aufnehmen und alte gezielt mit state: absent entfernen.
Kann ich mehrere Domains pflegen?
Grundsätzlich ja, getestet haben wir es mit einer Domain. Der Weg: In der Aufgabe domain: "{{ item.domain | default(netcup_domain) }}" ergänzen und bei Einträgen für eine andere Domain domain: in die Liste schreiben. Der API-Key gilt ohnehin für alle Domains des Kontos.
Gibt es eine Oberfläche dafür?
Ansible selbst hat keine. Wer Playbooks lieber aus dem Browser startet, kann Semaphore UI nutzen, siehe die Links unten.
Fazit
Mit dem Modul community.general.netcup_dns wird aus der Klickarbeit im CCP eine Liste, die Sie versionieren, prüfen und beliebig oft anwenden können. Im Test lief alles idempotent, die Zugangsdaten tauchten in keiner Ausgabe auf, und das API-Log im CCP zeigt jede Änderung nachvollziehbar. Merken sollten Sie sich die Sache mit solo und dass ein Eintrag zum Löschen erst state: absent braucht. Behalten Sie außerdem den Hinweis von netcup im Blick: Die Legacy-API soll auslaufen, und mit ihr dieser Weg, solange das Modul die neue REST-API nicht unterstützt.


