DEV Community

Viktor Vasylkovskyi
Viktor Vasylkovskyi

Posted on • Originally published at iac-toolbox.com

Provision CloudFront CDN with Terraform

Previous: Application Load Balancer SSL

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. If you haven't read about it, please refer to the previous notes:

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.

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 geographical 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

# modules/dns/main.tf

resource "aws_route53_zone" "main" {
  name = "viktorvasylkovskyi.com"
}

resource "aws_route53_record" "origin" {
  zone_id = var.main_zone_id
  name    = var.domain_name
  type    = "A"
  ttl     = 300
  records = [var.origin_ip]
}
Enter fullscreen mode Exit fullscreen mode

Make sure to add the variables in variables.tf:

# modules/dns/variables.tf
variable "main_zone_id" { type = string }
variable "domain_name" { type = string }
variable "origin_ip" { type = string }
Enter fullscreen mode Exit fullscreen mode

And the origin_ip is going to be the public IP of our Ec-2 instance. So in the main tf file we need to update the module call:

# main.tf
module "dns" {
  source        = "./modules/dns"
  domain_name   = "origin.your-domain.com"
  main_zone_id  = aws_route53_zone.main.zone_id
  origin_ip     = module.ec2.public_ip
}
Enter fullscreen mode Exit fullscreen mode

And let's make sure to output the FQDN of our origin:

# modules/dns/outputs.tf
output "origin_dns_record" {
  value       = aws_route53_record.origin.fqdn
  description = "The FQDN of the origin Route53 record"
}
Enter fullscreen mode Exit fullscreen mode

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:

# modules/cloudfront/main.tf

resource "aws_cloudfront_distribution" "cdn" {
  enabled             = true
  default_root_object = "index.html"
  origin {
    domain_name = var.aws_route53_origin_fqdn
    origin_id   = var.aws_route53_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 = var.aws_route53_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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Whoa! that is a lot 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 variable and test output:

# modules/cloudfront/variables.tf
variable "aws_route53_origin_fqdn" { type = string }
Enter fullscreen mode Exit fullscreen mode

And output:

# modules/cloudfront/outputs.tf

output "cloudfront_distribution_domain_name" {
  value = aws_cloudfront_distribution.cdn.domain_name
}
Enter fullscreen mode Exit fullscreen mode

Also, if you followed through previous article on setting up the API Gateway, then make sure to remove the API Gateway related code from the main.tf and variables.tf files as we are not using it here. Additionally, ensure that you have the security group allowing HTTP from anywhere:

# modules/security_group/main.tf
resource "aws_security_group" "my_app" {
  name   = "Security Group for API"
  vpc_id = var.vpc_id

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

... rest of the code ...
}
Enter fullscreen mode Exit fullscreen mode

Finally, update the main.tf file to pass the origin FQDN to CloudFront module:

# main.tf

module "cloudfront" {
  source                     = "./modules/cloudfront"
  aws_route53_origin_fqdn    = module.dns.origin_dns_record
}
Enter fullscreen mode Exit fullscreen mode

Also, ensure that we have outputs for CloudFront distribution domain name:

# outputs.tf
output "cloudfront_distribution_domain_name" {
  value = aws_cloudfront_distribution.cdn.domain_name
}

output "cloudfront_distribution_hosted_zone_id" {
  value = aws_cloudfront_distribution.cdn.hosted_zone_id
}
Enter fullscreen mode Exit fullscreen mode

Apply changes

Install the new module with terraform init and then run terraform apply and observe the cloudfront_distribution_domain_name output. It should return something like

d123456789abcdef.cloudfront.net. # example URL
Enter fullscreen mode Exit fullscreen mode

If everything went well then you should be able to open the URL in the browser. (Notice the URL in browser)

Provision SSL Certificate

You can follow the previous guide on Provisioning SSL Certificate to provision the SSL certificate using ACM. Ensure that you have the certificate ARN available for use in the API Gateway module.

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.

# modules/cloudfront/main.tf

resource "aws_cloudfront_distribution" "cdn" {
  depends_on = [var.acm_certificate]
}
Enter fullscreen mode Exit fullscreen mode

Ensure to add a variable for it:

# modules/cloudfront/variables.tf
variable "acm_certificate" { type = string }
Enter fullscreen mode Exit fullscreen mode

And bring this certificate from the main.tf file, from ssl module:

# main.tf

module "ssl_acm" {
  source              = "./modules/acm"
  aws_route53_zone_id = aws_route53_zone.main.zone_id
}

module "cloudfront_cdn" {
  source                    = "./modules/cloudfront"
  aws_route53_origin_fqdn   = module.aws_route53_record.origin_dns_record
  acm_certificate           = module.ssl_acm.aws_acm_certificate_arn
}
Enter fullscreen mode Exit fullscreen mode

Use SSL Certificate in CloudFront

We can now change the Viewer certificate: on CloudFront to use our validated ACM certificate for https://www.your-domain.com.

# modules/cloudfront/main.tf

resource "aws_cloudfront_distribution" "cdn" {
  depends_on = [var.acm_certificate]
  ... other code ...
  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.cert.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
  ... other code ...
}
Enter fullscreen mode Exit fullscreen mode

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.

# modules/dns/main.tf
resource "aws_route53_record" "origin" {
  zone_id = var.main_zone_id
  name    = "origin.viktorvasylkovskyi.com"
  type    = "A"
  ttl     = 300
  records = [var.origin_ip]
}

resource "aws_route53_record" "www" {
  zone_id = var.main_zone_id
  name    = "www.viktorvasylkovskyi.com"
  type    = "A"
  alias {
    name                   = var.target_domain_name
    zone_id                = var.hosted_zone_id
    evaluate_target_health = false
  }
}
Enter fullscreen mode Exit fullscreen mode

And make sure to use the correct variables:

# modules/dns/variables.tf
variable "main_zone_id" { type = string }
variable "target_domain_name" { type = string }
variable "hosted_zone_id" { type = string }
variable "origin_ip" { type = string }
Enter fullscreen mode Exit fullscreen mode

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.

Finally, let's glue it all together in the main.tf:

# main.tf
resource "aws_route53_zone" "main" {
  name = "your-domain.com"
}

module "ssl_acm" {
  source              = "./modules/acm"
  aws_route53_zone_id = aws_route53_zone.main.zone_id
}

module "cloudfront_cdn" {
  source                    = "./modules/cloudfront"
  aws_route53_origin_fqdn   = module.aws_route53_record.origin_dns_record
  acm_certificate           = module.ssl_acm.aws_acm_certificate_arn
}

module "aws_route53_record" {
  source       = "./modules/dns"
  main_zone_id = aws_route53_zone.main.zone_id
  origin_ip = module.ec2.public_ip
  target_domain_name = module.cloudfront_cdn.cloudfront_distribution_domain_name
  hosted_zone_id = module.cloudfront_cdn.cloudfront_distribution_hosted_zone_id
}
Enter fullscreen mode Exit fullscreen mode

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"
  }

  ...
}
Enter fullscreen mode Exit fullscreen mode

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."
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Destroying Infra

Remember, infra has costs. When you are done experimenting, you can destroy the infra like follows:

terraform destroy --auto-approve
Enter fullscreen mode Exit fullscreen mode

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

At this point you are well equipped to set you application in production as you now have HTTPS and a Ec-2 instance and can access to it via your domain name. Next, lets explore how to setup Application Load Balancer in front of our EC-2 instance. Continue reading the Provision Application Load Balancer with Terraform.


Previous: Application Load Balancer SSL | Next: AWS Secrets Manager

Top comments (0)