Setting up an SSL certificate using AWS and Terraform

Markus Hanslik
Aug 19 · 4 min read

Once you start using AWS and notice setting up everything via the UI is tedious and error-prone, you’ll love using Terraform to describe your infrastructure in a versioned and transparent way — it’s the AWS-agnostic alternative to CloudFormation, but it does come with drawbacks.

One of the drawbacks is that you are not just running into AWS’s pitfalls, but also Terraform’s own little quirks, which is why even simple things like setting up an SSL certificate for the first time may become time-consuming.

SSL certificates for CloudFront require us-east-1

The main thing to keep in mind is that when creating a SSL certificate issued by AWS for use in your Route53 domains, you must create it in the us-east-1 region to be used with CloudFront (https://docs.aws.amazon.com/acm/latest/userguide/acm-services.html); otherwise, AWS will not fail creating the certificate but it will not be possible to assign it to the CloudFront resource later on.

(If you want to use a certificate for an ALB, it must be created in the same region where the ALB is, meaning you may actually need to deploy multiple certificates. Just skip the provider values in the aws_acm_certificate and aws_acm_certificate_validation resources to create a certificate in your existing AWS provider’s region)

You may however not actually use Terraform to deploy your resources in us-east-1, or you may use a variable to be able to switch regions, resulting in the possibility of having the wrong region for the SSL certificate.

Add a specific provider for creating CloudFront-compatible SSL certificates

Hence, add a separate AWS provider somewhere to your Terraform scripts making sure this provider is located in the correct region, making use of the multi-provider feature of Terraform (https://www.terraform.io/docs/configuration-0-11/providers.html#multiple-provider-instances):

provider "aws" {
alias = "acm"
region = "us-east-1"
version = "2.24"
}

Note that this provider has an alias called „acm“ (as in, Amazon Certificate Manager) that we can use in Terraform files to reference to this specific provider, and that we are pinning this provider to a specific version, to not run into accidental side-effects of updates, differences between machines / CI runners etc; you may want to change the 2.24 version to your version of the AWS provider.

Using this provider, we can now tell AWS to create a certificate for us:

resource "aws_acm_certificate" "default" {
provider = "aws.acm"
domain_name = "${var.domain}"
subject_alternative_names = ["*.${var.domain}"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}

(The HCL syntax used in this tutorial is for Terraform 0.11. If you already upgraded to Terraform 0.12, you need to adopt the variable syntax as described here https://www.terraform.io/upgrade-guides/0-12.html)

In this example, the certificate is issued for the domain stored in the Terraform variable ${var.domain} and using a wildcard for all sub-domains of the same domain. (By using a variable for the domain name, you can keep your Terraform code environment-agnostic and would be able to spin up e.g. a staging environment with a different AWS account and with a different domain, but without changing any code).

The lifecycle policy is important later, if you recreate your certificates — it tells Terraform to first create a new certificate, instead of first removing the existing certificate leaving your domain without a certificate.

Additionally, as we want Terraform to be able to also validate the SSL certificate for us, we are using validation_method = "DNS" instead of email, so that the process of validating that the domain of the SSL belongs to us can be fully automated.

This means we need yet another AWS resource, storing the DNS entry AWS will be able to check on the domain:

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

Note that zone_id has to be replaced with the variable pointing to the the zone ID of your public zone. The other keys are used to fill the DNS record with the settings from the certificate for the DNS record.

To then actually finish the validation, there is a final AWS resource to be added in Terraform:

resource "aws_acm_certificate_validation" "default" {
provider = "aws.acm"
certificate_arn = "${aws_acm_certificate.default.arn}" validation_record_fqdns = [
"${aws_route53_record.validation.fqdn}",
]
}

This then connects our DNS record with our certificate, making sure AWS will be able to validate the certificate so that we can use it in our infrastructure.

You can then reference the certificate in your Terraform files for CloudFront without having to use the us-east-1 provider, just by using the ARN of the ACM:

resource "aws_cloudfront_distribution" "cdn" {
viewer_certificate {
acm_certificate_arn = "${aws_acm_certificate.default.0.arn}"
...
}
...
}

That’s it! If you liked this tutorial, please show your support by clapping for this story. 👍

Markus Hanslik

Written by

founds, develops and has erratic thoughts.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade