In this 3rd article, I will continue from the second article and show you how to deploy my blokaly.com website built with Hugo on to AWS, using HTTPS with CloudFront .

aws-route53-cloudfront-s3

For prerequisites and environment setup, please refer to my previous article:

Build

  1. First, we need to amend our S3 bucket configuration, making it private and only accessible from CloudFront
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#------------------------------------------------------------------------------  
# S3 bucket to host all website files.  
#------------------------------------------------------------------------------  
  
resource "aws_s3_bucket" "www_bucket" {  
  bucket        = "www.${var.bucket_name}"  
  force_destroy = true  
}  
  
resource "aws_s3_bucket_versioning" "www_bucket_ver" {  
  bucket = aws_s3_bucket.www_bucket.id  
  versioning_configuration {  
    status = "Disabled"  
  }  
}  
  
resource "aws_s3_bucket_acl" "www_bucket_acl" {  
  bucket = aws_s3_bucket.www_bucket.id  
  acl    = "private"  
}
  1. Next, we will create a SSL certificate and validate the certificate
 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
#------------------------------------------------------------------------------  
# Create SSL certificate and route53 records for validation 
# Then validate the certificate  
#------------------------------------------------------------------------------  
resource "aws_route53_zone" "main" {  
  name = var.domain_name  
}  
  
resource "aws_acm_certificate" "ssl_certificate" {  
  provider    = aws.acm_provider  
  domain_name = var.domain_name  
  
  # DNS validation requires the domain nameservers to already be pointing to AWS  
  validation_method         = "DNS"  
  subject_alternative_names = ["*.${var.domain_name}"]  
  
  lifecycle {  
    create_before_destroy = true  
  }  
}  
  
resource "aws_route53_record" "cert_validation" {  
  for_each = {  
    for dvo in aws_acm_certificate.ssl_certificate.domain_validation_options : dvo.domain_name => {  
      name   = dvo.resource_record_name  
      record = dvo.resource_record_value  
      type   = dvo.resource_record_type  
    }  
  }  
  allow_overwrite = true  
  name            = each.value.name  
  records         = [each.value.record]  
  ttl             = 60  
  type            = each.value.type  
  zone_id         = aws_route53_zone.main.id  
}  
  
resource "aws_acm_certificate_validation" "ssl_certificate_validation" {  
  provider                = aws.acm_provider  
  certificate_arn         = aws_acm_certificate.ssl_certificate.arn  
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]  
}
  1. We now configure the CloudFront for the main website and also automatically redirect the http traffics to https
 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#------------------------------------------------------------------------------  
# Cloudfront distribution for main www s3 site. 
# HTTP requests automatically redirected to HTTPS.  
#------------------------------------------------------------------------------  
resource "aws_cloudfront_origin_access_identity" "cloudfront_oai" {  
  comment = "S3-www.${var.bucket_name}"  
}  
  
resource "aws_cloudfront_distribution" "s3_distribution" {  
  provider = aws.acm_provider  
  
  origin {  
    domain_name = aws_s3_bucket.www_bucket.bucket_regional_domain_name  
    origin_id   = "S3-www.${var.bucket_name}"  
  
    s3_origin_config {  
      origin_access_identity = aws_cloudfront_origin_access_identity.cloudfront_oai.cloudfront_access_identity_path  
    }  
  }  
  enabled             = true  
  is_ipv6_enabled     = true  
  default_root_object = "index.html"  
  price_class         = "PriceClass_200"  
  wait_for_deployment = false  
  
  aliases = [var.domain_name, "www.${var.domain_name}"]  
  
  custom_error_response {  
    error_caching_min_ttl = 0  
    error_code            = 403  
    response_code         = 404  
    response_page_path    = "/404.html"  
  }  
  custom_error_response {  
    error_caching_min_ttl = 0  
    error_code            = 404  
    response_code         = 404  
    response_page_path    = "/404.html"  
  }  
  
  # Default cache behaviour  
  default_cache_behavior {  
    allowed_methods  = ["GET", "HEAD"]  
    cached_methods   = ["GET", "HEAD"]  
    target_origin_id = "S3-www.${var.bucket_name}"  
  
    forwarded_values {  
      query_string = false  
  
      cookies {  
        forward = "none"  
      }  
    }  
    viewer_protocol_policy = "redirect-to-https"  
    min_ttl                = 0  
    default_ttl            = 86400  
    max_ttl                = 31536000  
    compress               = true  
  
    function_association {  
      event_type   = "viewer-request"  
      function_arn = aws_cloudfront_function.viewer_request.arn  
    }  
  
    function_association {  
      event_type   = "viewer-response"  
      function_arn = aws_cloudfront_function.viewer_response.arn  
    }  
  }  
  restrictions {  
    geo_restriction {  
      restriction_type = "none"  
    }  
  }  
  viewer_certificate {  
    acm_certificate_arn      = aws_acm_certificate_validation.ssl_certificate_validation.certificate_arn  
    ssl_support_method       = "sni-only"  
    minimum_protocol_version = "TLSv1.1_2016"  
  }  
  
}

And provide permissions to allow the CloudFront to access the S3 bucket

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
resource "aws_s3_bucket_policy" "bucket-www_bucket_policy" {  
  bucket = aws_s3_bucket.www_bucket.id  
  policy = data.aws_iam_policy_document.iam-policy-www.json  
}  
  
data "aws_iam_policy_document" "iam-policy-www" {  
  statement {  
    sid       = "AllowCloudFront"  
    effect    = "Allow"  
    resources = ["${aws_s3_bucket.www_bucket.arn}/*"]  
    actions   = ["S3:GetObject"]  
    principals {  
      type        = "AWS"  
      identifiers = [aws_cloudfront_origin_access_identity.cloudfront_oai.iam_arn]  
    }  
  }
}
  1. There is one last thing we have to add before we can deploy our stack to AWS. It’s a CloudFront edge function to modify the requests, so if the path ended with a / or missing a file extension, then we automatically append the index.html or /index.html to the path. So for example, if the url path is https://www.blokaly.com/en/posts/2023-03/ntqxmqo/, then the function will convert it to https://www.blokaly.com/en/posts/2023-03/ntqxmqo/index.html to make sure AWS can retrieve the file from S3 bucket, otherwise you will see the 404 error.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
resource "aws_cloudfront_function" "viewer_request" {  
  name    = "cdn-viewer-request"  
  runtime = "cloudfront-js-1.0"  
  publish = true  
  code    = <<EOT  
  function handler(event) {  
    var request = event.request;  
    var uri = request.uri;  
  
    // Check whether the URI is missing a file name.  
    if (uri.endsWith('/')) {  
        request.uri += 'index.html';  
    }  
    // Check whether the URI is missing a file extension.  
    else if (!uri.includes('.')) {  
        request.uri += '/index.html';  
    }  
  
    return request;  
  }  
  EOT  
}
  1. Finally, we can output the CloudFront id, arn and domain name to the console
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
output "cf_id" {  
  value       = try(aws_cloudfront_distribution.s3_distribution.id, "")  
  description = "ID of CloudFront distribution"  
}  
  
output "cf_arn" {  
  value       = try(aws_cloudfront_distribution.s3_distribution.arn, "")  
  description = "ARN of CloudFront distribution"  
}  
  
output "cf_domain_name" {  
  value       = try(aws_cloudfront_distribution.s3_distribution.domain_name, "")  
  description = "Domain name corresponding to the distribution"  
}

After you successfully deploy the whole stack onto AWS, then you can build your hugo files locally:

1
$ hugo 

Then upload the generated files onto S3 bucket (assume file generated under docs folder):

1
$ aws s3 sync docs s3://<your domain name> --delete

Every time after you published new files to S3 bucket, you need to invalidate the CloudFront cache, so the latest files will be fetched from S3 bucket directly, rather than serving from the cache:

1
$ aws cloudfront create-invalidation --distribution-id <your cloudfront distribution id> --paths "/*" --no-cli-pager

This is the end of my 3-part articles about building my personal hugo blog site with AWS. Hope all these make sense to you.

References