Automate AWS Infrastructure with Terraform

Deploy a live S3 website using Terraform and a GitHub Actions CI/CD pipeline.

Introduction

30 Second Summary

Every time you click through the AWS console to deploy something, you are creating a process that only lives in your memory. If you leave the team, that knowledge walks out the door with you.

In this project, you will write Terraform configuration that defines your entire infrastructure as code, then build a GitHub Actions pipeline that deploys it automatically every time you push a change.

What You'll Build

You will push a code change to GitHub and watch a pipeline provision a live website on AWS without touching the console once.

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

  • A live static website hosted on S3 that you can open in any browser, deployed entirely from Terraform configuration files.
  • A GitHub Actions pipeline that automatically plans infrastructure changes on pull requests and applies them on merge to main.
  • First-hand experience detecting and fixing infrastructure drift when someone makes a manual change outside your code.
  • Secret Mission: Add a protected destroy workflow that requires manual approval before tearing down infrastructure.

Are there any prerequisites?

You will need an AWS account with admin access, a GitHub account, and Git installed on your machine. Experience with basic terminal commands and GitHub Actions is helpful.

No prior Terraform experience is required. This project teaches you from scratch.

Before We Start

Before any hands-on work begins, let's lock in what you're about to build and why it matters for a DevOps role.

Set Up Terraform and AWS Credentials

Before you can write a single line of infrastructure code, you need the tools that turn that code into real cloud resources.

In this step, you will install the Terraform CLI, configure your AWS CLI credentials, and set up a GitHub repository to hold your project.

In this step, get ready to:
  • Install the Terraform CLI and verify it works.
  • Configure AWS CLI credentials and verify access to your AWS account.
  • Create and clone a GitHub repository for this project.
Install the Terraform CLI

Terraform is the tool that reads your .tf configuration files and provisions the corresponding cloud resources. You need it installed locally before you can run any Terraform commands.

  • Check whether Terraform is already installed by running this command:
terraform version

Check your output and pick the tab that matches what you see.

✔️ I see version 1.15 or higher

You already have a compatible Terraform version installed. Move on to the next substep.

ⓧ I see an older version

You have Terraform installed, but the project requires version 1.15.0 or higher. Upgrade by reinstalling from the HashiCorp APT repository.

  • Add the HashiCorp GPG key and repository, then install the latest Terraform by running these commands:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
  • Verify the upgrade by running:
terraform version

You should now see Terraform v1.15.x in the output.

ⓧ Command not found

Terraform is not installed yet. Install it from the official HashiCorp APT repository.

  • Add the HashiCorp GPG key and repository, then install Terraform by running these commands:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
  • Verify the installation by running:
terraform version

You should see Terraform v1.15.x in the output.

What does terraform version tell you?

This command prints the installed Terraform version and the platform you are running on. The project requires 1.15.0 or higher because that is what the required_version constraint in the Terraform config enforces.

Still seeing an old version after installing?

If terraform version still shows an older version, you may have a previously installed binary on your PATH that takes precedence. Run which terraform to find its location and remove the old binary, then try again.

Help me troubleshoot my Terraform installation on Linux.

Configure AWS CLI credentials

Terraform uses the AWS CLI's credentials to authenticate with your AWS account. You need a working AWS CLI with an access key configured before Terraform can provision anything.

  • Check whether the AWS CLI is already installed by running:
aws --version

Check your output and pick the tab that matches.

✔️ I see a version number

The AWS CLI is already installed. Skip ahead to the configuration step below.

ⓧ Command not found

The AWS CLI is not installed. Install it using the official installer.

  • Download and install the AWS CLI by running these commands:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
  • Verify the installation by running:
aws --version

You should see a version string starting with aws-cli/2. confirming AWS CLI v2 is installed.

Now configure your credentials. The aws configure command will prompt you for four values.

  • Run the configuration wizard by running:
aws configure

What will aws configure ask you?

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

  • AWS Access Key ID - paste your IAM access key.
  • AWS Secret Access Key - paste your IAM secret key.
  • Default region name - enter us-east-1.
  • Default output format - press Enter to accept the default (json).
  • Enter your AWS Access Key ID when prompted.
  • Enter your AWS Secret Access Key when prompted.
  • Enter us-east-1 for the default region.
  • Press Enter to accept the default output format.

Now verify that your credentials work by calling the AWS Security Token Service.

  • Confirm your identity by running:
aws sts get-caller-identity

You should see a JSON response containing your AWS account ID, user ARN, and user ID.

Seeing an error instead of your account ID?

If you see an "Unable to locate credentials" or "InvalidClientTokenId" error, double-check that you pasted the correct access key and secret key during aws configure. You can re-run the command to overwrite the values.

Help me fix my AWS CLI credentials configuration.

Create and clone a GitHub repository

Your Terraform code and GitHub Actions workflow will live in a Git repository. You need a new repo on GitHub so you can push code and trigger automated pipelines later.

  • Open your browser and go to github.com/new.
  • Enter terraform-iac-pipeline as the repository name.
  • Leave the repository set to Public.
  • Do not check any initialization boxes (no README, no .gitignore, no license).
  • Click Create repository.

GitHub shows a Quick Setup page with a URL for your new empty repository.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone the empty repository by running:
git clone [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/terraform-iac-pipeline.git"]]
  • Move into the cloned project folder by running:
cd terraform-iac-pipeline
  • Confirm you are inside the repository by running:
ls -la

You should see a .git directory in the output, confirming this is a Git repository.

Clone failed or permission denied?

Make sure you copied the correct URL from the GitHub Quick Setup page. If you see a permission error, verify that your Git is configured with your GitHub credentials. You can use gh auth login or set up an SSH key.

Help me fix my git clone error.

Verify your full setup

Before you move on, confirm that both tools are ready and you are inside your project directory.

What do you expect to see if everything is set up correctly?

  • Run both verification commands:
terraform version
aws sts get-caller-identity

You should see Terraform v1.15.x from the first command and a JSON object with your AWS account ID from the second.

What does this confirm?

Terraform can run on your machine, your AWS credentials are valid, and you have a local Git repository ready for code. Everything is in place to start writing infrastructure configuration.

Your toolchain is ready. Next up, you will write Terraform configuration that provisions a live website on AWS, all from a single file.

Deploy Your First Infrastructure with Terraform

In Step 1 you installed Terraform and configured your AWS credentials. Now it is time to use those tools to deploy a real, live website.

Traditionally, hosting a website on AWS means clicking through dozens of console screens. With Infrastructure as Code, you describe everything in a text file and let Terraform build it for you. In this step, you will write the config, run it, and open your live site in a browser.

In this step, get ready to:
  • Write Terraform configuration that defines an S3 static website with public access.
  • Run the Terraform workflow (init, plan, apply) to provision real AWS resources.
  • Open your live website URL in a browser and see it working.
Create your website file and initialize Terraform

Terraform will upload a local HTML file to S3. You need to create that file first so Terraform can reference it.

  • Create a file called index.html in the root of your cloned repository by running:
touch index.html
  • Open index.html in your text editor and paste the following content:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DevOps IaC Project</title>
</head>
<body>
    <h1>Infrastructure as Code Works!</h1>
    <p>This website is managed entirely through Terraform.</p>
</body>
</html>

What does this file do?

This is the HTML page visitors will see when they open your website URL. Terraform will upload it to S3 and serve it as a static site.

  • Save index.html.

Now you need to tell Terraform what infrastructure to create. A Terraform configuration file declares which cloud provider to use, which resources to provision, and how they connect.

  • Create a file called main.tf in the same directory by running:
touch main.tf
  • Open main.tf and paste the following provider configuration and first two resources:
terraform {
  required_version = ">= 1.15.0"

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

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "website" {
  bucket = "devops-iac-project-[[BUCKET_SUFFIX=\"your unique suffix e.g. your GitHub username\"]]"
}

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 terraform block pins the minimum Terraform version and declares the AWS provider from hashicorp/aws. The provider block tells Terraform to use the us-east-1 region for all resources.

The aws_s3_bucket resource creates your S3 bucket with a globally unique name. The aws_s3_bucket_public_access_block resource disables all four public access restrictions so visitors can reach your site.

Why does the bucket name need to be unique?

S3 bucket names are globally unique across all AWS accounts. Adding your GitHub username or a random string prevents collisions with other learners.

  • Save main.tf.
  • Confirm both files exist by running:
ls

You should see index.html and main.tf in the output.

Don't see both files?

Make sure you are inside your cloned repository directory. Run pwd to confirm your location, then re-run the touch commands if needed.

Help me find my project files

With the provider declared, Terraform can download the plugin code it needs to talk to AWS.

  • Initialize Terraform by running:
terraform init

Terraform downloads the AWS provider and creates a .terraform.lock.hcl file that locks the provider version. You should see a message ending with "Terraform has been successfully initialized!"

Seeing a provider error?

If you see an error about the provider source, double-check that the required_providers block uses hashicorp/aws exactly. If you see a credentials error, run aws sts get-caller-identity to confirm your CLI is still authenticated.

Help me debug my terraform init error

Complete the infrastructure configuration

The bucket exists in code but it cannot serve a website yet. You need three more resources: a website configuration, a bucket policy that allows public reads, and an object to upload your HTML file.

  • Open main.tf and add the following code below the public access block resource:
resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document {
    suffix = "index.html"
  }
}

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?

The aws_s3_bucket_website_configuration resource enables static website hosting and tells S3 to serve index.html as the default page.

The aws_s3_bucket_policy resource attaches a JSON policy granting everyone read access to objects in the bucket. The depends_on line ensures Terraform disables the public access block before applying this policy. Without it, AWS would reject the policy because Block Public Access is still enabled.

  • Still in main.tf, add the final resource below the bucket policy:
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")
}

What do these pieces do?

The aws_s3_object resource uploads your local index.html into the bucket. The etag attribute uses filemd5() to detect content changes so Terraform will re-upload the file whenever you edit it.

  • Save main.tf.
  • Create a file called outputs.tf and paste the following:
output "website_url" {
  value       = aws_s3_bucket_website_configuration.website.website_endpoint
  description = "The URL of the static website"
}

Why an outputs file?

The outputs.tf file tells Terraform to print the website URL after provisioning completes. This saves you from hunting through the AWS console for the endpoint.

  • Save outputs.tf.

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files before continuing.

ⓧ I'd like to double check the full code

Your main.tf should contain these sections in order. Scroll up and confirm each chunk is present:

  • The terraform block with required_version and required_providers.
  • The provider "aws" block with region = "us-east-1".
  • The aws_s3_bucket resource with your unique bucket name.
  • The aws_s3_bucket_public_access_block resource with all four settings set to false.
  • The aws_s3_bucket_website_configuration resource.
  • The aws_s3_bucket_policy resource with depends_on.
  • The aws_s3_object resource referencing index.html.

Your outputs.tf should contain:

output "website_url" {
  value       = aws_s3_bucket_website_configuration.website.website_endpoint
  description = "The URL of the static website"
}
Deploy and verify your live website

Your configuration is complete. Time to let Terraform show you exactly what it plans to create before spending any money.

  • Preview the infrastructure changes by running:
terraform plan

At the bottom of the output you should see: Plan: 5 to add, 0 to change, 0 to destroy. This confirms Terraform will create 5 new resources and destroy nothing.

What are the 5 resources?

  • aws_s3_bucket.website (the bucket itself).
  • aws_s3_bucket_public_access_block.website (disables public access restrictions).
  • aws_s3_bucket_website_configuration.website (enables static hosting).
  • aws_s3_bucket_policy.website (allows public reads).
  • aws_s3_object.index (uploads your HTML file).

Plan shows errors instead of 5 resources?

A syntax error usually means a missing brace or quote in main.tf. Check that every opening { has a matching } and every string is wrapped in double quotes.

If you see an error about index.html not being found, make sure the file is in the same directory where you are running terraform plan.

Help me fix my terraform plan errors

Before you run the next command, predict: will Terraform ask for confirmation, or will it start creating resources immediately?

  • Apply your configuration by running:
terraform apply

Terraform shows the plan again and asks you to confirm. Type yes and press Enter.

After a few seconds, you should see "Apply complete! Resources: 5 added, 0 changed, 0 destroyed." followed by the website URL in the outputs section.

Apply failed with an access denied error?

If you see "AccessDenied" or "not authorized", your IAM user may not have S3 permissions. Confirm your credentials have the AmazonS3FullAccess policy attached in the IAM console.

If you see a "BucketAlreadyExists" error, someone else has already taken that bucket name. Change the suffix in your main.tf to something more unique, save, and run terraform apply again.

Help me fix my terraform apply failure

Now for the moment of truth. Before you open the URL, what do you expect to see in the browser?

  • Get your website URL by running:
terraform output website_url
  • Copy the URL from the terminal output and open it in your browser.

You should see a page with the heading "Infrastructure as Code Works!" and a paragraph below it. That page is live on the internet, hosted on S3, and was created entirely from your text files.

Page not loading or showing an error?

Make sure you are using http:// (not https://) in front of the URL. S3 website endpoints do not support HTTPS by default.

If you see a 403 Forbidden error, the bucket policy may not have applied correctly. Run terraform apply again to ensure all resources are in sync.

Help me access my S3 static website

Your website is live and every piece of it is defined in code. Next up, you will see what happens when someone makes a change outside of your code.

Discover Why Manual Changes Break Everything

In the previous step you deployed a live website entirely from a Terraform config file. That is the promise of Infrastructure as Code: your code is the single source of truth for what exists in the cloud.

But what happens when someone skips the code and makes a change directly in the AWS console? In this step, you will deliberately break the IaC contract to feel the pain that automation solves.

In this step, get ready to:
  • Make a manual change to your S3 bucket in the AWS console.
  • Detect infrastructure drift with terraform plan.
  • Fix drift with terraform apply and confirm the IaC contract holds.
Add a manual tag in the AWS console

You are about to simulate what happens when a teammate makes a "quick fix" directly in the console, bypassing your Terraform code entirely.

  • Open the S3 console at https://console.aws.amazon.com/s3/.
  • In the left navigation pane, choose General purpose buckets.
  • Click your bucket name (devops-iac-project-...) in the list.
  • Choose the Properties tab.
  • Scroll down to the Tags section and choose Add new Tag.
  • Enter Environment in the Key field.
  • Enter test in the Value field.
  • Choose Save changes.

You should see the tag appear in the Tags section with Key Environment and Value test.

What just happened?

You made a change to your infrastructure that Terraform knows nothing about. Your main.tf file does not mention any tags on the bucket. This gap between what your code says and what actually exists in AWS is called drift.

Detect drift with terraform plan

Now switch back to your terminal. Your code says the bucket has no tags. AWS says the bucket has one tag. What do you think Terraform will do when it compares the two?

  • Run terraform plan to compare your code against the live infrastructure:
terraform plan

Look at the output. Terraform reports that it will update the aws_s3_bucket.website resource. It shows the manually-added tag will be removed because your code does not define it.

What does this output mean?

Terraform compares the desired state (your .tf files) against the actual state (what exists in AWS). Any difference is drift. Terraform's plan tells you exactly what it will change to bring reality back in line with your code.

The - lines in the output show what will be removed (your manual tag). This is Terraform enforcing the rule: if it is not in code, it should not exist.

Not seeing any changes in the plan?

Make sure you saved the tag in the AWS console (the Save changes button must have been clicked). You can verify by refreshing the bucket's Properties tab and confirming the tag appears.

Also confirm you are running terraform plan from the same directory that contains your main.tf file.

help me understand why terraform plan is not detecting my manual tag change

Fix the drift with terraform apply

Terraform has identified the gap. Now let it enforce your code as the source of truth.

  • Apply the changes to bring your infrastructure back in line with your code:
terraform apply -auto-approve

Terraform removes the manual tag. Your bucket now matches your code exactly.

Why use -auto-approve here?

The -auto-approve flag skips the interactive "yes" confirmation prompt. You already reviewed what Terraform will do in the plan step, so approving again would be redundant. In automated pipelines (which you will build in the next step), this flag is required because there is no human at the terminal to type "yes".

Before you run the final check, predict what terraform plan will show now that you have applied. Will there still be changes, or will the plan be clean?

  • Confirm zero drift by running:
terraform plan

You should see the message: "No changes. Your infrastructure matches the configuration." This confirms the IaC contract is restored.

Still seeing planned changes?

If Terraform still shows changes after apply, the apply may not have completed successfully. Check the output of your previous terraform apply command for any errors. Common causes include expired AWS credentials or insufficient permissions.

help me debug why terraform plan still shows changes after I ran terraform apply

You just experienced the core problem that CI/CD automation solves. Running Terraform manually works for one person, but in a team someone will always "just quickly" change something in the console. Next up, you will build a GitHub Actions pipeline that enforces every change through code automatically.

Automate Deployment with GitHub Actions

You proved in the last step that manual console changes create drift that Terraform must fix. That works for one person at a terminal.

In a team, you need every infrastructure change reviewed before it lands and applied automatically after approval. A GitHub Actions workflow gives you exactly that: terraform plan runs on every pull request so reviewers see what will change, and terraform apply runs on merge to main so the infrastructure updates itself.

In this step, get ready to:
  • Create a GitHub Actions workflow that runs the full Terraform lifecycle.
  • Store your AWS credentials securely as repository secrets.
  • Push a change through a PR and watch the pipeline deploy it automatically.
Create the GitHub Actions workflow file

GitHub Actions workflows live in a special directory: .github/workflows/. Any YAML file in this folder becomes a workflow that GitHub runs automatically based on the triggers you define.

  • Create the workflow directory by running this command in your project folder:
mkdir -p .github/workflows
  • Confirm the directory was created by running:
ls .github/workflows

The directory should be empty for now. You will add your workflow file next.

  • Create a new file called terraform.yml inside .github/workflows/ and add the following trigger and job configuration:
name: Terraform

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

permissions:
  contents: read

jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest

What does this configuration do?

  • on: push and on: pull_request define when the workflow triggers. It runs on every push to main and on every PR targeting main.
  • permissions: contents: read follows the principle of least privilege. The workflow only needs to read your code.
  • runs-on: ubuntu-latest tells GitHub to run this job on a fresh Ubuntu virtual machine.
  • Below the runs-on: ubuntu-latest line, add the workflow steps:
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v4
        with:
          terraform_version: "1.15.8"

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v6
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Terraform Init
        run: terraform init

      - name: Terraform Format Check
        run: terraform fmt -check

      - name: Terraform Plan
        run: terraform plan -no-color

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve -no-color

What do these steps do?

  • actions/checkout@v4 clones your repository into the runner so Terraform can access your .tf files.
  • hashicorp/setup-terraform@v4 installs Terraform 1.15.8 on the runner, matching what you have locally.
  • aws-actions/configure-aws-credentials@v6 injects your AWS credentials from GitHub secrets into the runner's environment.
  • terraform fmt -check verifies your code is properly formatted. It fails the workflow if formatting is off.
  • The if: condition on the Apply step is critical. It means terraform apply only runs on pushes to main. PRs only get a plan, never an apply.
  • Save .github/workflows/terraform.yml.

✔️ Awesome, I've got everything!

Great. Double check you saved the file in the correct location: .github/workflows/terraform.yml.

ⓧ I'd like to double check the full code

Your terraform.yml should have these two sections in order:

  • The trigger and job configuration (name, on, permissions, and jobs with runs-on: ubuntu-latest).
  • The steps: section with seven steps: Checkout, Setup Terraform, Configure AWS Credentials, Terraform Init, Terraform Format Check, Terraform Plan, and Terraform Apply (with its if: condition).

Scroll up and confirm each section matches the code blocks above exactly.

File not in the right place?

Make sure the directory structure is exactly .github/workflows/ (with the dot prefix on .github). GitHub only detects workflows in this specific path.

If your editor created github/ without the leading dot, rename it to .github/.

Why isn't GitHub detecting my workflow file?

Configure GitHub repository secrets

Your workflow references ${{ secrets.AWS_ACCESS_KEY_ID }} and ${{ secrets.AWS_SECRET_ACCESS_KEY }}. These are encrypted values stored in your repository settings that GitHub injects into the runner at runtime.

  • Open your repository on GitHub in your browser.
  • Click Settings in the repository navigation bar.
  • In the left sidebar, under the Security section, select Secrets and variables, then click Actions.
  • Click the Secrets tab.
  • Click New repository secret.
  • In the Name field, type AWS_ACCESS_KEY_ID.
  • In the Secret field, paste the same AWS access key ID you configured with aws configure earlier.
  • Click Add secret.

You should see AWS_ACCESS_KEY_ID listed under Repository secrets.

  • Click New repository secret again.
  • In the Name field, type AWS_SECRET_ACCESS_KEY.
  • In the Secret field, paste your AWS secret access key.
  • Click Add secret.

You should now see both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY listed. GitHub encrypts these values so they never appear in logs or workflow output.

Why secrets instead of hardcoding credentials?

GitHub Actions automatically redacts secret values from workflow logs. If you hardcoded credentials in your workflow file, anyone with read access to the repo could see them.

Secrets are encrypted at rest and only injected into the runner environment at runtime. This keeps your AWS account safe even in a public repository.

Don't see the Settings tab?

The Settings tab only appears if you are the repository owner or have admin access. If you created the repository yourself, it should be visible.

If the tab is hidden behind a dropdown menu, click the three-dot menu icon in the repository navigation and select Settings from there.

Help me find where to add repository secrets in GitHub

Push changes and trigger the pipeline

Now you will update your website content, commit everything (the workflow file and the content change), and push to a branch. This triggers the full pipeline: plan on the PR, apply on merge.

  • In index.html, find these two lines inside the <body> tag:
    <h1>Infrastructure as Code Works!</h1>
    <p>This website is managed entirely through Terraform.</p>
  • Replace them with:
    <h1>Deployed by GitHub Actions!</h1>
    <p>This website is managed entirely through Terraform and deployed automatically via CI/CD.</p>
  • Save index.html.

Before committing, you need to exclude the .terraform/ directory. It contains downloaded provider binaries that are too large for GitHub and unnecessary because the workflow runs terraform init fresh.

  • Create a .gitignore file by running:
echo ".terraform/" > .gitignore
  • Create a new branch, stage all files, and commit by running these commands:
git checkout -b add-cicd-pipeline
git add .
git commit -m "Add Terraform CI/CD workflow and update website content"
  • Push the branch to GitHub by running:
git push origin add-cicd-pipeline

Git will print a URL to create a pull request. You will use the GitHub UI instead.

  • Switch to your browser and open your repository on GitHub.
  • You should see a banner saying your branch had recent pushes. Click Compare & pull request.
  • Leave the title and description as defaults, then click Create pull request.

GitHub immediately starts running your workflow. You can watch it in the Checks section at the bottom of the PR.

  • Click Details next to the Terraform check to see the full workflow log.
  • Expand the Terraform Plan step. You should see Terraform's plan output showing that it will update the aws_s3_object.index resource (because the HTML content changed).

Why does the plan show only the HTML change?

Your state file was committed along with the code. The runner has the same state you have locally, so Terraform knows the bucket and all other resources already exist. The only drift is the updated index.html content, which changes the etag hash.

The workflow ran terraform plan but did NOT run terraform apply. The if: condition prevented it because this is a PR, not a push to main.

  • Return to the PR page on GitHub.
  • Click Merge pull request, then click Confirm merge.

Merging pushes your changes to main. This triggers the workflow again, and this time the Apply step will execute.

  • Click the Actions tab in your repository navigation.
  • Click the most recent workflow run (triggered by the merge commit).
  • Expand the Terraform Apply step and confirm it shows Apply complete! Resources: 0 added, 1 changed, 0 destroyed. in the log output.

The workflow run should show a green checkmark. Your infrastructure was just updated automatically by a pipeline, with no manual Terraform commands.

Before you check the website, predict what you expect to see. Will the page still say "Infrastructure as Code Works!" or will it show the new content?

  • Get your website URL by running:
terraform output website_url
  • Open that URL in your browser.

You should see Deployed by GitHub Actions! as the heading. The pipeline detected your content change, ran terraform apply, and re-uploaded the HTML file to S3.

Still seeing the old content?

Try a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows). Browsers cache HTML aggressively.

If the Actions tab shows a red X on the workflow run, click into the failed step and read the error. Common causes: incorrect secret values (check for trailing spaces when pasting), or the secret names not matching AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY exactly.

My GitHub Actions Terraform workflow is failing. Help me debug it.

Why this approach over running Terraform manually?

Manual Terraform runs depend on one person's machine, credentials, and memory. If they forget to run terraform apply or apply from a stale branch, infrastructure drifts from code.

A CI/CD pipeline enforces the process. Every change goes through a PR (plan is visible to reviewers), and apply only happens on merge to main. Nobody can skip the review. Nobody can apply from the wrong branch.

Your infrastructure now deploys itself from code. Push a change, open a PR, review the plan, merge, and the pipeline handles the rest. Next up, you will tackle a stretch challenge: protecting dangerous operations like terraform destroy behind manual approval gates.

Secret mission

Add Destroy Protection with Environment Approval

In production, you never want a terraform destroy to run without someone approving it first. Can you create a protected destroy workflow that pauses for human approval before tearing down infrastructure?

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. If you completed the Secret Mission, your AWS resources are already destroyed. The GitHub repository, secrets, and local files still remain.

Cost warning

If you did NOT complete the Secret Mission, your S3 bucket is still live. The cost is negligible (under $0.01/month for a single HTML file), but it is not zero. If you want to stop all charges immediately, choose the Delete option below.

Resources you used:

  • S3 bucket devops-iac-project-your-unique-suffix with static website hosting (may already be destroyed via Secret Mission).
  • GitHub repository secrets (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY).
  • GitHub environment named destroy with protection rules.
  • Local project files (Terraform config, state files, HTML, workflow files).
  • Terraform CLI installed via APT.

Keep everything running

No action needed. Choose this if you plan to keep iterating on your Terraform project or want to reference the code later.

  • Your S3 bucket (if it still exists) costs under $0.01/month for a single HTML file with minimal traffic.
  • GitHub repository secrets and the destroy environment have no ongoing cost.
  • Local files and Terraform CLI remain on your machine for future projects.

Pause - I'll come back to this later

Remove the AWS resources to stop any charges, but keep your local files and GitHub repository so you can redeploy later with a single terraform apply.

  • If your S3 bucket still exists, run this from your project directory to destroy all AWS resources:
terraform destroy
  • Type yes when prompted to confirm.
  • If you already completed the Secret Mission, your AWS resources are gone. No action needed.
  • Keep your local project files and GitHub repository intact for when you return.

Delete - I don't want to use this again

Remove all project resources completely. Follow each step below.

Destroy AWS resources (skip if you completed the Secret Mission):

  • Run this from your project directory:
terraform destroy
  • Type yes when prompted. Terraform removes the S3 bucket and all associated resources.

Delete GitHub repository secrets:

  • Go to your repository on GitHub.
  • Click Settings.
  • In the left sidebar under Security, click Secrets and variables, then Actions.
  • Click the trash icon next to AWS_ACCESS_KEY_ID to delete it.
  • Click the trash icon next to AWS_SECRET_ACCESS_KEY to delete it.

Delete the GitHub environment:

  • In the same Settings page, click Environments in the left sidebar.
  • Click the trash icon next to the destroy environment.
  • Click I understand, delete this environment to confirm.

Delete local project files:

  • Run this command to remove your project folder and all Terraform state:
cd ~/Desktop
rm -rf devops-iac-project

Uninstall Terraform CLI:

  • Run this command to remove Terraform from your system:
sudo apt remove terraform
  • Optionally delete the GitHub repository entirely from Settings > General > scroll to Danger Zone > Delete this repository.

Nice Work!

Nice Work!

You just built a complete Infrastructure-as-Code pipeline from scratch. You can now point an interviewer at this repo and say: push a change, watch the infrastructure update itself.

You've learned how to:

  • Write Terraform configuration that provisions a live S3 static website and run the full workflow locally: init, plan, apply, and destroy.
  • Detect and fix infrastructure drift caused by manual console changes, proving why code must be the single source of truth.
  • Build a GitHub Actions pipeline that automatically runs terraform plan on pull requests and terraform apply on merge to main.
  • (Secret Mission) Created a destroy workflow gated behind GitHub environment approval rules, ensuring dangerous operations require human sign-off.

Ready to quiz yourself?