Provisioning ACM Certificates on AWS with Terraform

Provisioning ACM Certificates on AWS with Terraform

Every once in a while, technology provides you with an elegant way to convert a cumbersome multi-step process into a single, concise operation. Amazon Certificate Manager (ACM) does this for the process of provisioning, validating, and configuring Transport Layer Security (TLS) certificates. But, when ACM is combined with Terraform, that single, concise operation gets woven directly into your infrastructure configuration in a way that’ll leave you never wanting to provision ACM certificates through the console again. Read on to see how the Azavea ops team navigated this.

AWS Certificate Manager

AWS Certificate Manager is a service provided by Amazon that issues on-demand TLS certificates at no cost. Much like Let’s Encrypt, Amazon controls the Certificate Authority (Amazon Trust Services, LLC) behind the certificates, as well as the accompanying API to manage them.

The only gotcha is that ACM certificates can only be associated with AWS Elastic and Application Load Balancers, CloudFront distributions, and API Gateway endpoints.

Terraform

Because there is an HTTP API defined for ACM, we can manage ACM certificates via Amazon’s suite of SDKs. And, because the AWS Go SDK has support for it, Terraform can manage ACM resources.

Enter aws_acm_certificate, a Terraform resource for requesting and managing ACM certificates.

resource "aws_acm_certificate" "cert" {
  domain_name       = "example.com"
  validation_method = "DNS"
}

Now, aws_acm_certificate is a useful resource on its own, but the real magic comes when it is combined with acm_certificate_validation. That’s because acm_certificate_validation represents the successful validation of an aws_acm_certificate.

More concretely, acm_certificate_validation provides a mechanism to wait for an aws_acm_certificate resource to be validated before it can be used in your Terraform configuration.

Example with Terraform Resources

To walk through an example with pure Terraform resources, imagine that we’ve already created a hosted zone for example.com and associated it with a CloudFront distribution. Now, we want to serve traffic with that domain over HTTPS. To do that, we’ll need the following Terraform resources:

  • aws_acm_certificate: To request a certificate for example.com
  • aws_route53_record: To create a DNS record to validate the certificate request
  • aws_certificate_validation: To ensure that ACM validates our DNS record before certificate use

First, define a resource for the domain ACM certificate and set its validation method to use DNS:

resource "aws_acm_certificate" "default" {
  domain_name       = "example.com"
  validation_method = "DNS"
}

Then, use the outputs of aws_acm_certificate to create a Route 53 DNS record to confirm domain ownership:

data "aws_route53_zone" "external" {
  name = "example.com"
}

resource "aws_route53_record" "validation" {
  name    = "${aws_acm_certificate.default.domain_validation_options.0.resource_record_name}"
  type    = "${aws_acm_certificate.default.domain_validation_options.0.resource_record_type}"
  zone_id = "${data.aws_route53_zone.external.zone_id}"
  records = ["${aws_acm_certificate.default.domain_validation_options.0.resource_record_value}"]
  ttl     = "60"
}

After that, use the aws_acm_certificate_validation resource to wait for the newly created certificate to become valid:

resource "aws_acm_certificate_validation" "default" {
  certificate_arn = "${aws_acm_certificate.default.arn}"

  validation_record_fqdns = [
    "${aws_route53_record.validation.fqdn}",
  ]
}

Finally, use the aws_acm_certifcate_validation outputs to associate the certificate Amazon Resource Name (ARN) with the CloudFront distribution:

resource "aws_cloudfront_distribution" "s3_distribution" {

  ...

  aliases = ["example.com"]

  viewer_certificate {
    acm_certificate_arn      = "${aws_acm_certificate_validation.default.certificate_arn}"
    minimum_protocol_version = "TLSv1"
    ssl_support_method       = "sni-only"
  }
}

Making use of the aws_acm_certifcate_validation output is important, because the one provided by aws_acm_certificate looks identical, but is almost always going to be invalid right away. Using the output from the validation resource ensures that Terraform will wait for ACM to validate the certificate before resolving its ARN.

Example with a Terraform Module

In an effort to reduce these steps even further, we assembled a reusable Terraform module to encapsulate the ACM and Route 53 resources used above. Now, the process of creating, validating, and waiting for a valid certificate looks like this:

data "aws_route53_zone" "external" {
  name = "example.com"
}

module "cert" {
  source = "github.com/azavea/terraform-aws-acm-certificate?ref=0.1.0"

  domain_name           = "example.com"
  hosted_zone_id        = "${data.aws_route53_zone.external.zone_id}"
  validation_record_ttl = "60"
}

resource "aws_cloudfront_distribution" "s3_distribution" {

  ...

  aliases = ["example.com"]

  viewer_certificate {
    acm_certificate_arn      = "${module.cert.arn}"
    minimum_protocol_version = "TLSv1"
    ssl_support_method       = "sni-only"
  }
}

Voilà! Provisioning, validating, and configuring TLS certificates in a single, concise Terraform module.

Thanks to Taylor Nation and Rocky Breslow.