Skip to content

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

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

  1. Edit dns.tf:

    locals {
      subdomains = {
        # ... existing subdomains ...
        "api" = "API endpoint for new service"
      }
    }
    

  2. Preview changes:

    cd ~/Coding/terraform-infra
    terraform plan
    

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.

  1. Apply changes:

    terraform apply
    

  2. Verify DNS (wait 5 minutes for TTL):

    dig +short api.kua.cl
    # Should return: 116.203.109.220
    

Changing a TTL

Example: Increase TTL to 1 hour after stable migration

  1. Edit dns.tf:

    resource "cloudflare_record" "subdomains" {
      # ...
      ttl = 3600  # Changed from 300
    }
    

  2. Apply:

    terraform apply
    

Removing a Subdomain

  1. Remove from locals.subdomains in dns.tf
  2. Run terraform plan to preview deletion
  3. 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:

  1. Edit dns.tf - change IP to old VPS:

    resource "cloudflare_record" "wildcard" {
      # ...
      content = "100.80.53.55"  # Old VPS IP (was: hcloud_server.production_vps.ipv4_address)
    }
    
    resource "cloudflare_record" "subdomains" {
      # ...
      content = "100.80.53.55"  # Old VPS IP
    }
    

  2. Apply immediately:

    cd ~/Coding/terraform-infra
    terraform apply -auto-approve
    

  3. Wait 5 minutes for DNS propagation (TTL=300)

  4. Verify rollback:

    for domain in photos.kua.cl media.kua.cl cdn.kua.cl notes.kua.cl; do
      echo -n "$domain: "
      dig +short $domain
    done
    
    # All should return: 100.80.53.55
    

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



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