ACM Wildcard Cert Validation with Cloudflare: The Duplicate CNAME Trap
Requesting an ACM certificate for both mypie.io and *.mypie.io produces duplicate CNAME validation records. Our Terraform for_each hit a 'Duplicate object key' error and we went through three iterations to fix it.
We use ACM for TLS termination on our ALB. We needed a single certificate covering both the apex domain mypie.io and all subdomains *.mypie.io. ACM allows you to add both as Subject Alternative Names (SANs) on a single cert. What it does not tell you upfront is that both produce the same CNAME validation record.
The Setup
Our ACM cert request in Terraform looked like this:
resource "aws_acm_certificate" "wildcard" {
domain_name = "mypie.io"
subject_alternative_names = ["*.mypie.io"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
And we added a cloudflare_record for each validation record that ACM returned:
resource "cloudflare_record" "acm_validation" {
for_each = {
for dvo in aws_acm_certificate.wildcard.domain_validation_options :
dvo.domain_name => {
name = dvo.resource_record_name
value = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = var.cloudflare_zone_id
name = each.value.name
type = each.value.type
value = each.value.content
ttl = 60
}
First Error: Duplicate Cloudflare Record
The first apply created the mypie.io validation record. The second apply, after we added *.mypie.io as a SAN, hit:
Error: Failed to create DNS record
POST https://api.cloudflare.com/client/v4/zones/.../dns_records
Status: 400
{"success":false,"errors":[{"code":81057,"message":"The record already exists."}]}
ACM had generated a second domain_validation_options entry for *.mypie.io, but its resource_record_name and resource_record_value were identical to the entry for mypie.io. DNS validation for wildcards reuses the apex record. Cloudflare refused to create a duplicate.
In state we had the first record keyed as cloudflare_record.acm_validation["mypie.io"]. The new entry would have been cloudflare_record.acm_validation["*.mypie.io"] with the exact same DNS content.
Second Error: Duplicate Object Key in for_each
After reading about this pattern, we tried to deduplicate using resource_record_name as the key instead:
for_each = {
for dvo in aws_acm_certificate.wildcard.domain_validation_options :
dvo.resource_record_name => {
name = dvo.resource_record_name
type = dvo.resource_record_type
value = dvo.resource_record_value
}
}
New error:
Error: Invalid for expression
Two different items produced the key "_abc123.mypie.io." in this 'for'
expression. If duplicates are expected, use the ellipsis (...) after the
value expression to enable grouping by key.
The for_each map cannot have duplicate keys. Both mypie.io and *.mypie.io produce the same resource_record_name, so Terraform rejected the whole map construction.
The Fix: Ellipsis Grouping Syntax
The error message actually hints at the solution: use the ellipsis ... to enable grouping. When you append ... after the value expression in a for expression, Terraform groups duplicate keys into arrays:
resource "cloudflare_record" "acm_validation" {
for_each = {
for dvo in aws_acm_certificate.wildcard.domain_validation_options :
dvo.resource_record_name => dvo...
}
zone_id = var.cloudflare_zone_id
name = each.key
type = each.value[0].resource_record_type
content = each.value[0].resource_record_value
ttl = 60
proxied = false
}
Key changes:
- Use
dvo.resource_record_nameas the key (unique after deduplication) - Use
dvo...(ellipsis) — this groups alldvoobjects with the same key into a list - Access
each.value[0]because the grouped value is now a list - Use
each.key(which isresource_record_name) directly for the DNS record name
Fixing the Existing State
There was one more problem: we already had a state entry with the old key "mypie.io". Terraform would try to destroy the old record and create a new one under the new key. We could not let it destroy the live validation record while the cert was active.
The solution was to rename the state key with terraform state mv:
cd terraform/environments/dns
terraform state mv \
'cloudflare_record.acm_validation["mypie.io"]' \
'cloudflare_record.acm_validation["_abc123.mypie.io."]'
Where _abc123.mypie.io. is the actual resource_record_name value from ACM (the leading underscore and trailing dot are part of the DNS CNAME name format). You can get this value from:
terraform show | grep resource_record_name
# or
aws acm describe-certificate \
--certificate-arn arn:aws:acm:eu-central-1:<account-id>:certificate/<cert-id> \
--query 'Certificate.DomainValidationOptions[*].ResourceRecord'
After the state mv, terraform plan showed zero destructive changes — just the new key referencing the same existing Cloudflare record.
ACM Validation
With the CNAME record in place for both SANs, ACM validation completed automatically. The certificate status transitioned to ISSUED within a few minutes:
aws acm describe-certificate \
--certificate-arn arn:aws:acm:eu-central-1:<account-id>:certificate/<cert-id> \
--query 'Certificate.Status'
# "ISSUED"
Summary
When requesting ACM certs with both example.com and *.example.com:
- They share the same DNS validation CNAME
- Use
resource_record_nameas thefor_eachkey (notdomain_name) - Use the
...ellipsis grouping syntax to handle duplicates - Access
each.value[0]for the actual values - If you had a previous state entry with the old key,
terraform state mvit before applying