June 8, 2025
Provision CloudFront CDN with Terraform
Previously we have seen how to setup an EC-2 instance, run a simple web server and expose it at your domain using Route53. This allows us to access our web server using HTTP at port 80. The natural next step in that setup is to use HTTPS (HTTP + SSL) and enforce access at port 443 instead. We will do it here by adding Cloudfront and ACM to our terraform setup. Let's dive in.
Disclaimer
It is a good practice to place HTTPS termination in different place in architecture than a server itself due to good software design practices such as separation of concerns. In this approach, I chose to work with Cloudfront, however you can use other options as HTTPS termination like AWS Load Balancer (ALB) or AWS API Gateway. Since we have only 1 instance so far, Cloudfront will work great.
Adding Cloudfront distribution
I will confess that it is not super easy to add CloudFront + ACM for HTTPS for a beginner, so I will break down the steps from simplest cloud architecture to most complicated one hopefully clarifying the steps and the reason for taking them.
First step to add an HTTPS using Cloudfront is naturally to create a Cloudfront distribution. Cloudfront distribution is a CDN network that expands copies of your data towards other geografical locations. From the programmer point of view, Cloudfront is a URL - the distribution URL that uses the content from the origin
. Hence these are the two fundamental pieces that we need for now to make sure the distribution works. So let's dive in.
Adding Cloudfront - Creating Origin Server
First thing is to define our origin endpoint. We can do it using Route53, where the origin is a URL pointing to our Ec-2 instance. Let's do it
# Route53.tf
resource "aws_route53_zone" "main" {
name = "vvasylkovskyi.com"
}
resource "aws_route53_record" "origin" {
zone_id = aws_route53_zone.main.zone_id
name = "origin.your-domain.com"
type = "A"
ttl = 300
records = [aws_eip.portfolio.public_ip]
}
You can test now by running terraform apply
. Note, this origin.your-domain.com
is going to be available on HTTP now.
Adding Cloudfront - Define Distribution
Now that we have an origin, we can create distribution:
# cloud_front.tf
resource "aws_cloudfront_distribution" "cdn" {
enabled = true
default_root_object = "index.html"
origin {
domain_name = aws_route53_record.origin.fqdn
origin_id = aws_route53_record.origin.fqdn
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = aws_route53_record.origin.fqdn
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
price_class = "PriceClass_100"
viewer_certificate {
cloudfront_default_certificate = true
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
}
Whoa! that is alot of configs. One of the main pieces there is the target_origin_id
. This will be pointing to our origin.your-domain.com
. For now we will use cloudfront_default_certificate
which is CloudFront certificate.
Let's add a test output:
# output.tf
output "cloudfront_distribution_domain_name" {
value = aws_cloudfront_distribution.cdn.domain_name
}
We will use this to test.
Apply changes
Run terraform apply
and observe the cloudfront_distribution_domain_name
output. It should return something like
d123456789abcdef.cloudfront.net.
If everything went well then you should be able to open the URL in the browser.
Conclusion
I started this notes explaining how to add HTTPS using CloudFront, but I ended-up realizing that two concepts at the same time can be confusing so I decided to split them in two. This way, we can test things in sequence, and ensure that all the preconditions work for SSL. Next let's explore about how to add SSL certificate to 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 CloudFront Distribution
In the next step we are going to create a cloudfront distribution which contains an endpoint with HTTPS termination and is a place where we are going to place our SSL 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]
}
Next, we are going to add origin pointing to our ec2 instance.
# cloud_front.tf
resource "aws_cloudfront_distribution" "cdn" {
depends_on = [aws_acm_certificate_validation.cert]
origin {
domain_name = aws_instance.portfolio.public_dns
origin_id = "ec2-origin"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
}
This section defines where CloudFront fetches content from — your EC2 public DNS. Even though CloudFront will serve HTTPS to users, it talks to the EC2 instance over HTTP (http-only).
Default Cache Behavior: This forces all incoming requests to use HTTPS, even if the user types http://.
Other parts configure what methods to allow (GET, HEAD) and what to cache (in this case, not much since it's a basic setup).
resource "aws_cloudfront_distribution" "cdn" {
depends_on = [aws_acm_certificate_validation.cert]
origin {
domain_name = aws_instance.portfolio.public_dns
origin_id = "ec2-origin"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "ec2-origin"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
price_class = "PriceClass_100"
}
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]
origin {
domain_name = aws_instance.portfolio.public_dns
origin_id = "ec2-origin"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "ec2-origin"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
price_class = "PriceClass_100"
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cert.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
}
Route 53 Record Pointing to CloudFront
Now that Cloudfront setup is done, it is going to be available between our ec2 instance and our DNS. So now we have the Cloudfront doing SSL and sending requests to the ec2 instance. The last step is to enable our DNS Route 53 to send.
# 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
}
}
This sets an alias A record in Route 53 so your domain www.your-domain.com
points to the CloudFront distribution instead of the EC2 directly. Note, make sure that you remove the previous aws_route53_record: www
that was setting your Route53 to be HTTP.
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"
}
...
}
Output
Finally, let's add some output for debugging:
# output.tf
output "https_url" {
value = "https://www.your-domain.com"
description = "Public HTTPS endpoint for the EC2 app."
}
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.
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