Deploy AWS Infrastructure with Terraform

Provision a VPC, subnets, and EC2 web server on AWS using Terraform.

Introduction

30 Second Summary

Every time you click through the AWS Console to create a server, you're doing work that could be lost, forgotten, or impossible to repeat. Imagine if your entire cloud setup lived in a file you could version, share, and recreate with a single command.

In this project, you will write Terraform code that builds an entire AWS network and web server from scratch. You will go from an empty file to a live website without ever clicking "Create" in the console.

What You'll Build

A complete AWS infrastructure stack defined in HCL configuration files, including a custom VPC, subnets, networking rules, and a running EC2 web server.

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

  • A fully functional VPC with public and private subnets deployed entirely through Terraform code.
  • A running EC2 web server accessible via a public IP address, provisioned with zero console clicks.
  • A reusable Terraform project you can extend, version control, and use as a portfolio piece.
  • Secret Mission: Add a second EC2 instance in a private subnet and configure a bastion host pattern for secure access.

Are there any prerequisites?

You need an existing AWS account (Free Tier is fine). No prior Terraform experience is required.

Install Your Tools

To deploy infrastructure with code, you need three tools working together on your machine.

In this step, you will install VS Code for writing configuration files, the AWS CLI for connecting to your AWS account, and Terraform for turning your code into real cloud resources.

In this step, get ready to:
  • Install and verify VS Code.
  • Install and configure the AWS CLI.
  • Install and verify Terraform.
Install VS Code

VS Code is the code editor where you will write all of your Terraform configuration files.

  • Check if VS Code is already installed by running this command in a Command Prompt:
code --version

✔️ I see a version number

VS Code is ready to go. Skip ahead to the next substep.

ⓧ Command not found

  • Download the User Setup installer (Windows x64) from the VS Code download page:
  • https://code.visualstudio.com/Download
  • Run the downloaded installer and accept all defaults.
  • Close any open Command Prompt windows after installation finishes.
  • Open a new Command Prompt and run the version check again:
code --version

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

Still not seeing a version number?

Make sure you opened a brand new Command Prompt window after the installation. Existing windows do not pick up PATH changes.

If that does not help, try restarting your computer and running the command again.

Help me troubleshoot why code --version is not recognized in my Command Prompt after installing VS Code.

Install and configure the AWS CLI

The AWS CLI lets you interact with your AWS account from the terminal. Terraform uses the credentials stored by the AWS CLI to authenticate when creating resources.

  • Check if AWS CLI v2 is already installed by running this command in a Command Prompt:
aws --version

✔️ I see version 2.x

Your output starts with something like aws-cli/2.27.41. You are on AWS CLI v2 and ready to proceed to configuration below.

ⓧ I see version 1.x

AWS CLI v1 is outdated. You need v2 for this project.

  • Download the AWS CLI v2 MSI installer from this link:
  • https://awscli.amazonaws.com/AWSCLIV2.msi
  • Run the installer and accept all defaults. This replaces v1 with v2.
  • Close all Command Prompt windows and open a new one.
  • Verify by running:
aws --version

You should now see output starting with aws-cli/2. followed by a version number.

ⓧ Command not found

  • Download the AWS CLI v2 MSI installer from this link:
  • https://awscli.amazonaws.com/AWSCLIV2.msi
  • Run the downloaded installer and accept all defaults.
  • Close all Command Prompt windows and open a new one.
  • Verify the installation by running:
aws --version

You should see output starting with aws-cli/2. followed by a version number.

AWS CLI install not working?

Make sure you downloaded the file ending in .msi and ran it as an installer (double-click). Do not just extract it.

If aws --version still fails after install, restart your computer to ensure PATH changes take effect.

Help me troubleshoot why aws --version is not recognized after installing the AWS CLI v2 MSI on Windows.

Now that AWS CLI is installed, you need to give it your AWS credentials so Terraform can authenticate.

  • Configure your AWS credentials by running:
aws configure

What does this command do?

This command prompts you for four values, one at a time. It stores them in a file on your machine so that Terraform (and any other AWS tool) can use them to authenticate.

  • AWS Access Key ID: paste your access key from the AWS Console.
  • AWS Secret Access Key: paste your secret key (shown only once when you create the key).
  • Default region name: type us-east-1.
  • Default output format: type json.

Where do I find my Access Key ID and Secret Access Key?

In the AWS Console, click your username in the top-right corner and select Security credentials. Scroll to the Access keys section and click Create access key.

Copy the Access Key ID and Secret Access Key immediately. The secret key is only shown once at creation time.

  • Enter each value when prompted, pressing Enter after each one.
  • Verify the configuration worked by running:
aws sts get-caller-identity

You should see a JSON response containing your AWS account ID, user ARN, and user ID. This confirms your credentials are working.

Seeing an error about invalid credentials?

Double-check that you pasted the Access Key ID and Secret Access Key correctly with no extra spaces.

If you accidentally closed the key creation page before copying, create a new access key in the AWS Console and run aws configure again.

Help me fix AWS CLI credential errors after running aws configure on Windows.

Install Terraform

Terraform is the Infrastructure as Code tool that reads your configuration files and provisions real AWS resources. This project requires Terraform 1.15 or higher.

  • Check if Terraform is already installed by running this command in a Command Prompt:
terraform -version

✔️ I see version 1.15 or higher

Your output shows Terraform v1.15 or above. You are ready to go.

ⓧ I see an older version

You have Terraform installed, but you need version 1.15 or higher for this project.

  • Download the latest AMD64 zip for Windows from the Terraform install page:
  • https://developer.hashicorp.com/terraform/install
  • Extract the zip and replace the old terraform.exe in your existing Terraform folder with the new one.
  • Open a new Command Prompt and verify:
terraform -version

You should see Terraform v1.15 or higher in the output.

ⓧ Command not found

Terraform is not installed. Follow these steps to set it up.

  • Download the AMD64 zip for Windows from the Terraform install page:
  • https://developer.hashicorp.com/terraform/install
  • Create a folder called C:\terraform on your machine.
  • Extract the downloaded zip file into C:\terraform. You should see terraform.exe inside the folder.

Now you need to add that folder to your system PATH so Windows can find the terraform command.

  • Press the Windows key to open the Start Menu.
  • Type environment variables and select Edit the system environment variables.
  • Click the Environment Variables button at the bottom.
  • In the System variables section, select the row called Path and click Edit.
  • Click New and type C:\terraform.
  • Click OK on all open dialogs to save.
  • Close all Command Prompt windows and open a new one.
  • Verify by running:
terraform -version

You should see Terraform v1.15 or higher in the output.

Terraform command still not found?

Make sure terraform.exe is directly inside C:\terraform and not in a nested subfolder. Some zip extractors create an extra folder layer.

Confirm you added C:\terraform to the System variables Path (not User variables). Then open a brand new Command Prompt window.

Help me troubleshoot why terraform -version is not recognized after I added C:\terraform to my PATH on Windows.

Your toolchain is ready. Next up, you will create your first Terraform configuration file and connect it to AWS.

Configure the Terraform Provider

Terraform needs to know which cloud provider to work with and where to find its plugins.

In this step, you'll create your project folder, write the configuration that points Terraform at AWS, and initialize the project so Terraform can download the required plugin.

In this step, get ready to:
  • Create a project folder and open it in VS Code.
  • Write the Terraform provider configuration for AWS.
  • Initialize Terraform to download the AWS provider plugin.
Create the project folder

Every Terraform project lives in its own folder. Each .tf file inside that folder becomes part of your infrastructure definition.

  • Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, top-left of your keyboard).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir terraform-aws-infra
cd terraform-aws-infra
  • Confirm the folder exists by running:
ls

You should see the empty terraform-aws-infra folder listed on your Desktop.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the terraform-aws-infra folder, and click Open.

Why open the folder in VS Code?

Opening the folder sets it as your workspace root. VS Code's Explorer sidebar will show your project files, and the integrated terminal will start in the correct directory.

  • In the Explorer sidebar on the left, hover over the folder name and click the New File icon (a page with a plus sign).
  • Type main.tf and press Enter.

The file opens in the editor, ready for your first Terraform configuration.

Don't see the Explorer sidebar?

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

If VS Code asks whether you trust the folder, click Yes, I trust the authors.

Still stuck? Ask for help.

Write the provider block

Terraform uses a block called required_providers to know which plugin to download. The provider block then configures that plugin with your region.

  • In main.tf, paste the following configuration:
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  required_version = ">= 1.15"
}

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

What does this code do?

The terraform {} block defines project-level settings. Inside it, required_providers tells Terraform to download the AWS plugin from the HashiCorp registry.

The version = "~> 6.0" constraint means any 6.x release is acceptable, but not 7.0 or higher. This protects you from unexpected breaking changes.

The provider "aws" {} block configures the plugin itself. Setting region = "us-east-1" tells AWS where to create your resources.

  • Save main.tf by pressing Ctrl+S (Windows) or Cmd+S (macOS).

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on to initialization.

ⓧ I'd like to double check the full code

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

  required_version = ">= 1.15"
}

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

Terraform needs to download the AWS provider plugin before it can do anything with your configuration. The terraform init command handles this automatically.

  • In your VS Code integrated terminal, initialize the project by running:
terraform init

You should see a message ending with Terraform has been successfully initialized! along with a suggestion to run terraform plan.

  • Confirm the plugin was downloaded by running:
ls

You should see a .terraform directory and a .terraform.lock.hcl file alongside your main.tf.

What did terraform init create?

The .terraform directory stores the downloaded AWS provider plugin binary. Terraform reads from this folder every time you run a command.

The .terraform.lock.hcl file locks the exact provider version that was installed. This ensures everyone on a team uses the same version.

Seeing an error during init?

If you see a message about the provider not being found, double-check that source = "hashicorp/aws" is spelled exactly right in your main.tf.

If you see a network timeout, check your internet connection and try running terraform init again.

Help me troubleshoot terraform init errors.

Your Terraform project is now connected to AWS and ready to define infrastructure. Next up, you'll write the code that builds an entire VPC and networking layer.

Build the VPC and Networking

Your Terraform provider is configured and initialized. Now it's time to define actual AWS infrastructure in code.

Every cloud deployment starts with networking. You need a virtual network to isolate your resources, subnets to segment traffic, and a gateway to connect everything to the internet.

In this step, get ready to:
  • Create a VPC with public and private subnets.
  • Set up an internet gateway and route table for public internet access.
  • Preview your infrastructure plan with terraform plan.
Create the VPC and subnets

A VPC (Virtual Private Cloud) is your own isolated network inside AWS. Inside the VPC, subnets divide the network into segments with different access rules.

  • In main.tf, add the following code below the provider "aws" block:
# --- VPC and Networking ---

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "terraform-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "us-east-1a"

  tags = {
    Name = "public-subnet"
  }
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "private-subnet"
  }
}

What does this code do?

The aws_vpc resource creates an isolated virtual network with a CIDR block of 10.0.0.0/16. This gives you roughly 65,000 IP addresses to work with.

The public subnet sets map_public_ip_on_launch = true, meaning anything launched there automatically gets a public IP address. The private subnet has no public IP assignment, keeping resources hidden from the internet.

  • Save main.tf.
  • You won't see a change yet. The next chunk sets up internet routing, and then terraform plan will validate everything together.
Create the internet gateway and route table

A VPC is isolated by default. To let resources in the public subnet reach the internet, you need an internet gateway and a route table that directs outbound traffic through it.

  • In main.tf, add the following code below the private subnet resource:
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "terraform-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

What does this code do?

The internet gateway connects your VPC to the public internet. Without it, nothing inside the VPC can send or receive traffic from outside.

The route table defines a rule: any traffic headed to 0.0.0.0/0 (anywhere on the internet) should go through the internet gateway. The route table association binds this rule specifically to the public subnet, leaving the private subnet unreachable from the outside.

  • Save main.tf.
  • You won't see a visible change yet. The next substep runs terraform plan to validate all your networking code at once.

Seeing red squiggles in VS Code?

If VS Code shows syntax warnings, make sure each resource block has matching opening and closing braces. Also check that the nested route { } block inside the route table has its own matching braces.

help me find the syntax error in my Terraform code

✔️ 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

Here is the complete main.tf file up to this point:

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

  required_version = ">= 1.15"
}

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

# --- VPC and Networking ---

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "terraform-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "us-east-1a"

  tags = {
    Name = "public-subnet"
  }
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "private-subnet"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "terraform-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}
Preview the plan

Terraform lets you preview exactly what it will create before touching your AWS account. This dry run catches errors in your code and shows you the full picture of changes.

  • In your VS Code terminal, run this command to preview the infrastructure plan:
terraform plan

What does this output mean?

Terraform reads your main.tf file, compares it against the current state (empty, since you haven't applied anything), and shows what it would create. Each resource is listed with a + symbol indicating it will be added.

At the bottom, you'll see a summary line in the format Plan: X to add, 0 to change, 0 to destroy. where X is the number of resources Terraform will create. You should see 6 resources to add: the VPC, two subnets, the internet gateway, the route table, and the route table association.

  • Review the output in your terminal. You should see each of your 6 resources listed with + symbols and a summary line confirming resources to add.

Seeing errors instead of a plan?

If Terraform reports a syntax error, check that every resource block has matching opening { and closing } braces. A common mistake is missing the closing brace on the nested route { } block inside the route table.

If you see a credential or authentication error, verify that your AWS CLI is configured correctly by running aws sts get-caller-identity in your terminal. If that fails, re-run aws configure and enter your credentials again.

help me fix my terraform plan error

Your networking foundation is defined and validated. Next up, you'll add a security group and launch an EC2 instance that actually serves a web page through this network.

Define Security and Launch the EC2 Instance

Your network is built, but right now nothing can actually talk to it. A VPC without traffic rules is like a locked building with no doors.

In this step, you will define a security group that controls which traffic can flow in and out. Then you will launch an EC2 instance that serves a live web page from your public subnet.

In this step, get ready to:
  • Create a security group with HTTP and SSH ingress rules.
  • Launch a t3.micro EC2 instance running a web server.
  • Apply the configuration and access your web page in a browser.
Create a security group

A security group acts as a virtual firewall for your instance. You define rules that specify which ports accept inbound traffic and which outbound connections are allowed.

  • In main.tf, scroll to the bottom of the file (after the aws_route_table_association resource).
  • Add the security group and its traffic rules by pasting the following code:
# --- Security Group ---

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow HTTP and SSH inbound"
  vpc_id      = aws_vpc.main.id

  tags = {
    Name = "web-sg"
  }
}

resource "aws_vpc_security_group_ingress_rule" "http" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 80
  to_port           = 80
  ip_protocol       = "tcp"
}

resource "aws_vpc_security_group_ingress_rule" "ssh" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 22
  to_port           = 22
  ip_protocol       = "tcp"
}

resource "aws_vpc_security_group_egress_rule" "all_outbound" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

What does this code do?

This creates four resources that work together as one security layer.

  • aws_security_group "web" defines the group itself and attaches it to your VPC.
  • The http ingress rule opens port 80 so browsers can reach your web server.
  • The ssh ingress rule opens port 22 so you can connect remotely to the instance.
  • The egress rule with ip_protocol = "-1" allows all outbound traffic so the instance can download packages.
  • Save main.tf.

You won't see a change yet. The next chunk uses this security group to protect your EC2 instance.

Seeing a syntax error?

  • Make sure each resource block has matching opening and closing braces.
  • Check that vpc_id = aws_vpc.main.id is inside the aws_security_group block, not inside a rule block.
  • Confirm there is no trailing comma after the last attribute in each block.

Help me fix a syntax error in my Terraform security group code

Look up the latest Amazon Linux AMI and create the instance

Instead of hardcoding an AMI ID (which changes per region and over time), you will use a data source that automatically finds the latest Amazon Linux 2023 image.

  • Below the security group code you just added, paste the EC2 instance configuration:
# --- EC2 Instance ---

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
    #!/bin/bash
    dnf install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<h1>Hello from Terraform!</h1><p>This server was provisioned with Infrastructure as Code.</p>" > /var/www/html/index.html
  EOF

  tags = {
    Name = "terraform-web-server"
  }
}

What does this code do?

  • The data "aws_ami" block queries AWS for the most recent Amazon Linux 2023 AMI. The filters narrow results to x86_64 HVM images.
  • The aws_instance "web" resource launches a t3.micro instance in the public subnet with your security group attached.
  • The user_data script runs once at launch. It installs Apache (httpd), starts the web server, and writes a simple HTML page.

Why dnf instead of yum?

Amazon Linux 2023 uses dnf as its package manager. Earlier Amazon Linux versions used yum, but AL2023 moved to the newer tool.

  • Now add the output blocks at the very bottom of the file, after the instance resource:
# --- Outputs ---

output "web_server_public_ip" {
  description = "Public IP of the web server"
  value       = aws_instance.web.public_ip
}

output "web_server_url" {
  description = "URL to access the web server"
  value       = "http://${aws_instance.web.public_ip}"
}

What do outputs do?

Outputs print key values to your terminal after Terraform finishes applying. Here you will see the instance's public IP and a clickable URL so you can immediately open your web page.

  • Save main.tf.

Seeing red squiggles in VS Code?

  • Make sure the heredoc markers match exactly: <<-EOF at the start and EOF on its own line to close.
  • Check that the interpolation in the output uses ${aws_instance.web.public_ip} with the dollar sign and curly braces.

Help me debug my Terraform EC2 instance code

✔️ 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

Here is the full main.tf up to this point. Compare your file against it:

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

  required_version = ">= 1.15"
}

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

# --- VPC and Networking ---

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "terraform-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "us-east-1a"

  tags = {
    Name = "public-subnet"
  }
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "private-subnet"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "terraform-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# --- Security Group ---

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow HTTP and SSH inbound"
  vpc_id      = aws_vpc.main.id

  tags = {
    Name = "web-sg"
  }
}

resource "aws_vpc_security_group_ingress_rule" "http" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 80
  to_port           = 80
  ip_protocol       = "tcp"
}

resource "aws_vpc_security_group_ingress_rule" "ssh" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 22
  to_port           = 22
  ip_protocol       = "tcp"
}

resource "aws_vpc_security_group_egress_rule" "all_outbound" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

# --- EC2 Instance ---

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
    #!/bin/bash
    dnf install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<h1>Hello from Terraform!</h1><p>This server was provisioned with Infrastructure as Code.</p>" > /var/www/html/index.html
  EOF

  tags = {
    Name = "terraform-web-server"
  }
}

# --- Outputs ---

output "web_server_public_ip" {
  description = "Public IP of the web server"
  value       = aws_instance.web.public_ip
}

output "web_server_url" {
  description = "URL to access the web server"
  value       = "http://${aws_instance.web.public_ip}"
}
Apply the configuration

With all the code in place, it is time to provision real AWS resources. Terraform will create the security group, find the AMI, launch the instance, and return your public IP.

  • Deploy your entire infrastructure by running this command in the VS Code terminal:
terraform apply
  • Terraform will show a plan of all resources it will create. Review the list.
  • When you see the prompt asking if you want to perform these actions, type yes and press Enter.
  • Wait about 1-2 minutes for AWS to provision all resources.

What should I see?

Terraform will print progress as each resource is created. When it finishes, you will see a message confirming the resources were added and your two outputs printed below it.

The web_server_public_ip output shows the EC2 instance's IP address. The web_server_url output gives you the full URL you can open in a browser.

  • Copy the web_server_url value from the terminal output.
  • Paste it into your browser's address bar and press Enter.

You should see a web page displaying "Hello from Terraform!" and a line about Infrastructure as Code.

Page not loading?

  • Make sure you are using http:// (not https://) in the URL. Your server only listens on port 80.
  • Wait 1-2 minutes after apply completes. The instance needs time to boot and run the user_data script.
  • Verify your security group allows port 80 inbound. Check that from_port = 80 and to_port = 80 are set in the http ingress rule.

Help me troubleshoot why my Terraform EC2 web page is not loading

Your infrastructure is live. You just deployed a full VPC, networking stack, security rules, and a web server without clicking a single button in the AWS Console. Next up, you will inspect Terraform's state and learn how to modify and destroy your resources.

Explore and Destroy

Your EC2 instance is live and serving a web page from code alone. Now comes an equally important skill: managing infrastructure after it's deployed.

Terraform tracks every resource it creates in a state file. In this step, you will inspect that state, make a live change to see how Terraform detects and applies updates, and then destroy all resources with a single command.

In this step, get ready to:
  • Inspect Terraform's state file to see all managed resources.
  • Modify a resource to see Terraform's change detection in action.
  • Destroy all resources with a single command.
Inspect the state

Every time you run terraform apply, Terraform records what it created in a local state file. This file is how Terraform knows what it manages and what needs updating.

  • List all resources Terraform is tracking by running this command:
terraform state list

You should see a list of every resource from your configuration, including aws_vpc.main, aws_subnet.public, aws_instance.web, and more.

  • See the full details of your infrastructure by running:
terraform show

This displays the complete attributes of every resource, including IDs, IP addresses, and tags that AWS assigned at creation time.

What is Terraform state?

The state file (terraform.tfstate) is Terraform's record of what exists in the real world. Without it, Terraform would not know which resources it manages and which ones already existed.

Every time you run plan or apply, Terraform compares your configuration files against this state to determine what changes are needed.

Seeing an empty list?

Make sure you are running the command from inside the terraform-aws-infra folder where your main.tf and terraform.tfstate files live.

If you accidentally deleted the state file, Terraform loses track of your resources. You would need to import them or destroy them manually in the AWS Console.

help me troubleshoot why terraform state list is empty

Make a change

One of the most powerful features of Infrastructure as Code is declarative updates. You change your code, and Terraform figures out exactly what to modify in the real world.

  • In main.tf, scroll to the aws_vpc resource block near the top of the file.
  • Find the Name tag line:
    Name = "terraform-vpc"
  • Change the value to terraform-vpc-v2, so the line looks like this:
    Name = "terraform-vpc-v2"
  • Save main.tf.
  • Preview what Terraform will change by running:
terraform plan

You should see Terraform detect the tag change and propose an update to 1 resource. The plan shows a ~ symbol next to the VPC, meaning it will be modified in place (not destroyed and recreated).

What does the ~ symbol mean?

Terraform uses three symbols in its plans: + for create, ~ for update in place, and - for destroy. Changing a tag is a non-destructive update, so Terraform modifies the existing resource instead of replacing it.

  • Apply the change by running:
terraform apply
  • Terraform will show the plan and ask for confirmation. Type yes and press Enter.

Terraform updates the VPC tag in AWS within a few seconds. This is the declarative workflow: you describe what you want, and Terraform makes it so.

Seeing 0 resources to change?

Make sure you saved main.tf after editing the tag. Terraform reads the file from disk, so unsaved changes in VS Code are invisible to it.

help me figure out why terraform plan shows no changes

Destroy everything

You have proven you can create and update infrastructure with code. The final lifecycle skill is tearing it all down cleanly.

  • Destroy all resources managed by Terraform by running:
terraform destroy
  • Terraform will display a destruction plan showing every resource marked with - (destroy). Review the list to confirm it matches what you created.
  • Type yes and press Enter to confirm.

Terraform removes all resources in the correct dependency order (instance first, then networking, then VPC last). This takes about 1-2 minutes.

Why does order matter?

AWS will not let you delete a VPC that still has subnets or instances inside it. Terraform reads the dependency graph from your configuration and destroys resources in reverse order automatically. You never have to figure out the right sequence yourself.

  • Verify in the AWS Console that your resources are gone:
  • Open the AWS Management Console in your browser.
  • Navigate to the VPC service and confirm that terraform-vpc-v2 is no longer listed.
  • Navigate to the EC2 service and confirm that terraform-web-server is terminated or gone.

Destroy failed or timed out?

If the destroy fails on the security group, it sometimes means a dependency is still being cleaned up. Wait 30 seconds and run terraform destroy again.

If you see a credentials error, your AWS CLI session may have expired. Run aws configure again to re-enter your credentials.

help me troubleshoot a failed terraform destroy

Secret mission

Build a Bastion Host Pattern

In production environments, sensitive servers sit in private subnets with no direct internet access. The only way to reach them is through a hardened public instance called a bastion host. Add a second EC2 instance to your private subnet and lock its security group so only the public instance can connect to it.

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. Your EC2 instances consume AWS Free Tier credits while running, and will cost money if your credits run out.

Cost warning

The two t3.micro instances in this project consume Free Tier credits while they are running. If your credits are depleted, you will be charged the standard hourly rate.

If you are not actively using these resources, run terraform destroy to remove everything and avoid unexpected charges.

Resources you used:

  • AWS resources: 1 VPC, 2 subnets, 1 internet gateway, 1 route table, 2 security groups, 2 EC2 instances (t3.micro).
  • Local project folder: terraform-aws-infra containing main.tf, .terraform directory, and state files.
  • Installed tools: Terraform, AWS CLI v2, VS Code.
  • AWS credentials stored in %UserProfile%\.aws\ folder.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the project soon.

  • Your two EC2 instances will continue consuming Free Tier credits while running.
  • Monitor your billing dashboard in the AWS Console under Billing and Cost Management to track credit usage.
  • Terraform, AWS CLI, and VS Code remain installed on your machine. They take minimal disk space.

Pause - I'll come back to this later

Stop the running instances to pause credit consumption while keeping your Terraform files and state intact.

  • Open the AWS Console and navigate to EC2, then Instances.
  • Select both terraform-web-server and terraform-private-server.
  • Click Instance state, then Stop instance.
  • Confirm by clicking Stop.
  • When you are ready to resume, select the instances and click Instance state then Start instance.

Why stop instead of terminate?

Stopping an instance pauses charges while preserving its configuration. Terminating removes it permanently. Since Terraform manages your infrastructure, you can always recreate everything with terraform apply even after termination.

Delete - I don't want to use this again

Remove all project resources from AWS and your local machine. You can always recreate everything later by running terraform apply again.

Remove all AWS resources
  • Open your terminal in the terraform-aws-infra folder.
  • Run the following command to destroy all resources:
terraform destroy
  • Review the destruction plan showing all resources to be removed.
  • Type yes when prompted and press Enter.
  • Wait for Terraform to delete all resources in the correct dependency order.
Verify in the AWS Console
  • Open the AWS Console and navigate to VPC. Confirm that terraform-vpc is gone.
  • Navigate to EC2 then Instances. Confirm both instances show as terminated or are no longer listed.
Delete local project files

Remove the project folder from your machine.

On macOS or Linux:

rm -rf ~/Desktop/terraform-aws-infra

On Windows (Command Prompt):

rmdir /s /q "%USERPROFILE%\Desktop\terraform-aws-infra"
Remove AWS credentials (optional)
  • If you no longer need AWS CLI access, delete your stored credentials by removing the .aws folder.

On macOS or Linux:

rm -rf ~/.aws

On Windows (Command Prompt):

rmdir /s /q "%USERPROFILE%\.aws"
Uninstall tools (optional)
  • To uninstall AWS CLI v2 on Windows, open Settings, select Apps, find AWS Command Line Interface, and click Uninstall.
  • To remove Terraform on Windows, delete the folder where you extracted the binary (e.g., C:\terraform) and remove that folder from your system PATH.
  • To uninstall VS Code on Windows, open Settings, select Apps, find Microsoft Visual Studio Code, and click Uninstall.

Nice Work!

Nice Work!

You just provisioned an entire AWS infrastructure stack with code. No clicking, no console, no guesswork.

What you learned:

  • Wrote HCL configuration defining a VPC with public and private network segmentation.
  • Used the full Terraform lifecycle (init, plan, apply, destroy) to manage infrastructure from creation to teardown.
  • Launched a web server on EC2 accessible from the internet, provisioned entirely through code.
  • Built a bastion host pattern for secure access to private resources.

Ready to quiz yourself?