593 lines
13 KiB
Markdown
593 lines
13 KiB
Markdown
# 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
|
|
|
|
```text
|
|
┌─────────────┐
|
|
│ 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:**
|
|
|
|
```hcl
|
|
# 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:**
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```yaml
|
|
---
|
|
- 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:**
|
|
|
|
```bash
|
|
cd ansible
|
|
uv run ansible-playbook playbooks/provision-vm-with-dns.yml
|
|
```
|
|
|
|
## Workflow 2: Bulk IP Address Management
|
|
|
|
### Reserve IP Range in NetBox
|
|
|
|
```python
|
|
#!/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:**
|
|
|
|
```bash
|
|
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":
|
|
|
|
```python
|
|
#!/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
|
|
|
|
```python
|
|
#!/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:**
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```yaml
|
|
# 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:**
|
|
|
|
```bash
|
|
ansible-inventory -i ansible/inventory/netbox.yml --list --yaml
|
|
```
|
|
|
|
**Use in playbook:**
|
|
|
|
```yaml
|
|
---
|
|
- 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:**
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```yaml
|
|
---
|
|
- 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:
|
|
|
|
```hcl
|
|
tags = ["terraform", "production-dns", "service-type"]
|
|
```
|
|
|
|
### 2. Reserve Before Deploy
|
|
|
|
Reserve IPs in NetBox before deploying VMs:
|
|
|
|
```text
|
|
Status: reserved → active (after deployment)
|
|
```
|
|
|
|
### 3. Validate Names
|
|
|
|
Use naming convention validation before creating:
|
|
|
|
```bash
|
|
./tools/validate_dns_naming.py docker-01-nexus.spaceships.work
|
|
```
|
|
|
|
### 4. Monitor Sync Status
|
|
|
|
Regular audits to ensure NetBox and PowerDNS are in sync:
|
|
|
|
```bash
|
|
./tools/dns-audit.py spaceships.work
|
|
```
|
|
|
|
### 5. Use Descriptions
|
|
|
|
Document in NetBox description field:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
dig @192.168.3.1 <fqdn> +short
|
|
dig @192.168.3.1 -x <ip> +short
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### DNS Records Not Created
|
|
|
|
#### Check 1: Tag matching
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```python
|
|
# In NetBox: Plugins → NetBox PowerDNS Sync → Zones
|
|
# Verify zone exists and tag rules match
|
|
```
|
|
|
|
#### Check 3: Manual sync
|
|
|
|
```bash
|
|
# In NetBox UI: Plugins → NetBox PowerDNS Sync → Zones → <zone> → Sync Now
|
|
```
|
|
|
|
### DNS Resolution Failures
|
|
|
|
**Check PowerDNS API:**
|
|
|
|
```bash
|
|
curl -H "X-API-Key: $POWERDNS_API_KEY" \
|
|
http://192.168.3.1:8081/api/v1/servers/localhost/zones/spaceships.work
|
|
```
|
|
|
|
**Check DNS server:**
|
|
|
|
```bash
|
|
dig @192.168.3.1 spaceships.work SOA
|
|
```
|
|
|
|
## Further Reading
|
|
|
|
- [NetBox PowerDNS Sync Plugin](../reference/sync-plugin-reference.md)
|
|
- [Terraform NetBox Provider](../reference/terraform-provider-guide.md)
|
|
- [DNS Naming Conventions](naming-conventions.md)
|