Deploy a Static Website with Terraform
Use Terraform to provision and deploy a static website on AWS S3.
Introduction
30 Second Summary
Every time you click through the AWS Console to create a resource, you are doing work that cannot be repeated, reviewed, or shared with your team. Professional infrastructure teams solve this by writing code that defines their cloud resources.
In this project, you will deploy a live static website to AWS S3 using Terraform. You will write the same multi-file project structure that professional teams use and practice the complete Terraform lifecycle from first init to final destroy.
What You'll Build
A publicly-accessible static website hosted on AWS S3, provisioned entirely with Terraform configuration files that follow real-world team conventions.
By the end of this project, you'll have:
- A professional multi-file Terraform project with separate files for provider settings, variables, resources, and outputs.
- A live static website accessible via a public URL, deployed entirely through code.
- First-hand experience with the full Terraform lifecycle: write, init, plan, apply, modify, and destroy.
- Secret Mission: Add a tagging strategy using terraform.tfvars and locals to mirror production team patterns.
Are there any prerequisites?
You need an AWS account with the AWS CLI configured, the Terraform CLI installed, and VS Code ready to go. Step 1 will verify all three are working before you write any code.
Before We Start
Before writing any Terraform code, you need to confirm that your tools are installed and working. Terraform must be at the right version, and the AWS CLI must be connected to your AWS account.
You will also create the project folder structure that mirrors how professional teams organize their Terraform projects.
In this step, get ready to:
- Verify Terraform CLI shows version 1.2 or higher.
- Verify AWS CLI connects to your AWS account.
- Create the terraform-s3-website/ project directory with a website/ subdirectory.
Open VS Code and verify your Terraform version
Terraform is the tool that translates your configuration files into real cloud infrastructure. This project requires version 1.2 or higher.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the built-in terminal by pressing Ctrl+` (the backtick key, above Tab) on both macOS and Windows.
- Check your Terraform version by running this command:
terraform version
You should see output starting with Terraform v followed by a version number and your platform.
✔️ I see version 1.2 or higher
Your Terraform installation meets the minimum version requirement for this project. You are ready to move on.
ⓧ I see an older version
You need to upgrade Terraform to version 1.2 or higher. The latest version is 1.15.6.
macOS
- Upgrade Terraform using Homebrew by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
- Run terraform version again to confirm it now shows 1.2 or higher.
Windows
- Download the latest Terraform binary from the official install page.
- Replace your existing terraform.exe with the new binary.
- Close and reopen your terminal, then run terraform version to confirm it shows 1.2 or higher.
ⓧ Command not found
Terraform is not installed on your machine. Follow the steps below to install it.
macOS
- Install Terraform using Homebrew by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
- Run terraform version to confirm the installation was successful.
Windows
- Download the latest Terraform binary from the official install page.
- Extract the zip file and move terraform.exe to a folder in your system PATH.
- Close and reopen your terminal, then run terraform version to confirm it shows a version number.
Verify your AWS CLI configuration
The AWS CLI connects Terraform to your AWS account. This command confirms that your credentials are set up and working.
- Verify your AWS CLI is connected to your account by running this command:
aws sts get-caller-identity
You should see JSON output containing your Account, UserId, and Arn fields.
✔️ I see my account details
Your AWS CLI is configured and authenticated. You are ready to move on.
ⓧ I see an error
Your AWS CLI is not configured or your credentials have expired. You need to set up your credentials before continuing.
- Run the following command and enter your AWS Access Key ID, Secret Access Key, default region, and output format when prompted:
aws configure
- After entering your credentials, run aws sts get-caller-identity again to confirm it now returns your account details.
Still seeing an error?
Double-check that your Access Key ID and Secret Access Key are correct in the AWS Console under IAM > Users > your user > Security credentials.
help me fix my AWS CLI credentials
Create the project directory
Every Terraform project lives in its own folder. You will create one on your Desktop so it is easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder, move into it, and create the website/ subdirectory by running these commands:
mkdir terraform-s3-website
cd terraform-s3-website
mkdir website
- Confirm the directory structure by running:
ls
You should see website listed in the output.
What is this folder structure for?
The terraform-s3-website/ folder will hold all your .tf configuration files. The website/ subdirectory will hold the HTML files that Terraform uploads to S3.
Don't see the website folder?
Make sure you ran cd terraform-s3-website before running mkdir website. If you skipped the cd command, the website folder was created on your Desktop instead.
help me fix my project directory structure
Your environment is ready and your project folder is set up. Next, you will write your first Terraform configuration files.
Write the Terraform Configuration
Your project directory is ready. Now it's time to fill it with the Terraform configuration that defines your entire website infrastructure.
Professional Terraform projects split configuration across multiple files, each with a single responsibility. You will create four files: one for Terraform settings, one for input variables, one for resources, and one for outputs. This is the same pattern you will find in company codebases.
In this step, get ready to:
- Create terraform.tf and variables.tf to define provider requirements and input variables.
- Create main.tf with the AWS provider and four S3 resources.
- Create outputs.tf and validate the full configuration.
Create terraform.tf and variables.tf
Every Terraform project starts with a settings block that pins which providers and Terraform versions are allowed. This prevents surprises when teammates run the same code with different tool versions.
- In VS Code, make sure you have the terraform-s3-website folder open.
- Create a new file called terraform.tf in the root of your terraform-s3-website folder.
- Paste the following code into terraform.tf:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.2"
}
What does this code do?
- required_providers tells Terraform to download the AWS provider from HashiCorp's registry. The ~> 6.0 constraint means "any 6.x version" so you get patches but not breaking major upgrades.
- required_version ensures anyone running this project has Terraform 1.2 or newer.
- Save terraform.tf.
- You won't see a change yet. The next file defines the variables this configuration will use.
✔️ Awesome, I've got everything!
Great. Make sure you have saved terraform.tf before moving on.
ⓧ I'd like to double check the full code
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.2"
}
Next, you will create the variables file. Variables let you parameterize your configuration so the same code works across different environments without any edits.
- Create a new file called variables.tf in the root of your terraform-s3-website folder.
- Paste the following code into variables.tf:
variable "bucket_name" {
description = "Name of the S3 bucket for the static website"
type = string
}
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
What does this code do?
- bucket_name has no default, which means Terraform will ask for it every time you run a command. This forces you to choose a globally unique bucket name.
- aws_region defaults to us-east-1 so you can skip it during plan and apply unless you want a different region.
- Save variables.tf.
- Confirm both files are properly formatted by running this in your terminal:
terraform fmt
If both files are correctly formatted, the command produces no output. If you see a filename printed, Terraform fixed the spacing for you.
Seeing an error about unsupported arguments?
- Double check that source and version are inside the aws = { } block, not at the top level.
- Make sure required_version is inside the terraform { } block, not inside required_providers.
- help me fix my terraform.tf formatting
✔️ Awesome, I've got everything!
Great. Make sure you have saved variables.tf before moving on.
ⓧ I'd like to double check the full code
variable "bucket_name" {
description = "Name of the S3 bucket for the static website"
type = string
}
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
Create main.tf with the provider and resources
The main.tf file is where your actual infrastructure lives. You will define the AWS provider and four resources that together create a publicly accessible S3 website bucket.
- Create a new file called main.tf in the root of your terraform-s3-website folder.
- Start by pasting the provider block and first two resources into main.tf:
provider "aws" {
region = var.aws_region
}
resource "aws_s3_bucket" "website" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_public_access_block" "website" {
bucket = aws_s3_bucket.website.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
What does this code do?
- The provider block tells Terraform to use AWS in the region defined by your variable.
- aws_s3_bucket creates the bucket itself, using the name you will supply when you run terraform apply.
- aws_s3_bucket_public_access_block disables all four of AWS's default "block public access" protections. Without this, AWS prevents any public bucket policy from taking effect.
- You won't see a change yet. The next chunk adds the bucket policy that actually grants public read access.
Now add the bucket policy resource. This is the IAM policy that allows anyone on the internet to read your website files.
- Below the aws_s3_bucket_public_access_block resource in main.tf, paste the following:
resource "aws_s3_bucket_policy" "website" {
bucket = aws_s3_bucket.website.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.website.arn}/*"
}
]
})
depends_on = [aws_s3_bucket_public_access_block.website]
}
What does this code do?
- jsonencode() lets you write the JSON policy using HCL syntax, including interpolation of the bucket ARN.
- Principal = "*" means "everyone on the internet." Combined with s3:GetObject, this allows public read access to all objects in the bucket.
- depends_on forces Terraform to apply the public access block before attempting this policy. Without it, the policy would fail because AWS still has public access protections enabled.
Why use depends_on here?
Terraform normally figures out the order of operations by following resource references. Here, the bucket policy does not directly reference the public access block in its arguments, so Terraform does not know it depends on it.
The explicit depends_on tells Terraform: "apply the public access block first, then the policy." This is called an explicit dependency.
- You won't see a change yet. The next chunk configures S3 to serve the bucket as a website.
The final resource tells S3 which files to serve as the homepage and 404 page.
- Below the aws_s3_bucket_policy resource in main.tf, paste the following:
resource "aws_s3_bucket_website_configuration" "website" {
bucket = aws_s3_bucket.website.id
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
What does this code do?
- index_document sets index.html as the file served when visitors hit the root URL.
- error_document sets error.html as the page shown for any 404 requests.
- This resource also exposes a website_endpoint attribute that gives you the public URL for your site. You will reference this in outputs.tf next.
- Save main.tf.
- Confirm the file is properly formatted by running:
terraform fmt main.tf
No output means main.tf is already correctly formatted. If Terraform prints the filename, it fixed the spacing for you.
Seeing a syntax error?
- Check that every opening brace { has a matching closing brace }. The jsonencode section is especially easy to mis-close because it has nested braces and brackets.
- Make sure depends_on uses square brackets: depends_on = [aws_s3_bucket_public_access_block.website].
- help me fix my main.tf syntax error
✔️ Awesome, I've got everything!
Great. Double check you have saved main.tf before moving on.
ⓧ I'd like to double check the full code
provider "aws" {
region = var.aws_region
}
resource "aws_s3_bucket" "website" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_public_access_block" "website" {
bucket = aws_s3_bucket.website.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "website" {
bucket = aws_s3_bucket.website.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.website.arn}/*"
}
]
})
depends_on = [aws_s3_bucket_public_access_block.website]
}
resource "aws_s3_bucket_website_configuration" "website" {
bucket = aws_s3_bucket.website.id
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
Create outputs.tf and validate the configuration
Outputs expose values from your deployment so you can use them without digging through the AWS console. Here, you will output the website URL and bucket name.
- Create a new file called outputs.tf in the root of your terraform-s3-website folder.
- Paste the following code into outputs.tf:
output "website_url" {
description = "URL of the static website"
value = "http://${aws_s3_bucket_website_configuration.website.website_endpoint}"
}
output "bucket_name" {
description = "Name of the S3 bucket"
value = aws_s3_bucket.website.id
}
What does this code do?
- website_url builds the full HTTP URL by referencing the website_endpoint attribute from your website configuration resource. After deploying, Terraform prints this so you can visit your site immediately.
- bucket_name outputs the bucket ID for reference. This is useful when you need to pass the bucket name to other scripts or automation.
- Save outputs.tf.
All four files are written. Time to validate that everything is correct. First, initialize the project so Terraform downloads the AWS provider. Then validate the configuration.
- Initialize Terraform by running:
terraform init
What does this command do?
Terraform init downloads the AWS provider plugin specified in terraform.tf and creates a .terraform directory plus a .terraform.lock.hcl lock file. This only needs to happen once per project (or after changing providers).
- Confirm your configuration is syntactically valid by running:
terraform validate
- You should see the message: Success! The configuration is valid.
- Also confirm formatting across all files by running:
terraform fmt
No output means every file is already properly formatted.
Validation failed?
- If you see a "provider not installed" error, make sure you ran terraform init first.
- If you see a "reference to undeclared resource" error, check that resource names match exactly. For example, aws_s3_bucket.website must match the label "website" on your aws_s3_bucket resource.
- help me fix my terraform validate error
✔️ Awesome, I've got everything!
Great. All four configuration files are validated and ready for deployment.
ⓧ I'd like to double check the full code
output "website_url" {
description = "URL of the static website"
value = "http://${aws_s3_bucket_website_configuration.website.website_endpoint}"
}
output "bucket_name" {
description = "Name of the S3 bucket"
value = aws_s3_bucket.website.id
}
Your Terraform project structure is now complete. Next up, you will create the actual website HTML files and deploy everything to AWS with a single command.
Create Website Files and Deploy
Your Terraform configuration is written, but it references HTML files that don't exist yet.
In this step, you'll create those HTML files, add Terraform resources to upload them to S3, and then run the full deployment workflow to bring your website live.
In this step, get ready to:
- Create HTML files for the static website.
- Add S3 object resources to upload files through Terraform.
- Deploy all 6 resources to AWS and verify the live website.
Create the website HTML files and add upload resources
Your website configuration expects two files: index.html for the homepage and error.html for 404 errors. You'll create both inside the website/ folder.
- In VS Code's Explorer sidebar, right-click the website folder and select New File.
- Name the file index.html and press Enter.
- Paste the following HTML into website/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Terraform Website</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background-color: #f5f5f5;
}
h1 { color: #7b42bc; }
.status {
background: #e6f9e6;
border: 1px solid #4caf50;
padding: 1rem;
border-radius: 4px;
}
a { color: #7b42bc; }
</style>
</head>
<body>
<h1>Hello from Terraform!</h1>
<div class="status">
<p>This website was deployed using Terraform and AWS S3.</p>
<p>Infrastructure as Code is working.</p>
</div>
<p><a href="/about.html">About this project</a></p>
</body>
</html>
What does this page do?
This is a simple homepage with a purple heading, a green status box confirming the deployment works, and a link to an about page you'll add later.
- Save website/index.html.
- Create another new file in the website folder by right-clicking the folder and selecting New File again.
- Name the file error.html and press Enter.
- Paste the following HTML into website/error.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Not Found</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background-color: #f5f5f5;
}
h1 { color: #e53935; }
a { color: #7b42bc; }
</style>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<a href="/">Go back home</a>
</body>
</html>
What does this page do?
This is the error page that S3 serves whenever someone visits a URL that doesn't map to a file in your bucket. It shows a red 404 heading and a link back to the homepage.
- Save website/error.html.
Now you need to tell Terraform how to upload these files to S3. You'll add two aws_s3_object resources to main.tf.
- Switch back to main.tf in VS Code.
- Scroll to the bottom of the file, below the closing } of the aws_s3_bucket_website_configuration resource.
- Add the following two resource blocks at the bottom of the file:
resource "aws_s3_object" "index" {
bucket = aws_s3_bucket.website.id
key = "index.html"
source = "website/index.html"
content_type = "text/html"
etag = filemd5("website/index.html")
}
resource "aws_s3_object" "error" {
bucket = aws_s3_bucket.website.id
key = "error.html"
source = "website/error.html"
content_type = "text/html"
etag = filemd5("website/error.html")
}
What do these resources do?
- bucket tells Terraform which S3 bucket to upload into.
- key is the filename the object will have inside the bucket.
- source points to the local file on your computer.
- content_type ensures the browser renders the file as HTML instead of downloading it.
- etag = filemd5(...) creates a hash of the file contents. If you change the HTML later, Terraform detects the hash changed and re-uploads the file.
- Save main.tf.
Seeing a red underline in VS Code?
Make sure you placed the new resources after the closing } of the previous resource, not inside it. Each resource block must be at the top level of the file.
Help me fix a 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
provider "aws" {
region = var.aws_region
}
resource "aws_s3_bucket" "website" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_public_access_block" "website" {
bucket = aws_s3_bucket.website.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "website" {
bucket = aws_s3_bucket.website.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.website.arn}/*"
}
]
})
depends_on = [aws_s3_bucket_public_access_block.website]
}
resource "aws_s3_bucket_website_configuration" "website" {
bucket = aws_s3_bucket.website.id
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
resource "aws_s3_object" "index" {
bucket = aws_s3_bucket.website.id
key = "index.html"
source = "website/index.html"
content_type = "text/html"
etag = filemd5("website/index.html")
}
resource "aws_s3_object" "error" {
bucket = aws_s3_bucket.website.id
key = "error.html"
source = "website/error.html"
content_type = "text/html"
etag = filemd5("website/error.html")
}
Initialize Terraform and review the plan
Before Terraform can create anything in AWS, it needs to download the AWS provider plugin. This is what terraform init does.
- Download the AWS provider by running this command in your terminal:
terraform init
- You should see the message "Terraform has been successfully initialized!" in your terminal output.
What did terraform init do?
It downloaded the AWS provider (specified as hashicorp/aws in your terraform.tf) into a .terraform directory and created a .terraform.lock.hcl lock file to pin the exact provider version.
Seeing an error about provider requirements?
Make sure your terraform.tf file has the correct required_providers block with source = "hashicorp/aws". Also check that you're running the command from inside the terraform-s3-website directory.
Help me debug a terraform init error
Next, you'll preview what Terraform plans to create before committing to it. This is the plan step in the Terraform workflow.
- Preview the resources Terraform will create by running this command:
terraform plan -var="bucket_name=[[BUCKETNAME="your-unique-bucket-name"]]"
Why does the bucket name need to be unique?
S3 bucket names are globally unique across all AWS accounts. Pick something like yourname-terraform-website-2026 to avoid name conflicts.
- Review the plan output. You should see "Plan: 6 to add, 0 to change, 0 to destroy" at the bottom.
What are the 6 resources?
- aws_s3_bucket.website creates the bucket itself.
- aws_s3_bucket_public_access_block.website disables the default public access restrictions.
- aws_s3_bucket_policy.website allows the public to read objects.
- aws_s3_bucket_website_configuration.website enables static website hosting.
- aws_s3_object.index and aws_s3_object.error upload your HTML files.
Apply the configuration and visit your live website
The plan looks good. Time to make it real.
- Deploy all 6 resources to AWS by running this command:
terraform apply -var="bucket_name=[[BUCKETNAME="your-unique-bucket-name"]]"
- Terraform will show the same plan and ask for confirmation. Type yes and press Enter.
- Wait for the deployment to finish. You should see "Apply complete! Resources: 6 added, 0 changed, 0 destroyed." followed by your outputs.
- Find the website_url value in the outputs section of the terminal output. It will look like http://your-bucket-name.s3-website-us-east-1.amazonaws.com.
- Copy that URL and paste it into your browser's address bar.
You should see your "Hello from Terraform!" page with the purple heading and green status box.
What is Terraform state?
When Terraform creates resources, it records their details in a state file (terraform.tfstate). This is how Terraform knows what it manages and can detect when something drifts from your configuration.
- Confirm Terraform is tracking all 6 resources by running:
terraform state list
- You should see 6 resources listed: aws_s3_bucket.website, aws_s3_bucket_policy.website, aws_s3_bucket_public_access_block.website, aws_s3_bucket_website_configuration.website, aws_s3_object.error, and aws_s3_object.index.
Website not loading?
- Make sure you're using http:// not https:// in the URL. S3 website hosting does not support HTTPS without CloudFront.
- If you see "403 Forbidden", the bucket policy may not have applied yet. Wait 30 seconds and refresh.
- If you see "404 Not Found", check that website/index.html exists and that your aws_s3_object resource has key = "index.html" (not the full path).
Help me debug my Terraform S3 website deployment
Your website is live. You wrote configuration, Terraform turned it into real AWS resources, and you can see the result in your browser. Next up, you'll modify your infrastructure to add a new page and watch Terraform detect the changes.
Modify Infrastructure
Your website is live and serving real traffic. Now comes the moment that makes Terraform powerful: updating infrastructure that already exists.
Remember the "About this project" link on your homepage? It currently leads to your 404 error page because about.html does not exist yet. In this step, you will create that page, tell Terraform about it, and watch Terraform detect exactly what needs to change.
In this step, get ready to:
- Create an about page and register it as a Terraform resource.
- Run terraform plan to preview the change.
- Apply the change and verify the new page is live.
Create the about page and add its Terraform resource
You need two things to serve a new page through your Terraform-managed website: the HTML file itself, and an aws_s3_object resource in main.tf that tells Terraform to upload it.
- Create a new file called about.html inside your website/ folder and paste in the following HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About - My Terraform Website</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background-color: #f5f5f5;
}
h1 { color: #7b42bc; }
a { color: #7b42bc; }
</style>
</head>
<body>
<h1>About This Project</h1>
<p>This static website is hosted on AWS S3 and was deployed entirely using Terraform.</p>
<p>The infrastructure is defined as code across multiple configuration files, following professional conventions.</p>
<a href="/">Back to home</a>
</body>
</html>
What does this page do?
This is a simple HTML page that describes your project. It includes a link back to the homepage so visitors can navigate between pages.
- Save website/about.html.
The HTML file exists on your machine now, but Terraform does not know about it yet. You need to add a resource block that tells Terraform to upload this file to S3.
- In main.tf, scroll to the bottom of the file (below the aws_s3_object.error resource) and add the following resource block:
resource "aws_s3_object" "about" {
bucket = aws_s3_bucket.website.id
key = "about.html"
source = "website/about.html"
content_type = "text/html"
etag = filemd5("website/about.html")
}
What does this resource do?
This follows the same pattern as your existing aws_s3_object.index and aws_s3_object.error resources.
- key sets the filename in S3 (the URL path visitors use).
- source points to the local file Terraform will upload.
- etag = filemd5(...) tells Terraform to re-upload the file whenever its content changes.
- Save main.tf.
Not sure where to add the resource?
The new aws_s3_object.about block goes after the closing } of the aws_s3_object.error resource. Make sure it is not nested inside another block.
Help me place the new resource block in main.tf
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before continuing.
ⓧ I'd like to double check the full code
provider "aws" {
region = var.aws_region
}
resource "aws_s3_bucket" "website" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_public_access_block" "website" {
bucket = aws_s3_bucket.website.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "website" {
bucket = aws_s3_bucket.website.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.website.arn}/*"
}
]
})
depends_on = [aws_s3_bucket_public_access_block.website]
}
resource "aws_s3_bucket_website_configuration" "website" {
bucket = aws_s3_bucket.website.id
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
resource "aws_s3_object" "index" {
bucket = aws_s3_bucket.website.id
key = "index.html"
source = "website/index.html"
content_type = "text/html"
etag = filemd5("website/index.html")
}
resource "aws_s3_object" "error" {
bucket = aws_s3_bucket.website.id
key = "error.html"
source = "website/error.html"
content_type = "text/html"
etag = filemd5("website/error.html")
}
resource "aws_s3_object" "about" {
bucket = aws_s3_bucket.website.id
key = "about.html"
source = "website/about.html"
content_type = "text/html"
etag = filemd5("website/about.html")
}
Preview the changes with terraform plan
Before applying any change to live infrastructure, you should always preview it first. The terraform plan command compares your updated configuration against the current state and shows exactly what Terraform will create, modify, or destroy.
- Preview your changes by running this command:
terraform plan -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"
What should I see?
Terraform compares your configuration to the deployed state and reports what needs to change. You should see output showing 1 resource to add (the new aws_s3_object.about).
This is Terraform's change detection in action. It knows your bucket, policy, and other resources already match the configuration, so it only plans work for the new resource you added.
Seeing an error instead of a plan?
Make sure you replaced your-unique-bucket-name with the same bucket name you used in the previous step. The name must match exactly.
If you see a syntax error, confirm that the new resource block in main.tf has matching opening and closing braces and is not nested inside another resource.
Help me fix my terraform plan error
Apply the changes and verify your new page
The plan looks good. Time to make it real.
- Apply the changes by running this command:
terraform apply -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"
- Terraform will show you the plan again and ask for confirmation. Type yes and press Enter.
What does the output show?
After confirming, Terraform uploads about.html to your S3 bucket. The output confirms the apply is complete and shows your website_url output again.
- Open your website URL in the browser (the same website_url output from Terraform).
- Click the About this project link on your homepage.
- You should see the about page with the heading "About This Project" and a "Back to home" link.
About link still showing a 404?
Make sure terraform apply completed successfully and showed "Apply complete" in the output. If it did, try a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows).
Also confirm that the key in your aws_s3_object.about resource is exactly about.html (matching the link in your homepage).
Help me fix my about page not loading
You just modified live infrastructure with a single command. Terraform detected exactly what changed and only touched the resources that needed updating. Next up, you will explore how Terraform tracks all of this behind the scenes in its state file, then tear everything down cleanly.
Inspect State and Destroy
Your website is live and you have seen Terraform detect and apply changes to existing infrastructure.
Before you tear everything down, it is worth understanding how Terraform keeps track of what it has deployed. Terraform stores a record of every resource it manages in a state file. In this step, you will explore that state, then destroy all your AWS resources cleanly.
In this step, get ready to:
- Inspect Terraform state to see how it tracks your deployed resources.
- Destroy all 7 AWS resources with a single command.
- Verify that state is empty and the bucket no longer exists.
Explore Terraform state
Every time you run terraform apply, Terraform writes a snapshot of what it created into a local state file. This is how Terraform knows what already exists and what needs to change on the next run.
- List all resources Terraform is currently tracking by running this command:
terraform state list
You should see 7 resource addresses printed, one per line:
aws_s3_bucket.website
aws_s3_bucket_policy.website
aws_s3_bucket_public_access_block.website
aws_s3_bucket_website_configuration.website
aws_s3_object.about
aws_s3_object.error
aws_s3_object.index
What is Terraform state?
The state file is a JSON record that maps each resource block in your .tf files to the real AWS resource it created. Without it, Terraform would have no way to know what exists in your account.
This is why production teams store state remotely (in S3, for example) so that every team member works from the same source of truth.
- Inspect the bucket resource's full attributes by running:
terraform state show aws_s3_bucket.website
You should see a detailed printout of the bucket's attributes including its ARN, region, and current configuration.
What does this command do?
While terraform state list gives you the addresses of all tracked resources, terraform state show drills into one resource and displays all its attributes as Terraform sees them. This is useful for debugging or confirming what Terraform has recorded.
Seeing "No state file" or zero resources?
Make sure your terminal is inside the terraform-s3-website folder where you ran terraform apply. The state file lives in the same directory as your .tf files.
help me debug why terraform state list shows nothing
Destroy all resources
Terraform can tear down everything it created with a single command. It reads the state file, determines the correct deletion order based on resource dependencies, and removes each resource from AWS.
- Destroy all 7 resources by running this command:
terraform destroy -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"
- Terraform shows you a plan of what it will delete. Type yes and press Enter to confirm.
What is happening during destroy?
Terraform deletes resources in the correct dependency order. Objects are removed first, then the website configuration, then the bucket policy, then the public access block, and finally the bucket itself.
This is the reverse of creation order. Terraform knows these dependencies from your configuration (the depends_on and implicit references you wrote) and handles the sequencing automatically.
Once the command completes, you should see a message that says:
Destroy complete! Resources: 7 destroyed.
Destroy failing with a permissions error?
Make sure you are using the same bucket name you used during terraform apply. If the name does not match state, Terraform cannot locate the real resource to delete.
Also confirm your AWS CLI credentials are still valid by running aws sts get-caller-identity.
help me fix my terraform destroy error
Verify destruction is complete
A clean destroy means Terraform's state should now be empty. Let's confirm that nothing is left.
- Confirm the state is empty by running:
terraform state list
This time, the command should return no output at all. That means Terraform is no longer tracking any resources.
- Optionally, open the AWS Console in your browser.
- Navigate to S3 and search for your bucket name.
- Confirm that the bucket no longer appears in the list.
Why verify in the console?
Terraform state is a local record. Checking the AWS Console confirms the real-world resource is actually gone, not just removed from Terraform's tracking. In production, this habit catches orphaned resources that cost money.
State list still shows resources?
If terraform state list still shows entries, the destroy did not fully complete. Re-run terraform destroy with the same -var flag and check for error messages in the output.
help me figure out why resources remain after terraform destroy
Secret mission
Add Tags with terraform.tfvars and Locals
In professional Terraform repos, you'll see .tfvars files, locals blocks, and tagging strategies everywhere. This mission teaches you all three patterns so you can recognize and use them immediately at work.
Clean Up Your Resources
Clean Up Your Resources
If you followed Step 5 or the Secret Mission to the end, your AWS resources are already destroyed. The only things remaining are the local project files on your machine.
Decide whether to keep your local files for reference or delete them entirely.
Resources you used:
- AWS S3 bucket with public website configuration and uploaded HTML objects (already destroyed via terraform destroy).
- Local terraform-s3-website/ project directory (Terraform configuration files, website HTML files, state files, and the .terraform/ provider cache).
Keep everything running
No action needed. Choose this if you want to keep the project files as a reference for future Terraform work.
- Your AWS resources are already destroyed, so there are no ongoing costs.
- Your local project files remain in the terraform-s3-website/ folder on your Desktop.
- You can re-deploy the website at any time by running terraform apply from the project directory.
Pause - I'll come back to this later
Your cloud resources are already torn down, so there is nothing to pause on the AWS side. Your local files stay on disk until you decide to delete them.
- No AWS costs are accumulating.
- When you come back, run terraform apply from the terraform-s3-website/ directory to redeploy your website.
Delete - I don't want to use this again
Remove all project files from your machine. Your AWS resources are already destroyed, so this step only covers local cleanup.
- If you re-applied your infrastructure after the destroy in Step 5 or the Secret Mission, run the following command from inside the terraform-s3-website/ directory to destroy any remaining AWS resources:
terraform destroy
What if I don't have a terraform.tfvars file?
If you did not complete the Secret Mission and have no terraform.tfvars file, pass the variable directly:
terraform destroy -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"
- Type yes when prompted to confirm destruction.
- Once AWS resources are confirmed destroyed, navigate up one directory and delete the project folder.
On macOS or Linux:
cd ~/Desktop
rm -rf terraform-s3-website
On Windows:
cd ~\Desktop
Remove-Item -Recurse -Force terraform-s3-website
- Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see terraform-s3-website in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just deployed a live static website to AWS S3 using nothing but Terraform configuration files.
You've learned how to:
- Deployed a static website to AWS S3 entirely through Terraform code, using the professional multi-file pattern (terraform.tf, variables.tf, main.tf, outputs.tf).
- Practiced the complete Terraform lifecycle: init, plan, apply, modify, and destroy.
- Used variables, outputs, resource dependencies, and state inspection to understand how Terraform tracks and manages infrastructure.
- Secret Mission: Implemented terraform.tfvars, locals, and a consistent tagging strategy used in production teams.
Ready to quiz yourself?