Terraform Cloud Project Bootcamp with Andrew Brown — 1.5.0 Content Delivery Network

Gwen Leigh
7 min readOct 7, 2023

This article is part of my Terraform journey with Terraform Bootcamp by Andrew Brown and Andrew Bayko with Chris Williams and Shala.

My wild career adventure into Cloud Computing continues with Andrews, and I highly, highly recommend you to come join us if you are interested. Check out Andrews’ free youtube learning contents. You can also buy some paid courses here to support their good cause.

Agenda

Video here: 1 5 0 Content Delivery Network

Issue #31 Goals

  • ✅ 1) Set up CloudFront Distribution
  • ✅ 2) Set up CloudFront OAC (Origin Controls)
  • ✅ 3) Bucket Policy

Workflow

We will be tackling the CDN part through this video session.

1. Refactor ./modules/terrahouse_aws/main.tf by resource types

cd into ./modules/terrahouse_aws then create:

code resource-cdn.tf
code resource-storage.tf

Now move (refactor) the corresponding code blocks in main.tf at root to either resource-cdn.tf or resource-storage.tf depending on their functions. You can find the final codes here:

Once you are done refactoring and the code is tf valid, run terraform apply and the resources will be created as below.

# module.terrahouse_aws.aws_cloudfront_distribution.s3_distribution will be created
+ resource "aws_cloudfront_distribution" "s3_distribution" {
+ arn = (known after apply)
+ caller_reference = (known after apply)
+ comment = "Static website hosting for: nomadiachi-in-terratown-bucket"
+ default_root_object = "index.html"
+ domain_name = (known after apply)
+ enabled = true
+ etag = (known after apply)
+ hosted_zone_id = (known after apply)
+ http_version = "http2"
+ id = (known after apply)
+ in_progress_validation_batches = (known after apply)
+ is_ipv6_enabled = true
+ last_modified_time = (known after apply)
+ price_class = "PriceClass_200"
+ retain_on_delete = false
+ staging = false
+ status = (known after apply)
+ tags = {
+ "UserUuid" = "aa0d742c-..."
}
+ tags_all = {
+ "UserUuid" = "aa0d742c-..."
}
+ trusted_key_groups = (known after apply)
+ trusted_signers = (known after apply)
+ wait_for_deployment = true

+ default_cache_behavior {
+ allowed_methods = [
+ "DELETE",
+ "GET",
+ "HEAD",
+ "OPTIONS",
+ "PATCH",
+ "POST",
+ "PUT",
]
+ cached_methods = [
+ "GET",
+ "HEAD",
]
+ compress = false
+ default_ttl = 3600
+ max_ttl = 86400
+ min_ttl = 0
+ target_origin_id = "MyS3Origin"
+ trusted_key_groups = (known after apply)
+ trusted_signers = (known after apply)
+ viewer_protocol_policy = "allow-all"

+ forwarded_values {
+ headers = (known after apply)
+ query_string = false
+ query_string_cache_keys = (known after apply)

+ cookies {
+ forward = "none"
+ whitelisted_names = (known after apply)
}
}
}

+ origin {
+ connection_attempts = 3
+ connection_timeout = 10
+ domain_name = (known after apply)
+ origin_access_control_id = (known after apply)
+ origin_id = "MyS3Origin"
}

+ viewer_certificate {
+ cloudfront_default_certificate = true
+ minimum_protocol_version = "TLSv1"
}
}

# module.terrahouse_aws.aws_cloudfront_origin_access_control.default will be created
+ resource "aws_cloudfront_origin_access_control" "default" {
+ description = "Origin Access Controls for Static Website Hosting nomadiachi-in-terratown-bucket"
+ etag = (known after apply)
+ id = (known after apply)
+ name = "OAC nomadiachi-in-terratown-bucket"
+ origin_access_control_origin_type = "s3"
+ signing_behavior = "always"
+ signing_protocol = "sigv4"
}

# module.terrahouse_aws.aws_s3_bucket.website_bucket will be created
+ resource "aws_s3_bucket" "website_bucket" {
+ acceleration_status = (known after apply)
+ acl = (known after apply)
+ arn = (known after apply)
+ bucket = "nomadiachi-in-terratown-bucket"
+ bucket_domain_name = (known after apply)
+ bucket_prefix = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ object_lock_enabled = (known after apply)
+ policy = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ tags = {
+ "UserUuid" = "aa0d742c-..."
}
+ tags_all = {
+ "UserUuid" = "aa0d742c-..."
}
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
}

# module.terrahouse_aws.aws_s3_bucket_policy.bucket_policy will be created
+ resource "aws_s3_bucket_policy" "bucket_policy" {
+ bucket = "nomadiachi-in-terratown-bucket"
+ id = (known after apply)
+ policy = (known after apply)
}

# module.terrahouse_aws.aws_s3_bucket_website_configuration.website_configuration will be created
+ resource "aws_s3_bucket_website_configuration" "website_configuration" {
+ bucket = "nomadiachi-in-terratown-bucket"
+ id = (known after apply)
+ routing_rules = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)

+ error_document {
+ key = "error.html"
}

+ index_document {
+ suffix = "index.html"
}
}

# module.terrahouse_aws.aws_s3_object.error_html will be created
+ resource "aws_s3_object" "error_html" {
+ acl = (known after apply)
+ bucket = "nomadiachi-in-terratown-bucket"
+ bucket_key_enabled = (known after apply)
+ content_type = (known after apply)
+ etag = "2b96c2762607896d07b95d7eba4ac39e"
+ force_destroy = false
+ id = (known after apply)
+ key = "error.html"
+ kms_key_id = (known after apply)
+ server_side_encryption = (known after apply)
+ source = "./public/error.html"
+ storage_class = (known after apply)
+ tags_all = (known after apply)
+ version_id = (known after apply)
}

# module.terrahouse_aws.aws_s3_object.index_html will be created
+ resource "aws_s3_object" "index_html" {
+ acl = (known after apply)
+ bucket = "nomadiachi-in-terratown-bucket"
+ bucket_key_enabled = (known after apply)
+ content_type = (known after apply)
+ etag = "831e8b36c4ac8c46618a89886088b7c4"
+ force_destroy = false
+ id = (known after apply)
+ key = "index.html"
+ kms_key_id = (known after apply)
+ server_side_encryption = (known after apply)
+ source = "/workspace/terraform-beginner-bootcamp-2023/public/index.html"
+ storage_class = (known after apply)
+ tags_all = (known after apply)
+ version_id = (known after apply)
}

Plan: 7 to add, 0 to change, 0 to destroy.

Troubleshooting

Cache Invalidation

In the video, Andrew’s CF distribution works but the browser downloads the index.html file instead of displaying the content. He tries clearing the cache. This may be due to the missing content_type configuration. Andrew explicitly defines it to be "text/html" then clears the CF distribution caches manually on AWS CloudFront console (it is called ‘invalidate cache’ in CF).

To invalidate cache, navigate to: CloudFront > specific Distribution > Invalidation tab, then > “Create invalidation”.

Dash + asterisk will invalidate everything.

However, even after the cache invalidation, Andrew’s CF distribution would still download the files from the bucket!

Andrew’s solution was making some more changes to the html files so terraform picks up those changes and reapplies the change set. Then he invalidated cache and then tried accessing the distribution and it worked!

Access Denied

In my case, on the other hand, I kept running into the access denied issue. This may be because the restrictions configuration in ./modules/terrahouse_aws/resource-cdn.tf so I removed it.

Access issue
Remove the restrictions
Terraform change set applying.
Even after removing the restrictions, I still cannot access the page.

But I would still run into the same issue. It turns out, the actual culprit was the bucket_policy configuration. As we are being denied access, the logical assumption is that something should be wrong with the bucket access configuration. I compared my code with Andrew’s, and realised that the CloudFront distribution ID was missing in my resource-storage.tf configuration.

resource "aws_s3_bucket_policy" "bucket_policy" {
bucket = aws_s3_bucket.website_bucket.bucket
...
"Condition" = {
"StringEquals" = {
"AWS:SourceArn": "arn:aws:cloudfront::
${data.aws_caller_identity.current.account_id}:distribution/
${aws_cloudfront_distribution.s3_distribution.id}" # <- add this.
}
}
}
})
}

Notes

Data sources

The data source data.aws_iam_policy_document.allow_access_from_another_account below is a good example of how data source works. It is not referring to any local file but it is grabbing the data on the server through an API call. This, in turn, means that we will have to configure the bucket policy if we want to use this data source, as it interacts with the bucket on AWS cloud and to do so requires permissions.

resource "aws_s3_bucket_policy" "bucket_policy" {
bucket = aws_s3_bucket.website_bucket.bucket
# policy = data.aws_iam_policy_document.allow_access_from_another_account.json
policy = jsonencode({

--

--

Gwen Leigh

Cloud Engineer to be. Actively building my profile, network and experience in the cloud space. .