Unexpectedly Hard: Overcoming Pitfalls in aws_acm_certificate setup

Lesha.png
Date: February 19, 2025

If you’ve ever validated an ACM certificate with Terraform, you’ve likely used a snippet like this:

resource "aws_acm_certificate" "this" {
  domain_name       = "*.${data.aws_route53_zone.this.name}"
  validation_method = "DNS"
}

resource "aws_route53_record" "certificate_validation" { 
  for_each = {
    for dvo in try(aws_acm_certificate.this.domain_validation_options, {}) : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.this.zone_id
}

And even though it seems like it should work, it fails two different ways:

  • Error: Invalid for_each argument
  • Error: UnsupportedCertificate: PENDING_VALIDATION

Error: Invalid for_each argument

│   on ../shared/acm.tf line 30, in resource "aws_route53_record" "certificate_validation":
│   30:   for_each = {
│   31:     for dvo in try(aws_acm_certificate.this.domain_validation_options, {}) : dvo.domain_name => {
│   32:       name   = dvo.resource_record_name
│   33:       record = dvo.resource_record_value
│   34:       type   = dvo.resource_record_type
│   35:     }
│   36:   }
│     ├────────────────
│     │ aws_acm_certificate.this.domain_validation_options is set of object with 1 element
│ The "for_each" map includes keys derived from resource attributes that cannot be determined
until apply, and so Terraform cannot determine the full set of keys that will identify the
│ instances of this resource.
│ When working with unknown values in for_each, it's better to define the map keys statically
│ in your configuration and place apply-time results only in the map values.
│ Alternatively, you could use the -target planning option to first apply only the resources
│ that the for_each value depends on, and then apply a second time to fully converge.

This happens because of for_each - number of records depends on the number of .domain_validation_options, which are not known until the certificate is created. One way to fix this is to avoid using it and create a single record for the first domain validation option:

resource "aws_acm_certificate" "this" {
  domain_name       = "*.${data.aws_route53_zone.this.name}"
  validation_method = "DNS"
}

locals {
  certificate_validation_options = tolist(aws_acm_certificate.this.domain_validation_options)[0]
}

resource "aws_route53_record" "certificate_validation" {
  allow_overwrite = true

  ttl     = 60
  name    = local.certificate_validation_options.resource_record_name
  records = [local.certificate_validation_options.resource_record_value]
  type    = local.certificate_validation_options.resource_record_type

  zone_id = data.aws_route53_zone.this.zone_id
}

Error: UnsupportedCertificate

Error: creating ELBv2 Listener: operation error Elastic Load Balancing v2: CreateListener, api error UnsupportedCertificate: The certificate 'arn:aws:acm:us-east-1:123456789101:certificate/c1a6175f-c93c-4c27-b272-f8787ac9ac6c' must have a fully-qualified domain name, a supported signature, and a supported key size.

This is not very specific, but we can check the certificate status to see if it’s validated.

aws acm describe-certificate \
    --certificate-arn arn:aws:acm:us-east-1:123456789101:certificate/c1a6175f-c93c-4c27-b272-f8787ac9ac6c \
{
    "Certificate": {
        "CertificateArn": "arn:aws:acm:us-east-1:123456789101:certificate/c1a6175f-c93c-4c27-b272-f8787ac9ac6c",
        "DomainValidationOptions": [
            {
                "ValidationStatus": "PENDING_VALIDATION",
            }
        ]
    }
}

This happens because terraform doesn’t wait for the certificate to be validated before proceeding. In this case, the certificate is still pending validation, and the ELBv2 listener creation fails.

Solution: Use aws_acm_certificate_validation resource

resource "aws_acm_certificate_validation" "this" {
  certificate_arn         = aws_acm_certificate.this.arn
  validation_record_fqdns = [aws_route53_record.primary_domain_certificate_validation.fqdn]
}

This is not a real-world entity existing in AWS, but a Terraform resource that waits for the certificate to be validated.

So, wherever you need the certificate ARN (e.g., when creating a listener), reference:

certificate_arn = aws_acm_certificate_validation.this.certificate_arn

instead of:

certificate_arn = aws_acm_certificate.this.arn

This change will force the Terraform to respect the validation process and only proceed once the certificate is validated.

Final, working code

resource "aws_acm_certificate" "this" {
  domain_name       = "*.${data.aws_route53_zone.this.name}"
  validation_method = "DNS"
}

locals {
  certificate_validation_options = tolist(aws_acm_certificate.this.domain_validation_options)[0]
}

resource "aws_route53_record" "certificate_validation" {
  allow_overwrite = true

  ttl     = 60
  name    = local.certificate_validation_options.resource_record_name
  records = [local.certificate_validation_options.resource_record_value]
  type    = local.certificate_validation_options.resource_record_type

  zone_id = data.aws_route53_zone.this.zone_id
}

resource "aws_acm_certificate_validation" "this" {
  certificate_arn         = aws_acm_certificate.this.arn
  validation_record_fqdns = [aws_route53_record.certificate_validation.fqdn]
}
Lesha_head.png

Alexey Eremin

Cloud Engineering Consultant Read More

Tags

Share Blog