Files
gh-basher83-lunar-claude-pl…/skills/netbox-powerdns-integration/workflows/dns-automation.md
2025-11-29 18:00:21 +08:00

13 KiB

DNS Automation Workflows

Overview

This guide covers end-to-end workflows for automating DNS record management using NetBox as the source of truth and PowerDNS as the authoritative DNS server.

Architecture

┌─────────────┐
│  Terraform  │───┐
│   Ansible   │   │
│   Manual    │   │
└─────────────┘   │
                  ▼
            ┌──────────┐
            │  NetBox  │ (Source of Truth)
            │   IPAM   │
            └────┬─────┘
                 │
                 │ netbox-powerdns-sync plugin
                 │
                 ▼
            ┌──────────┐
            │ PowerDNS │ (Authoritative DNS)
            │   API    │
            └────┬─────┘
                 │
                 │ Zone files / API
                 │
                 ▼
            ┌──────────┐
            │   DNS    │ (Resolvers query here)
            │ Clients  │
            └──────────┘

Workflow 1: Create VM with Automatic DNS

Using Terraform

End-to-end automation:

# 1. Create VM in Proxmox
resource "proxmox_vm_qemu" "docker_host" {
  name        = "docker-01-nexus"
  target_node = "foxtrot"
  vmid        = 101

  clone      = "ubuntu-template"
  full_clone = true

  cores   = 4
  memory  = 8192

  network {
    bridge = "vmbr0"
    model  = "virtio"
    tag    = 30
  }

  disk {
    storage = "local-lvm"
    type    = "scsi"
    size    = "50G"
  }

  # Cloud-init IP configuration
  ipconfig0 = "ip=192.168.1.100/24,gw=192.168.1.1"

  sshkeys = file("~/.ssh/id_rsa.pub")
}

# 2. Register in NetBox (triggers DNS sync)
resource "netbox_ip_address" "docker_host" {
  ip_address  = "192.168.1.100/24"
  dns_name    = "docker-01-nexus.spaceships.work"
  status      = "active"
  description = "Docker host for Nexus container registry"

  tags = [
    "terraform",
    "production-dns",  # Triggers auto DNS sync
    "docker-host"
  ]

  # Ensure VM is created first
  depends_on = [proxmox_vm_qemu.docker_host]
}

# 3. DNS records automatically created by netbox-powerdns-sync plugin:
#    A:   docker-01-nexus.spaceships.work → 192.168.1.100
#    PTR: 100.1.168.192.in-addr.arpa → docker-01-nexus.spaceships.work

# 4. Output for verification
output "vm_fqdn" {
  value = netbox_ip_address.docker_host.dns_name
}

output "vm_ip" {
  value = split("/", netbox_ip_address.docker_host.ip_address)[0]
}

Apply workflow:

cd terraform/netbox-vm/
tofu init
tofu plan
tofu apply

# Verify DNS
dig @192.168.3.1 docker-01-nexus.spaceships.work +short
# Returns: 192.168.1.100

# Verify PTR
dig @192.168.3.1 -x 192.168.1.100 +short
# Returns: docker-01-nexus.spaceships.work

Using Ansible

Playbook for VM with DNS:

---
- name: Provision VM with automatic DNS
  hosts: localhost
  gather_facts: false

  vars:
    vm_name: docker-01-nexus
    vm_ip: 192.168.1.100
    vm_fqdn: "{{ vm_name }}.spaceships.work"
    proxmox_node: foxtrot

  tasks:
    # 1. Create VM in Proxmox
    - name: Clone template to create VM
      community.proxmox.proxmox_kvm:
        api_host: "{{ proxmox_api_host }}"
        api_user: "{{ proxmox_api_user }}"
        api_token_id: "{{ proxmox_token_id }}"
        api_token_secret: "{{ proxmox_token_secret }}"
        node: "{{ proxmox_node }}"
        vmid: 101
        name: "{{ vm_name }}"
        clone: ubuntu-template
        full: true
        storage: local-lvm
        net:
          net0: 'virtio,bridge=vmbr0,tag=30'
        ipconfig:
          ipconfig0: 'ip={{ vm_ip }}/24,gw=192.168.1.1'
        cores: 4
        memory: 8192
        agent: 1
        state: present
      register: vm_result

    - name: Start VM
      community.proxmox.proxmox_kvm:
        api_host: "{{ proxmox_api_host }}"
        api_user: "{{ proxmox_api_user }}"
        api_token_id: "{{ proxmox_token_id }}"
        api_token_secret: "{{ proxmox_token_secret }}"
        node: "{{ proxmox_node }}"
        vmid: 101
        state: started

    # 2. Register in NetBox
    - name: Create IP address in NetBox
      netbox.netbox.netbox_ip_address:
        netbox_url: "{{ netbox_url }}"
        netbox_token: "{{ netbox_token }}"
        data:
          address: "{{ vm_ip }}/24"
          dns_name: "{{ vm_fqdn }}"
          status: active
          description: "Docker host for Nexus container registry"
          tags:
            - name: production-dns
            - name: ansible
            - name: docker-host

    # 3. Wait for DNS propagation
    - name: Wait for DNS record
      ansible.builtin.command: dig @192.168.3.1 {{ vm_fqdn }} +short
      register: dns_check
      until: dns_check.stdout == vm_ip
      retries: 10
      delay: 5
      changed_when: false

    # 4. Verify DNS resolution
    - name: Verify DNS forward resolution
      ansible.builtin.command: dig @192.168.3.1 {{ vm_fqdn }} +short
      register: forward_dns
      changed_when: false

    - name: Verify DNS reverse resolution
      ansible.builtin.command: dig @192.168.3.1 -x {{ vm_ip }} +short
      register: reverse_dns
      changed_when: false

    - name: Report DNS status
      ansible.builtin.debug:
        msg:
          - "VM created: {{ vm_name }}"
          - "IP: {{ vm_ip }}"
          - "FQDN: {{ vm_fqdn }}"
          - "Forward DNS: {{ forward_dns.stdout }}"
          - "Reverse DNS: {{ reverse_dns.stdout }}"

Run playbook:

cd ansible
uv run ansible-playbook playbooks/provision-vm-with-dns.yml

Workflow 2: Bulk IP Address Management

Reserve IP Range in NetBox

#!/usr/bin/env python3
# /// script
# dependencies = ["pynetbox"]
# ///

import pynetbox
import os

netbox = pynetbox.api(
    os.getenv("NETBOX_URL"),
    token=os.getenv("NETBOX_TOKEN")
)

# Define IP range for Docker hosts
docker_ips = [
    {"ip": "192.168.1.100/24", "dns": "docker-01-nexus.spaceships.work", "desc": "Nexus registry"},
    {"ip": "192.168.1.101/24", "dns": "docker-02-gitlab.spaceships.work", "desc": "GitLab CI/CD"},
    {"ip": "192.168.1.102/24", "dns": "docker-03-monitoring.spaceships.work", "desc": "Monitoring stack"},
]

for entry in docker_ips:
    ip = netbox.ipam.ip_addresses.create(
        address=entry["ip"],
        dns_name=entry["dns"],
        description=entry["desc"],
        status="reserved",
        tags=[{"name": "production-dns"}, {"name": "docker-host"}]
    )
    print(f"Created: {ip.dns_name}{entry['ip']}")

Run script:

export NETBOX_URL="https://netbox.spaceships.work"
export NETBOX_TOKEN="your-api-token"

uv run reserve-docker-ips.py

Update Status to Active

When VMs are deployed, update IPs from "reserved" to "active":

#!/usr/bin/env python3
# /// script
# dependencies = ["pynetbox"]
# ///

import pynetbox
import os
import sys

netbox = pynetbox.api(
    os.getenv("NETBOX_URL"),
    token=os.getenv("NETBOX_TOKEN")
)

fqdn = sys.argv[1] if len(sys.argv) > 1 else "docker-01-nexus.spaceships.work"

# Find IP by DNS name
ips = netbox.ipam.ip_addresses.filter(dns_name=fqdn)
if not ips:
    print(f"No IP found for {fqdn}")
    sys.exit(1)

ip = ips[0]
ip.status = "active"
ip.save()

print(f"Updated {ip.dns_name}: {ip.address} → active")

Workflow 3: DNS Record Auditing

Verify NetBox and PowerDNS are in Sync

#!/usr/bin/env python3
# /// script
# dependencies = ["pynetbox", "requests"]
# ///

import pynetbox
import requests
import os
import sys

netbox = pynetbox.api(
    os.getenv("NETBOX_URL"),
    token=os.getenv("NETBOX_TOKEN")
)

powerdns_url = os.getenv("POWERDNS_URL", "http://192.168.3.1:8081/api/v1")
powerdns_key = os.getenv("POWERDNS_API_KEY")
zone = sys.argv[1] if len(sys.argv) > 1 else "spaceships.work"

# Get NetBox IPs tagged for DNS
netbox_ips = netbox.ipam.ip_addresses.filter(tag="production-dns")

# Get PowerDNS records
headers = {"X-API-Key": powerdns_key}
pdns_resp = requests.get(f"{powerdns_url}/servers/localhost/zones/{zone}", headers=headers)
pdns_zone = pdns_resp.json()

# Extract A records from PowerDNS
pdns_records = {}
for rrset in pdns_zone.get("rrsets", []):
    if rrset["type"] == "A":
        name = rrset["name"].rstrip(".")
        for record in rrset["records"]:
            pdns_records[name] = record["content"]

# Compare
print("NetBox → PowerDNS Sync Status\n")
print(f"{'DNS Name':<45} {'NetBox IP':<15} {'PowerDNS IP':<15} {'Status'}")
print("-" * 90)

for nb_ip in netbox_ips:
    if not nb_ip.dns_name:
        continue

    dns_name = nb_ip.dns_name.rstrip(".")
    nb_addr = str(nb_ip.address).split("/")[0]
    pdns_addr = pdns_records.get(dns_name, "MISSING")

    if pdns_addr == nb_addr:
        status = "✓ SYNCED"
    elif pdns_addr == "MISSING":
        status = "✗ NOT IN POWERDNS"
    else:
        status = f"✗ MISMATCH"

    print(f"{dns_name:<45} {nb_addr:<15} {pdns_addr:<15} {status}")

Run audit:

export NETBOX_URL="https://netbox.spaceships.work"
export NETBOX_TOKEN="your-netbox-token"
export POWERDNS_URL="http://192.168.3.1:8081/api/v1"
export POWERDNS_API_KEY="your-powerdns-key"

uv run dns-audit.py spaceships.work

Workflow 4: Dynamic Inventory for Ansible

Use NetBox as Inventory Source

Create dynamic inventory file:

# ansible/inventory/netbox.yml
plugin: netbox.netbox.nb_inventory
api_endpoint: https://netbox.spaceships.work
token: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  ...

# Group hosts by tags
group_by:
  - tags

# Group hosts by device role
compose:
  ansible_host: primary_ip4

# Filter to only include VMs with production-dns tag
query_filters:
  - tag: production-dns

Test inventory:

ansible-inventory -i ansible/inventory/netbox.yml --list --yaml

Use in playbook:

---
- name: Configure all Docker hosts
  hosts: tag_docker_host  # Automatically grouped by tag
  become: true

  tasks:
    - name: Ensure Docker is running
      ansible.builtin.systemd:
        name: docker
        state: started
        enabled: true

    - name: Report host info
      ansible.builtin.debug:
        msg: "Configuring {{ inventory_hostname }} ({{ ansible_host }})"

Run with dynamic inventory:

cd ansible
uv run ansible-playbook -i inventory/netbox.yml playbooks/configure-docker-hosts.yml

Workflow 5: Cleanup and Decommission

Remove VM and DNS Records

Terraform destroy workflow:

cd terraform/netbox-vm/
tofu destroy

# This will:
# 1. Remove NetBox IP address record
# 2. netbox-powerdns-sync plugin removes DNS records
# 3. Proxmox VM is deleted

Ansible decommission playbook:

---
- name: Decommission VM and remove DNS
  hosts: localhost
  gather_facts: false

  vars:
    vm_fqdn: docker-01-nexus.spaceships.work
    vm_ip: 192.168.1.100

  tasks:
    - name: Remove IP from NetBox
      netbox.netbox.netbox_ip_address:
        netbox_url: "{{ netbox_url }}"
        netbox_token: "{{ netbox_token }}"
        data:
          address: "{{ vm_ip }}/24"
        state: absent

    # DNS records automatically removed by plugin

    - name: Verify DNS record removed
      ansible.builtin.command: dig @192.168.3.1 {{ vm_fqdn }} +short
      register: dns_check
      failed_when: dns_check.stdout != ""
      changed_when: false

    - name: Delete VM from Proxmox
      community.proxmox.proxmox_kvm:
        api_host: "{{ proxmox_api_host }}"
        api_user: "{{ proxmox_api_user }}"
        api_token_id: "{{ proxmox_token_id }}"
        api_token_secret: "{{ proxmox_token_secret }}"
        node: foxtrot
        vmid: 101
        state: absent

Best Practices

1. Always Use Tags

Tag IP addresses for automatic DNS sync:

tags = ["terraform", "production-dns", "service-type"]

2. Reserve Before Deploy

Reserve IPs in NetBox before deploying VMs:

Status: reserved → active (after deployment)

3. Validate Names

Use naming convention validation before creating:

./tools/validate_dns_naming.py docker-01-nexus.spaceships.work

4. Monitor Sync Status

Regular audits to ensure NetBox and PowerDNS are in sync:

./tools/dns-audit.py spaceships.work

5. Use Descriptions

Document in NetBox description field:

Description: Docker host for Nexus container registry
             Owner: Platform Team
             Related: docker-02-gitlab.spaceships.work

6. Test DNS Resolution

Always verify DNS after creation:

dig @192.168.3.1 <fqdn> +short
dig @192.168.3.1 -x <ip> +short

Troubleshooting

DNS Records Not Created

Check 1: Tag matching

# Verify IP has production-dns tag
curl -H "Authorization: Token $NETBOX_TOKEN" \
  "$NETBOX_URL/api/ipam/ip-addresses/?address=192.168.1.100" | jq '.results[0].tags'

Check 2: Plugin configuration

# In NetBox: Plugins → NetBox PowerDNS Sync → Zones
# Verify zone exists and tag rules match

Check 3: Manual sync

# In NetBox UI: Plugins → NetBox PowerDNS Sync → Zones → <zone> → Sync Now

DNS Resolution Failures

Check PowerDNS API:

curl -H "X-API-Key: $POWERDNS_API_KEY" \
  http://192.168.3.1:8081/api/v1/servers/localhost/zones/spaceships.work

Check DNS server:

dig @192.168.3.1 spaceships.work SOA

Further Reading