UPDATE: This pull request has been merged into Terraform
A recent patch on the Terraform GitHub repository adds
support for CloudFront
distributions to the
Terraform AWS Provider. The
patch has not been merged into Terraform mainline yet, but I wanted to
share my experience setting up an S3 static site, fronted with
CloudFront and DNS routed with
Route53. Until the CloudFront PR gets
merged, you’ll have to build the branch from source in order to use
the aws_cloudfront_distribution
resource. The words you’re reading
right now were served up from this very Terraform configuration via
CloudFront, migrated from a simple nginx setup. If you haven’t used
Terraform before, please review the
Introduction and Getting Started Guide
before proceeding.
Step 1: Setup your S3 Static Site Bucket
The first thing you need to do is setup an S3 bucket to act as your ‘origin’. This is where all your static HTML files and assets will live. Here’s what the code looks like:
provider "aws" { alias = "prod" region = "us-east-1" access_key = "${var.aws_access_key}" secret_key = "${var.aws_secret_key}" } resource "aws_s3_bucket" "origin_blakesmith_me" { provider = "aws.prod" bucket = "origin.blakesmith.me" acl = "public-read" policy = <<POLICY { "Version":"2012-10-17", "Statement":[{ "Sid":"PublicReadForGetBucketObjects", "Effect":"Allow", "Principal": "*", "Action":"s3:GetObject", "Resource":["arn:aws:s3:::origin.blakesmith.me/*" ] } ] } POLICY website { index_document = "index.html" } }
After running terraform apply
, you will have an S3 bucket that’s
setup to serve HTTP traffic from the root of the bucket. Let’s examine
some of the important parameters:
policy
: Bucket policy that makes the bucket publicly readable.website
: Configure the S3 bucket to serve up a static website, in this case setting the defaultindex_document
toindex.html
.
You can find other configurations on the aws_s3_bucket resource page.
After uploading your static site to the S3 bucket, you should already
be able to view the website at
http://${bucketname}.s3-website-${aws_region}.amazonaws.com
. As an
example, my blog can be served up at
http://origin.blakesmith.me.s3-website-us-east-1.amazonaws.com/.
Step 2: Add a Route53 Record for your Origin
We need a DNS entry for this origin. In this example, we’ll create one at http://origin.blakesmith.me. This will give us a helpful DNS record that will not route through CloudFront and can be used to access the S3 bucket static site directly with no caching or other CloudFront routing rules applied.
resource "aws_route53_zone" "blakesmith_me" { provider = "aws.prod" name = "blakesmith.me" } resource "aws_route53_record" "origin" { provider = "aws.prod" zone_id = "${aws_route53_zone.blakesmith_me.zone_id}" name = "origin.blakesmith.me" type = "A" alias { name = "${aws_s3_bucket.origin_blakesmith_me.website_domain}" zone_id = "${aws_s3_bucket.origin_blakesmith_me.hosted_zone_id}" evaluate_target_health = false } }
First we setup our top level zone, and create a record that’s
associated with that zone. We setup an alias
record configuration
that targets our S3 bucket we created before. Here are the important
parameters:
zone_id
: A reference to our top level DNS zone idalias#zone_id
: A reference to the S3 bucket’s existing zone identifier
Once you terraform apply
this, you should be able to access your
origin at the above DNS record and the behavior should be the same as
when you set up the S3 bucket static website hosting directly. Notice that
http://origin.blakesmith.me has no behavior
change from the simple S3 static bucket site we setup before.
Step 3: Setup your CloudFront Distribution
We have all the basic pieces in place, now comes to the meat: Let’s setup a CloudFront distribution that will use the origin we just configured to serve up the website at the edge. If you’re not too familiar with CDNs, think of them as a “big distributed cache across the globe”. After we configure this distribution, our content will be served from edge servers closest to visitor’s location.
resource "aws_cloudfront_distribution" "blakesmith_distribution" { provider = "aws.prod" origin { domain_name = "origin.blakesmith.me.s3.amazonaws.com" origin_id = "blakesmith_origin" s3_origin_config {} } enabled = true default_root_object = "index.html" aliases = ["blakesmith.me", "www.blakesmith.me"] price_class = "PriceClass_200" retain_on_delete = true default_cache_behavior { allowed_methods = [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT" ] cached_methods = [ "GET", "HEAD" ] target_origin_id = "blakesmith_origin" forwarded_values { query_string = true cookies { forward = "none" } } viewer_protocol_policy = "allow-all" min_ttl = 0 default_ttl = 3600 max_ttl = 86400 } viewer_certificate { cloudfront_default_certificate = true } restrictions { geo_restriction { restriction_type = "none" } } }
There’s a lot going on here, so let’s break it down a bit. The most important part is our origin declaration:
origin { domain_name = "origin.blakesmith.me.s3.amazonaws.com" origin_id = "blakesmith_origin" s3_origin_config { } }
domain_name
: points to the origin Route53 record we created in the last steporigin_id
: A unique identifier for this origin configuration. Since you can setup multiple origins, used to link caching behavior to origin configurations.s3_origin_config
: Extra S3 origin options, we leave this blank
Next we have some other important top level declarations:
enabled = true default_root_object = "index.html" aliases = ["blakesmith.me", "www.blakesmith.me"] price_class = "PriceClass_200" retain_on_delete = true
enabled
: Enable our CloudFront distributiondefault_root_object
: Use index.html as our root objectaliases
: HTTP hostnames you will be serving your site from. These must match your DNS records, or you will get 403 Forbidden Errors.price_class
: How CloudFront will prioritize where traffic gets served from based on price. See: CloudFront Pricing.retain_on_delete
: Causes CloudFront deletions to simplydisable
your distribution. Useful since CloudFront distributions can take upwards of 15 minutes to propagate.
Then we setup our basic caching behavior:
default_cache_behavior { allowed_methods = [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT" ] cached_methods = [ "GET", "HEAD" ] target_origin_id = "blakesmith_origin" forwarded_values { query_string = true cookies { forward = "none" } } viewer_protocol_policy = "allow-all" min_ttl = 0 default_ttl = 3600 max_ttl = 86400 }
The most important part is that we reference our target_origin_id
to
link these two stanza configurations together.
allowed_methods
: Which HTTP verbs we permit our distribution to servecached_methods
: Which HTTP verbs we let this behavior apply totarget_origin_id
: The name of the previous origin_id in ourorigin
stanzaforwarded_values
: Entities that will be passed from the edge to our origin.viewer_protocol_policy
: Which HTTP protocol policy to enforce. One of:allow-all
,https-only
, orredirect-to-https
.min_ttl
: Minimum time (seconds) to live for objects in the distribution cachemax_ttl
: Maximum time (seconds) objects can live in the distribution cachedefault_ttl
: The default time (seconds) objects will live in the distribution cache
Finally, allow CloudFront to use its default SSL cert and serve anywhere:
viewer_certificate { cloudfront_default_certificate = true } restrictions { geo_restriction { restriction_type = "none" } }
Once you run terraform apply
with your patched version of Terraform
and wait the 10-15 minutes for AWS to asynchronously setup your
distribution, you will have a CloudFront provided domain name that you
can validate your setup with. For example, this website is viewable
via CloudFront domain name at:
d1u25xzl6dnmgy.cloudfront.net
Until the aws_cloudfront_resource
gets released, you’ll have to
consult the
documentation provided in the pull request
if you need to deviate from my simple setup here. There are also other
helpful examples in
the integration tests
if you want to see other settings in action.
Step 4: Add Root Route53 Records
The last step adds Route53 records to reference the CloudFront distribution we just setup.
resource "aws_route53_record" "root" { provider = "aws.prod" zone_id = "${aws_route53_zone.blakesmith_me.zone_id}" name = "blakesmith.me" type = "A" alias { name = "${aws_cloudfront_distribution.blakesmith_distribution.domain_name}" zone_id = "Z2FDTNDATAQYW2" evaluate_target_health = false } } resource "aws_route53_record" "www" { provider = "aws.prod" zone_id = "${aws_route53_zone.blakesmith_me.zone_id}" name = "www.blakesmith.me" type = "A" alias { name = "${aws_cloudfront_distribution.blakesmith_distribution.domain_name}" zone_id = "Z2FDTNDATAQYW2" evaluate_target_health = false } }
Here we setup our apex zone and www record to point to our CloudFront distribution. The two critical new pieces you should observe are:
alias#name
: This ALIAS name references our CloudFront distribution created in the previous stepzone_id
: This is a fixed hardcoded constant zone_id that is used for all CloudFront distributions
One final terraform apply
and voilà! You have your final product: A
static site being served from an S3 bucket and fronted by the AWS
CloudFront distribution with Route53 knitting everything together.
You can verify everything is working by examining the HTTP response headers and looking for the CloudFront headers:
> GET / HTTP/1.1 > User-Agent: curl/7.37.1 > Host: blakesmith.me > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/html < Content-Length: 36938 < Connection: keep-alive < Date: Sat, 02 Apr 2016 17:48:17 GMT < Last-Modified: Fri, 01 Apr 2016 11:44:21 GMT < ETag: "1ae13b1b0471e67bad10eb95347f99da" < Accept-Ranges: bytes * Server AmazonS3 is not blacklisted < Server: AmazonS3 < Age: 29 < X-Cache: Hit from cloudfront < Via: 1.1 62e12fdf0f65bd8388f763f504606830.cloudfront.net (CloudFront) < X-Amz-Cf-Id: 4F85Bkl9_nSPQvqjDWAEMYkssuPA04gl8V5qLLIU3cPlS5E1Gtam7A== <
Helpful headers:
Server
: The origin server identifier, in our caseAmazonS3
Age
: The age (in seconds) of the object you just retrieved from the distribution cacheX-Cache
: Whether the HTTP request was a cache hit or miss.
Here is the full code example if you’d like to see the full setup that powers this site.
Happy Terraforming!