Static blog with S3 and Cloudfront using Terraform

3 minute read

Intro

Using Amazon S3 and CloudFront is a nice combination for static web hosting, which after hand, was pretty easy to set up.

Terraform

Terraform is a nice tool which lets us provision infrastructure using code. So instead of clicking around in the AWS console, we run terraform apply and it creates infrastructure defined the the terraform files.

First thing i did was creating a S3 bucket and DynamoDB table to store the terraform state in. For this, i used Cloudposse tfstate-backend module. This was rather simple, in main.tf at root directory of the terraform project, insert:

 1module "terraform_state_backend" {
 2  source  = "cloudposse/tfstate-backend/aws"
 3  version = "0.38.1"
 4  name       = "TF_State_s3bucket_name"
 5  attributes = ["state"]
 6
 7  terraform_backend_config_file_path = "."
 8  terraform_backend_config_file_name = "backend.tf"
 9  force_destroy                      = false
10}

and after:

  1. terraform init, which downloads the terraform modules and providers.
  2. terraform apply, which creates state bucket and DynamoDB lock table (along with other specified resources). State is still stored locally.
  3. Running terraform init -force-copy moves our state to the S3 backend.

S3

Creating a bucket in terraform:

 1resource "aws_s3_bucket" "bkt" {
 2  bucket = var.domain
 3
 4  tags = {
 5    Name        = ".."
 6    Environment = ".."
 7    # Tag = "field"
 8  }
 9}
10
11resource "aws_s3_bucket_acl" "pub-acl" {
12  bucket = aws_s3_bucket.bkt.id
13  acl    = "public-read"
14}
15
16
17resource "aws_s3_bucket_website_configuration" "s3_bkt_web_conf" {
18  bucket = aws_s3_bucket.bkt.id
19  index_document {
20    suffix = "index.html"
21  }
22
23  error_document {
24    key = "error.html"
25  }
26}

Nothing too complex here. Made it public so people can access it (and CloudFront). Might look into origin access identity (OAI) later.

Amazon Certificate Manager (ACM)

To use SSL/TLS with CloudFront, we need an ACM certificate. This needs to be in the us-east-1 region and since my default region is set to eu-north-1, i needed to create a new provider for terraform, so i added this in the main.tf file:

1provider "aws" {
2  alias  = "east_1_provider"
3  region = "us-east-1"
4}

Then use the provider inside the aws_acm_certificate resource like so:

 1resource "aws_acm_certificate" "cert" {
 2  provider          = aws.east_1_provider
 3  domain_name       = var.domain
 4  validation_method = "DNS"
 5
 6  tags = {
 7    Description = "Certificate for ${var.domain}"
 8  }
 9
10}

I initially created this in the web console, but was easily importable into terraform with its ARN:

1terraform import aws_acm_certificate.cert "arn:aws:acm:us-east-1:012345678901:certificate/11111111-2222-3333-aaaa-111111111111"

CloudFront

Ran into issues here. When specifying the domain_name you’re supposed to use the website endpoint and not the S3 bucket itself. When changing the domain_name to the website endpont, i ran into a error:

1aws_cloudfront_distribution.s3_distribution: InvalidArgument: The parameter
2Origin DomainName does not refer to a valid S3 bucket.

The problem was that i did not specify the custom_origin_config block, which was supposedly required.

In the viewer_certificate block, we specify our ACM certificate ARN created earlier.

 1resource "aws_cloudfront_distribution" "s3_distribution" {
 2  origin {
 3    domain_name = aws_s3_bucket_website_configuration.s3_bkt_web_conf.website_endpoint
 4    origin_id   = "myWebsiteS3"
 5    custom_origin_config {
 6      http_port              = "80"
 7      https_port             = "443"
 8      origin_protocol_policy = "http-only"
 9      origin_ssl_protocols   = ["TLSv1.2"]
10    }
11  }
12  restrictions {
13    geo_restriction {
14      restriction_type = "none"
15    }
16  }
17
18  enabled             = true
19  is_ipv6_enabled     = true
20  default_root_object = "index.html"
21  viewer_certificate {
22    acm_certificate_arn = aws_acm_certificate.cert.arn
23    ssl_support_method  = "sni-only"
24  }
25
26
27  aliases = ["cederqvist.dev"]
28
29  default_cache_behavior {
30    allowed_methods  = ["GET", "HEAD"]
31    cached_methods   = ["GET", "HEAD"]
32    target_origin_id = "myWebsiteS3"
33
34    cache_policy_id        = data.aws_cloudfront_cache_policy.policy.id
35    viewer_protocol_policy = "redirect-to-https"
36    min_ttl                = 0
37    default_ttl            = 7200
38    max_ttl                = 86400
39  }
40
41  price_class = "PriceClass_200"
42
43  tags = {
44    Description = "CDN for ${var.domain}"
45  }
46
47}
48
49data "aws_cloudfront_cache_policy" "policy" {
50  id = var.cache_optimized_policy
51}

Route53 records

For route53 records, i did these manually.. as i had lots of records already created and did not have the energy for importing them into terraform, maybe i’ll do this later.

To route the traffic to my CloudFront distribution, i created an aliased A type record pointing to my CloudFront distribution. Also created AAAA record for IPV6, because no reason not to.