Local variable isn't correctly evaluated before resource creation

I’m trying to improve aws_acm_certificate module to make possible create multiple certificates in one time. But, I’m running in an annoying issue. This is my current code:

variable "aws_region" {
  default = "eu-west-2"
}
variable "certs" {
  default = {
    "wildcard-my.zone.com" = {
      domain_name = "*.my.zone.com"
      validation_method = "DNS"
      dns_zone = "myzone.com."
      subject_alternative_names = [
        "*.us-east-2.my.zone.com",
      ]
    }
  }
}

provider "aws" {
  version = "~> 2.7"
  region  = var.aws_region
}

resource "aws_acm_certificate" "certs" {
  for_each = var.certs

  domain_name               = each.value.domain_name
  subject_alternative_names = lookup(each.value, "subject_alternative_names", [])
  validation_method         = each.value.validation_method

  tags = {
    Environment = lookup(each.value, "environment", "shared")
    Terraform   = true
    Name = each.key
  }
}

data "aws_route53_zone" "cert_zones" {
  for_each = var.certs
  name = each.value.dns_zone
}

locals {
  validation_records = flatten([
    for k, v in aws_acm_certificate.certs:
      [
        for a, b in v.domain_validation_options:
          merge(
            b,
            { zone_id = lookup(lookup(data.aws_route53_zone.cert_zones,k,{}), "id", "") },
            { cert_arn = v.arn },
            { fqdn = replace(b.resource_record_name, "/(.*)\\./", "$1") }
          )
      ]
  ])
}
resource "aws_route53_record" "records" {
  count = length(local.validation_records)

  zone_id = local.validation_records[count.index].zone_id
  name    = local.validation_records[count.index].resource_record_name
  type    = local.validation_records[count.index].resource_record_type
  ttl     = 60

  records = [
    local.validation_records[count.index].resource_record_value
  ]

  allow_overwrite = true

  depends_on = [
    aws_acm_certificate.certs,
    data.aws_route53_zone.cert_zones
  ]
}

output "validation_records" {
  value = local.validation_records
}

output "validation_records_l" {
  value = length(local.validation_records)
}

output "route53" {
  value = aws_route53_record.records
}

When I run terraform plan, I get this result:

Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

data.aws_route53_zone.cert_zones["wildcard-my.zone.com"]: Refreshing state...

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# aws_acm_certificate.certs["wildcard-my.zone.com"] will be created
+ resource "aws_acm_certificate" "certs" {
    + arn                       = (known after apply)
    + domain_name               = "*.my.zone.com"
    + domain_validation_options = (known after apply)
    + id                        = (known after apply)
    + subject_alternative_names = [
        + "*.us-east-2.my.zone.com",
    ]
    + tags                      = {
        + "Environment" = "shared"
        + "Name"        = "wildcard-my.zone.com"
        + "Terraform"   = "true"
    }
    + validation_emails         = (known after apply)
    + validation_method         = "DNS"
}

# aws_route53_record.records[0] will be created
+ resource "aws_route53_record" "records" {
    + allow_overwrite = true
    + fqdn            = (known after apply)
    + id              = (known after apply)
    + name            = (known after apply)
    + records         = (known after apply)
    + ttl             = 60
    + type            = (known after apply)
    + zone_id         = (known after apply)
}

Plan: 2 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

As you can see, only 1 route53 record will be created. But, local.validation_records is a list with 2 objects. This is the result of terraform apply:

terraform apply -auto-approve
data.aws_route53_zone.cert_zones["wildcard-my.zone.com"]: Refreshing state...
aws_acm_certificate.certs["wildcard-my.zone.com"]: Creating...
aws_acm_certificate.certs["wildcard-my.zone.com"]: Creation complete after 6s [id=arn:aws:acm:eu-west-2:11111111111111:certificate/eeeeeeeee-xxxxx-yyyy-aaaaa-fffffffffff]
aws_route53_record.records[0]: Creating...
aws_route53_record.records[0]: Still creating... [10s elapsed]
aws_route53_record.records[0]: Still creating... [20s elapsed]
aws_route53_record.records[0]: Still creating... [30s elapsed]
aws_route53_record.records[0]: Creation complete after 38s [id=ZZZZZZZZZZZ__66666666666666666666666666666.my.zone.com._CNAME]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

route53 = [
    {
        "alias" = []
        "allow_overwrite" = true
        "failover_routing_policy" = []
        "fqdn" = "_66666666666666666666666666666.my.zone.com"
        "geolocation_routing_policy" = []
        "id" = "ZZZZZZZZZZZ__66666666666666666666666666666.my.zone.com._CNAME"
        "latency_routing_policy" = []
        "name" = "_66666666666666666666666666666.my.zone.com"
        "records" = [
        "_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.olprtlswtu.acm-validations.aws.",
        ]
        "ttl" = 60
        "type" = "CNAME"
        "weighted_routing_policy" = []
        "zone_id" = "ZZZZZZZZZZZ"
    },
]
validation_records = [
    {
        "cert_arn" = "arn:aws:acm:eu-west-2:11111111111111:certificate/eeeeeeeee-xxxxx-yyyy-aaaaa-fffffffffff"
        "domain_name" = "*.my.zone.com"
        "fqdn" = "_66666666666666666666666666666.my.zone.com"
        "resource_record_name" = "_66666666666666666666666666666.my.zone.com."
        "resource_record_type" = "CNAME"
        "resource_record_value" = "_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.olprtlswtu.acm-validations.aws."
        "zone_id" = "ZZZZZZZZZZZ"
    },
    {
        "cert_arn" = "arn:aws:acm:eu-west-2:11111111111111:certificate/eeeeeeeee-xxxxx-yyyy-aaaaa-fffffffffff"
        "domain_name" = "*.us-east-2.my.zone.com"
        "fqdn" = "_55555555555555555555555555.us-east-2.my.zone.com"
        "resource_record_name" = "_55555555555555555555555555.us-east-2.my.zone.com."
        "resource_record_type" = "CNAME"
        "resource_record_value" = "_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.olprtlswtu.acm-validations.aws."
        "zone_id" = "ZZZZZZZZZZZ"
    },
]
validation_records_l = 2

After apply, if I run terraform plan, it will inform that will create the missing record. I would like to know if this behavior is expected and if there is a solution for it.

Hi @galindro!

This behavior is a quirk of how Terraform is modelling the fact that the AWS provider cannot predict how many cert elements there will be at plan time. The AWS provider has defined cert in such a way that an unknown number of certs is represented as a single object whose attributes are all unknown, and so downstream references to it at plan time see it has having a single element.

The most ideal behavior here would be for the AWS provider to pre-populate the cert value as a known list of objects whose values themselves are unknown, but I don’t known ACM well enough to know if the provider has enough information here to indicate that. If it seems like it should, you might consider opening an issue in the AWS provider repository to discuss whether the AWS provider could potentially produce a more accurate plan here.

Tks for feedback @apparentlymart. I opened the issue -> https://github.com/terraform-providers/terraform-provider-aws/issues/10404