Skip to main content

Command Palette

Search for a command to run...

DAY #39: [DAY-14] AWS Terraform: Hosting A Static Website using AWS S3 And Cloudfront

Let’s Learn How to Host a Static Website on AWS with S3 and CloudFront

Updated
13 min read
DAY #39: [DAY-14] AWS Terraform: Hosting A Static Website using AWS S3 And Cloudfront
A
Sharing real-time progress, roadblocks, and open source lessons. Hoping my notes help someone else feel less stuck. "Stop waiting to feel ready, stop waiting for the perfect moment, start messy, start scared and start before you feel qualified"

Introduction

Hello and welcome back!

If you’ve been following the series, you know that yesterday, on Day 13, we spent time exploring Terraform data sources. We saw how Terraform can fetch information from AWS and reuse it, helping us keep our configurations clean, organized, and dynamic.

And today, we finally begin that “something bigger”.

This is Day 14 of 30 Days of AWS Terraform, and until now, we’ve been learning concepts one by one. But from today onward, we’re stepping into real mini-projects.

Onboarding the Power of Terraform | by Aiswarya🦋 | Medium

To start this phase, we’ll tackle a project that many beginners find comforting: hosting a static website using an S3 bucket, with CloudFront in front of it. We may have tried this manually in the AWS console at some point i.e. uploading HTML files to a bucket, enabling static website hosting, maybe even connecting a domain. But doing all of this with Terraform is different, and it shows how we can automate and manage infrastructure efficiently.

Before we dive into code, it’s important to understand what we’re building and why it matters. CloudFront and S3 aren’t just two AWS services placed together, they solve real problems. For example, when our website has visitors from different parts of the world, these services work together to make the website secure, fast, and cost-efficient, all while keeping the S3 bucket private.

Before we dive in, here’s a personal tidbit:

Fact #14: What I love about working in tech is that there’s always something new to learn. Sometimes it can feel a bit intimidating with so much happening all the time, but that’s also what makes it exciting.

And by the end of today’s blog, you’ll have your first real Terraform-powered static website up and running.

Understanding S3 + CloudFront Together

Before we touch any Terraform code, let’s take a moment to understand why we need both S3 and CloudFront and how they work together to make our website fast, secure, and reliable.

Imagine our website has visitors scattered all around the world. Some might be in India, others in the United States, Australia, Latin America, or Europe. Now, if all our website files i.e. like HTML pages, CSS styles, JavaScript, images, and videos are stored in a single S3 bucket, every visitor is trying to reach that same bucket, no matter where they are.

If we serve the website directly from S3, a few challenges arise:

  • Cost: Every time a visitor far from the bucket downloads a file, AWS charges for the data transfer. These charges can add up quickly if our website has many visitors.

  • Performance: Visitors far away will notice slower loading times, because the files have to travel long distances across the internet.

  • Security: To make the website publicly accessible, the S3 bucket must be open to everyone. This can expose our files to attacks and isn’t safe for production use.

This is where CloudFront comes in. We can think of CloudFront as a network of helpers, stationed all around the globe. These helpers are called edge locations. When someone requests a file from our website, CloudFront serves it from the nearest edge location instead of the original S3 bucket.

Here’s what this achieves:

  • Faster delivery: Files are stored temporarily at the edge locations. When someone requests the same file nearby, it is served immediately without going all the way to S3.

  • Lower cost: Since the data travels a shorter distance, transfer fees are smaller.

  • Better security: Users never access the S3 bucket directly. Only CloudFront can fetch files, keeping our bucket private.

Each cached file has a TTL (time to live), usually 24 hours by default. This means that if another user nearby requests the same file within this period, CloudFront serves it from its cache. This reduces load on the S3 bucket and makes the website feel snappy.

By combining S3 and CloudFront in this way, we create a website that is secure, fast, and cost-efficient, no matter where our users are.

Keeping this in mind will help us understand why we create each Terraform resource: the private S3 bucket, origin access control, bucket policies, CloudFront distribution, and finally uploading our static files. Each step in Terraform has a purpose, and together they tell the story of a website that works smoothly for everyone.

Setting Up the S3 Bucket with Terraform

Now that we understand why we’re using S3 and CloudFront together, let’s move into the “how.” We’ll start with the S3 bucket i.e. the heart of our static website. This is a place where all our website files will live: your HTML, CSS, JavaScript, images, and any other static content.

In Terraform, creating this bucket is surprisingly straightforward, but before we rush to the code, let’s pause and understand what we are doing. We want the bucket to have a unique, reusable name so we don’t run into conflicts with other buckets in AWS. Terraform allows us to do this using a variable-based prefix, which is a way of letting us define part of the name dynamically. Here’s what that looks like:

Here’s what’s happening in beginner-friendly terms:

  • aws_s3_bucket tells Terraform, “Hey, I want you to create a new S3 bucket.”

  • bucket_prefix lets us build a unique bucket name by combining a fixed string (day-14-demo-) with whatever value we set in var.bucket_prefix.

  • The magic of using a prefix is that if we run this code multiple times, Terraform can still create new, unique buckets without manual renaming.

Once we have the bucket, we need to make sure it’s private. Even though AWS buckets are private by default, explicitly blocking public access is like double-checking and making sure it is really secure. It prevents anyone from accidentally accessing our buckets. Here’s the Terraform block for that:

Let’s unpack this:

  • block_public_acls = true → Any access control lists (ACLs) that are public will be blocked.

  • block_public_policy = true → No public bucket policies can be applied.

  • ignore_public_acls = true → Existing public ACLs are ignored to avoid accidental exposure.

  • restrict_public_buckets = true → The bucket cannot be made public accidentally in the future.

With the bucket created and secured, we’ve now laid the foundation for a website that’s both safe and ready to serve content efficiently. The next step will be setting up Origin Access Control, which allows CloudFront to fetch files from this private bucket safely

Configuring Origin Access Control and Bucket Policy

After creating and securing our S3 bucket, the next step is to allow CloudFront to access the bucket safely. Because the bucket is private, users cannot directly access it. Only CloudFront should fetch the files. To achieve this, we use Origin Access Control (OAC) and a bucket policy.

1. Origin Access Control (OAC)

Origin Access Control is a configuration in CloudFront that ensures all requests from CloudFront to the S3 bucket are authenticated and secure. Without it, CloudFront cannot read from a private bucket. Terraform allows us to create OAC easily:

Detailed explanation of each field:

  • name
    A descriptive name for the OAC. We can choose any name, but it’s best to keep it clear and consistent.

  • description
    Provides additional context about the purpose of the OAC. Helpful when managing multiple distributions.

  • origin_access_control_origin_type = "s3"
    Specifies that the origin CloudFront will access is an S3 bucket. This is required because the signing and permissions differ depending on the origin type.

  • signing_behavior = "always"
    Ensures every request from CloudFront to the S3 bucket is signed. Signing is required for authentication, allowing CloudFront to access a private bucket.

  • signing_protocol = "sigv4"
    Specifies the AWS signature protocol used to sign requests. SigV4 is the latest secure protocol that encrypts and validates the request.

This configuration guarantees that CloudFront can securely read from the private S3 bucket while keeping the bucket inaccessible to other users or services.

2. S3 Bucket Policy

While OAC enables secure communication, the bucket policy defines explicit permissions on the S3 bucket. It tells AWS: “This CloudFront distribution is allowed to read these files.”

Terraform configuration:

Detailed explanation:

  • bucket
    Specifies the S3 bucket to which the policy applies.

  • depends_on
    Ensures Terraform creates this bucket policy after the public access block and CloudFront distribution are ready. This prevents errors during deployment.

  • policy = jsonencode({...})
    The policy itself, written in JSON, defines who can do what on the bucket.

    • Version
      AWS uses this to determine the policy syntax version.

    • Statement
      Contains the permission rules. Each statement describes one rule.

      • Sid: A unique identifier for the statement.

      • Effect = "Allow": Grants the permission.

      • Principal = { Service = "cloudfront.amazonaws.com" }: Grants access only to CloudFront. No other user or service can read the files.

      • Action = ["s3:GetObject"]: Specifies that CloudFront can read objects (but cannot modify or delete them).

      • Resource = bucket ARN + / *: Applies to all objects in the bucket.

      • Condition = { StringEquals = { "AWS:SourceArn" = CloudFront distribution ARN } }: Ensures only this specific CloudFront distribution can access the bucket.

Together, OAC and the bucket policy make the S3 bucket private, secure, and accessible only to CloudFront. Users will never access the S3 bucket directly, but CloudFront can fetch and cache files efficiently for global access.

Uploading Static Website Files to the S3 Bucket

Now that our S3 bucket is private and CloudFront has its secure key to access it, we can start placing our website files into the bucket.

Our website consists of static files: index.html, CSS for styling, JavaScript for interactivity, and maybe some images or videos. These are the files our users will see and interact with when they visit our site.

Terraform provides a very neat way to do this using the aws_s3_object resource. Here’s what it looks like in code:

Let’s understand this, step by step:

Let’s go slowly, so it’s easy to understand:

  1. for_each = fileset("${path.module}/www", "/*")**

    • This tells Terraform to look in the www folder and find all files and subfolders.

    • Terraform will create an S3 object for each file it finds.

  2. bucket = aws_s3_bucket.day_14_demo_bucket.id

    • This is the S3 bucket we created earlier. Every file will be uploaded here.
  3. key = each.value

    • The “key” is the name or path of the file inside the S3 bucket. This is how CloudFront and users will reference the file.
  4. source = "${path.module}/www/${each.value}"

    • This points to the actual file on our computer, so Terraform knows what to upload.
  5. etag = filemd5("${path.module}/www/${each.value}")

    • Terraform uses this to check if a file has changed. If the file is updated locally, Terraform will upload the new version.
  6. content_type = lookup(...)

    • This tells the browser what type of file it is, such as text/html for HTML, text/css for CSS, or image/png for PNG images.

    • If Terraform doesn’t recognize the file type, it uses a generic type (application/octet-stream).

By using this resource, we ensure that all website files are uploaded correctly, with the right type, and ready for CloudFront to serve.

Once the files are uploaded, our website is ready to be delivered through CloudFront, making it fast, secure, and globally accessible.

Once all the files are safely uploaded, we’ll move to the next step: creating the CloudFront distribution. This is where our website will start becoming fast, globally accessible, and secure.

Creating the CloudFront Distribution

Now that our S3 bucket is ready and all our website files are uploaded, the next step is to make the website fast, secure, and accessible from anywhere in the world. That’s what CloudFront does.

CloudFront is a service that delivers our website to users efficiently. It ensures that:

  • Our private S3 bucket stays private.

  • Files are delivered quickly to users, no matter where they are.

  • Visitors connect securely using HTTPS.

We will use Terraform to create a CloudFront distribution, which is just a configuration that tells CloudFront how to serve our website. Here’s the code we’ll use:

Let’s carefully go through this line by line, so it’s easy to understand:

  1. origin

    • This tells CloudFront which S3 bucket to use.

    • We also provide the Origin Access Control we created earlier. This is necessary so CloudFront can access the bucket safely, while keeping it private from everyone else.

  2. enabled = true

    • This simply turns the CloudFront distribution on. Without this, it won’t serve any content.
  3. is_ipv6_enabled = true

    • This ensures visitors using newer internet addresses (IPv6) can access your website.
  4. default_root_object = "index.html"

    • When someone visits our main website address, CloudFront will serve the index.html page automatically.
  5. default_cache_behavior

    • allowed_methods and cached_methods: Only GET and HEAD requests are allowed. For a static website, this is all we need.

    • forwarded_values: We are not sending cookies or query strings, which makes caching simpler and faster.

    • viewer_protocol_policy = "redirect-to-https": Ensures visitors connect securely using HTTPS.

    • min_ttl, default_ttl, max_ttl: These settings tell CloudFront how long to keep a copy of each file cached at the edge locations. Cached files load faster for users and reduce requests to your S3 bucket.

  6. price_class = "PriceClass_100"

    • Limits which locations CloudFront uses to deliver our website, keeping costs reasonable.
  7. viewer_certificate

    • CloudFront provides a default HTTPS certificate so our site is secure without extra setup.

Once this distribution is active, we can use Terraform outputs to get the website URL, distribution ID, and bucket name for easy reference.

Understanding Terraform Outputs

At this point, we have created:

  • A private S3 bucket containing all our website files

  • A CloudFront distribution to deliver the site globally

Everything is working behind the scenes. But after running Terraform, how do we quickly find the important information we need? For example:

  • What is the website URL?

  • What is the CloudFront distribution ID?

  • What is the S3 bucket name?

We could look through the AWS console to find these, but that can be slow and confusing, especially for beginners.

This is where Terraform outputs help. Outputs are like a summary table that Terraform shows us after deployment. They make it easy to see and use the information you need, without searching.

Here’s how we define them:

1. Website URL

After deployment, this output gives the link to our live website.

  • We can copy this link and open it in a browser.

  • CloudFront serves our website files from here, so we don’t access the S3 bucket directly.

  • This makes it fast, secure, and globally accessible.

2. CloudFront Distribution ID

Every CloudFront distribution has a unique identifier.

  • We may need this ID later if we want to update the distribution or check its settings in AWS.

  • Terraform shows it to us automatically, so we don’t need to search in the console.

For beginners, knowing this ID is helpful because it connects the configuration we wrote in Terraform to the actual CloudFront service in AWS.

3. S3 Bucket Name

This output shows the name of our private S3 bucket.

  • We can use this name if we want to look at the files in the bucket, check permissions, or connect it with other AWS services.

  • Even though the bucket is private, Terraform makes it easy to reference the exact bucket name whenever needed.

By defining these outputs, you have all the critical information in one place after deployment.

  • We don’t need to hunt in the AWS console.

  • We can quickly access the website, manage CloudFront, or check your S3 bucket.

Conclusion

With this, we have completed Day 14 of our 30-day AWS Terraform journey. Today was our first demo session, where we saw an S3 bucket and CloudFront distribution in action.

I know this blog contains many moving parts, and some sections, especially when we look at the code, might feel hard to understand at first. That’s completely normal, and it’s part of the learning process.

One suggestion that usually helps me make sense of things is this:

  1. Read the explanation for a piece of code.

  2. Then go to the actual line of code and try to connect it with the explanation.

  3. Repeat this slowly for each section.

It does take time and effort, but this approach usually helps the concepts “click” and makes the learning much more meaningful.

If some parts still feel confusing, which is completely understandable, here is a video by Piyush Sachdeva that explains everything clearly and walks you through the demo step by step.

Remember, learning cloud infrastructure and Terraform is a gradual process, and every small effort adds up. Keep practicing, and soon these concepts will feel much more natural.

Happy learning!