Terraform Recipes: CloudFront distribution from an S3 bucket

In this new short series of articles, I want to share Terraform recipes to common tasks. These will be highly opinionated (as everything on this site is), but I believe that these are fairly close to the ideal approach.

Terraform Recipes: CloudFront distribution from an S3 bucket

In this new short series of articles, I want to share Terraform recipes to common tasks. These will be highly opinionated (as everything on this site is), but I believe that these are fairly close to the ideal approach.

Today’s goals 💪

  • Create an S3 bucket to store static website assets in;
  • Secure the bucket so that it is not accessible directly;
  • Create a CloudFront distribution with the S3 bucket as an origin.

Creating the correct identity 🆔

Somewhat counter-intuitively perhaps, the first thing we should set up is the CloudFront Origin Access Identity that CloudFront will use to access the S3 bucket. Fortunately, this is also the most easy part.

###################################
# CloudFront Origin Access Identity
###################################
resource "aws_cloudfront_origin_access_identity" "gitbook" {
  comment = "gitbook"
}

Setting up the permissions 💂‍♂️

With that in place, we can prepare a data resource that will later be attached to the S3 bucket. As this is a data resource, it will not create any actual AWS resources.

###################################
# IAM Policy Document
###################################
data "aws_iam_policy_document" "read_gitbook_bucket" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.gitbook.arn}/*"]

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

  statement {
    actions   = ["s3:ListBucket"]
    resources = [aws_s3_bucket.gitbook.arn]

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

Creating the Bucket 🗑️

Finally, we’re ready to create the bucket and attach the correct access policy to it. While we’re at it, let’s also block any future public access to the bucket.

###################################
# S3
###################################
resource "aws_s3_bucket" "gitbook" {
  bucket = "gitbook.milanvit.net"
  acl    = "public-read"

  website {
    index_document = "index.html"
  }
}

###################################
# S3 Bucket Policy
###################################
resource "aws_s3_bucket_policy" "read_gitbook" {
  bucket = aws_s3_bucket.gitbook.id
  policy = data.aws_iam_policy_document.read_gitbook_bucket.json
}

###################################
# S3 Bucket Public Access Block
###################################
resource "aws_s3_bucket_public_access_block" "gitbook" {
  bucket = aws_s3_bucket.gitbook.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = false
}

Spread out the word 🗣️

Finally, we can create the CloudFront distribution. Bear in mind that most changes to CloudFront take between 5-10 minutes to propagate.

There are two references to resources that we haven’t created in this article (web_acl_id and the viewer_certificate section), so feel free to delete the first one, and replace the content of the required viewer_certificate section with cloudfront_default_certificate = true.

The entire definition is quite lengthy, but in most cases, it really is mostly default values.

###################################
# CloudFront
###################################
resource "aws_cloudfront_distribution" "gitbook" {
  enabled             = true
  default_root_object = "index.html"
  aliases             = [aws_s3_bucket.gitbook.bucket]
  # Huh? Is the next line a spoiler of a future article?
  web_acl_id          = aws_waf_web_acl.gitbook.id

  default_cache_behavior {
    allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = aws_s3_bucket.gitbook.bucket
    viewer_protocol_policy = "redirect-to-https"
    compress               = true

    min_ttl     = 0
    default_ttl = 5 * 60
    max_ttl     = 60 * 60

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }
  }

  origin {
    domain_name = aws_s3_bucket.gitbook.bucket_regional_domain_name
    origin_id   = aws_s3_bucket.gitbook.bucket

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.gitbook.cloudfront_access_identity_path
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    # Huh? Another spoiler?
    acm_certificate_arn      = aws_acm_certificate_validation.cf_gitbook.certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2018"
  }
}

And you should be good to go! Don’t forget to create a CNAME DNS record that points to the value of aws_cloudfront_distribution.gitbook.domain_name, unless you’re fine with the ugly subdomain names that CloudFront generates for you.

Let me know in the comments if you’d be interested in hearing how to correctly set up permissions for a new IAM user & configure CircleCI for automated deployments to the S3 bucket!