Aws_acm_certificate.app_cert.domain_validation_options is a set of object, known only after apply

I’ve been working through a Terraform (+ CI/CD) workshop which was taught in an earlier version of Terraform, but I decided to it in 1.0.11 with AWS provider 3.65.0… just to see what sort of the difference would be. I’ve hit a blocker when dealing with ACM to get the certificate, and I need some advice on how to proceed.

The error I’m getting is during the plan stage:

[ckerr@ck-vm-rhel8-localdomain recipe-app-api-devops]$ docker-compose -f deploy/docker-compose.yml run --rm terraform plan
Creating deploy_terraform_run ... done
╷
│ Error: Invalid for_each argument
│ 
│   on dns.tf line 44, in resource "aws_route53_record" "app_cert_validation_records":
│   44:   for_each = {
│   45:     for dvo in aws_acm_certificate.app_cert.domain_validation_options : dvo.domain_name => {
│   46:       name   = dvo.resource_record_name
│   47:       type   = dvo.resource_record_type
│   48:       record = dvo.resource_record_value
│   49:     }
│   50:   }
│     ├────────────────
│     │ aws_acm_certificate.app_cert.domain_validation_options is a set of object, known only after apply
│ 
│ The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how
│ many instances will be created. To work around this, use the -target argument to first apply only the resources that the
│ for_each depends on.
╵
Releasing state lock. This may take a few moments...
ERROR: 1

I have a file called dns.tf that looks after the DNS and SSL certificates, with the intention that Amazon’s Certificate Manager (ACM) will acquire the certificate. That file looks like this, and I’ve included commentary in the hope that someone might be able to identify where my thinking has gone wrong.

// BELIEF: this is the DNS zone we will be operating in. No issues there.
//
data "aws_route53_zone" "zone" {
  name = "${var.dns_zone_name}."
}

// BELIEF: this creates a CNAME record that points to our AWS ELB instance for our application.
// This is where it departs a little from the documentation, but I'm not sure if the documentation
// is just being a bit sparse, or if the validation records are meant to also be part of this object.
//
resource "aws_route53_record" "app" {
  zone_id = data.aws_route53_zone.zone.zone_id
  name    = "${lookup(var.subdomain, terraform.workspace)}.${data.aws_route53_zone.zone.name}"
  type    = "CNAME"
  ttl     = "300"

  records = [aws_lb.api.dns_name]
}

// BELIEF: this models the certificate for our application.
//
resource "aws_acm_certificate" "app_cert" {
  domain_name       = aws_route53_record.app.fqdn
  validation_method = "DNS"

  tags = local.common_tags

  lifecycle {
    create_before_destroy = true
  }
}

// BELIEF: to prove ownership of the domain we need to be able to prove that we can
// place certain records into the domain as part of a DNS challenge method.
// I believe this is meant to refer to just those challenge records.
// As this is the dynamic part, its unlikely that the domain validation options would
// be known ahead of time, and so this is where I'm coming unstuck.
//
resource "aws_route53_record" "app_cert_validation_records" {
  allow_overwrite = true
  zone_id         = data.aws_route53_zone.zone.zone_id
  ttl             = "60"

  for_each = {
    for dvo in aws_acm_certificate.app_cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  name    = each.value.name
  type    = each.value.type
  records = [each.value.record]
}

// BELIEF: This doesn't create anything; its just a placeholder for the validation
// process... presumably for dependency reasons.
// It basically just associates the FQDN (in the certificate) with its validation records.
//
resource "aws_acm_certificate_validation" "app_cert_validation_process" {
  certificate_arn         = aws_acm_certificate.app_cert.arn
  validation_record_fqdns = [for record in aws_route53_record.app_cert_validation_records : record.fqdn]
}

// Reading https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-3-upgrade#resource-aws_acm_certificate
// I don't see where the problem is.

In case it matters, there is another resource that is depending on this, which is the ELB instance which uses the certificate:

resource "aws_lb_listener" "api_https" {
  load_balancer_arn = aws_lb.api.arn
  port              = 443
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate_validation.app_cert_validation_process.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api.arn
  }
}

I’ve been looking to follow https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-3-upgrade#resource-aws_acm_certificate, which indicates that a plan should work and I should see the text “(known after apply)” in the output.

I take it, based on my research to date, is that the root cause is that the AWS provider can’t / won’t predict what the domain validation options will be ahead of time… and yet the upgrade documentation indicates that, while this is only something that is known after applying, it shouldn’t cause a plan failure and I shouldn’t need any ugly -target workaround.

I tried using a depends_on, but that’s not going to help with the plan stage.

Full code is in Cameron Kerr / recipe-app-api-devops · GitLab if it helps.

Thanks for reading (and thanks to the Terraform developers for the really great error messages),
Cameron