June 8, 2025

Provision SSL and HTTPS with Terraform using Cloudfront, ACM, Route53 and Ec2

At this point, you should have provisioned EC-2 and CloudFront. If not, check these notes:

In this note, we will provision SSL certificate using ACM, and add HTTPS termination on our Cloudfront distribution.

ACM Certificate for HTTPS

Cloudfront requires SSL certificates to be in us-east-1 region regardles of where our resources are. So we will begin by adding a new provider:

# ssl.tf

provider "aws" {
  alias                    = "us_east_1"
  region                   = "us-east-1"
  shared_credentials_files = ["./.aws-credentials"]
  profile                  = "terraform"
}

Note, even if your original provider is us-east-1, we need to define the alias for SSL to work. Next, let's create an ACM resource.

# ssl.tf
resource "aws_acm_certificate" "cert" {
  provider                  = aws.us_east_1
  domain_name               = "your-domain.com"
  validation_method         = "DNS"
  subject_alternative_names = ["www.your-domain.com", "your-domain.com"]

  lifecycle {
    create_before_destroy = true
  }
}

This requests an SSL/TLS certificate from AWS Certificate Manager for your domain. We choose DNS validation (simplest in Terraform if using Route 53).Note that we are adding lifecycle.create_before_destroy. This is because in case you change something on this resource, since Cloud Front will be using the certificate, this directive allows to duplicate certificate, and replace old one before destroying it thus allowing cloud front to switch certificates without issues.

DNS Validation via Route 53

Next, we need to create the required DNS record to prove domain ownership to ACM. If you are curious in details, there is a step-by-step way of doing that manually described here Installing SSL and Moving to HTTPS on Our Website with Let's Encrypt.

# ssl.tf
resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
      name  = dvo.resource_record_name
      type  = dvo.resource_record_type
      value = dvo.resource_record_value
    }
  }

  name    = each.value.name
  type    = each.value.type
  zone_id = aws_route53_zone.main.zone_id
  records = [each.value.value]
  ttl     = 60
}

In the code above, the for_each turns the set into a map indexed by domain_name (which is unique). We are dynamically generating multiple DNS records (1 per domain to validate) using for_each. aws_acm_certificate.cert.domain_validation_options is a list of instructions from AWS on how to validate your domain. We loop over each dvo (domain validation option), and create a map like:

{
  "www.your-domain.com" = {
    name  = "_xyz.www.your-domain.com."
    type  = "CNAME"
    value = "_abc.acm-validations.aws."
  }
}

This is how AWS verifies domain ownership: you create a DNS record with those values. The

  records = [each.value.value]
  ttl     = 60
  • records: The actual value to put in the DNS record (like _abc.acm-validations.aws.)
  • ttl: Time-to-live (how long DNS resolvers cache it), set to 60 seconds for fast propagation.

Next, we are adding a resource that tells AWS to wait for validation to complete before proceeding. It depends on the DNS record being correct.

# ssl.tf

resource "aws_acm_certificate_validation" "cert" {
  provider        = aws.us_east_1
  certificate_arn = aws_acm_certificate.cert.arn

  validation_record_fqdns = [
    for record in aws_route53_record.cert_validation : record.fqdn
  ]
}

The FQDNS stands for Fully Qualified Domain Name btw.

Adding new Route53 for HTTPS pointing to CloudFront Distribution

Now that we have successfully validated the certificate, next lets ensure that we have a URL that will be using cloud front distribution. Here we essentially are adding new Route53 record, which will be pointing to the cloudfront distribution URL:

# route53.tf


resource "aws_route53_record" "www_https" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "www.your-domain.com"
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.cdn.domain_name
    zone_id                = aws_cloudfront_distribution.cdn.hosted_zone_id
    evaluate_target_health = false
  }
}

Making CloudFront Distribution use our new certificatie

Now that we have a certificate, and we can access to our cloudfront distribution from a custom domain name such as http://www.your-domain.com. But of course we want it to be https (with an s as in SSL). So the next step to do that is to instruct our CloudFront to use the right certificate.

First we need to set a rule to wait for the certificate to be validated before creating the distribution.

# cloud_front.tf

resource "aws_cloudfront_distribution" "cdn" {
  depends_on = [aws_acm_certificate_validation.cert]
  aliases = ["www.your-domain.com"]
}

The aliases create Alternate domain names also known as CNAME. This is essential for our SSL handshake to work well. Next, we already have rules that instruct CloudFront to redirect traffic to our origin which is our ec-2 instance. The only missing piece is to change the viewer_certificate. Viewer certificate: Tells CloudFront to use the validated ACM certificate for https://www.your-domain.com.

# cloud_front.tf

resource "aws_cloudfront_distribution" "cdn" {
  depends_on = [aws_acm_certificate_validation.cert]
  aliases = ["www.your-domain.com"]

  ...
  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.cert.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  ...
}

And that is it. All set for cloudfront and ACM.

Update security groups to allow port 443

Lastly, let's not forget to expose port 443 in our VPC security group:

# security_group.tf

resource "aws_security_group" "my-app" {
  name   = "SSH Port"
  vpc_id = aws_vpc.main.id

  ...

  ingress {
    cidr_blocks = [
      "0.0.0.0/0"
    ]
    from_port = 443
    to_port   = 443
    protocol  = "tcp"
  }

  ...
}

Applying changes

Since we have added new provider (alias), we need to install it using terraform init first.

terraform init

All should be working now, lets try to provision our new infrastructure. Run: terraform apply --auto-approve.

Final Validations

You should be able to open your app using https://www.your-domain.com. However if it is not working, we can troubleshoot:

Check DNS propagation

Run:

dig www.your-domain.com +short

If it still returns an IP address, it means the A record is pointing directly to EC2 and not CloudFront — which won't support HTTPS directly.

Check for SSL handshake

Run:

curl -Iv https://www.your-domain.com

Conclusion

We have covered quite a bit of things today:

  • SSL certificate via ACM
  • HTTPS access via CloudFront
  • DNS validation using Route 53
  • A secure, CDN-backed endpoint for your EC2 app at https://www.your-domain.com

That was alot, hope you have all set well! Happy hacking.