Easy AWS ACM DNS Validation with Terraform

Perform AWS ACM DNS validation in Terraform without having to start using loops in your IaaC.

Easy AWS ACM DNS Validation with Terraform

When you create a certificate in AWS ACM, it can be validated by using the Email or DNS method. For the DNS one, Amazon Certificate Manager will offer you to create by hand the validation record inside Route 53, however, if you want to have your certificates in Terraform, this means that this validation record can linger in your hosted zone after destroying the certificate with a terraform destroy.

One way to do so is to also create a Route53 record right after the certificate to validate it. In that sense, we want to ensure that the record will be created with a minimal amount of hardcoded values in it. To achieve this we have to take a look inside the state generated by the creation of the certificate:

resource "aws_acm_certificate" "example" {
    arn                       = "arn:aws:acm:region:account_id:certificate/foo-bar"
    domain_name               = "foo.bar.io"
    domain_validation_options = [
        {
            domain_name           = "foo.bar.io"
            resource_record_name  = "_xxxxx.foo.bar.io."
            resource_record_type  = "CNAME"
            resource_record_value = "_yyyyyyy.zzzzzz.acm-validations.aws."
        },
    ]
    id                        = "arn:aws:acm:region:account_id:certificate/foo-bar"

The values that we are looking for are located inside the domain_validation_options object. That makes it a bit tricky to recover as we either need to loop inside the object to recover the values we want, this inserting some logic inside our infrastructure declaration, or manipulate the data and feed it to the record resource. I don't like loops in Terraform so I will choose the second option.

First, we need to isolate the object we want. For the purpose of showing you, I will be using an output to capture the functions I will be using:

output "test" {
    value = aws_acm_certificate.example.domain_validation_options
}

Changes to Outputs:
  + test = [
      + {
          + domain_name           = "foo.bar.io"
          + resource_record_name  = "_xxxxxxxxx.foo.bar.io."
          + resource_record_type  = "CNAME"
          + resource_record_value = "_yyyyyyyyyy.zzzzzzzzzz.acm-validations.aws."
        },
    ]

To be able to recover the data inside the set object structure, we will need to change it to a list by using the tolist function:

output "test" {
    value = tolist(aws_acm_certificate.example.domain_validation_options)[0]
}

Changes to Outputs:
  + test = {
      + domain_name           = "foo.bar.io"
      + resource_record_name  = "_xxxxxxxxxxxx.foo.bar.io."
      + resource_record_type  = "CNAME"
      + resource_record_value = "_yyyyyyyyyy.zzzzzzzzzzz.acm-validations.aws."
    }

Now that the set is out of the way, we can now use the lookup function to recover the values we want to create our record with:

output "test" {
    value = lookup(tolist(aws_acm_certificate.example.domain_validation_options)[0], "resource_record_name")
}

Changes to Outputs:
  + test = "_xxxxxxxxxxxx.foo.bar.io."

By using this method, you can now create the certificate by applyingthe same functions to the resource_record_type and resource_record_value keys:

resource "aws_route53_record" "this" {
  allow_overwrite = true
  name            = lookup(tolist(aws_acm_certificate.example.domain_validation_options)[0], "resource_record_name")
  records         = [lookup(tolist(aws_acm_certificate.example.domain_validation_options)[0], "resource_record_value")]
  ttl             = 300
  type            = lookup(tolist(aws_acm_certificate.example.domain_validation_options)[0], "resource_record_type")
  zone_id         = var.hosted_zone_id
}

Once applying this resource in terraform, you will need to wait up to 30 minutes.