DNS Management with Terraform¶
Managing Cloudflare DNS records with Terraform - Complete guide to the production DNS infrastructure.
Live & Production
This DNS configuration is LIVE and managing all kua.cl DNS records via Terraform + Cloudflare.
Table of Contents¶
- Overview
- Current DNS Records
- DNS Architecture
- Terraform Configuration
- Managing DNS Records
- Emergency Rollback
- Verification
Overview¶
DNS Provider¶
Provider: Cloudflare
Domain: kua.cl
Management: Terraform (Infrastructure as Code)
Repository: ~/Coding/terraform-infra/dns.tf
Benefits of Terraform DNS¶
✅ Automatic updates - VPS IP changes automatically update DNS ✅ Version controlled - All DNS changes tracked in Git ✅ Reproducible - Can rebuild entire DNS configuration from code ✅ Fast rollback - TTL=300 (5 minutes) for quick emergency rollback ✅ No manual errors - DNS records defined in code, not clicking in UI
Current DNS Records¶
Production VPS (116.203.109.220)¶
| Record | Type | Value | TTL | Purpose |
|---|---|---|---|---|
| *.kua.cl | A | 116.203.109.220 | 300 | Wildcard catch-all for all subdomains |
| photos.kua.cl | A | 116.203.109.220 | 300 | Immich photo management |
| cloud.kua.cl | A | 116.203.109.220 | 300 | Immich (alias for photos) |
| media.kua.cl | A | 116.203.109.220 | 300 | Kuanary media CDN API |
| cdn.kua.cl | A | 116.203.109.220 | 300 | imgproxy image optimization |
| notes.kua.cl | A | 116.203.109.220 | 300 | Obsidian note taking |
| docs.kua.cl | A | 116.203.109.220 | 300 | Infrastructure documentation (this site) |
| secrets.kua.cl | A | 116.203.109.220 | 300 | Infisical secrets manager |
| n8n.kua.cl | A | 116.203.109.220 | 300 | n8n workflow automation |
Development VPS (46.224.125.1)¶
| Record | Type | Value | TTL | Purpose |
|---|---|---|---|---|
| dev.kua.cl | A | 46.224.125.1 | 300 | Development VPS (open-webui) |
Legacy Records (Manually Managed)¶
| Record | Type | Value | TTL | Purpose |
|---|---|---|---|---|
| kua.cl (root) | A | Cloudflare Proxy | Auto | Root domain (manually managed in Cloudflare) |
| www.kua.cl | A | Cloudflare Proxy | Auto | WWW subdomain (manually managed in Cloudflare) |
Note: Root and WWW records are managed manually in Cloudflare UI (already existed before Terraform). The wildcard (*.kua.cl) record handles all other subdomains.
DNS Architecture¶
Record Structure¶
┌─────────────────────────────────────────────────────────────────┐
│ CLOUDFLARE DNS (kua.cl) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Manual Records (Cloudflare UI): │
│ ├─ kua.cl (root) → Cloudflare Proxy │
│ └─ www.kua.cl → Cloudflare Proxy │
│ │
│ Terraform-Managed Records: │
│ ├─ *.kua.cl → 116.203.109.220 (wildcard) │
│ ├─ photos.kua.cl → 116.203.109.220 │
│ ├─ cloud.kua.cl → 116.203.109.220 │
│ ├─ media.kua.cl → 116.203.109.220 │
│ ├─ cdn.kua.cl → 116.203.109.220 │
│ ├─ notes.kua.cl → 116.203.109.220 │
│ ├─ docs.kua.cl → 116.203.109.220 │
│ ├─ secrets.kua.cl → 116.203.109.220 │
│ ├─ n8n.kua.cl → 116.203.109.220 │
│ └─ dev.kua.cl → 46.224.125.1 │
│ │
└─────────────────────────────────────────────────────────────────┘
TTL Strategy¶
TTL = 300 seconds (5 minutes)
Why 5 minutes? - Fast DNS propagation for emergency rollback - If new VPS has issues, can rollback to old IP in 5 minutes - Minimal cache time = faster cutover during migrations
Before migration, TTL was lowered from 3600s (1 hour) to 300s (5 minutes) to enable fast rollback.
Terraform Configuration¶
File: dns.tf¶
Located at ~/Coding/terraform-infra/dns.tf
Wildcard Record¶
# Wildcard subdomain - all *.kua.cl points to Production VPS
resource "cloudflare_record" "wildcard" {
zone_id = var.cloudflare_zone_id
name = "*"
content = hcloud_server.production_vps.ipv4_address
type = "A"
proxied = false
ttl = 300
comment = "Managed by Terraform - Wildcard for all subdomains"
}
Dynamic IP: hcloud_server.production_vps.ipv4_address references the VPS IP, so if VPS is recreated, DNS updates automatically.
Explicit Service Records¶
# Common subdomains (explicit records for important services)
locals {
subdomains = {
"secrets" = "Infisical secrets manager"
"cloud" = "Immich photos (alias for photos subdomain)"
"photos" = "Immich photo management"
"media" = "Kuanary media CDN"
"cdn" = "imgproxy image optimization"
"notes" = "Obsidian note taking"
"n8n" = "n8n automation"
"docs" = "Infrastructure documentation (MkDocs)"
}
}
resource "cloudflare_record" "subdomains" {
for_each = local.subdomains
zone_id = var.cloudflare_zone_id
name = each.key
content = hcloud_server.production_vps.ipv4_address
type = "A"
proxied = false
ttl = 300
comment = "Managed by Terraform - ${each.value}"
allow_overwrite = true # Allow Terraform to take over existing records
}
Development VPS Record¶
# Development VPS DNS record
resource "cloudflare_record" "dev" {
zone_id = var.cloudflare_zone_id
name = "dev"
content = hcloud_server.development_vps.ipv4_address
type = "A"
proxied = false
ttl = 300
comment = "Managed by Terraform - Development VPS"
}
Outputs¶
output "dns_subdomains" {
description = "All configured subdomains"
value = {
for name, record in cloudflare_record.subdomains :
name => "${record.name}.${var.domain} -> ${record.content}"
}
}
Managing DNS Records¶
Adding a New Subdomain¶
Example: Add api.kua.cl pointing to Production VPS
-
Edit dns.tf:
-
Preview changes:
Output:
Terraform will perform the following actions:
# cloudflare_record.subdomains["api"] will be created
+ resource "cloudflare_record" "subdomains" {
+ name = "api"
+ content = "116.203.109.220"
+ type = "A"
+ ttl = 300
}
Plan: 1 to add, 0 to change, 0 to destroy.
-
Apply changes:
-
Verify DNS (wait 5 minutes for TTL):
Changing a TTL¶
Example: Increase TTL to 1 hour after stable migration
-
Edit dns.tf:
-
Apply:
Removing a Subdomain¶
- Remove from locals.subdomains in dns.tf
- Run terraform plan to preview deletion
- Run terraform apply to delete record
Emergency Rollback¶
Scenario: New VPS has critical issue, need to rollback DNS to old VPS¶
Rollback Time: 5 minutes (TTL=300)
Steps:
-
Edit dns.tf - change IP to old VPS:
-
Apply immediately:
-
Wait 5 minutes for DNS propagation (TTL=300)
-
Verify rollback:
Rollback Complete: All traffic now goes to old VPS
Verification¶
Check All DNS Records¶
# From local machine
cd ~/Coding/terraform-infra
# View all configured DNS records
terraform output dns_subdomains
Output:
dns_subdomains = {
"cdn" = "cdn.kua.cl -> 116.203.109.220"
"cloud" = "cloud.kua.cl -> 116.203.109.220"
"docs" = "docs.kua.cl -> 116.203.109.220"
"media" = "media.kua.cl -> 116.203.109.220"
"n8n" = "n8n.kua.cl -> 116.203.109.220"
"notes" = "notes.kua.cl -> 116.203.109.220"
"photos" = "photos.kua.cl -> 116.203.109.220"
"secrets" = "secrets.kua.cl -> 116.203.109.220"
}
Verify DNS Resolution¶
# Check all Production VPS subdomains
for domain in photos.kua.cl cloud.kua.cl media.kua.cl cdn.kua.cl notes.kua.cl secrets.kua.cl n8n.kua.cl docs.kua.cl; do
echo -n "$domain: "
dig +short $domain
done
# Check Development VPS
echo -n "dev.kua.cl: "
dig +short dev.kua.cl
Expected output:
photos.kua.cl: 116.203.109.220
cloud.kua.cl: 116.203.109.220
media.kua.cl: 116.203.109.220
cdn.kua.cl: 116.203.109.220
notes.kua.cl: 116.203.109.220
secrets.kua.cl: 116.203.109.220
n8n.kua.cl: 116.203.109.220
docs.kua.cl: 116.203.109.220
dev.kua.cl: 46.224.125.1
Test HTTP/HTTPS Access¶
# Test HTTPS (if Caddy/Traefik configured)
curl -I https://photos.kua.cl
curl -I https://media.kua.cl
curl -I https://docs.kua.cl
Troubleshooting¶
Issue: DNS not updating after terraform apply¶
Cause: DNS caching (TTL not expired yet)
Solution:
# Wait for TTL to expire (5 minutes)
# Or flush local DNS cache:
# macOS
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
# Linux
sudo systemd-resolve --flush-caches
# Then verify
dig +short photos.kua.cl
Issue: Terraform shows "already exists" error¶
Cause: Record already exists in Cloudflare (created manually before Terraform)
Solution: Use allow_overwrite = true in resource:
resource "cloudflare_record" "subdomains" {
# ...
allow_overwrite = true # Terraform takes over existing record
}
Issue: Wildcard not catching subdomain¶
Cause: Explicit A record takes precedence over wildcard
Example: If both *.kua.cl and test.kua.cl exist, test.kua.cl wins
Solution: This is expected behavior. Wildcard is a fallback.
Migration History¶
DNS Cutover Timeline¶
Before Migration (Old VPS):
- All DNS records pointed to 100.80.53.55 (old Hetzner VPS)
- TTL was 3600s (1 hour)
Pre-Migration Steps: 1. Lowered TTL to 300s (5 minutes) - 24 hours before migration 2. Waited 24 hours for old TTL to expire
Migration Day:
1. Ran terraform apply to create new VPS + update DNS
2. DNS automatically updated to 116.203.109.220
3. Waited 5 minutes for DNS propagation
4. Verified all services accessible on new VPS
Post-Migration: - Kept TTL at 300s for 7-day verification period - Can increase TTL to 3600s after stability confirmed
Related Documentation¶
- Terraform Overview - General Terraform workflow
- Disaster Recovery - Recovery procedures including DNS rollback
- Services Overview - All services using these DNS records
Quick Reference¶
Environment Variables¶
# Set these before running Terraform
export CLOUDFLARE_API_KEY="d7f671a87a8337c7605db47ced761703e7199"
export CLOUDFLARE_EMAIL="kdoi@email.com"
export HCLOUD_TOKEN="HWrZL9wRdSNFSbAdoZSFm4km8xkKKOdmO5ShYdSz9yoOmAZgYKRi5CojJmRBfbQ6"
Common Commands¶
# Navigate to Terraform directory
cd ~/Coding/terraform-infra
# Preview DNS changes
terraform plan
# Apply DNS changes
terraform apply
# View current DNS configuration
terraform output dns_subdomains
# Verify DNS resolution
dig +short photos.kua.cl
# Emergency rollback (change IPs in dns.tf first)
terraform apply -auto-approve
Last updated: December 2025 - Post-migration to Terraform-managed infrastructure