Deploy a Static Website with Terraform

Use Terraform to provision an S3 static website with IaC from scratch.

Introduction

30 Second Summary

Every time you click through the AWS console to create a resource, you are performing a manual process that is impossible to reliably repeat, share with a teammate, or undo with confidence. Infrastructure as Code solves this by letting you define your cloud resources in files that you can version-control, review, and run again and again.

In this project, you will use Terraform to deploy a live static website on AWS S3. You will write configuration files from scratch, provision real cloud resources, and tear them all down with a single command.

What You'll Build

A complete Terraform project that provisions an S3 static website on AWS, including public access configuration, HTML file uploads, and a live URL you can open in your browser.

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

  • A Terraform project with organized HCL configuration files (main.tf, variables.tf, outputs.tf) that you can reuse as a template.
  • A live static website hosted on S3 with public access, an index page, and a custom error page.
  • Hands-on experience with the full Terraform lifecycle: init, plan, apply, modify, and destroy.
  • A multi-page site with reusable variables managed through a .tfvars file (Secret Mission).

Are there any prerequisites?

You need an AWS account with admin access and basic comfort using the terminal. Step 1 walks you through installing every tool from scratch, so no prior Terraform experience is required.

Install Terraform and Configure AWS Credentials

Before you can define any infrastructure in code, you need two tools on your machine. Terraform reads your configuration files and talks to cloud providers to create, update, or delete resources. The AWS CLI handles authentication so Terraform can act on your AWS account.

In this step, you will install both tools, wire up your AWS credentials, and create the project folder where all your Terraform files will live.

In this step, get ready to:
  • Install the Terraform CLI and AWS CLI using Homebrew.
  • Configure your AWS credentials so Terraform can provision resources.
  • Create a project directory with three empty configuration files.
Install Terraform and the AWS CLI

Both tools install through Homebrew, the macOS package manager. If you already have Homebrew installed, you can skip straight to the Terraform install.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check if Homebrew is already installed by running this command:
brew --version

I see a version number

Homebrew is ready. Skip ahead to the Terraform install below.

Command not found

  • Install Homebrew by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

What happens during the install?

Homebrew will ask for your macOS password and confirmation before installing. This is normal. The install may take a few minutes.

Follow the instructions Homebrew prints at the end

On Apple Silicon Macs, Homebrew prints two commands you must run to add it to your PATH. Copy and run both lines exactly as shown in your terminal output. If you skip this, the brew command will not be found.

  • After the install completes, close and reopen your terminal.
  • Verify Homebrew is working by running:
brew --version

You should now see a version number printed in the terminal.

  • Add the HashiCorp tap and install Terraform by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

What is a tap?

A tap is a third-party repository of Homebrew formulas. HashiCorp hosts their own tap to distribute the latest official Terraform builds.

  • Verify Terraform installed correctly by running:
terraform -version

You should see Terraform v1.14.9 or a similar version number.

Seeing command not found?

  • Close and reopen your terminal, then try again. Homebrew sometimes needs a fresh shell to update your PATH.
  • If you are on an Apple Silicon Mac, make sure you ran the two PATH commands Homebrew printed after it was installed.

Still stuck? Help me troubleshoot why terraform -version is not working on my Mac.

  • Install the AWS CLI by running:
brew install awscli
  • Verify the AWS CLI installed correctly by running:
aws --version

You should see aws-cli/2.x.x followed by Python and OS details.

AWS CLI not found?

  • Close and reopen your terminal, then try again.
  • If you previously installed the AWS CLI through a different method (like the macOS installer), Homebrew may conflict. Run which aws to check which version your shell is finding.

Need more help? Help me troubleshoot why aws --version is not working after installing via Homebrew.

Configure your AWS credentials

Terraform uses the AWS CLI's stored credentials to authenticate with your AWS account. You need an IAM access key pair to set this up.

I have my access keys ready

Great. Keep your Access Key ID and Secret Access Key handy for the next command.

I need to create access keys

  • Open the AWS Management Console in your browser.
  • Search for IAM in the top search bar and open it.
  • Click Users in the left sidebar.
  • Click your IAM username.
  • Select the Security credentials tab.
  • Scroll down to Access keys and click Create access key.
  • Select Command Line Interface (CLI) as the use case.
  • Check the confirmation checkbox and click Next.
  • Click Create access key.
  • Copy both the Access key ID and Secret access key to a safe place. You will not be able to see the secret key again after closing this page.

Keep your secret key safe

Never share your access keys or commit them to version control. Treat them like passwords.

  • Configure your AWS credentials by running this command:
aws configure

What does aws configure ask for?

The command prompts you for four values, one at a time:

  • AWS Access Key ID: paste the access key you copied from IAM.
  • AWS Secret Access Key: paste the secret key you copied from IAM.
  • Default region name: enter us-east-1.
  • Default output format: enter json.
  • Verify your credentials are working by running this command:
aws sts get-caller-identity

You should see a JSON response with your Account, UserId, and Arn. This confirms Terraform will be able to authenticate with AWS.

Seeing an authentication error?

  • Double-check that you pasted the Access Key ID and Secret Access Key correctly. Trailing spaces can break authentication.
  • Make sure the IAM user whose keys you are using has admin permissions (or at minimum, S3 full access).
  • Run aws configure again to re-enter your credentials from scratch.

Still not working? Help me troubleshoot my AWS CLI authentication error.

Create your project directory

Every Terraform project lives in its own directory. Terraform looks for .tf files in the current directory when you run commands.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir terraform-static-site
cd terraform-static-site
  • Create the three configuration files by running this command:
touch main.tf variables.tf outputs.tf
  • Confirm the files exist by running:
ls

You should see main.tf, outputs.tf, and variables.tf in the output.

What are these three files for?

  • main.tf is where you define your infrastructure resources (the S3 bucket, permissions, and website configuration).
  • variables.tf is where you declare input variables that make your configuration reusable.
  • outputs.tf is where you define values Terraform should display after it finishes, like your website URL.

This three-file structure is a Terraform convention. Splitting configuration into purpose-specific files keeps your project organized as it grows.

  • Open the terraform-static-site folder in your text editor. If you use VS Code, run this command from inside the folder:
code .

Pro tip: any text editor works

If you prefer a different editor, open the terraform-static-site folder from your editor's File menu. Terraform files are plain text, so any editor will work.

VS Code not opening from the terminal?

The code command needs to be installed separately. In VS Code, press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Shell Command: Install 'code' command in PATH, and select it. Then try code . again.

Need help? Help me open my project folder in VS Code.

Your tools are installed and your project directory is ready. Next up, you will write your first Terraform configuration to define an S3 bucket in code.

Write Your First Terraform Configuration

Your tools are installed and your AWS credentials are configured. Now it's time to write the code that defines your infrastructure.

Terraform uses HCL (HashiCorp Configuration Language) to describe the infrastructure you want to build. In this step, you'll define variables for reusability, configure the AWS provider, and create your first resource: an S3 bucket.

In this step, get ready to:
  • Define reusable variables in variables.tf.
  • Configure the AWS provider and create an S3 bucket resource in main.tf.
  • Run terraform init, terraform plan, and terraform apply to create the bucket in AWS.
Define your Terraform variables

Terraform variables let you parameterize your configuration instead of hard-coding values like bucket names and regions. You define them in a dedicated file so the rest of your configuration can reference them.

  • Open variables.tf in your editor and add the following code:
variable "bucket_name" {
  description = "The name of the S3 bucket. Must be globally unique."
  type        = string
}

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

What does this code do?

  • bucket_name is a required variable with no default. Terraform will ask you to provide a value every time you run a command.
  • aws_region has a default of us-east-1, so you only need to override it if you want a different region.
  • The description field documents what each variable is for. It shows up when Terraform prompts for input.
  • Save variables.tf.

You won't see any output yet. These variables are used in the next substep when you configure the AWS provider.

Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

I'd like to double check the full code

variable "bucket_name" {
  description = "The name of the S3 bucket. Must be globally unique."
  type        = string
}

variable "aws_region" {
  description = "The AWS region to deploy resources in."
  type        = string
  default     = "us-east-1"
}
Configure the AWS provider and create an S3 bucket

Every Terraform project needs a provider. A provider is a plugin that lets Terraform talk to a specific cloud platform or service.

You'll configure the AWS provider first, then define the S3 bucket resource you want Terraform to create.

  • Open main.tf in your editor and add the following code to set up the provider:
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

What does this code do?

  • The terraform block tells Terraform which provider plugins to download. Here you're requesting the hashicorp/aws provider, version 6.x.
  • The ~> 6.0 constraint means "any version in the 6.x range." This lets you get patch updates without unexpected major changes.
  • The provider "aws" block configures how Terraform connects to AWS. It references var.aws_region from the variable you just defined.
  • Save main.tf.

No visible change yet. The next chunk adds the actual resource Terraform will create.

  • Below the provider block in main.tf, add the S3 bucket resource:
# S3 Bucket
resource "aws_s3_bucket" "website" {
  bucket        = var.bucket_name
  force_destroy = true

  tags = {
    Name        = "My Terraform Website"
    Environment = "dev"
  }
}

What does this code do?

  • resource "aws_s3_bucket" "website" tells Terraform to create an S3 bucket. "website" is the local name you'll use to reference this bucket elsewhere in your code.
  • bucket = var.bucket_name sets the actual bucket name in AWS to whatever value you pass in for the variable.
  • force_destroy = true lets Terraform delete the bucket even when it contains files. Without this, terraform destroy would fail.
  • tags add metadata labels to the bucket. These help you identify and organize resources in the AWS console.
  • Save main.tf.

You won't see a change yet. The next substep runs Terraform to bring this configuration to life.

Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

I'd like to double check the full code

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# S3 Bucket
resource "aws_s3_bucket" "website" {
  bucket        = var.bucket_name
  force_destroy = true

  tags = {
    Name        = "My Terraform Website"
    Environment = "dev"
  }
}
Run your first Terraform workflow

Terraform follows a three-step workflow: init downloads provider plugins, plan previews changes, and apply creates the actual resources.

  • Download the AWS provider plugin by running this command in your terminal:
terraform init

You should see output ending with Terraform has been successfully initialized! along with a message about the installed provider version.

What did terraform init do?

Terraform read your required_providers block and downloaded the hashicorp/aws plugin into a hidden .terraform directory. It also created a .terraform.lock.hcl file that locks the exact provider version for consistency.

Seeing an error during init?

  • If you see a syntax error, double check that your main.tf and variables.tf match the code from the previous substeps exactly. Missing braces or quotes are the most common cause.
  • If you see Error: No such file or directory, make sure your terminal is inside the terraform-static-site folder.

Help me debug my terraform init error

Before creating anything, you should always preview what Terraform will do. The plan command acts as a safety net by showing you exactly what changes will happen.

What name should I pick for my bucket?

S3 bucket names must be globally unique across all of AWS. They can only contain lowercase letters, numbers, and hyphens, and must be between 3 and 63 characters. A good pattern is to include your name or initials, like nextwork-terraform-janedoe.

  • Preview the resources Terraform will create by running this command:
terraform plan -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"

You should see output ending with Plan: 1 to add, 0 to change, 0 to destroy. That one resource is your S3 bucket.

What is terraform plan showing me?

The plan output lists every resource Terraform will create, modify, or destroy. Lines prefixed with + mean "will be created." You'll also see the specific attributes like your bucket name and tags. Nothing has been created yet.

Seeing a credentials error?

  • If you see Error: No valid credential sources found, your AWS CLI credentials may not be configured. Run aws configure and enter your access key, secret key, and region.
  • If you see a bucket naming error, make sure your name uses only lowercase letters, numbers, and hyphens.

Help me fix my terraform plan error

The plan looks good. Time to make it real.

  • Create the S3 bucket in AWS by running this command:
terraform apply -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"
  • Terraform will display the same plan you just reviewed and ask for confirmation. Type yes and press Enter.

You should see output ending with Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Your S3 bucket now exists in AWS. Terraform created it based entirely on the code you wrote.

What is a Terraform state file?

After applying, Terraform created a terraform.tfstate file in your project directory. This file tracks every resource Terraform manages and their current configuration. Terraform compares this file against your code on every plan and apply to figure out what needs to change.

Bucket name already taken?

  • If you see BucketAlreadyExists, someone else already owns a bucket with that name. Pick a more unique name and run the apply command again with the new name.
  • If you see a timeout or network error, check your internet connection and try again.

Help me fix my terraform apply error

Your S3 bucket is live in AWS, created entirely through code. Next up, you'll configure it to host a website and upload your HTML files.

Configure S3 for Static Website Hosting

Your S3 bucket exists on AWS, but right now it is just an empty storage container. To turn it into a website, you need to configure public access, set a permissions policy, and enable static website hosting.

In this step, you will add four new Terraform resources that configure the bucket for website hosting. You will also create HTML files and upload them through Terraform so every part of your site is managed as code.

In this step, get ready to:
  • Add ownership controls, public access, bucket policy, and website configuration resources to main.tf.
  • Create index.html and error.html files with website content.
  • Run terraform apply to create all 6 new resources in AWS.
Add S3 configuration resources

AWS blocks public access on S3 buckets by default. For a public website, you need to explicitly allow public access and attach a policy that grants read permissions to everyone.

  • In main.tf, find the closing } of your aws_s3_bucket "website" resource.
  • Add the ownership controls and public access block resources below it by copying this code:
# Ownership controls
resource "aws_s3_bucket_ownership_controls" "website" {
  bucket = aws_s3_bucket.website.id

  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

# Public access block - allow public access
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 do these resources do?

  • aws_s3_bucket_ownership_controls sets the bucket owner as the preferred owner of all objects. This is required before you can apply public access settings.
  • aws_s3_bucket_public_access_block disables all four public access blocking settings. By default, AWS blocks public access on every new bucket. Setting all four to false removes that protection so your website files can be read by anyone.

You will not see a change yet. The next chunk adds the policy that actually grants public read access.

  • Still in main.tf, add the bucket policy resource below the public access block by copying this code:
# Bucket policy - allow public read
resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id

  depends_on = [aws_s3_bucket_public_access_block.website]

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "PublicReadGetObject"
        Effect    = "Allow"
        Principal = "*"
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.website.arn}/*"
      }
    ]
  })
}

What does this policy do?

  • The jsonencode function is a built-in Terraform function that converts an HCL map into a JSON string. AWS bucket policies must be in JSON format, so jsonencode lets you write the policy in readable HCL and have Terraform handle the conversion.
  • The policy grants s3:GetObject permission to * (everyone) for all objects in the bucket. This is what makes your website files publicly readable.
  • depends_on tells Terraform to create the public access block before this policy. Without it, AWS would reject the public policy because the bucket still has public access blocked.

You will not see a change yet. The next resource enables the actual website hosting feature on the bucket.

  • Add the website configuration resource below the bucket policy in main.tf by copying this code:
# Website configuration
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 resource do?

  • This enables static website hosting on your S3 bucket. Without it, the bucket stores files but does not serve them as web pages.
  • index_document tells S3 which file to serve when someone visits the root URL of your site.
  • error_document tells S3 which file to serve when someone requests a page that does not exist.
  • Save main.tf.

Seeing red squiggles or syntax errors?

  • Make sure each resource block has matching opening and closing braces {}. A missing brace will break the entire file.
  • Check that the bucket = aws_s3_bucket.website.id line appears in every new resource. Each resource needs to reference the bucket it belongs to.
  • Verify that the jsonencode block in the bucket policy uses parentheses jsonencode({...}) not just braces.

Help me fix a syntax error in my main.tf file.

Create the HTML files and upload resources

Your bucket is configured for website hosting, but it has no content to serve. You need two HTML files: a home page and an error page for missing URLs.

  • Create a new file called index.html in your terraform-static-site folder (the same folder as main.tf).
  • Paste the following content into 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: 600px;
            margin: 60px auto;
            padding: 0 20px;
            background-color: #f5f5f5;
            color: #333;
        }
        h1 { color: #5c4ee5; }
        p { line-height: 1.6; }
        .badge {
            display: inline-block;
            background-color: #5c4ee5;
            color: white;
            padding: 4px 12px;
            border-radius: 12px;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <h1>Hello from Terraform!</h1>
    <p><span class="badge">Deployed with IaC</span></p>
    <p>This website was deployed to AWS S3 entirely through Terraform configuration files. No console clicking required.</p>
    <p>Infrastructure as Code means this entire setup is version-controlled, repeatable, and can be torn down with a single command.</p>
</body>
</html>
  • Save index.html.
  • Create another new file called error.html in the same folder.
  • Paste the following content into 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: 600px;
            margin: 60px auto;
            padding: 0 20px;
            background-color: #f5f5f5;
            color: #333;
            text-align: center;
        }
        h1 { color: #e55c5c; font-size: 48px; }
        p { line-height: 1.6; }
        a { color: #5c4ee5; text-decoration: none; }
        a:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <h1>404</h1>
    <p>Oops! The page you are looking for does not exist.</p>
    <p><a href="/">Go back home</a></p>
</body>
</html>
  • Save error.html.
  • Confirm both files exist by running:
ls

You should see error.html, index.html, main.tf, outputs.tf, and variables.tf in the output.

Now you need to tell Terraform to upload these files to S3. Terraform manages file uploads through aws_s3_object resources.

  • In main.tf, add two aws_s3_object resources below the website configuration by copying this code:
# Upload index.html
resource "aws_s3_object" "index" {
  bucket       = aws_s3_bucket.website.id
  key          = "index.html"
  source       = "index.html"
  content_type = "text/html"
  etag         = filemd5("index.html")
}

# Upload error.html
resource "aws_s3_object" "error" {
  bucket       = aws_s3_bucket.website.id
  key          = "error.html"
  source       = "error.html"
  content_type = "text/html"
  etag         = filemd5("error.html")
}

What does this code do?

  • key is the name of the file inside S3. source is the path to the local file on your machine.
  • content_type = "text/html" tells S3 to serve the file as an HTML page. Without this, browsers would download the file instead of rendering it.
  • etag = filemd5("index.html") computes a hash of the file's contents. If you edit the HTML later, the hash changes and Terraform knows to re-upload the file. Without etag, Terraform would not detect content changes.
  • Save main.tf.

Seeing an error about the source file?

  • Make sure index.html and error.html are in the same folder as main.tf. The source path is relative to the Terraform project directory.
  • Check that the file names match exactly. Index.html is not the same as index.html.

Help me fix a file source error in my aws_s3_object resource.

Awesome, I've got everything!

Double check you have saved main.tf, index.html, and error.html.

I'd like to double check the full code

Here is the full main.tf file as it should look right now:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# S3 Bucket
resource "aws_s3_bucket" "website" {
  bucket        = var.bucket_name
  force_destroy = true

  tags = {
    Name        = "My Terraform Website"
    Environment = "dev"
  }
}

# Ownership controls
resource "aws_s3_bucket_ownership_controls" "website" {
  bucket = aws_s3_bucket.website.id

  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

# Public access block - allow public access
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
}

# Bucket policy - allow public read
resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id

  depends_on = [aws_s3_bucket_public_access_block.website]

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "PublicReadGetObject"
        Effect    = "Allow"
        Principal = "*"
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.website.arn}/*"
      }
    ]
  })
}

# Website configuration
resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "error.html"
  }
}

# Upload index.html
resource "aws_s3_object" "index" {
  bucket       = aws_s3_bucket.website.id
  key          = "index.html"
  source       = "index.html"
  content_type = "text/html"
  etag         = filemd5("index.html")
}

# Upload error.html
resource "aws_s3_object" "error" {
  bucket       = aws_s3_bucket.website.id
  key          = "error.html"
  source       = "error.html"
  content_type = "text/html"
  etag         = filemd5("error.html")
}
Apply the new resources

Your configuration files are ready. Time to let Terraform create the 6 new resources in AWS. This is where you see Terraform's power. It knows the bucket already exists and only creates what is new.

  • Preview the changes Terraform will make by running this command:
terraform plan -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"

What should I see?

Use the same bucket name you used in Step 2. Terraform will show Plan: 6 to add, 0 to change, 0 to destroy. at the bottom of the output. The 6 resources are: ownership controls, public access block, bucket policy, website configuration, and the two S3 object uploads.

  • Create all 6 resources by running this command:
terraform apply -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"
  • Terraform will show the same plan. Type yes and press Enter to confirm.

You should see Apply complete! Resources: 6 added, 0 changed, 0 destroyed. at the bottom of your terminal output.

Seeing an access denied or policy error?

  • If you see AccessDenied, make sure the public access block resource is included in your main.tf and that the bucket policy has depends_on = [aws_s3_bucket_public_access_block.website]. Terraform must disable public access blocking before applying a public policy.
  • If you see an error about index.html or error.html not found, confirm the HTML files are saved in the same directory where you are running terraform apply.
  • If you see a bucket name conflict, your chosen name may already be taken globally. Choose a different unique name and try again.

Help me debug my terraform apply error.

Your S3 bucket is now a fully configured static website with public access, a hosting configuration, and HTML content uploaded. Next up, you will add Terraform outputs to surface your live website URL and visit the site in your browser.

Add Outputs and Visit Your Live Website

Your S3 bucket is fully configured with static website hosting, public access, a bucket policy, and two HTML files uploaded. All that infrastructure exists in AWS, but right now you have no easy way to find your website URL without digging through state files or the AWS console.

Terraform outputs solve this problem. They let you extract and display key values from your infrastructure after every apply. You'll define outputs for your website URL and bucket ARN, then visit your live site in the browser.

In this step, get ready to:
  • Define Terraform outputs to surface your website URL and bucket ARN.
  • Visit your live website and test the error page.
  • Edit your HTML and watch Terraform detect and apply the change.
Define your Terraform outputs

Outputs are how Terraform surfaces useful information after an apply. Instead of searching for your website URL, Terraform prints it right in the terminal.

  • Open outputs.tf in your editor and add the following code:
output "website_url" {
  description = "The URL of the static website."
  value       = aws_s3_bucket_website_configuration.website.website_endpoint
}

output "bucket_arn" {
  description = "The ARN of the S3 bucket."
  value       = aws_s3_bucket.website.arn
}

What does this code do?

  • website_url pulls the website_endpoint attribute from your website configuration resource. This is the public URL where your site is hosted.
  • bucket_arn pulls the ARN (Amazon Resource Name) from your S3 bucket. ARNs are unique identifiers for every resource in AWS.
  • Save outputs.tf.

You won't see a change yet. Outputs take effect on the next terraform apply.

Awesome, I've got everything!

Double check you've saved outputs.tf before moving on.

I'd like to double check the full code

output "website_url" {
  description = "The URL of the static website."
  value       = aws_s3_bucket_website_configuration.website.website_endpoint
}

output "bucket_arn" {
  description = "The ARN of the S3 bucket."
  value       = aws_s3_bucket.website.arn
}
Apply and visit your live website
  • Run terraform apply to register your new outputs:
terraform apply -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"
  • Terraform prompts you to confirm the apply. Type yes and press Enter.

What should I see?

Terraform shows that no resources changed because you only added outputs, not infrastructure. After the apply, your outputs are printed at the bottom:

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

bucket_arn = "arn:aws:s3:::your-bucket-name"
website_url = "your-bucket-name.s3-website-us-east-1.amazonaws.com"
  • Copy the website_url value from the terminal output.
  • Open your browser and paste the URL into the address bar.

You should see your index page with the purple "Hello from Terraform!" heading and the "Deployed with IaC" badge.

  • Add /nonexistent to the end of the URL and press Enter.

You should see your 404 error page with the big red "404" heading and a "Go back home" link.

Site not loading?

  • Make sure you are using http:// and not https://. S3 website endpoints do not support HTTPS without CloudFront.
  • Check that you copied the full website_url value. It should look like your-bucket-name.s3-website-us-east-1.amazonaws.com.
  • If you see "403 Forbidden", your bucket policy may not have applied correctly. Run terraform apply again to ensure all resources are in sync.
  • My Terraform website isn't loading. I'm seeing an error when I visit the website_url output. Help me debug this.
Update your site with Terraform

Deploying once is great, but the real power of Terraform is managing changes over time. When you edit a file that Terraform tracks, it detects the difference and applies only what changed.

  • Open index.html in your editor.
  • Find this line:
<h1>Hello from Terraform!</h1>
  • Change the heading text to anything you like. For example, update it to:
<h1>Hello from Terraform and NextWork!</h1>
  • Save index.html.
  • Preview the change by running terraform plan:
terraform plan -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"

Terraform shows 1 to change. It detected that your local index.html file changed and knows the S3 object needs to be re-uploaded.

How does Terraform detect file changes?

The etag = filemd5("index.html") line in your aws_s3_object resource computes a hash of the file contents. When you edit the file, the hash changes, and Terraform knows it needs to re-upload.

Without etag, Terraform would not detect content changes because the file path stays the same.

  • Apply the change by running:
terraform apply -var="bucket_name=[[BUCKET_NAME="your-unique-bucket-name"]]"
  • Type yes and press Enter to confirm.
  • Refresh your browser tab with the website URL.

You should see your updated heading on the page. This is the core Terraform workflow: write, plan, apply.

Still seeing the old heading?

  • Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows). Your browser may be caching the old page.
  • Make sure you saved index.html before running terraform apply. Terraform reads the file from disk, so unsaved changes are invisible to it.
  • Check the terraform apply output. If it says 0 changed, Terraform did not detect a difference. Verify your edit was saved correctly.
  • I updated my index.html but the browser still shows the old content. Help me figure out why.

Your website is live and you have experienced the full Terraform development loop: write a change, preview it with plan, and push it with apply. Next up, you'll tear down every resource with a single command.

Tear Down Your Infrastructure

Your website is live on S3 and Terraform is surfacing its URL through outputs. You have experienced the core "write, plan, apply" workflow firsthand.

One critical step remains to complete the full Terraform lifecycle: tearing everything down. Cloud resources left running can lead to surprise charges, so knowing how to cleanly destroy infrastructure is just as important as knowing how to create it.

In this step, get ready to:
  • Inspect your Terraform state to see every resource Terraform is managing.
  • Destroy all AWS resources with a single command.
Inspect your Terraform state

Terraform tracks every resource it creates in a state file. This is how it knows what exists in your AWS account, what needs updating, and what to delete.

  • View all the resources Terraform is managing by running this command:
terraform state list

You should see 7 resources listed in your terminal output.

What are these 7 resources?

Each line maps directly to a resource block you defined in main.tf:

  • aws_s3_bucket.website is the S3 bucket itself.
  • aws_s3_bucket_ownership_controls.website, aws_s3_bucket_public_access_block.website, and aws_s3_bucket_policy.website are the access and permissions configuration.
  • aws_s3_bucket_website_configuration.website is the static website hosting setup.
  • aws_s3_object.index and aws_s3_object.error are your uploaded HTML files.
Destroy all AWS resources

The terraform destroy command removes every resource in your state file from AWS. Terraform calculates the correct deletion order automatically, so dependent resources are removed before the resources they depend on.

  • Tear down your entire infrastructure by running this command:
terraform destroy -var="bucket_name=[[BUCKET_NAME=\"your-unique-bucket-name\"]]"

What happens when you run this?

Terraform reads your state file and builds a destruction plan. It lists every resource it will delete and waits for your confirmation. Nothing is removed until you explicitly type yes.

  • Review the destruction plan. You should see all 7 resources marked with a red minus sign, indicating they will be deleted.
  • Type yes and press Enter to confirm the destruction.

Terraform deletes each resource in the correct dependency order. Objects are removed first, then the bucket configuration, then the bucket itself. You should see Destroy complete! Resources: 7 destroyed. in your terminal output.

Seeing an error during destroy?

  • If you see a "bucket is not empty" error, make sure force_destroy = true is set on your aws_s3_bucket.website resource in main.tf. Run terraform apply first to update the bucket, then try terraform destroy again.
  • If you see a credentials error, verify your AWS credentials are still configured by running aws sts get-caller-identity.
  • Terraform destroy failed and I'm not sure why.

What just happened?

You just removed an entire cloud infrastructure setup with a single command. Your S3 bucket, its public access configuration, the website hosting setup, and both HTML files are all gone from AWS. Your local Terraform files (main.tf, variables.tf, outputs.tf) still exist on your machine. You can run terraform apply at any time to recreate the entire setup from scratch.

Secret mission

Multi-Page Site with Terraform Variables

Your site works, but real Terraform projects don't hard-code values on the command line. Take your project further by adding a second page and managing your configuration through a .tfvars file. This is how professional teams handle environment-specific settings.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project created AWS S3 resources that are covered by the free tier (5 GB storage and 20,000 GET requests per month) for the first 12 months of your AWS account.

Cost warning

If your AWS account is past the 12-month free tier period, leaving an S3 bucket with public objects could incur small storage and request charges. If you are unsure whether your free tier is still active, the safest option is to delete your resources now.

Resources you used:

  • S3 bucket configured for static website hosting.
  • S3 objects (index.html and error.html) uploaded to the bucket.
  • S3 bucket policy, public access block, and ownership controls.
  • Local project directory (terraform-static-site) with Terraform configuration files and state.
  • AWS CLI credentials configured on your machine.

Keep everything running

No action needed. Choose this if your AWS account is still within the free tier period and you want to keep your static website live.

  • Your S3 bucket and website will remain accessible at the URL from your Terraform output.
  • Your local Terraform files and state file will stay in your terraform-static-site directory.
  • You can run terraform destroy at any time later to clean up.

Pause - I'll come back to this later

S3 is a storage service with no running processes to pause. Your best option is to destroy the AWS resources now and re-create them later from your saved Terraform files.

  • If you already ran terraform destroy in Step 5, your AWS resources are already deleted. No further action needed.
  • If your resources are still running (for example, after completing the Secret Mission), navigate to your project directory and run:
terraform destroy
  • Type yes when prompted to confirm.
  • Keep your local terraform-static-site directory. When you are ready to come back, run terraform apply to recreate everything from your configuration files.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your AWS resources and your local project files.

  • If you already ran terraform destroy in Step 5, your AWS resources are already deleted. Skip to deleting local files.
  • If your AWS resources are still running, navigate to your project directory and run:
terraform destroy
  • Type yes when prompted to confirm.
  • Delete the local project directory:
cd ~ && rm -rf terraform-static-site
  • Terraform CLI and AWS CLI can stay installed on your machine for future projects. If you want to remove them, run:
brew uninstall hashicorp/tap/terraform
brew uninstall awscli

Mission accomplished

That's a Wrap!

You just went from an empty directory to a live website on AWS, all through code. Every resource was defined in HCL, previewed with plan, and created with apply. That is Infrastructure as Code in action.

Here's what you accomplished:

  • Wrote HCL configuration files that defined an S3 bucket, public access policies, website hosting, and file uploads as declarative code.
  • Ran the full Terraform lifecycle (init, plan, apply, and destroy) to provision, update, and tear down real AWS infrastructure.
  • Used Terraform variables and outputs to make your configuration flexible and surface your live website URL automatically.
  • Added a multi-page site with navigation and managed configuration through a .tfvars file.

Ready to quiz yourself?