Initial commit
This commit is contained in:
592
skills/netbox-powerdns-integration/workflows/dns-automation.md
Normal file
592
skills/netbox-powerdns-integration/workflows/dns-automation.md
Normal file
@@ -0,0 +1,592 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user