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_name as the key (unique after deduplication)
  • Use dvo... (ellipsis) — this groups all dvo objects with the same key into a list
  • Access each.value[0] because the grouped value is now a list
  • Use each.key (which is resource_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:

  1. They share the same DNS validation CNAME
  2. Use resource_record_name as the for_each key (not domain_name)
  3. Use the ... ellipsis grouping syntax to handle duplicates
  4. Access each.value[0] for the actual values
  5. If you had a previous state entry with the old key, terraform state mv it before applying