Using Terraform to deploy a Cloudfront distribution pointing to an S3 bucket

This post shows how to automate the deployment of a Cloudfront distribution that exposes an S3 bucket content using Terraform. I use the same infrastructure to power this blog.

Infrastructure as code

I would strongly recommend to never point and click at a cloud provider (like AWS) administration console unless you’re testing something.

Infrastructure as code means automating the deployment of infrastructure using some form of code. Terraform is a tool that will take descriptive code as input and process it into API calls to cloud providers.

This post will show you how to use Terraform to create an S3 bucket, a Cloudfront distribution, an SSL certificate, and optionally DNS records and a domain name on AWS.

Terraform

The rest of this post assumes you know how to create a Terraform project, configure AWS as the provider, and iterate on infrastructure using terraform plan and terraform apply commands.

If it’s the first time you work with Terraform, I recommend following the official tutorial.

S3

S3 is an object storage service. You can use API calls to basically upload, download, list and delete objects (files).

In this scenario, we’ll use S3 to host files that we want to distribute on the Internet using Cloudfront (AWS CDN).

We’ll need 2 buckets:

This code creates the two buckets. The private ACL is the default ACL. It ensures the buckets are not publicly exposed.

The IAM policy document is a bucket policy that will be bound to the content bucket and will allow Cloudfront to access its content.

The origin access identity is an object created without any parameter that will be bound to both the Cloudfront distribution and the bucket policy to identify a given Cloudfront distribution when it requests files of an S3 bucket.

s3.tf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
resource "aws_s3_bucket" "blog" {
  bucket = "blog.example.org"
  acl    = "private"
}

resource "aws_s3_bucket" "logs" {
  bucket = "logs.blog.example.org"
  acl    = "private"
}

data "aws_iam_policy_document" "blog_s3_policy" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.blog.arn}/*"]

    principals {
      type        = "AWS"
      identifiers = ["${aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn}"]
    }
  }

  statement {
    actions   = ["s3:ListBucket"]
    resources = ["${aws_s3_bucket.blog.arn}"]

    principals {
      type        = "AWS"
      identifiers = ["${aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn}"]
    }
  }
}

resource "aws_s3_bucket_policy" "blog" {
  bucket = "${aws_s3_bucket.blog.id}"
  policy = "${data.aws_iam_policy_document.blog_s3_policy.json}"
}


locals {
  s3_origin_id = "blogs3origin"
}

resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
}

ACM

ACM is an Amazon service and Certificate Authority that provides free SSL certificates to be used on other AWS services.

This block requests a certificate for the blog.example.org domain and requests validation using DNS records. In order to validate that you own the domain, AWS will provide a CNAME that you must add on your domain’s DNS server zone. It will attempt to vaidate it every few minutes.

acm.tf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
resource "aws_acm_certificate" "cert" {
  domain_name       = "blog.example.org"
  validation_method = "DNS"
  lifecycle {
    create_before_destroy = true
  }
  tags {
    Name = "blog.example.org"
  }
}
output "acm_dns_validation" {
  value = "${aws_acm_certificate.cert.domain_validation_options}"
}

Cloudfront

We’ll need 1 distribution with 1 origin.

The origin domain name can be obtained from the blog S3 bucket output variable bucket_regional_domain_name.

The origin access identity is what will allow the Cloudfront distribution to access files in the S3 bucket.

The logging configuration defines the S3 bucket where you want Cloudfront to upload logs.

The aliases define the domain names (hosts) that the distribution will accept requests for.

The default cache behavior defines how the cache will operate. This example only allows GET and HEAD requests, and doesn’t forward query strings and cookies. The compress parameter toggles whether Cloudfront will gzip the files when requested by a browser. HTML, CSS and Javascript can be compressed at a quite high rate. This feature can save costs and increase the loading speed of your website. Note that this parameter toggles Cloudfront compression. The TTLs define the minimum, default and maximum age of any cached item served by Cloudfront. If your origin supplies a TTL, it will be used provided that it’s between the min and max boundaries. If it doesn’t provide any, then the default will be used. When a file TTL expires, then Cloudfront will trigger a request to the origin the next time a request comes for that file.

The price class defines the set of edge locations from which your files will be served. You can look at the documentation here to better understand each price class. The idea here is that serving files from some locations like Australia and South America is way more expensive than in North America or Europe. You can control where you want your files to be hosted.

The viewer certificate section allows you to configure Cloudfront to terminate SSL sessions with a given certificate. In this example, the SSL certificate is generated for free by AWS in the ACM service.

cloudfront.tf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
resource "aws_cloudfront_distribution" "blog" {
  origin {
    domain_name = "${aws_s3_bucket.blog.bucket_regional_domain_name}"
    origin_id   = "${local.s3_origin_id}"

    s3_origin_config {
      origin_access_identity = "${aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path}"
    }
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"

  logging_config {
    include_cookies = false
    bucket          = "${aws_s3_bucket.logs.bucket_domain_name}"
    prefix          = ""
  }

  aliases = ["blog.example.org"]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "${local.s3_origin_id}"

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }
    compress = true
    viewer_protocol_policy = "allow-all"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  price_class = "PriceClass_200"

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

Deploying infrastructure

With those 3 files in your Terraform projects, you’ll end up creating two S3 buckets, an ACM TLS certificate and a CloudFront distribution to expose the S3 bucket’s content publicly.