Deploy a VPC and Web Server with Terraform

Build a complete AWS network and EC2 web server using Terraform IaC.

Introduction

30 Second Summary

Every website you visit runs on servers and networks that someone had to build. Professional cloud engineers build this infrastructure by writing code, not by clicking through dashboards.

In this project, you will use Terraform to deploy a complete network and web server on AWS using Infrastructure as Code. You will define everything in code files, deploy from your terminal, and push to GitHub as a portfolio piece.

What You'll Build

A production-style AWS network with a custom VPC, public subnet, internet gateway, security group, and an EC2 instance serving a web page, all defined and deployed using Terraform.

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

  • A custom VPC network with a public subnet, internet gateway, and route table deployed entirely through Terraform code.
  • A running EC2 web server accessible via public IP, provisioned with zero console clicks.
  • A professional GitHub repository with clean documentation and all your Terraform code version-controlled.
  • Secret Mission: Add a private subnet and a Mermaid architecture diagram to your README.

Are there any prerequisites?

You need an AWS account (within the 12-month free tier) and a GitHub account. You will install Terraform and the AWS CLI in Step 1.

Install Terraform and AWS CLI

Before you can write any infrastructure code, you need two tools on your machine: Terraform (which turns your code into real cloud resources) and the AWS CLI (which lets your terminal talk to your AWS account).

In this step, you will install both tools on Windows, then connect the AWS CLI to your account so Terraform can create resources on your behalf.

In this step, get ready to:
  • Install Terraform 1.15.3 on Windows.
  • Install AWS CLI v2 on Windows.
  • Configure AWS credentials for programmatic access.
Install Terraform on Windows

Terraform is an Infrastructure as Code tool that reads .tf files and creates real cloud resources from them. You need it installed on your machine before you can run any Terraform commands.

  • Check if Terraform is already installed by running this command in a new terminal window:
terraform -help

✔️ I see a list of Terraform commands

Terraform is already installed. Now confirm you have the right version by running:

terraform version

You should see Terraform v1.15 or higher in the output. If so, skip ahead to the AWS CLI section below.

ⓧ I see an older version (below 1.15)

You have Terraform installed, but your version is too old for this project. Follow the steps in the next tab to download and install the latest version. The new binary will replace the old one when you update your PATH.

ⓧ 'terraform' is not recognized

Terraform is not installed yet. Follow the steps below to install it.

  • Open your browser and go to the Terraform install page:
  • https://developer.hashicorp.com/terraform/install
  • Under the Windows section, click the AMD64 binary download link for version 1.15.3.
  • Once the zip file downloads, right-click it and select Extract All.
  • Choose a permanent location for the extracted terraform.exe file. A good choice is C:\terraform.
  • Move terraform.exe into that folder.

Now you need to add this folder to your system PATH so Windows can find the terraform command from any terminal.

  • Press the Windows key to open your search bar.
  • Type Environment Variables and select Edit the system environment variables.
  • Click Environment Variables at the bottom of the window.
  • Under System variables, find and select Path, then click Edit.
  • Click New and type the path to your Terraform folder (e.g. C:\terraform).
  • Click OK on all open windows to save.
  • Close your current terminal and open a brand new one. This is important because PATH changes only take effect in new terminal sessions.
  • Verify Terraform is installed by running:
terraform -help

You should see a list of available Terraform commands including init, plan, and apply.

Still seeing 'terraform' is not recognized?

Make sure you opened a NEW terminal window after editing the PATH. Existing terminals will not pick up the change.

Double-check that terraform.exe is actually inside the folder you added to PATH. Open that folder in File Explorer and confirm the file is there.

Help me fix my Terraform PATH issue on Windows.

What is Terraform?

Terraform is an Infrastructure as Code tool made by HashiCorp. Instead of clicking through a cloud console to create resources, you write configuration files that describe what you want. Terraform reads those files and creates the resources for you.

The biggest advantage is repeatability. You can destroy everything and recreate it identically with a single command. You can also version-control your infrastructure alongside your application code.

Install AWS CLI v2 on Windows

The AWS CLI is a command-line tool that lets you interact with AWS services from your terminal. Terraform uses the credentials stored by the AWS CLI to authenticate with your AWS account.

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

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

The AWS CLI v2 is already installed. You are ready to configure your credentials. Skip ahead to the next section.

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

You have version 1 of the AWS CLI, which is outdated. Follow the install steps in the next tab to install version 2. The v2 installer will handle the upgrade for you.

ⓧ 'aws' is not recognized

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

  • Download the AWS CLI v2 installer for Windows by opening this link in your browser:
  • https://awscli.amazonaws.com/AWSCLIV2.msi
  • Run the downloaded AWSCLIV2.msi file.
  • Follow the on-screen prompts. Accept the license agreement and click Next through each screen.
  • Click Install when prompted. Windows may ask for administrator permission.
  • Click Finish once the installation completes.
  • Close your current terminal and open a new one (the installer adds aws to your PATH automatically, but only new terminals pick it up).
  • Verify the installation by running:
aws --version

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

Still seeing 'aws' is not recognized?

Make sure you opened a brand new terminal window after the install finished. The MSI installer adds aws to your PATH, but existing terminals will not see it.

If a new terminal still does not find the command, try restarting your computer. This forces Windows to reload all PATH entries.

Help me troubleshoot my AWS CLI installation on Windows.

Configure AWS credentials

Terraform needs permission to create resources in your AWS account. You provide this by creating an IAM user with programmatic access, then storing its credentials on your machine using the AWS CLI.

  • Open the AWS Console in your browser and sign in to your account.
  • In the search bar at the top, type IAM and select IAM from the results.
  • In the left sidebar, click Users.
  • Click Create user.
  • Enter a username (e.g. terraform-user) and click Next.
  • Select Attach policies directly.
  • Search for AdministratorAccess and check the box next to it.
  • Click Next, then Create user.

Why AdministratorAccess?

For learning purposes, AdministratorAccess gives Terraform full permissions to create any resource. In a real production environment, you would create a policy with only the specific permissions needed (least-privilege principle).

Now you need to create an access key for this user so the AWS CLI can authenticate.

  • Click on your newly created user's name to open their details.
  • Click 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 box at the bottom and click Next.
  • Click Create access key.
  • Copy your Access Key ID and Secret Access Key to a safe place. You will not be able to see the secret key again after leaving this page.

Do not share your access keys

Your Access Key ID and Secret Access Key grant full control of your AWS account. Never commit them to Git, paste them in public forums, or share them with anyone.

Now configure the AWS CLI with your credentials so Terraform can use them.

  • Run the following command in your terminal:
aws configure

The command will prompt you for four values. Enter them one at a time:

  • AWS Access Key ID: paste the Access Key ID you copied.
  • AWS Secret Access Key: paste the Secret Access Key you copied.
  • Default region name: type us-east-1.
  • Default output format: type json.

What does aws configure do?

This command stores your credentials in a hidden folder at %UserProfile%\.aws\ on Windows. When Terraform runs, it automatically reads credentials from this location. You only need to do this once.

  • Verify your configuration works by running:
aws sts get-caller-identity

You should see a JSON response containing your account ID, user ARN, and user ID. This confirms the AWS CLI can successfully connect to your account.

Seeing 'Unable to locate credentials'?

Run aws configure again and make sure you are pasting the correct Access Key ID and Secret Access Key without extra spaces.

If you accidentally closed the access key page before copying the secret, go back to IAM, delete that access key, and create a new one.

Help me fix my AWS CLI credentials issue.

Your tools are installed and your AWS credentials are configured. Next up, you will create the Terraform project structure and write your first configuration files.

Set Up Your Terraform Project

Your tools are installed and your AWS credentials are configured. Now it's time to create the actual Terraform project that will hold all your infrastructure code.

In this step, you'll create a clean project structure with separate files for different concerns, tell Terraform which cloud provider to use, and initialize the project so Terraform can download the AWS plugin.

In this step, get ready to:
  • Create the project directory and file structure.
  • Configure the AWS provider in your Terraform code.
  • Initialize Terraform to download the AWS plugin.
Create your project structure

Every Terraform project lives in its own directory with a set of .tf files that each serve a specific purpose.

  • Open VS Code's integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
  • 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-vpc-webserver
cd terraform-aws-vpc-webserver
  • Initialize a Git repository by running:
git init
  • Create the project files by running this command:

macOS

touch main.tf variables.tf outputs.tf .gitignore

Windows

New-Item -Path main.tf, variables.tf, outputs.tf, .gitignore -ItemType File
  • Confirm the files exist by running:
ls

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

Don't see all four files?

On Windows PowerShell, dotfiles like .gitignore may not appear in the default list view. Run Get-ChildItem -Force to reveal all files including hidden ones.

If you see a "New-Item" error on Windows, make sure your terminal is PowerShell (not Command Prompt). Help me create my project files

Configure the AWS provider

Terraform needs to know which cloud provider you're using and which version of the provider plugin to download. You'll split this configuration across two files: main.tf for the provider setup and variables.tf for reusable values.

  • Open .gitignore in VS Code and add the following content to keep Terraform's generated files out of version control:
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
*.tfvars

What does this file do?

  • .terraform/ is the directory where Terraform downloads provider plugins. It's large and regenerated by terraform init, so it should never be committed.
  • *.tfstate files contain the current state of your infrastructure. They can hold sensitive data and should stay local.
  • *.tfvars files store variable values that may include secrets. Keep them out of version control.
  • Save .gitignore.
  • Open main.tf and add the Terraform configuration block and AWS provider:
terraform {
  required_version = "~> 1.15"

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

provider "aws" {
  region = var.aws_region
}

What does this code do?

  • required_version = "~> 1.15" ensures anyone running this project uses Terraform 1.15.x. The ~> operator allows patch updates but not major or minor version changes.
  • required_providers tells Terraform to download the AWS provider plugin from hashicorp/aws at version 5.x.
  • provider "aws" configures the AWS plugin to deploy resources in the region defined by the aws_region variable (which you'll define next).
  • Save main.tf.
  • Open variables.tf and define the project variables:
variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "us-east-1"
}

variable "project_name" {
  description = "Name prefix for all resources"
  type        = string
  default     = "my-vpc-webserver"
}

What does this code do?

  • aws_region defaults to us-east-1, matching the region you configured in the AWS CLI. This keeps your code and credentials in sync.
  • project_name provides a consistent prefix for naming all AWS resources. It makes cleanup and identification easier.
  • Using variables instead of hard-coded values means you can reuse this project in different regions or with different names without editing multiple files.
  • Save variables.tf.

Why separate files?

Terraform loads all .tf files in a directory as one configuration. Splitting into main.tf, variables.tf, and outputs.tf is a community convention that keeps your project organized as it grows.

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is .gitignore:

.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
*.tfvars

Here is main.tf:

terraform {
  required_version = "~> 1.15"

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

provider "aws" {
  region = var.aws_region
}

Here is variables.tf:

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

variable "project_name" {
  description = "Name prefix for all resources"
  type        = string
  default     = "my-vpc-webserver"
}
Initialize Terraform

With your configuration files in place, Terraform needs to download the AWS provider plugin before it can do anything else.

  • Download the AWS provider plugin by running:
terraform init

What does this command do?

terraform init reads your main.tf configuration, downloads the hashicorp/aws provider plugin matching your version constraint, and creates a .terraform directory to store it.

It also generates a .terraform.lock.hcl file that locks the exact provider version. This ensures consistent deployments across machines.

You should see output ending with Terraform has been successfully initialized! followed by a reminder about running terraform plan.

  • Confirm the .terraform directory was created by running:
ls .terraform

You should see a providers folder listed inside it.

Seeing an error during init?

  • If you see a version constraint error, make sure your required_version in main.tf says "~> 1.15" and that you installed Terraform 1.15.3.
  • If you see a network timeout, check your internet connection. Terraform needs to reach registry.terraform.io to download the provider.
  • If you see "No configuration files", confirm you are inside the terraform-aws-vpc-webserver directory by running pwd.

Help me fix my terraform init error

Your project structure is ready and Terraform knows how to talk to AWS. Next up, you'll write the code that builds an entire network from scratch.

Build the Network Infrastructure

Your Terraform project is initialized and ready to go. Now it is time to define the actual cloud resources that make up your network.

Every application running in AWS needs a network to live on. In this step, you will write the Terraform code that creates a complete VPC network from scratch, including a public subnet, internet gateway, and route table.

In this step, get ready to:
  • Create a VPC with DNS support enabled.
  • Build a public subnet with internet connectivity.
  • Preview your planned infrastructure with terraform plan.
Create the VPC

A VPC (Virtual Private Cloud) is your own isolated network inside AWS. Think of it as the walls of a building. Everything you deploy (servers, databases, load balancers) lives inside your VPC.

  • In main.tf, add the VPC resource block below the existing provider "aws" block by copying this code:
# --- VPC ---
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

What does this code do?

This defines a VPC resource named main with the following configuration:

  • cidr_block = "10.0.0.0/16" reserves a block of 65,536 IP addresses for your network.
  • enable_dns_support and enable_dns_hostnames let resources inside the VPC resolve domain names and receive public DNS hostnames.
  • The Name tag uses your project_name variable so the VPC is easy to identify in the AWS Console.
  • Save main.tf.
  • You will not see a change yet. The next substep adds the remaining networking resources, then you will verify everything with terraform plan.
Create the public subnet and internet gateway

A VPC alone is like an empty building. You need a subnet (a floor in the building) to place resources on, and an internet gateway (the front door) to connect that floor to the outside world.

  • In main.tf, add the following code below the VPC resource block you just created:
# --- Public Subnet ---
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       = "${var.aws_region}a"

  tags = {
    Name = "${var.project_name}-public-subnet"
  }
}

# --- Internet Gateway ---
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-igw"
  }
}

# --- Route Table ---
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 = "${var.project_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?

These four resources work together to give your subnet internet access:

  • aws_subnet "public" creates a subnet with 256 IP addresses. map_public_ip_on_launch = true means any server launched here automatically gets a public IP.
  • aws_internet_gateway "main" attaches a gateway to your VPC, which is the bridge between your private network and the public internet.
  • aws_route_table "public" defines a route table with a rule that sends all outbound traffic (0.0.0.0/0) through the internet gateway.
  • aws_route_table_association "public" links the route table to your subnet so the routing rules actually apply.
  • Save main.tf.
  • You will not see a visible change yet. The next substep uses terraform plan to verify all your code is correct.

Seeing a syntax error when you save?

Make sure every opening brace { has a matching closing brace }. HCL requires balanced braces for every block.

Also check that you have a blank line separating each resource block. Missing separators can cause unexpected errors.

help me fix a syntax error in my Terraform main.tf file

✔️ 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 file as it should look at this point:

terraform {
  required_version = "~> 1.15"

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

provider "aws" {
  region = var.aws_region
}

# --- VPC ---
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

# --- Public Subnet ---
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       = "${var.aws_region}a"

  tags = {
    Name = "${var.project_name}-public-subnet"
  }
}

# --- Internet Gateway ---
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-igw"
  }
}

# --- Route Table ---
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 = "${var.project_name}-public-rt"
  }
}

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

Before deploying anything to AWS, Terraform lets you preview exactly what it will create. This is the "plan" phase of the Terraform workflow.

  • Preview the resources Terraform will create by running this command:
terraform plan

Terraform reads your .tf files, compares them against the current state (empty, since you have not deployed anything yet), and shows you a list of resources it will create.

You should see output ending with a summary line like Plan: 5 to add, 0 to change, 0 to destroy. confirming five resources will be created.

What are the five resources?

The plan output lists each resource Terraform will create. Your five resources are:

  • aws_vpc.main (your VPC).
  • aws_subnet.public (your public subnet).
  • aws_internet_gateway.main (the internet gateway).
  • aws_route_table.public (the route table).
  • aws_route_table_association.public (the link between subnet and route table).

Seeing errors instead of a plan?

If you see a message about missing closing braces or unexpected tokens, double check your main.tf against the full code in the tab group above.

If you see a credentials error, make sure you ran aws configure in the same terminal session and entered valid credentials.

help me fix errors in my terraform plan output

Your network blueprint is locked in. Next up, you will add a security group and launch an EC2 web server on top of this infrastructure.

Add Security and Launch the Web Server

Your network is built, but right now nothing can actually talk to it. Without a security group, AWS blocks all traffic to and from your resources by default.

In this step, you will create a security group that allows web traffic in, launch an EC2 instance running a web server, and see your custom page live on the internet.

In this step, get ready to:
  • Create a security group with HTTP and SSH access rules.
  • Launch an EC2 web server using Terraform's user_data for automated setup.
  • Deploy everything and verify your web page is live.
Create the security group

A security group acts as a virtual firewall for your EC2 instance. You define which ports are open and who can connect. For a web server, you need port 80 (HTTP) for browser traffic and port 22 (SSH) for remote administration.

  • In main.tf, add the following security group resource below the aws_route_table_association block:
# --- Security Group ---
resource "aws_security_group" "web" {
  name        = "${var.project_name}-web-sg"
  description = "Allow HTTP and SSH inbound traffic"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "SSH from anywhere"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-web-sg"
  }
}

What does this code do?

The ingress blocks define inbound rules. Port 80 lets browsers reach your web page. Port 22 lets you SSH into the instance for debugging.

The egress block with protocol -1 allows all outbound traffic so the instance can download packages and updates.

The cidr_blocks = ["0.0.0.0/0"] means "from any IP address." In production you would restrict SSH to your own IP, but for learning this keeps things simple.

  • Save main.tf.
  • You will not see infrastructure changes yet. The next chunk adds the compute resource this security group protects.

Seeing a syntax error?

Make sure each ingress block has matching opening and closing braces. A missing } is the most common cause.

Check that vpc_id = aws_vpc.main.id references the VPC you created in the previous step.

Help me fix a syntax error in my Terraform security group

Launch the EC2 instance

Now you need an actual server to protect. You will use a data block to automatically find the latest Amazon Linux 2023 AMI, then launch an instance with a startup script that installs Apache and serves your custom HTML page.

  • In main.tf, add the following code below your security group block:
# --- EC2 Instance ---
data "aws_ami" "amazon_linux_2023" {
  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_2023.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl start httpd
              systemctl enable httpd
              echo "<html><head><title>My Cloud Portfolio</title></head><body><h1>Hello from Terraform!</h1><p>This web server was deployed using Infrastructure as Code.</p><p>Built by Alphaleo01</p></body></html>" > /var/www/html/index.html
              EOF

  tags = {
    Name = "${var.project_name}-web-server"
  }
}

What does this code do?

The data "aws_ami" block queries AWS for the most recent Amazon Linux 2023 image. This means your code always uses the latest patched AMI without hardcoding an ID that will go stale.

The aws_instance resource launches a t2.micro (free-tier eligible) in your public subnet, protected by the security group you just created.

The user_data script runs automatically when the instance boots. It installs Apache (httpd), starts the web server, and writes a custom HTML page.

  • Save main.tf.

Worried about the heredoc syntax?

The <<-EOF and EOF markers define a multi-line string. Make sure EOF appears on its own line with no trailing spaces.

If you see an error about an unterminated string, check that the closing EOF is not indented differently than the opening marker.

Help me fix a heredoc error in my Terraform user_data

✔️ Awesome, I've got everything!

Great. Double check you have saved main.tf with both the security group and EC2 instance code.

ⓧ I'd like to double check the full code

Here is the complete main.tf file at this point:

terraform {
  required_version = "~> 1.15"

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

provider "aws" {
  region = var.aws_region
}

# --- VPC ---
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

# --- Public Subnet ---
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       = "${var.aws_region}a"

  tags = {
    Name = "${var.project_name}-public-subnet"
  }
}

# --- Internet Gateway ---
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-igw"
  }
}

# --- Route Table ---
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 = "${var.project_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        = "${var.project_name}-web-sg"
  description = "Allow HTTP and SSH inbound traffic"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "SSH from anywhere"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-web-sg"
  }
}

# --- EC2 Instance ---
data "aws_ami" "amazon_linux_2023" {
  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_2023.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl start httpd
              systemctl enable httpd
              echo "<html><head><title>My Cloud Portfolio</title></head><body><h1>Hello from Terraform!</h1><p>This web server was deployed using Infrastructure as Code.</p><p>Built by Alphaleo01</p></body></html>" > /var/www/html/index.html
              EOF

  tags = {
    Name = "${var.project_name}-web-server"
  }
}

Now you need to tell Terraform what information to display after deployment. Outputs let you quickly grab the public IP and other details without digging through the AWS console.

  • Open outputs.tf and add the following output definitions:
output "web_server_public_ip" {
  description = "Public IP address 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}"
}

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.web.id
}

What do these outputs do?

Terraform prints output values to your terminal after every apply. The web_server_url output gives you a clickable link to your live web page. The vpc_id and instance_id help you cross-reference resources in the AWS console.

  • Save outputs.tf.
  • You will not see changes yet. The next substep deploys everything to AWS.

✔️ Awesome, I've got everything!

Great. Make sure both main.tf and outputs.tf are saved.

ⓧ I'd like to double check the full code

Here is the complete outputs.tf file:

output "web_server_public_ip" {
  description = "Public IP address 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}"
}

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.web.id
}
Deploy everything

Your code is ready. Time to make it real. The terraform apply command reads your configuration, builds an execution plan, and asks for confirmation before creating resources on AWS.

  • Deploy your infrastructure by running this command:
terraform apply

What happens when you run this?

Terraform shows you an execution plan listing every resource it will create. You will see lines prefixed with + for new resources. At the bottom it asks for confirmation.

Type yes and press Enter to proceed. Terraform will create the security group, look up the AMI, and launch the EC2 instance. This typically takes 30-60 seconds.

When finished, you will see Apply complete! followed by your output values including the public IP.

  • When prompted, type yes and press Enter.
  • Wait for the apply to complete.
  • Copy the web_server_url value from the terminal output.
  • Paste the URL into your browser. You should see a page that says Hello from Terraform! with your name below it.

Page not loading in the browser?

Make sure you are using http:// (not https://) in the URL. Your server only listens on port 80.

If the page times out, wait 1-2 minutes. The user_data script needs time to install packages and start Apache after the instance boots.

If it still does not load, verify your security group allows inbound traffic on port 80 and that map_public_ip_on_launch = true is set on your subnet.

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

Your infrastructure is live. You just deployed a complete VPC, security group, and web server without clicking a single button in the AWS console. Next up, you will push this code to GitHub as a professional portfolio piece.

Push to GitHub as a Portfolio Piece

Your infrastructure is live and serving a real web page. That is a serious accomplishment, but right now it only exists on your machine and in AWS.

In this step, you will document your project with a professional README and push everything to GitHub. This turns your Terraform code into a portfolio piece that employers can browse, clone, and verify.

In this step, get ready to:
  • Write a professional README documenting your infrastructure.
  • Commit and push your code to GitHub.
  • Verify your repository renders correctly as a portfolio piece.
Write a professional README

A README is the first thing anyone sees when they visit your repository. It tells employers what you built, how the architecture works, and how to deploy it themselves.

  • Create a new file called README.md in your terraform-aws-vpc-webserver folder.
  • Paste the following content into README.md:
# Terraform AWS VPC Web Server

A complete AWS network infrastructure deployed using Terraform. This project creates a custom VPC with a public subnet, internet gateway, route table, security group, and an EC2 instance running a web server.

## Architecture Overview

This infrastructure includes:

- A VPC with CIDR block 10.0.0.0/16
- A public subnet (10.0.1.0/24) with auto-assigned public IPs
- An internet gateway for outbound internet access
- A route table directing traffic through the internet gateway
- A security group allowing HTTP (port 80) and SSH (port 22) inbound
- An EC2 instance (t2.micro) running Amazon Linux 2023 with Apache (httpd)

## How to Deploy

1. Clone this repository:

```
git clone https://github.com/Alphaleo01/terraform-aws-vpc-webserver.git
cd terraform-aws-vpc-webserver
```

2. Ensure you have Terraform installed and AWS credentials configured:

```
terraform -help
aws configure
```

3. Initialize and apply:

```
terraform init
terraform apply
```

4. After the apply completes, copy the `web_server_public_ip` output and open it in your browser.

## AWS Resources Created

| Resource | Type | Purpose |
|----------|------|--------|
| VPC | aws_vpc | Isolated network (10.0.0.0/16) |
| Public Subnet | aws_subnet | Hosts the web server (10.0.1.0/24) |
| Internet Gateway | aws_internet_gateway | Connects VPC to the internet |
| Route Table | aws_route_table | Routes traffic to the gateway |
| Security Group | aws_security_group | Allows HTTP and SSH access |
| EC2 Instance | aws_instance (t2.micro) | Runs the web server |

## Free Tier Eligibility

This project uses a t2.micro EC2 instance, which is covered under the AWS Free Tier (750 hours/month for 12 months). VPC, subnets, internet gateways, route tables, and security groups have no additional charge.

## Clean Up

To destroy all resources and stop any potential charges:

```
terraform destroy
```

What does this README include?

  • A project title and one-line description so visitors instantly know what this is.
  • An architecture overview listing every AWS resource and its role.
  • Clone-and-deploy instructions so someone else can replicate your infrastructure.
  • A free tier note showing you understand cost management.
  • Save README.md.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

# Terraform AWS VPC Web Server

A complete AWS network infrastructure deployed using Terraform. This project creates a custom VPC with a public subnet, internet gateway, route table, security group, and an EC2 instance running a web server.

## Architecture Overview

This infrastructure includes:

- A VPC with CIDR block 10.0.0.0/16
- A public subnet (10.0.1.0/24) with auto-assigned public IPs
- An internet gateway for outbound internet access
- A route table directing traffic through the internet gateway
- A security group allowing HTTP (port 80) and SSH (port 22) inbound
- An EC2 instance (t2.micro) running Amazon Linux 2023 with Apache (httpd)

## How to Deploy

1. Clone this repository:

```
git clone https://github.com/Alphaleo01/terraform-aws-vpc-webserver.git
cd terraform-aws-vpc-webserver
```

2. Ensure you have Terraform installed and AWS credentials configured:

```
terraform -help
aws configure
```

3. Initialize and apply:

```
terraform init
terraform apply
```

4. After the apply completes, copy the `web_server_public_ip` output and open it in your browser.

## AWS Resources Created

| Resource | Type | Purpose |
|----------|------|--------|
| VPC | aws_vpc | Isolated network (10.0.0.0/16) |
| Public Subnet | aws_subnet | Hosts the web server (10.0.1.0/24) |
| Internet Gateway | aws_internet_gateway | Connects VPC to the internet |
| Route Table | aws_route_table | Routes traffic to the gateway |
| Security Group | aws_security_group | Allows HTTP and SSH access |
| EC2 Instance | aws_instance (t2.micro) | Runs the web server |

## Free Tier Eligibility

This project uses a t2.micro EC2 instance, which is covered under the AWS Free Tier (750 hours/month for 12 months). VPC, subnets, internet gateways, route tables, and security groups have no additional charge.

## Clean Up

To destroy all resources and stop any potential charges:

```
terraform destroy
```
Commit and push to GitHub

Your code is ready to be version-controlled and shared. You will stage all files, commit them, create a GitHub repository, and push your code up.

  • Stage all project files by running this command:
git add .
  • Commit your files with a descriptive message by running:
git commit -m "Deploy VPC and web server infrastructure with Terraform"

What do these commands do?

  • git add . stages every file in your project folder for the next commit.
  • git commit -m saves a snapshot of your staged files with a message describing what changed.

Seeing an error about identity?

If git asks you to set your name and email, run these two commands first (replacing the placeholders with your details):

git config --global user.email "your-email@example.com"
git config --global user.name "Your Name"

Then run the git commit command again.

help me fix a git identity error when committing

Now you need a place to push your code. You will create a new repository on GitHub.

  • Open your browser and go to github.com.
  • Click the + icon in the top-right corner of the page.
  • Select New repository from the dropdown.
  • Enter terraform-aws-vpc-webserver as the repository name.
  • Leave the repository set to Public.
  • Do NOT check "Add a README file" (you already have one locally).
  • Click Create repository.

Why leave it empty?

GitHub offers to initialize the repo with a README and .gitignore, but you already have both locally. Adding them on GitHub would create a conflict when you try to push.

  • Back in your terminal, connect your local repo to GitHub by running:
git remote add origin https://github.com/Alphaleo01/terraform-aws-vpc-webserver.git
  • Push your code to GitHub by running:
git push -u origin main

What do these commands do?

  • git remote add origin tells your local Git repo where the remote GitHub repository lives.
  • git push -u origin main uploads your commits to the main branch on GitHub. The -u flag sets this as the default remote so future pushes only need git push.

You should see output showing your files being uploaded to GitHub.

Push rejected or authentication failed?

If you see an authentication error, GitHub requires a personal access token instead of a password. Generate one from Settings > Developer settings > Personal access tokens on GitHub, and paste it when prompted for a password.

If the push is rejected because the remote contains work you do not have locally, make sure you did NOT initialize the GitHub repo with a README or .gitignore.

help me fix a git push authentication or rejection error

Verify your portfolio piece

The final check is making sure everything looks professional on GitHub.

  • In your browser, visit https://github.com/Alphaleo01/terraform-aws-vpc-webserver.
  • Confirm that all your files are visible: .gitignore, main.tf, variables.tf, outputs.tf, and README.md.
  • Scroll down and verify your README renders with the architecture overview table and deploy instructions.

Why does the README render automatically?

GitHub detects README.md files and renders them as formatted HTML below the file list. This is why markdown formatting (headings, tables, code blocks) appears styled rather than as raw text.

README not rendering or files missing?

If the README shows as raw text, make sure the file is named exactly README.md (case-sensitive). If files are missing, check that you ran git add . before committing. You can run git status to see if anything was left unstaged.

help me fix my GitHub README or missing files

Secret mission

Add a Private Subnet and Architecture Diagram

Real production networks don't put everything in one public subnet. Take your infrastructure to the next level by adding a private subnet and documenting your architecture with a visual diagram in your README.

Clean Up Your Resources

Clean Up Your Resources

Your EC2 instance and VPC infrastructure are covered by the AWS Free Tier, so there are no immediate costs. However, the Free Tier has limits (750 hours/month for t2.micro). Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Cost warning

Your EC2 instance is Free Tier eligible for 750 hours/month (enough to run one instance 24/7). If you launch additional EC2 instances or your account passes the 12-month Free Tier window, you will start incurring charges.

The safest option is to run terraform destroy after you have verified your web page works and your GitHub repo looks good. You can always redeploy with terraform apply.

Resources you used:

  • 1x EC2 instance (t2.micro) - Free Tier eligible (750 hrs/month for 12 months).
  • 1x public IPv4 address - Free Tier eligible (750 hrs/month when attached to an EC2 instance).
  • VPC, public subnet, private subnet, internet gateway, route table, security group - no charge.
  • Local project directory: terraform-aws-vpc-webserver.
  • GitHub repository: terraform-aws-vpc-webserver.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your web server accessible.

  • Your infrastructure is safe within the Free Tier as long as this is your only running t2.micro instance.
  • Monitor your usage in the AWS Console under Billing and Cost Management to make sure you stay within Free Tier limits.
  • Your code remains on GitHub, so you can always reference or redeploy it.

Pause - I'll come back to this later

Stop the EC2 instance to halt compute charges while keeping your infrastructure in place. The VPC and networking resources are free, so they can stay.

  • Open the AWS Console and navigate to EC2 > Instances.
  • Select your my-vpc-webserver-web-server instance.
  • Click Instance state > Stop instance.
  • Your EBS volume still counts against Free Tier storage (30 GB gp2/gp3 per month), but compute hours stop accumulating.
  • To resume later, select the instance and click Instance state > Start instance. Note that the public IP will change on restart.

Delete - I don't want to use this again

Remove all AWS resources, local files, and the GitHub repository. Your Terraform code makes redeployment easy if you change your mind.

Destroy all AWS resources:

  • Open your terminal and navigate to your project directory:
cd ~/Desktop/terraform-aws-vpc-webserver
  • Run the destroy command:
terraform destroy
  • Type yes when prompted to confirm. Terraform removes the EC2 instance, security group, route table, internet gateway, subnets, and VPC.

Delete local project files:

cd ~/Desktop
rmdir /s /q terraform-aws-vpc-webserver

On macOS or Linux, use this instead:

cd ~/Desktop
rm -rf terraform-aws-vpc-webserver

Delete the GitHub repository:

  • Go to your repository page on GitHub: https://github.com/Alphaleo01/terraform-aws-vpc-webserver.
  • Click Settings.
  • Scroll down to the Danger Zone section and click Delete this repository.
  • Follow the confirmation prompts and type the repository name to confirm deletion.

Delete stored AWS credentials (optional):

  • If you no longer need the IAM user you created, go to the AWS Console under IAM > Users, select the user, and click Delete.
  • Remove local credentials by deleting the .aws folder in your user profile directory.

Nice Work!

Nice Work!

You just deployed real cloud infrastructure as code. That is what cloud engineers do every day.

You've learned how to:

  • Built a complete VPC network with a public subnet, internet gateway, route table, and security group using Terraform.
  • Launched an EC2 web server with automated configuration using user_data and zero console clicks.
  • Managed infrastructure lifecycle end to end with terraform plan, terraform apply, and terraform destroy, then published a professional portfolio piece on GitHub.
  • Secret Mission: Added a private subnet and a Mermaid architecture diagram to demonstrate network segmentation knowledge.

Ready to quiz yourself?