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
.
For prerequisites and environment setup, please refer to my previous article:
Build#
- 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"
}
|
- 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]
}
|
- 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]
}
}
}
|
- 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
}
|
- 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:
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#