Deploy a Website to AWS with Terraform

Use Terraform to provision S3 and CloudFront for a live website on AWS.

Introduction

30 Second Summary

Every time you update a website and need to manually click through a cloud console to redeploy it, you risk forgetting a step or misconfiguring something. What if your entire hosting infrastructure was defined in code, and deploying it was as simple as running one command?

In this project, you will write Terraform configuration files that automatically provision an AWS S3 bucket and a CloudFront CDN to host your physiotherapy website on production-grade cloud infrastructure.

What You'll Build

A fully automated Infrastructure as Code deployment that provisions an S3 bucket for storage, a CloudFront distribution for fast global delivery, and the security policies connecting them.

By the end of this project, you'll have:

  • A complete Terraform project with main.tf, variables.tf, and outputs.tf that define your cloud infrastructure.
  • A live website served globally through an AWS CloudFront CDN URL with HTTPS enabled.
  • The ability to tear down and rebuild your entire hosting infrastructure with a single command.
  • Secret Mission: Add a GitHub Actions CI/CD pipeline that auto-deploys your website whenever you push changes.

Are there any prerequisites?

You should have basic coding knowledge and familiarity with using a terminal or command line. You will also need your existing physiotherapy website files (HTML, CSS, and JS with a Supabase backend) ready to deploy.

Before We Start

Before We Start...

Set Up Your AWS Account and Install Tools

Before you can deploy your physiotherapy website to the cloud, you need to set up access to AWS and install the tools that let you manage cloud infrastructure from your terminal.

In this step, you will create an AWS account, set up credentials for programmatic access, and install two command-line tools: the AWS CLI and Terraform.

In this step, get ready to:
  • Create an AWS account and set up an IAM user with programmatic access.
  • Install and configure the AWS CLI v2.
  • Install Terraform 1.15.6 and add it to your system PATH.
Create an AWS account and IAM user

AWS provides a free tier for new accounts that covers everything you need for this project at no cost.

  • Open https://aws.amazon.com/free in your browser and click Create a Free Account.
  • Follow the sign-up steps to create your account (you will need an email address, phone number, and a credit or debit card for identity verification).
  • Once your account is active, sign in to the AWS Management Console.

Now you will create an IAM user. IAM is how AWS manages permissions for who can do what in your account.

  • In the console search bar at the top, type IAM and select the IAM service.
  • In the left navigation pane, click Users.
  • Click Create user.
  • On the Specify user details page, enter a user name (e.g., terraform-deployer) and click Next.
  • On the Set permissions page, select Attach policies directly.
  • Search for AdministratorAccess and check the box next to it.
  • Click Next, then click Create user.

Your user is created. Now generate an access key so the AWS CLI can authenticate as this user.

  • Click View user to open the user details page.
  • Select the Security credentials tab.
  • Click Create access key.
  • On the Access key best practices & alternatives page, select Other and click Next.
  • Click Create access key.
  • On the Retrieve access key page, click Download .csv file to save your Access Key ID and Secret Access Key securely.

Save your Secret Access Key now

This is the only time AWS will show you the Secret Access Key. If you lose it, you will need to create a new access key. Save it somewhere secure and never share it publicly.

What is AdministratorAccess?

The AdministratorAccess policy gives your IAM user full permissions to create and manage any AWS resource. For a learning project, this is the simplest way to get started.

In production, you would use a more restrictive policy that only grants the specific permissions needed.

Install and configure the AWS CLI

The AWS CLI lets you interact with AWS services directly from your terminal. You need version 2 for this project.

  • Check if you already have the AWS CLI installed by running this command in your terminal:
aws --version

✔️ I see aws-cli/2.x.x

Your AWS CLI v2 is ready. You can skip to the configuration step below.

ⓧ I see aws-cli/1.x.x

You have version 1 installed, which is outdated for this project. Install version 2 alongside it.

  • Download the AWS CLI v2 installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
  • Run the downloaded AWSCLIV2.msi file and follow the installer prompts (accept all defaults).
  • Close and reopen your terminal, then run aws --version again to confirm it now shows aws-cli/2.x.x.

ⓧ Command not found

The AWS CLI is not installed yet. Follow these steps to install it.

  • Download the AWS CLI v2 installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
  • Run the downloaded AWSCLIV2.msi file and follow the installer prompts (accept all defaults).
  • Close and reopen your terminal, then run aws --version to confirm installation.

You should see output similar to aws-cli/2.27.41 Python/3.11.6 Windows/10 exe/AMD64.

Now configure the AWS CLI with the credentials you saved earlier.

  • Configure the AWS CLI by running this command:
aws configure

What will this command ask?

The aws configure command prompts you for four values. Enter them one at a time as each prompt appears:

  • AWS Access Key ID: paste the Access Key ID from your downloaded CSV file.
  • AWS Secret Access Key: paste the Secret Access Key from your downloaded CSV file.
  • Default region name: type us-east-1.
  • Default output format: type json.
  • After entering all four values, verify the configuration by running:
aws --version

If it returns a version string without errors, your AWS CLI is correctly configured.

Seeing an error about credentials?

Make sure you pasted the Access Key ID and Secret Access Key exactly as they appear in your downloaded CSV. Trailing spaces or missing characters will cause authentication failures.

If you accidentally closed the key page without saving, go back to the IAM console, delete the old access key, and create a new one.

Help me troubleshoot my AWS CLI configuration.

Install Terraform

Terraform is the Infrastructure as Code tool you will use to define and deploy your AWS resources. This project uses version 1.15.6.

  • Check if you already have Terraform installed by running this command:
terraform --version

✔️ I see Terraform v1.15.6 or higher

Your Terraform installation is ready. You can skip ahead to the next step.

ⓧ I see an older version

You have an older version of Terraform installed. Download the latest version to replace it.

  • Go to the Terraform install page at https://developer.hashicorp.com/terraform/install.
  • Download the Windows AMD64 zip file.
  • Extract terraform.exe from the zip and copy it into your existing Terraform folder (e.g., C:\terraform), replacing the old file.
  • Close and reopen your terminal, then run terraform --version to confirm it shows Terraform v1.15.6.

ⓧ Command not found

Terraform is not installed yet. Follow these steps to install it.

Now add C:\terraform to your system PATH so you can run terraform from any terminal location.

  • Press the Windows key, type Edit the system environment variables, and press Enter.
  • In the System Properties window, click Environment Variables....
  • In the System variables section, select the row named Path and click Edit.
  • Click New and type C:\terraform.
  • Click OK on all open dialogs to save your changes.
  • Close and reopen your terminal.
  • Verify the installation by running:
terraform --version

You should see Terraform v1.15.6 in the output.

Why add to your system PATH?

The PATH tells Windows where to find executable files. Without adding C:\terraform to your PATH, you would need to type the full file path every time you run a Terraform command.

By adding it to PATH, you can simply type terraform from any directory.

Still seeing 'command not found' after adding to PATH?

Make sure you closed and reopened your terminal after editing the PATH. Already-open terminal windows do not pick up PATH changes.

Double-check that terraform.exe is inside C:\terraform (not in a subfolder like C:\terraform\terraform_1.15.6).

Help me fix my Terraform PATH setup on Windows.

Your toolchain is ready. Next up, you will write your first Terraform configuration files to define the AWS infrastructure for your website.

Write Your Terraform Configuration

Your AWS CLI and Terraform are installed and configured. Now it's time to define your infrastructure in code.

Terraform reads .tf files to understand what cloud resources you want. You'll write three files that tell Terraform exactly how to set up an S3 bucket for your website.

In this step, get ready to:
  • Create your Terraform project folder and configuration files.
  • Configure the AWS provider and define an S3 bucket resource.
  • Initialize Terraform and preview your infrastructure plan.
Create your project folder and configure the AWS provider

Your Terraform project lives in its own folder, separate from your website files. This folder will contain only Infrastructure as Code configuration.

  • Switch back to your terminal in VS Code by pressing Ctrl+` (backtick key, top-left of your keyboard).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir physio-website-infra
cd physio-website-infra
  • Open this folder in VS Code by running:
code .

What does this command do?

The code . command opens the current folder in a new VS Code window. The dot (.) means "this directory."

  • VS Code may ask if you trust the authors of this folder. Click Yes, I trust the authors.
  • Create the three Terraform files by right-clicking in the empty Explorer sidebar on the left.
  • Click New File and name it main.tf.
  • Repeat to create variables.tf.
  • Repeat to create outputs.tf.
  • Confirm the files exist by checking the Explorer sidebar. You should see all three files listed: main.tf, variables.tf, and outputs.tf.

Don't see the Explorer sidebar?

Press Ctrl+Shift+E (Windows) or Cmd+Shift+E (macOS) to toggle it open.

If VS Code opened your home directory instead of physio-website-infra, click File > Open Folder and navigate to Desktop/physio-website-infra.

Help me open my project folder in VS Code.

Now let's write the Terraform configuration. The main.tf file is where you define your provider (which cloud platform to use) and the resources you want to create.

  • Open main.tf by clicking on it in the Explorer sidebar, then paste the following code:
# Tell Terraform which provider plugin to download
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.5.0"
}

# Configure the AWS provider with your region
provider "aws" {
  region = var.aws_region
}

# Create an S3 bucket to store your website files
resource "aws_s3_bucket" "website" {
  bucket = var.bucket_name
}

What does this code do?

The terraform block tells Terraform to download the AWS provider plugin (version 5.x) so it knows how to talk to AWS.

The provider block configures that plugin to work in your chosen region. The var.aws_region reference pulls the value from a variable you'll define next.

The aws_s3_bucket resource creates an S3 bucket with the name you'll provide. Think of it as a folder in the cloud that holds your website files.

Your main.tf references two variables: aws_region and bucket_name. You need to define these in variables.tf so Terraform knows what values to expect.

  • Open variables.tf and paste the following code:
# Define the AWS region variable with a default value
variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "us-east-1"
}

# Define the bucket name variable (no default - you'll provide this)
variable "bucket_name" {
  description = "Name of the S3 bucket for website hosting"
  type        = string
}

What does this code do?

Variables make your Terraform code reusable. The aws_region variable defaults to us-east-1 (so you don't have to type it every time).

The bucket_name variable has no default. Terraform will prompt you to enter a unique name when you run it. S3 bucket names must be globally unique across all of AWS.

  • Save both files. Press Ctrl+S (Windows) or Cmd+S (macOS) in each tab.

✔️ Awesome, I've got everything!

Great. Double check you've saved variables.tf before moving on.

ⓧ I'd like to double check the full code

Here's what variables.tf should look like:

variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "us-east-1"
}

variable "bucket_name" {
  description = "Name of the S3 bucket for website hosting"
  type        = string
}
Add the website hosting configuration

Your S3 bucket exists, but AWS doesn't know it should serve files as a website yet. You need a separate resource that enables static website hosting on the bucket.

  • In main.tf, place your cursor after the closing } of the aws_s3_bucket resource. Add a blank line, then paste this code below it:
# Configure the bucket to serve files as a website
resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "index.html"
  }
}

What does this code do?

This resource tells AWS to treat your bucket as a web server. The index_document block sets which file to load when someone visits the root URL.

The error_document block handles missing pages. Setting it to index.html ensures your single-page app routes work correctly instead of showing a generic AWS error page.

Notice how bucket = aws_s3_bucket.website.id references the bucket resource you created above. Terraform uses these references to understand the order it needs to create things.

Finally, let's set up outputs.tf so Terraform prints useful information after it creates your resources.

  • Open outputs.tf and paste this code:
# Print the CloudFront URL after deployment (used in Step 3)
output "cloudfront_domain_name" {
  description = "The CloudFront distribution domain name (your website URL)"
  value       = aws_cloudfront_distribution.website.domain_name
}

# Print the distribution ID (needed for cache invalidation)
output "cloudfront_distribution_id" {
  description = "The CloudFront distribution ID (needed for cache invalidation)"
  value       = aws_cloudfront_distribution.website.id
}

# Print the bucket name for reference
output "s3_bucket_name" {
  description = "The S3 bucket name"
  value       = aws_s3_bucket.website.id
}

What does this code do?

Outputs are like return values. After Terraform finishes creating your infrastructure, it prints these values so you can easily find your website URL and resource IDs.

The first two outputs reference a CloudFront resource you haven't created yet. That's fine. You'll add the CloudFront distribution in the next step. Terraform will resolve these references once all the resources exist.

  • Save all three files. Press Ctrl+S (Windows) or Cmd+S (macOS) in each open tab.

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before moving on.

ⓧ I'd like to double check the full code

Here's what main.tf should look like at this point:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.5.0"
}

provider "aws" {
  region = var.aws_region
}

resource "aws_s3_bucket" "website" {
  bucket = var.bucket_name
}

resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "index.html"
  }
}

Here's what outputs.tf should look like:

output "cloudfront_domain_name" {
  description = "The CloudFront distribution domain name (your website URL)"
  value       = aws_cloudfront_distribution.website.domain_name
}

output "cloudfront_distribution_id" {
  description = "The CloudFront distribution ID (needed for cache invalidation)"
  value       = aws_cloudfront_distribution.website.id
}

output "s3_bucket_name" {
  description = "The S3 bucket name"
  value       = aws_s3_bucket.website.id
}
Initialize Terraform and preview your plan

Your configuration files are written. Before Terraform can do anything, it needs to download the AWS provider plugin you specified. This is what terraform init does.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (Windows) or Cmd+` (macOS).
  • Make sure you're inside the physio-website-infra folder. The terminal prompt should show this folder name.
  • Initialize your Terraform project by running:
terraform init
  • You should see output ending with Terraform has been successfully initialized! and a note about the .terraform.lock.hcl dependency lock file.

Seeing an error during init?

  • If you see "Could not load plugin": check your internet connection. Terraform needs to download the AWS provider from the registry.
  • If you see "terraform: command not found": make sure the folder containing terraform.exe (e.g. C:\terraform) is on your system PATH. Close and reopen your terminal after adding it.
  • If you see a syntax error: double check your main.tf and variables.tf against the code above. Missing braces or quotes are the most common culprit.

Help me fix a terraform init error.

Now let's preview what Terraform plans to create. The terraform plan command reads your configuration and shows you exactly what it will do, without actually creating anything.

  • Preview your infrastructure plan by running:
terraform plan
  • Terraform will prompt you for a value for bucket_name (since it has no default). Enter a globally unique name like physio-website-your-name (all lowercase, hyphens only).
  • At the bottom of the output, look for a summary line that says Plan: 3 to add, 0 to change, 0 to destroy. This confirms Terraform found your resources and knows what to create.

Why 3 resources?

Terraform counts the S3 bucket, the website configuration, and the bucket policy (which it derives from your configuration) as separate resources. Each one maps to a distinct AWS API call.

Seeing errors in the plan output?

  • Warnings about the CloudFront outputs are expected at this stage. You haven't created the CloudFront resource yet. Terraform may show these as warnings rather than errors.
  • If you see "No valid credential sources found": run aws configure again and confirm your Access Key ID and Secret Access Key are correct.
  • If you see a syntax error with a line number: open the referenced file and check that line for missing quotes, braces, or typos.

Help me fix a terraform plan error.

Your Terraform configuration is written and validated. Next up, you'll add a CloudFront CDN distribution and deploy everything to AWS for real.

Add CloudFront CDN and Deploy Infrastructure

In the previous step, you wrote Terraform configuration for your S3 bucket and verified the plan. Now it's time to add the real power: a CloudFront CDN that delivers your website globally with HTTPS.

Your S3 bucket stores the files, but it can't serve them securely or quickly on its own. CloudFront sits in front of S3, caching your website at edge locations around the world and providing an HTTPS URL. You'll also add a security policy so that only CloudFront can read your bucket's contents.

In this step, get ready to:
  • Add CloudFront CDN and security resources to your Terraform configuration.
  • Deploy all infrastructure to AWS with terraform apply.
  • Get your live CloudFront distribution URL.
Add the CloudFront CDN resources to your configuration

CloudFront needs two supporting resources: an Origin Access Control (OAC) that tells CloudFront how to authenticate with S3, and a bucket policy that grants CloudFront permission to read your files.

  • In VS Code, open main.tf and scroll to the bottom of the file (after the aws_s3_bucket_website_configuration resource).
  • Add the Origin Access Control and CloudFront distribution resources by pasting this code at the end of the file:
resource "aws_cloudfront_origin_access_control" "website" {
  name                              = "${var.bucket_name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "website" {
  origin {
    domain_name              = aws_s3_bucket.website.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.website.id
    origin_id                = "s3-${var.bucket_name}"
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  comment             = "Physiotherapy website CDN"

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "s3-${var.bucket_name}"

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  price_class = "PriceClass_100"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

What does this code do?

The aws_cloudfront_origin_access_control resource creates a security mechanism that signs every request CloudFront makes to your S3 bucket using the sigv4 protocol. This ensures only CloudFront can fetch your files.

The aws_cloudfront_distribution resource is the CDN itself. It points to your S3 bucket as the origin, serves index.html by default, redirects all HTTP traffic to HTTPS, and uses PriceClass_100 (edge locations in the US, Canada, and Europe) to keep costs low.

  • Save main.tf.
  • You won't see a change yet. The next chunk adds the bucket policy that completes the security connection.
  • Still in main.tf, add the S3 bucket policy resource at the very end of the file (below the CloudFront distribution):
resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "AllowCloudFrontServicePrincipal"
        Effect    = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.website.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.website.arn
          }
        }
      }
    ]
  })
}

What does this bucket policy do?

This policy tells S3: only allow s3:GetObject requests that come from the cloudfront.amazonaws.com service principal. The Condition block further restricts it to requests from your specific CloudFront distribution ARN.

Without this policy, nobody (including CloudFront) could read objects from your private bucket. With it, only your CDN has access.

  • Save main.tf.

Seeing red squiggles in VS Code?

If VS Code shows syntax highlighting errors in your .tf file, make sure you have matching opening and closing braces for every block. HCL uses curly braces {} and square brackets [] that must be balanced.

help me find the syntax error in my main.tf file

✔️ Awesome, I've got everything!

Great. Double check you've saved main.tf before moving on.

ⓧ I'd like to double check the full code

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.5.0"
}

provider "aws" {
  region = var.aws_region
}

resource "aws_s3_bucket" "website" {
  bucket = var.bucket_name
}

resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "index.html"
  }
}

resource "aws_cloudfront_origin_access_control" "website" {
  name                              = "${var.bucket_name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "website" {
  origin {
    domain_name              = aws_s3_bucket.website.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.website.id
    origin_id                = "s3-${var.bucket_name}"
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  comment             = "Physiotherapy website CDN"

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "s3-${var.bucket_name}"

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  price_class = "PriceClass_100"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "AllowCloudFrontServicePrincipal"
        Effect    = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.website.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.website.arn
          }
        }
      }
    ]
  })
}
Deploy all resources with terraform apply

Your configuration is complete. Time to create the real AWS resources.

  • Deploy your infrastructure by running this command in your terminal:
terraform apply

What happens when you run this?

Terraform shows you a plan of all resources it will create (S3 bucket, CloudFront OAC, CloudFront distribution, and bucket policy). It then asks you to confirm by typing yes.

After you confirm, Terraform makes API calls to AWS to provision each resource. CloudFront distributions can take 15-20 minutes to fully deploy across all edge locations globally. Be patient and let the command finish.

  • Review the plan Terraform shows you. Confirm it lists resources to add (you should see 4-5 resources).
  • Type yes when prompted and press Enter to start the deployment.
  • Wait for the apply to complete. You'll see Terraform creating each resource one by one.

Seeing an error during apply?

If you see an access denied error, check that your AWS CLI credentials have the AdministratorAccess policy attached (from Step 1).

If you see a syntax error, go back to main.tf and check for mismatched braces or missing quotes. Use the full-code double-check tab above to compare.

help me fix my terraform apply error

Note your CloudFront distribution URL

Once terraform apply finishes successfully, Terraform prints your output values in the terminal.

  • Find the cloudfront_domain_name value in the Terraform output. It looks like d1234abcdef.cloudfront.net.
  • Copy this URL and save it somewhere. This is where your website will be accessible in the next step.
  • Also note the cloudfront_distribution_id value. You'll need this if you set up the Secret Mission later.

What are these outputs?

The cloudfront_domain_name is the public URL where your website will be served. CloudFront gives you a free HTTPS-enabled domain automatically.

The cloudfront_distribution_id is an identifier you'll use to invalidate cached files when you update your website content.

Your infrastructure is live on AWS. Next up, you'll upload your website files and see the physiotherapy site running on your new CloudFront URL.

Upload Your Website Files and Verify

Your AWS infrastructure is provisioned and ready to go. The S3 bucket and CloudFront distribution are live, but the bucket is still empty.

In this step, you'll upload your physiotherapy website files to S3, confirm everything loads correctly at your CloudFront URL, and push your Terraform code to GitHub as a portfolio piece.

In this step, get ready to:
  • Upload your website files to S3 using the AWS CLI.
  • Verify your site loads at the CloudFront URL.
  • Push your Terraform code to a GitHub repository.
Sync your website files to S3

Your CloudFront CDN is pointed at your S3 bucket, but there are no files to serve yet. You need to upload your physiotherapy website files into the bucket.

  • Return to your terminal in VS Code (which should still be in the physio-website-infra folder).
  • Retrieve your S3 bucket name by running:
terraform output s3_bucket_name
  • Note down the bucket name that appears in your terminal output.
  • Sync your website files to the S3 bucket by running this command:
aws s3 sync [[WEBSITE_FOLDER="path/to/your/website/folder"]] s3://[[S3_BUCKET="your-bucket-name"]] --delete

What does this command do?

The aws s3 sync command uploads all files from your local website folder to your S3 bucket. It compares what is already in S3 with your local files and only uploads what has changed.

The --delete flag removes any files from S3 that no longer exist locally. This keeps your bucket perfectly in sync with your local folder.

  • You should see output listing each file being uploaded (e.g. upload: ./index.html to s3://your-bucket/index.html).

Seeing an access denied error?

Make sure the bucket name in your command matches the exact bucket name from terraform output s3_bucket_name. Bucket names are case-sensitive.

If you see a "NoSuchBucket" error, double check that terraform apply completed successfully in the previous step.

help me fix an access denied error when running aws s3 sync

Verify the site at your CloudFront URL

Your files are now in S3. CloudFront is configured to serve them globally over HTTPS. Time to see your site live.

  • Retrieve your CloudFront domain name by running:
terraform output cloudfront_domain_name
  • Copy the URL from the output (it looks like d1234abcdef.cloudfront.net).
  • Paste the URL into your browser's address bar and press Enter.
  • Verify that your physiotherapy website loads correctly. Check that images appear, text renders, and the layout looks right.
  • Test your Supabase-powered features (login, forms, database calls) to confirm they still work from the new URL.

Why do Supabase features still work?

Your frontend makes API calls to Supabase's hosted endpoints. These calls go directly from the browser to Supabase's servers, regardless of where your frontend is hosted. Moving your static files from a local server to CloudFront does not affect the backend connection.

Site not loading or showing errors?

CloudFront distributions can take a few minutes to fully propagate. If you see an "AccessDenied" XML error, wait 2-3 minutes and refresh.

If you see a blank page, check that your website folder contained an index.html file at the root level (not nested inside a subfolder).

help me troubleshoot my site not loading on the CloudFront URL

Push your Terraform code to GitHub

Your infrastructure code is valuable. Pushing it to GitHub preserves your work and makes it accessible as a portfolio piece. You need to make sure sensitive files (credentials and Terraform state) never get committed.

  • In VS Code, create a new file called .gitignore in your physio-website-infra folder.
  • Add the following content to .gitignore:
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl

What does this .gitignore do?

The .terraform/ directory contains downloaded provider plugins (large binary files that don't belong in source control). The *.tfstate files contain your actual infrastructure state, which can include sensitive information like resource IDs and ARNs.

The .terraform.lock.hcl file locks provider versions. Some teams commit this, but for a learning project you can safely ignore it.

  • Save .gitignore.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
  • Go to GitHub in your browser and create a new repository. Name it physio-website-infra (or similar). Leave it empty (do not add a README or .gitignore from GitHub).
  • Back in your terminal, initialize a Git repository and push your code by running these commands:
git init -b main
git add .
git commit -m "Initial commit: Terraform infrastructure for physio website"
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/physio-website-infra.git"]]
git push -u origin main

What do these commands do?

The git init -b main command creates a new Git repository with main as the default branch. The git add . stages all files (except those in .gitignore).

The git remote add origin connects your local repo to the GitHub repository you just created. Finally, git push -u origin main uploads everything to GitHub.

  • Refresh your GitHub repository page in the browser. You should see main.tf, variables.tf, outputs.tf, and .gitignore listed.
  • Confirm that .terraform/ and any .tfstate files are NOT visible in the repository.

Git push asking for authentication?

GitHub requires authentication for push operations. If prompted, sign in with your GitHub credentials or set up a personal access token.

If you see a "remote already exists" error, you may have already added a remote. Run git remote -v to check the current remote URL.

help me push my Terraform code to GitHub

Your physiotherapy website is live on a production-grade CDN, and your infrastructure code is safely stored in GitHub. Next up, take on the Secret Mission to automate deployments with a CI/CD pipeline.

Secret mission

Add a GitHub Actions CI/CD Pipeline

Your website is live on CloudFront, but updating it still requires running aws s3 sync manually every time you make a change. What if pushing code to GitHub automatically deployed your website? In this secret mission, you will build a CI/CD pipeline with GitHub Actions that syncs your files to S3 and invalidates the CloudFront cache on every push to main.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Both the S3 bucket and CloudFront distribution are covered by the AWS Free Tier for new accounts. After 12 months or if you exceed free tier limits, costs are typically under $1-3/month for a small website.

Cost warning

The CloudFront Free plan includes 100 GB data transfer and 1M requests per month at $0. S3 storage is covered by 5 GB of free credits.

If your free tier expires or you exceed the limits, expect $1-3/month for a small static website. If you want to avoid any future charges, choose the Delete tab below and run terraform destroy.

Resources you used:

  • AWS S3 bucket (website file storage).
  • AWS CloudFront distribution (CDN serving your website).
  • AWS IAM user (programmatic access keys).
  • GitHub repository (Terraform code and CI/CD workflow).
  • Local physio-website-infra project folder.
  • AWS CLI v2 (installed via MSI).

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your physiotherapy website live.

  • Your site stays live at the CloudFront URL at near-zero cost within the free tier.
  • AWS CLI and Terraform remain installed on your machine.
  • Your GitHub Actions CI/CD pipeline remains active and will deploy on every push to main.

Pause - I'll come back to this later

S3 and CloudFront have no pause option, but they cost nothing within the free tier for a small website.

  • To stop automatic deployments, disable your GitHub Actions workflow. Go to your repository on GitHub, click the **Actions** tab, select the **Deploy Website** workflow, click the three-dot menu, and choose **Disable workflow**.
  • Your website will remain live at the CloudFront URL, but pushes to main will no longer trigger a deployment.
  • To resume later, re-enable the workflow from the same menu.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow these steps in order.

Step 1: Empty your S3 bucket.

Terraform cannot delete a bucket that still contains files. Empty it first by running this command:

aws s3 rm s3://[[BUCKET_NAME="your-bucket-name"]] --recursive

Step 2: Destroy all Terraform-managed AWS resources.

  • Open your terminal and navigate to your physio-website-infra folder.
  • Run the following command:
terraform destroy
  • Review the plan and type yes when prompted.
  • This removes your S3 bucket, CloudFront distribution, Origin Access Control, and bucket policy.

Step 3: Delete your IAM user.

  • Sign in to the AWS Management Console and open the IAM console.
  • In the left navigation pane, choose Users.
  • Select the checkbox next to the IAM user you created for this project.
  • Click Delete at the top of the page.
  • Enter the username to confirm, then choose Delete.

Step 4: Delete your GitHub repository.

  • On GitHub, navigate to the main page of your repository.
  • Click Settings.
  • Scroll down to the Danger Zone section and click Delete this repository.
  • Follow the confirmation prompts to verify the repository name and complete the deletion.

Step 5: Delete your local project folder.

  • Run the appropriate command for your operating system:

macOS

rm -rf ~/Desktop/physio-website-infra

Windows

rmdir /s /q %USERPROFILE%\Desktop\physio-website-infra

Step 6: Uninstall AWS CLI (optional).

  • Open Programs and Features in your Control Panel (or run appwiz.cpl from a command prompt).
  • Select the entry named AWS Command Line Interface and choose Uninstall.
  • Confirm that you want to uninstall the AWS CLI.
  • Optionally, delete the .aws folder in your user profile to remove stored credentials.

Step 7: Remove Terraform (optional).

  • Delete the folder where you extracted terraform.exe (e.g., C:\terraform).
  • Remove that folder from your system PATH. Press the Windows key, search for "environment variables", open Edit the system environment variables, edit the Path variable, and delete the Terraform entry.

Nice Work!

Nice Work!

Nice work! You just deployed a real physiotherapy website to production-grade AWS infrastructure, fully managed by code you wrote yourself.

You've learned how to:

  • Write Terraform configuration that provisions an S3 bucket, CloudFront CDN distribution, and security policies connecting them.
  • Deploy a live website to AWS with HTTPS and global CDN delivery using the full Terraform workflow: init, plan, apply, and destroy.
  • Push a complete Infrastructure as Code project to GitHub as a portfolio piece that proves you can provision cloud infrastructure programmatically.
  • Secret Mission: Built a CI/CD pipeline with GitHub Actions that automatically syncs website files to S3 and invalidates the CloudFront cache on every push.

Ready to quiz yourself?