The DNS Configuration Tax
Every cloud architect has been here: you’re launching a new site, and you hit the DNS configuration phase.
The checklist:
- Request SSL certificate in AWS ACM
- Copy the validation CNAME (name:
_abc123..., value:_xyz789...) - Log into Cloudflare (or Route 53, or whoever)
- Add the CNAME record
- Wait 5-10 minutes
- Refresh AWS console repeatedly
- Certificate finally validates
- Create CloudFront distribution
- Copy CloudFront domain name (d1234abcd.cloudfront.net)
- Log back into Cloudflare
- Add CNAME for root domain
- Add CNAME for www subdomain
- Test DNS:
dig graycloudarch.com - Oh crap, forgot to set it to “DNS only” (gray cloud)
- Fix it, test again
Time: 20-30 minutes if everything goes right Typo risk: High (those validation records are 60+ characters) Fun factor: Zero
And this is just one domain. I’m running two brands (graycloudarch.com + cloudpatterns.io), so multiply everything by 2.
This is “DNS hell”—and I wanted out.
The Terraform Epiphany
I manage AWS infrastructure for a living. At my day job, I built an Enterprise Cloud Platform with 6 AWS accounts, all managed by Terraform.
The rule: If you’re doing it twice, automate it.
So why was I clicking through Cloudflare’s UI like it’s 2015?
The answer: I didn’t know Cloudflare had a Terraform provider. Once I found out, everything changed.
The Solution: Terraform Orchestrates AWS + Cloudflare
Key insight: Terraform can manage resources across multiple providers in the same configuration.
terraform apply
│
├─→ AWS Provider
│ ├─ Creates ACM certificate
│ ├─ Exposes validation records as outputs
│ └─ Waits for validation
│
└─→ Cloudflare Provider
├─ Reads ACM validation records
├─ Creates DNS records automatically
└─ Returns control to AWS provider
Terraform orchestrates the entire dance. You just run terraform apply.
The Code: 100 Lines That Changed Everything
Step 1: Request ACM Certificate
resource "aws_acm_certificate" "graycloudarch" {
provider = aws.us-east-1 # CloudFront requires us-east-1
domain_name = "graycloudarch.com"
validation_method = "DNS"
subject_alternative_names = ["www.graycloudarch.com"]
lifecycle {
create_before_destroy = true
}
tags = {
Name = "graycloudarch.com"
Site = "graycloudarch"
}
}
Nothing magical here. Standard ACM certificate request with DNS validation.
The magic is in the next step.
Step 2: Automatic DNS Validation with for_each
This is where most people get stuck. ACM gives you validation records, but you have to manually add them to DNS.
Not anymore.
resource "cloudflare_record" "graycloudarch_cert_validation" {
for_each = {
for dvo in aws_acm_certificate.graycloudarch.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.cloudflare_zone.graycloudarch.id
name = each.value.name
value = each.value.record
type = each.value.type
ttl = 60
proxied = false # Must be false for ACM validation
}
Let’s break this down.
The for_each Loop
for_each = {
for dvo in aws_acm_certificate.graycloudarch.domain_validation_options : dvo.domain_name => { ... }
}
What this does:
aws_acm_certificate.graycloudarch.domain_validation_optionsis a list of validation records ACM generates- For each record (
dvo), create a map entry keyed bydvo.domain_name - The value is an object with
name,record, andtype
Why this works:
- ACM generates 2 validation records (one for apex domain, one for www)
- Terraform iterates over both
- Each iteration creates a Cloudflare DNS record
The result: Two DNS records created automatically, no copy-paste.
The cloudflare_record Resource
zone_id = data.cloudflare_zone.graycloudarch.id
name = each.value.name
value = each.value.record
type = each.value.type
ttl = 60
proxied = false
Key details:
zone_id: Fetched viadatasource (more on this later)name: The long_abc123...CNAME name from ACMvalue: The equally long_xyz789...validation valueproxied = false: Critical—ACM validation requires DNS-only mode
Without proxied = false, ACM validation fails. Cloudflare’s proxy rewrites DNS responses, breaking ACME DNS-01 validation.
Step 3: Wait for Validation
resource "aws_acm_certificate_validation" "graycloudarch" {
provider = aws.us-east-1
certificate_arn = aws_acm_certificate.graycloudarch.arn
validation_record_fqdns = [for record in cloudflare_record.graycloudarch_cert_validation : record.hostname]
}
This is the glue.
What it does:
- Tells Terraform to wait for ACM validation to complete
- Depends on Cloudflare DNS records being created first
- Blocks until validation succeeds
Without this resource:
- Terraform would request the cert and immediately move on
- You’d have to run
terraform applyagain after validation - Not truly automated
With this resource:
- Terraform waits (usually 2-5 minutes)
- Validation completes
- Terraform continues to next step
This is idempotent. If validation already completed (e.g., re-running terraform apply), this step is a no-op.
Step 4: CloudFront DNS Records (The Easy Part)
Once the certificate is validated, pointing your domain to CloudFront is straightforward:
resource "cloudflare_record" "graycloudarch_root" {
zone_id = data.cloudflare_zone.graycloudarch.id
name = "@"
value = module.graycloudarch.cloudfront_domain_name
type = "CNAME"
ttl = 1 # Auto
proxied = false
}
resource "cloudflare_record" "graycloudarch_www" {
zone_id = data.cloudflare_zone.graycloudarch.id
name = "www"
value = module.graycloudarch.cloudfront_domain_name
type = "CNAME"
ttl = 1
proxied = false
}
Two records:
@(apex domain): graycloudarch.com → CloudFrontwww: www.graycloudarch.com → CloudFront
Why proxied = false here too?
CloudFront is already a CDN. Using Cloudflare’s proxy on top creates:
- Double latency (request hits Cloudflare edge, then CloudFront edge)
- SSL termination conflicts
- More moving parts to debug
Use Cloudflare for DNS only. It’s excellent at that.
The Data Sources: Getting Zone IDs
Cloudflare needs a zone_id to create records. You could hardcode it:
zone_id = "abc123def456" # Don't do this
But that’s brittle. If you transfer the domain or change Cloudflare accounts, the zone ID changes.
Better approach: Data source
data "cloudflare_zone" "graycloudarch" {
name = "graycloudarch.com"
}
# Use it:
zone_id = data.cloudflare_zone.graycloudarch.id
What this does:
- Queries Cloudflare API: “What’s the zone ID for graycloudarch.com?”
- Returns the current zone ID
- Uses it in resources
Benefits:
- Works across Cloudflare account changes
- Self-documenting (you can see it’s looking up “graycloudarch.com”)
- No hardcoded IDs
The Dependencies: Terraform Graph Magic
Terraform automatically figures out the order of operations based on resource dependencies:
aws_acm_certificate.graycloudarch
↓ (domain_validation_options)
cloudflare_record.graycloudarch_cert_validation
↓ (hostname)
aws_acm_certificate_validation.graycloudarch
↓ (certificate_arn)
module.graycloudarch (CloudFront)
↓ (cloudfront_domain_name)
cloudflare_record.graycloudarch_root
You don’t have to tell Terraform the order. It infers dependencies from references:
cloudflare_recordreferencesaws_acm_certificate.domain_validation_options→ Terraform knows to create the cert firstaws_acm_certificate_validationreferencescloudflare_record.hostname→ Terraform knows to create DNS records firstcloudflare_record.graycloudarch_rootreferencesmodule.graycloudarch.cloudfront_domain_name→ Terraform knows to create CloudFront first
This is why Terraform is better than bash scripts. Scripts fail if you run steps out of order. Terraform always runs them in the right order.
Scaling to Multiple Domains
Here’s the beautiful part: adding a second domain is 10 lines of code.
# ACM certificate
resource "aws_acm_certificate" "cloudpatterns" {
provider = aws.us-east-1
domain_name = "cloudpatterns.io"
validation_method = "DNS"
subject_alternative_names = ["www.cloudpatterns.io"]
}
# Validation records (same for_each pattern)
resource "cloudflare_record" "cloudpatterns_cert_validation" {
for_each = {
for dvo in aws_acm_certificate.cloudpatterns.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.cloudflare_zone.cloudpatterns.id
name = each.value.name
value = each.value.record
type = each.value.type
ttl = 60
proxied = false
}
# Validation wait
resource "aws_acm_certificate_validation" "cloudpatterns" {
provider = aws.us-east-1
certificate_arn = aws_acm_certificate.cloudpatterns.arn
validation_record_fqdns = [for record in cloudflare_record.cloudpatterns_cert_validation : record.hostname]
}
# CloudFront DNS records
resource "cloudflare_record" "cloudpatterns_root" { /* ... */ }
resource "cloudflare_record" "cloudpatterns_www" { /* ... */ }
Copy-paste the pattern, change the names. That’s it.
Manual approach: Do all 15 steps again for the second domain.
Terraform approach: Copy 10 lines, run terraform apply.
The Before & After
Before: Manual DNS Configuration
For two domains (graycloudarch.com + cloudpatterns.io):
Time:
- 30 minutes per domain = 1 hour total
- Plus fixing typos: +15 minutes
Steps:
- 30+ manual actions (clicks, copy-pastes, waits)
Errors:
- Typo in validation CNAME: 20% chance
- Forgot to set DNS-only mode: 30% chance
- Added wrong CloudFront domain: 10% chance
Documentation:
- “I think I did this last time…” (unreliable memory)
Scaling:
- Third domain = another 30 minutes
- No reusability
After: Terraform Automation
For two domains:
Time:
- Write Terraform once: 30 minutes
- Deploy both domains:
terraform apply(35 minutes, mostly waiting) - Total: 35 minutes (and future domains are free)
Steps:
- 1 command (
terraform apply)
Errors:
- 0% (Terraform validates before applying)
Documentation:
- The code IS the documentation
- Git history shows what changed and why
Scaling:
- Third domain = copy 10 lines, run
terraform apply(5 minutes)
The Real Win: Mental Overhead
The time savings are great. But the bigger win is mental overhead.
Manual DNS configuration requires:
- Context switching (AWS console → Cloudflare → AWS → Cloudflare)
- Remembering state (“Did I already add the www record?”)
- Focus (“Don’t typo this 60-character validation string”)
- Testing (“Let me check if it worked…”)
Terraform requires:
- Run
terraform apply - Go make coffee
I get my focus back. I can write this blog post while Terraform deploys infrastructure.
Lessons Learned
1. Cloudflare’s Terraform Provider is Excellent
It covers 95% of Cloudflare features:
- DNS records
- Page rules
- Firewall rules
- Workers (if you’re into that)
Documentation: https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs
Well-maintained: Updates within days of new Cloudflare features
2. for_each Loops are the Secret Weapon
Most Terraform tutorials show count:
resource "aws_instance" "example" {
count = 3
# ...
}
for_each is better for dynamic resources:
resource "cloudflare_record" "validation" {
for_each = toset(var.validation_records)
# ...
}
Why?
countuses index (0, 1, 2…). If you remove item 1, Terraform re-indexes everything (destroys and recreates resources)for_eachuses keys (domain names, IDs). Removing one item doesn’t affect others
For ACM validation, for_each is essential because ACM generates validation records dynamically.
3. aws_acm_certificate_validation is Non-Obvious
This resource doesn’t create anything in AWS. It’s a waiter.
Without it:
$ terraform apply
# Creates cert, creates DNS records, exits
# Cert is still "Pending Validation"
# CloudFront creation fails (needs validated cert)
$ terraform apply # Run again later
# Now cert is validated, CloudFront succeeds
With it:
$ terraform apply
# Creates cert, creates DNS records
# Waits for validation (2-5 minutes)
# Creates CloudFront with validated cert
# Everything works in one apply
Always use aws_acm_certificate_validation for automated workflows.
4. proxied = false is Critical
Spent 30 minutes debugging why ACM validation failed. The problem:
proxied = true # Wrong!
Cloudflare’s proxy rewrites DNS responses. ACM’s validation servers see Cloudflare’s IP, not your validation record.
Solution:
proxied = false # Correct
ACM validation requires DNS-only mode. Remember this or suffer.
Cost Analysis
Terraform: Free (open source) Cloudflare API: Free (included in free tier) GitHub (storing Terraform code): Free (public repo) or $4/month (private repos)
Total cost of automation: $0 (or $4/month if you want private repos)
Time saved per deployment: 30 minutes Value of time (as a consultant): $150/hour ROI per deployment: $75
Over 12 months (12 deployments): $900 saved
One-time investment: 30 minutes to write Terraform Payback period: After 1 deployment
What’s Next
Enhancements I’m Adding
1. Modules for Reusability Instead of copy-pasting, extract to a module:
module "domain" {
source = "./modules/domain"
domain_name = "graycloudarch.com"
enable_www = true
}
2. Terraform Cloud for State Currently using local state. Upgrading to remote state in S3 + DynamoDB for:
- State locking (prevent concurrent applies)
- State history (rollback if needed)
- Team collaboration (if I hire help)
3. Preview Environments Use Terraform workspaces for staging:
terraform workspace new staging
terraform apply
# Deploys to staging-graycloudarch.com
4. Automated Testing
Use terraform plan in CI/CD:
on: [pull_request]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- run: terraform plan
# Fail PR if plan shows destructive changes
Why This Matters for Side Hustles
I’m building this side hustle to $3K/month by March 31, 2026. That’s 9 weeks away.
Every hour I save on infrastructure is an hour I can spend on:
- Writing blog posts (like this one)
- LinkedIn outreach to potential clients
- Building my Udemy course
- Actually talking to customers
Manual DNS configuration doesn’t make money. Published content makes money.
By automating with Terraform, I’m optimizing for the constraint that matters: my time.
Conclusion
Going from manual DNS configuration to fully automated Terraform took:
- 30 minutes to write the initial code
- 100 lines of Terraform
- Zero regrets
What I got:
- ✅ ACM certificates with auto-validation
- ✅ CloudFront DNS records automatically managed
- ✅ No more typos, no more forgotten steps
- ✅ Reusable for future domains
- ✅ Version-controlled in Git
If you’re managing multiple domains:
- Stop clicking through consoles
- Use Terraform + Cloudflare provider
- Automate ACM validation with
for_each - Never look back
Your time is worth more than $0/hour. Automate the boring stuff.
Want help automating your infrastructure? Book a consultation or check out my course on cloud automation.