Deploy a Web Server with Terraform
Provision a live AWS web server entirely through Terraform IaC.
Introduction
30 Second Summary
Every time a cloud team needs a new server, they don't click through a console and hope they remember the right settings. They write code that defines exactly what they need, then deploy it with a single command.
In this project, you will use Terraform to write Infrastructure as Code that provisions a complete AWS environment and deploys a live web server. You'll define networking, security, and compute resources in code files, then bring them all to life with one terminal command.
What You'll Build
A portfolio web page served by an EC2 instance inside a fully Terraform-provisioned VPC, with every resource defined in HCL files and deployed with a single command.
By the end of this project, you'll have:
- A VPC with a public subnet and internet gateway, all defined in Terraform code.
- A live EC2 web server accessible via a public IP address, bootstrapped automatically with user data.
- A structured Terraform project with separate files for networking, security, compute, and outputs, pushed to GitHub.
- Secret Mission: Add cost governance tags and auto-shutdown scheduling to demonstrate cloud cost awareness.
Are there any prerequisites?
You'll need an existing AWS account and basic familiarity with running commands in a terminal. No prior Terraform experience is required.
Install and Configure Your Tools
To deploy infrastructure on AWS using Terraform, you need two tools on your machine. Terraform translates your code into real cloud resources. The AWS CLI authenticates your machine so Terraform can talk to your AWS account.
In this step, you will install both tools and create the credentials that connect them to your AWS account.
In this step, get ready to:
- Install Terraform and add it to your system PATH.
- Install the AWS CLI v2.
- Create IAM credentials and configure the CLI to use them.
Install Terraform on Windows
Terraform is a single executable file. You download it, place it in a folder, and tell Windows where to find it by updating your system PATH.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Check if Terraform is already installed by running:
terraform version
✔️ I see version 1.15 or higher
Terraform is ready to go. Skip ahead to the next substep (Install AWS CLI v2 on Windows).
ⓧ I see an older version
You have an outdated version of Terraform that may not support the provider features this project uses. Follow the steps below to replace it with the latest version.
- Visit the official Terraform install page at https://developer.hashicorp.com/terraform/install.
- Download the AMD64 Windows zip file.
- Extract terraform.exe from the zip and replace the old file in its current location (find it by running where terraform in your terminal).
- Close and reopen your VS Code terminal.
- Run terraform version again to confirm it shows 1.15 or higher.
ⓧ Command not found
Terraform is not installed yet. Follow these steps to set it up.
- Visit the official Terraform install page at https://developer.hashicorp.com/terraform/install.
- Download the AMD64 Windows zip file.
- Create a new folder called C:\terraform.
- Extract terraform.exe from the zip into C:\terraform.
- Now add that folder to your system PATH so Windows can find the command from any terminal.
To update your system PATH:
- Press the Windows key, type environment variables, and select Edit the system environment variables.
- Click Environment Variables at the bottom of the dialog.
- Under System variables, find and select Path, then click Edit.
- Click New and type C:\terraform.
- Click OK on all open dialogs to save.
Why do you need to update PATH?
PATH is a list of folders that Windows searches when you type a command. Without adding C:\terraform to this list, your terminal would not know where to find terraform.exe.
- Close your current VS Code terminal and open a fresh one (Ctrl+`). The new terminal picks up the updated PATH.
- Verify by running:
terraform version
You should see output showing Terraform v1.15.6 (or the latest 1.15.x release).
Still seeing 'command not found' after updating PATH?
Make sure you opened a new terminal after saving the PATH change. Existing terminals do not pick up environment variable updates.
Double-check that terraform.exe is inside C:\terraform (not nested in a subfolder from the zip extraction).
Help me fix my Terraform PATH setup on Windows.
Install AWS CLI v2 on Windows
The AWS CLI lets you manage AWS resources from the command line. Terraform also uses the CLI's saved credentials to authenticate with your account.
- Check if the AWS CLI is already installed by running:
aws --version
✔️ I see aws-cli/2.x.x
AWS CLI v2 is installed and ready. Skip ahead to the next substep (Create new IAM credentials and configure the CLI).
ⓧ I see aws-cli/1.x.x
You have an older version of the AWS CLI. Version 2 is required for this project. Follow the steps below to upgrade.
- Download the official AWS CLI v2 MSI installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
- Run the MSI file and follow the prompts. The installer will upgrade your existing version in place.
- Close and reopen your VS Code terminal.
- Run aws --version again. You should now see aws-cli/2.x.x.
ⓧ Command not found
AWS CLI is not installed yet. Follow these steps.
- Download the official AWS CLI v2 MSI installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
- Run the downloaded AWSCLIV2.msi file and follow the installer prompts. Accept the defaults.
- Close and reopen your VS Code terminal so it picks up the new PATH entry.
- Verify by running:
aws --version
You should see output starting with aws-cli/2. followed by version numbers.
AWS CLI installer not working?
If the installer fails, try right-clicking the MSI file and selecting Run as administrator. Some machines require elevated permissions for system-wide installs.
Help me troubleshoot my AWS CLI v2 installation on Windows.
Create new IAM credentials and configure the CLI
Terraform needs credentials to create resources in your AWS account. You will create a dedicated IAM user with an access key, then tell the AWS CLI to use that key.
- Sign in to the AWS Management Console at https://console.aws.amazon.com.
- In the search bar at the top, type IAM and select the IAM service.
- In the left navigation pane, click Users.
- Click Create user.
- Enter a user name such as terraform-deployer and click Next.
- On the permissions page, select Attach policies directly.
- Search for AdministratorAccess and check the box next to it.
- Click Next, review the summary, and click Create user.
Why AdministratorAccess?
This policy gives Terraform full permissions to create VPCs, subnets, security groups, and EC2 instances. In production, you would scope permissions down to least-privilege. For a learning project that you will destroy at the end, AdministratorAccess keeps things simple.
Now create an access key for this user:
- Click on your new user's name (terraform-deployer) to open its details page.
- Select the Security credentials tab.
- Scroll to the Access keys section and click Create access key.
- Select Command Line Interface (CLI) as the use case, acknowledge the recommendation, and click Next.
- Click Create access key.
- Copy the Access key ID and Secret access key and save them somewhere safe (a password manager, a temporary text file you will delete later). You will not be able to see the secret key again after closing this page.
- Click Done.
Save your secret key now
AWS shows the secret access key only once at creation time. If you close this page without copying it, you will need to delete the key and create a new one.
With the access key ready, configure the AWS CLI to use it:
- In your VS Code terminal, run the configure command:
aws configure
What will this command ask you?
The aws configure command prompts for four values, one at a time:
- AWS Access Key ID - paste the access key ID you just copied.
- AWS Secret Access Key - paste your secret access key.
- Default region name - type us-east-1.
- Default output format - type json.
- Enter each value when prompted and press Enter after each one.
- Verify that your credentials work by running:
aws sts get-caller-identity
You should see a JSON response containing your account number, user ID, and ARN. This confirms the CLI is authenticated and ready.
Seeing 'Unable to locate credentials'?
This means aws configure did not save correctly. Run aws configure again and carefully paste the Access Key ID and Secret Access Key without extra spaces.
If you see 'InvalidClientTokenId', the access key may have been deleted or you copied it incorrectly. Go back to IAM in the console, delete the old key, and create a new one.
Help me fix my AWS CLI credentials configuration.
Your tools are locked and loaded. Next up, you will create your Terraform project structure and write the first configuration files.
Initialize Your Terraform Project
Your tools are installed and configured. Now it's time to create the actual Terraform project that will define your infrastructure.
Terraform projects are organized into separate .tf files, each handling a specific responsibility. This modularity makes your code easier to read, maintain, and extend as your infrastructure grows.
In this step, get ready to:
- Create a project folder with four Terraform files.
- Write the provider and variable configurations.
- Run terraform init to download the AWS provider plugin.
Create the project folder structure
Before writing any infrastructure code, you need a dedicated folder to hold your Terraform files. Each file will have a clear responsibility.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir terraform-portfolio
cd terraform-portfolio
- Create the four Terraform files by running these commands:
macOS
touch main.tf variables.tf networking.tf outputs.tf
Windows
New-Item -ItemType "File" -Path "main.tf", "variables.tf", "networking.tf", "outputs.tf"
What are these files for?
- main.tf holds your provider configuration (which cloud to connect to).
- variables.tf defines input variables so you can reuse values without hardcoding them.
- networking.tf will hold your VPC, subnet, and gateway resources (coming in the next step).
- outputs.tf will display useful values like your server's public IP after deployment.
- Confirm the files exist by running:
ls
You should see main.tf, variables.tf, networking.tf, and outputs.tf listed in the output.
- Open the project folder in VS Code by running:
code .
VS Code reloads with your terraform-portfolio folder as the workspace. You should see all four files in the Explorer sidebar on the left.
What if VS Code asks me to trust the folder?
VS Code may show a trust prompt the first time you open a new folder. Click Yes, I trust the authors to continue.
Write the provider configuration and variables
The provider configuration tells Terraform which cloud platform to use and how to authenticate. Variables let you define reusable values in one place.
- In the Explorer sidebar, click main.tf to open it in the editor.
- Paste the following code into main.tf:
terraform {
required_version = ">= 1.15.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
What does this code do?
- required_version = ">= 1.15.0" ensures anyone running this project uses Terraform 1.15.0 or newer.
- source = "hashicorp/aws" tells Terraform to download the official AWS provider from the Terraform Registry.
- version = "~> 6.0" allows any 6.x release but not 7.0. This prevents breaking changes from a major version bump.
- region = var.aws_region reads the region from a variable you will define next, instead of hardcoding it.
- Below the provider block, add the locals block that defines common tags for all your resources:
locals {
common_tags = {
Project = var.project_name
Environment = "development"
ManagedBy = "terraform"
}
}
What are locals and why use tags?
A locals block defines values you reuse across your configuration. Here, common_tags is a map of tags that will be attached to every AWS resource you create.
Tags help you identify who owns a resource, what project it belongs to, and how it was deployed. This is essential for cost tracking and governance in any organization.
- Save main.tf.
Seeing red squiggly lines in VS Code?
VS Code may not recognize .tf files by default. The red underlines will disappear after you run terraform init in the next substep. You can also install the HashiCorp Terraform extension from the VS Code marketplace for syntax highlighting.
Help me fix Terraform syntax highlighting in VS Code
- Click variables.tf in the Explorer sidebar to open it.
- Paste the following code into variables.tf:
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
variable "project_name" {
description = "Name used for tagging and naming resources"
type = string
default = "terraform-portfolio"
}
What does this code do?
- Each variable block declares an input that other files can reference with var.<name>.
- aws_region defaults to us-east-1 so Terraform knows which AWS region to deploy into.
- project_name defaults to terraform-portfolio and is used in resource names and tags to keep everything identifiable.
- Save variables.tf.
✔️ Awesome, I've got everything!
Great. Double check you have saved both main.tf and variables.tf.
ⓧ I'd like to double check the full code
Here is the complete main.tf:
terraform {
required_version = ">= 1.15.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
locals {
common_tags = {
Project = var.project_name
Environment = "development"
ManagedBy = "terraform"
}
}
Here is the complete variables.tf:
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
variable "project_name" {
description = "Name used for tagging and naming resources"
type = string
default = "terraform-portfolio"
}
Initialize Terraform and download the AWS provider
Terraform needs to download the AWS provider plugin before it can create any resources. The terraform init command handles this automatically.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Initialize your Terraform project by running:
terraform init
Terraform downloads the AWS provider plugin and creates the lock file. You should see Terraform has been successfully initialized! at the end of the output.
What did terraform init do?
- Created a .terraform directory that stores the downloaded provider plugin (the AWS provider binary).
- Created a .terraform.lock.hcl file that pins the exact provider version. This ensures everyone on your team uses the same provider version.
- Verify the initialization by checking the Explorer sidebar in VS Code.
You should see the .terraform folder and .terraform.lock.hcl file alongside your four .tf files.
Seeing an error about provider requirements?
Make sure you saved both main.tf and variables.tf before running terraform init. Terraform reads the files on disk, not what is open in the editor.
Also check that your terminal's working directory is inside the terraform-portfolio folder. Run pwd to confirm your current location.
Help me troubleshoot my terraform init error
Your Terraform project is structured and initialized. Next up, you will define the network layer that gives your web server a path to the internet.
Build the Network Layer
Your Terraform project is initialized and connected to AWS. Now it is time to define the actual cloud infrastructure you want to create.
Every web server needs a network to live in. In AWS, that network is a VPC (Virtual Private Cloud). Think of it as your own private section of the AWS cloud where you control the IP addresses, routing, and access rules.
In this step, get ready to:
- Define a VPC, subnet, and internet gateway in Terraform code.
- Create a route table that directs traffic from your subnet to the internet.
- Preview your infrastructure with terraform plan.
Define the VPC, subnet, and internet gateway
Your network needs three foundational pieces. The VPC defines your private address space. A public subnet carves out a section of that space where resources can receive public IPs. An internet gateway connects your VPC to the public internet.
- Open networking.tf in VS Code (it should be empty).
- Add the VPC, subnet, and internet gateway resources by pasting this code:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(local.common_tags, {
Name = "${var.project_name}-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 = "${var.aws_region}a"
tags = merge(local.common_tags, {
Name = "${var.project_name}-public-subnet"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-igw"
})
}
What does this code do?
Each block defines an AWS resource in HCL (HashiCorp Configuration Language), the syntax Terraform uses.
- aws_vpc "main" creates a VPC with the IP range 10.0.0.0/16 (65,536 possible addresses). DNS support is enabled so resources can resolve domain names.
- aws_subnet "public" carves out a smaller range (10.0.1.0/24, 256 addresses) inside the VPC. map_public_ip_on_launch = true means any instance launched here automatically gets a public IP.
- aws_internet_gateway "main" is the bridge between your VPC and the public internet. Without it, nothing inside the VPC can reach the outside world.
- merge(local.common_tags, {...}) applies your shared tags (Project, Environment, ManagedBy) plus a unique Name tag to each resource.
You will not see a change yet. The next chunk adds routing, and then you will preview everything together with terraform plan.
Create the route table and subnet association
A subnet with a public IP still cannot reach the internet without a route telling traffic where to go. A route table contains those directions. You need one rule: send all outbound traffic (0.0.0.0/0) to the internet gateway.
- In networking.tf, add the route table and its subnet association below the internet gateway block:
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 = merge(local.common_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 two resources complete the network plumbing.
- aws_route_table "public" defines a single route: any traffic headed for 0.0.0.0/0 (anywhere on the internet) should be forwarded to the internet gateway.
- aws_route_table_association "public" links the route table to your public subnet. Without this link, the subnet uses the VPC's default route table (which has no internet route).
- Save networking.tf.
Seeing a red squiggle or syntax error?
- Make sure every opening brace { has a matching closing brace }. HCL is sensitive to mismatched braces.
- Check that string interpolations use the format ${var.project_name} with the dollar sign and curly braces inside double quotes.
- Confirm you have a blank line between each resource block for readability.
Why isn't my networking.tf saving without errors?
✔️ Awesome, I've got everything!
Great. Double check you have saved networking.tf with all five resource blocks (VPC, subnet, internet gateway, route table, route table association).
ⓧ I'd like to double check the full code
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(local.common_tags, {
Name = "${var.project_name}-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 = "${var.aws_region}a"
tags = merge(local.common_tags, {
Name = "${var.project_name}-public-subnet"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-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 = merge(local.common_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 with terraform plan
Before creating anything in AWS, Terraform lets you preview exactly what it will do. The terraform plan command reads your code, compares it to the current state (empty right now), and shows you a detailed list of resources it would create.
Why is this step important?
In a Cloud Architecture role, you never deploy infrastructure without reviewing the plan first. This is the equivalent of Bicep's what-if operation in Azure. It catches mistakes before they cost money or break things.
- Run the plan command in your terminal:
terraform plan
Terraform will output a list of resources prefixed with + symbols, meaning they will be created. At the bottom, you should see a summary line:
Plan: 5 to add, 0 to change, 0 to destroy.
What should I see in the plan output?
Each of the five resources you defined in networking.tf appears with a + prefix and its attributes listed below it. Some values show (known after apply) because AWS assigns them at creation time (like VPC IDs and subnet IDs).
The five resources are: aws_vpc.main, aws_subnet.public, aws_internet_gateway.main, aws_route_table.public, and aws_route_table_association.public.
Seeing an error instead of a plan?
- If you see a provider authentication error, run aws sts get-caller-identity to confirm your AWS credentials are still configured.
- If you see a syntax error pointing to a specific line in networking.tf, check that line for missing braces or typos.
- Make sure you are running the command from inside your terraform-portfolio project folder (the one containing main.tf).
Help me debug my terraform plan error.
Your network layer is defined and validated. Next up, you will add a security group and deploy an actual EC2 web server into this network.
Deploy Compute with a Web Server
Your network layer is live. You have a VPC, a public subnet, an internet gateway, and a route table ready to carry traffic to the outside world.
Now it's time to put something on that network. In this step, you'll create a security group to control who can reach your server, then launch an EC2 instance that automatically installs a web server and serves your portfolio page.
In this step, get ready to:
- Create a security group that allows HTTP and SSH traffic.
- Launch an EC2 instance bootstrapped with Apache and your portfolio HTML.
- Deploy everything with a single command and visit your live page.
Create the security group
Before launching a server, you need to define what traffic is allowed in and out. A security group acts as a virtual firewall for your EC2 instance.
- Create a new file called security.tf in your terraform-portfolio folder.
- Add the security group resource by pasting this code:
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg"
description = "Allow HTTP and SSH inbound traffic"
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-web-sg"
})
}
What does this code do?
This creates a security group named terraform-portfolio-web-sg inside your VPC. It doesn't define any traffic rules yet. The rules come next as separate resources.
Using merge(local.common_tags, {...}) applies your project-wide tags plus a resource-specific Name tag. This pattern keeps tagging consistent across every resource.
- Below the security group resource, add the traffic rules:
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 do these rules allow?
- The http rule allows anyone on the internet to reach port 80 (web traffic). This is what makes your page publicly accessible.
- The ssh rule allows inbound connections on port 22 for remote terminal access.
- The all_outbound rule with ip_protocol = "-1" allows all outbound traffic so your instance can download packages during setup.
- Save security.tf.
Seeing a syntax error?
- Make sure every opening brace { has a matching closing brace }. Count them in your file.
- Check that each string value is wrapped in double quotes, not single quotes.
- Help me debug my security.tf syntax
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg"
description = "Allow HTTP and SSH inbound traffic"
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${var.project_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"
}
Create the compute instance
Your security group is ready. Now you'll define the actual server. Terraform needs two things: which operating system image (AMI) to use, and the instance configuration itself.
- Create a new file called compute.tf in your terraform-portfolio folder.
- Add the AMI data source that finds the latest Amazon Linux 2023 image:
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
What is an AMI data source?
An AMI (Amazon Machine Image) is a template containing the operating system and software for your instance. Instead of hardcoding a specific AMI ID (which changes by region and over time), this data source queries AWS at deploy time to find the most recent Amazon Linux 2023 image.
The filters narrow results to x86_64 architecture with HVM virtualization. This guarantees you always get a compatible, up-to-date image.
- Below the data source, add the EC2 instance resource with its user data bootstrap script:
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.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
dnf update -y
dnf install -y httpd
systemctl start httpd
systemctl enable httpd
cat > /var/www/html/index.html << 'HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloud Portfolio</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; background: #f8f9fa; }
h1 { color: #1a1a2e; }
.cert { background: #e8f4fd; padding: 0.5rem 1rem; border-radius: 4px; margin: 0.25rem 0; display: inline-block; }
.project { background: white; padding: 1rem; border-radius: 8px; margin: 1rem 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.tag { font-size: 0.8rem; background: #28a745; color: white; padding: 0.2rem 0.5rem; border-radius: 3px; }
</style>
</head>
<body>
<h1>Cloud Engineering Portfolio</h1>
<p><span class="tag">Deployed with Terraform</span></p>
<h2>Certifications</h2>
<div class="cert">AWS Certified Solutions Architect</div>
<div class="cert">AWS Certified Cloud Practitioner</div>
<div class="cert">AWS Certified AI Practitioner</div>
<h2>Projects</h2>
<div class="project">
<h3>Infrastructure as Code Web Server</h3>
<p>Provisioned VPC, subnet, security groups, and EC2 using Terraform. Deployed this page with a single command.</p>
</div>
<div class="project">
<h3>AWS Serverless ETL Pipeline</h3>
<p>Built a serverless data pipeline with Lambda, Glue, Athena, and QuickSight processing 5,000+ recipes weekly.</p>
</div>
<div class="project">
<h3>ML-Driven S&P 500 Market Prediction</h3>
<p>Designed end-to-end ML pipelines using Random Forest and KNN classifiers on financial time-series data.</p>
</div>
<h2>AWS to Azure Translation</h2>
<table style="width:100%; border-collapse: collapse;">
<tr style="background:#1a1a2e; color:white;"><th style="padding:0.5rem;">AWS</th><th style="padding:0.5rem;">Azure Equivalent</th></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">VPC</td><td style="padding:0.5rem; border:1px solid #ddd;">Virtual Network</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">Security Group</td><td style="padding:0.5rem; border:1px solid #ddd;">Network Security Group</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">EC2</td><td style="padding:0.5rem; border:1px solid #ddd;">Virtual Machine</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">Terraform (HCL)</td><td style="padding:0.5rem; border:1px solid #ddd;">Terraform or Bicep</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">CloudFormation</td><td style="padding:0.5rem; border:1px solid #ddd;">ARM Templates / Bicep</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">AWS CLI</td><td style="padding:0.5rem; border:1px solid #ddd;">Azure CLI / PowerShell</td></tr>
</table>
</body>
</html>
HTML
EOF
tags = merge(local.common_tags, {
Name = "${var.project_name}-web-server"
})
}
What does this instance do?
- instance_type = "t2.micro" picks a small, low-cost instance. This costs approximately $0.0116/hour in us-east-1.
- subnet_id and vpc_security_group_ids place the instance in your public subnet and attach your firewall rules.
- user_data is a bootstrap script that runs once when the instance first launches. It installs Apache (httpd), starts the web server, and writes your portfolio HTML to the default web directory.
The HTML includes your certifications, projects, and an AWS-to-Azure translation table. This is the page visitors will see when they open your public IP.
- Save compute.tf.
Heredoc syntax tripping you up?
- The <<-EOF and closing EOF must be on their own lines. Make sure there's no trailing whitespace after EOF.
- The inner << 'HTML' and HTML markers work the same way for the nested heredoc inside the bash script.
- Help me debug my heredoc syntax
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.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
dnf update -y
dnf install -y httpd
systemctl start httpd
systemctl enable httpd
cat > /var/www/html/index.html << 'HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloud Portfolio</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; background: #f8f9fa; }
h1 { color: #1a1a2e; }
.cert { background: #e8f4fd; padding: 0.5rem 1rem; border-radius: 4px; margin: 0.25rem 0; display: inline-block; }
.project { background: white; padding: 1rem; border-radius: 8px; margin: 1rem 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.tag { font-size: 0.8rem; background: #28a745; color: white; padding: 0.2rem 0.5rem; border-radius: 3px; }
</style>
</head>
<body>
<h1>Cloud Engineering Portfolio</h1>
<p><span class="tag">Deployed with Terraform</span></p>
<h2>Certifications</h2>
<div class="cert">AWS Certified Solutions Architect</div>
<div class="cert">AWS Certified Cloud Practitioner</div>
<div class="cert">AWS Certified AI Practitioner</div>
<h2>Projects</h2>
<div class="project">
<h3>Infrastructure as Code Web Server</h3>
<p>Provisioned VPC, subnet, security groups, and EC2 using Terraform. Deployed this page with a single command.</p>
</div>
<div class="project">
<h3>AWS Serverless ETL Pipeline</h3>
<p>Built a serverless data pipeline with Lambda, Glue, Athena, and QuickSight processing 5,000+ recipes weekly.</p>
</div>
<div class="project">
<h3>ML-Driven S&P 500 Market Prediction</h3>
<p>Designed end-to-end ML pipelines using Random Forest and KNN classifiers on financial time-series data.</p>
</div>
<h2>AWS to Azure Translation</h2>
<table style="width:100%; border-collapse: collapse;">
<tr style="background:#1a1a2e; color:white;"><th style="padding:0.5rem;">AWS</th><th style="padding:0.5rem;">Azure Equivalent</th></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">VPC</td><td style="padding:0.5rem; border:1px solid #ddd;">Virtual Network</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">Security Group</td><td style="padding:0.5rem; border:1px solid #ddd;">Network Security Group</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">EC2</td><td style="padding:0.5rem; border:1px solid #ddd;">Virtual Machine</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">Terraform (HCL)</td><td style="padding:0.5rem; border:1px solid #ddd;">Terraform or Bicep</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">CloudFormation</td><td style="padding:0.5rem; border:1px solid #ddd;">ARM Templates / Bicep</td></tr>
<tr><td style="padding:0.5rem; border:1px solid #ddd;">AWS CLI</td><td style="padding:0.5rem; border:1px solid #ddd;">Azure CLI / PowerShell</td></tr>
</table>
</body>
</html>
HTML
EOF
tags = merge(local.common_tags, {
Name = "${var.project_name}-web-server"
})
}
Add outputs and deploy your infrastructure
Your infrastructure code is complete. The last piece is telling Terraform which values to display after a successful deployment so you can easily find your server's public IP.
- Open outputs.tf (which is currently empty) and add these output definitions:
output "web_server_public_ip" {
description = "Public IP address of the web server"
value = aws_instance.web.public_ip
}
output "website_url" {
description = "URL to access the portfolio website"
value = "http://${aws_instance.web.public_ip}"
}
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
What do outputs do?
Outputs are values Terraform prints to your terminal after a successful apply. Without them, you'd have to dig through the AWS Console to find your instance's IP address.
The website_url output wraps the IP in a full http:// URL so you can copy-paste it directly into your browser.
- Save outputs.tf.
- Deploy all your infrastructure by running this command in your terminal:
terraform apply
What happens during terraform apply?
Terraform first runs a plan (just like terraform plan did in the previous step) showing you exactly what it will create. It then asks for confirmation before making any changes to your AWS account.
This is the equivalent of Bicep's deployment command in Azure. The key difference: Terraform stores state locally (in terraform.tfstate), while Bicep relies on Azure Resource Manager to track state.
- Terraform will display a plan showing 7 resources to create. When you see the confirmation prompt, type yes and press Enter.
- Wait for the deployment to complete. This typically takes 1-2 minutes as AWS provisions the VPC, subnet, security group, and EC2 instance.
What should I see?
When deployment finishes, you'll see a message like:
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
Outputs:
web_server_public_ip = "<your-ip-address>"
website_url = "http://<your-ip-address>"
vpc_id = "vpc-xxxxxxxxxxxxxxxxx"
The IP address in your output is unique to your deployment.
- Copy the website_url value from the terminal output.
- Paste it into your browser's address bar and press Enter.
- You should see your Cloud Engineering Portfolio page with your certifications, projects, and the AWS-to-Azure translation table.
Page not loading?
- Give it 30-60 seconds. The EC2 instance needs time to run the user data script after launch. The dnf update command can take a moment.
- Make sure you're using http:// (not https://). Your security group only allows port 80, which is plain HTTP.
- Check that security.tf has the HTTP ingress rule on port 80. If it's missing, add it, then run terraform apply again.
- Help me troubleshoot my deployment
Your infrastructure is live. A single terraform apply command created an entire network, security layer, and web server from code. Next up, you'll document your project and learn how to tear it all down cleanly.
Document and Destroy
Your portfolio page is live and accessible from anywhere in the world. Every resource behind it is defined in code, deployed with a single command.
In this final step, you will document your project so it is ready for a recruiter or interviewer to read on GitHub. Then you will tear down all the infrastructure to stop the meter running on your AWS bill.
In this step, get ready to:
- Create a README with architecture notes and an AWS-to-Azure translation table.
- Push your project to GitHub with a proper .gitignore.
- Destroy all AWS resources with a single Terraform command.
Create the README with architecture notes
A README is the first file anyone sees when they open your repository. It explains what the project does, how to run it, and why you built it this way.
- In VS Code, create a new file called README.md in the same folder as your .tf files.
- Paste the following content into README.md:
# terraform-portfolio
A portfolio web page served by an EC2 instance inside a fully Terraform-provisioned VPC.
## Architecture
- VPC (10.0.0.0/16) with public subnet (10.0.1.0/24)
- Internet Gateway and route table for public access
- Security Group allowing HTTP (80) and SSH (22) inbound
- EC2 t2.micro instance running Apache httpd with portfolio HTML
## Commands
- `terraform init` - Initialize project and download providers
- `terraform plan` - Preview infrastructure changes
- `terraform apply` - Deploy infrastructure
- `terraform destroy` - Tear down all resources
## AWS to Azure Translation
| AWS | Azure Equivalent |
|-----|------------------|
| VPC | Virtual Network |
| Subnet | Subnet |
| Security Group | Network Security Group |
| EC2 | Virtual Machine |
| Internet Gateway | Internet routing (built into Azure VNets) |
| Terraform | Terraform or Bicep |
| AWS CLI | Azure CLI or PowerShell |
What does this README cover?
- The Architecture section lists exactly what Terraform provisions, so a reviewer can see the scope at a glance.
- The Commands section shows the four-step Terraform workflow (init, plan, apply, destroy) that maps directly to Bicep's what-if, deploy, and delete pattern in Azure.
- The AWS to Azure Translation table demonstrates you can translate between cloud platforms, a key skill for multi-cloud teams.
- Save README.md.
Formatting looks wrong in preview?
Make sure there is a blank line between each heading (##) and the content below it. Markdown requires blank lines before and after headings to render correctly.
Help me fix my README formatting
Initialize git and push to GitHub
Version control turns your project folder into a trackable history of changes. A .gitignore file tells Git which files to skip, keeping secrets and large generated files out of your repository.
- Create a new file called .gitignore in the same folder as your .tf files.
- Paste the following content into .gitignore:
.terraform/
*.tfstate
*.tfstate.backup
terraform.tfvars
.terraform.lock.hcl
Why exclude these files?
.terraform/ contains downloaded provider binaries (hundreds of MB). State files (*.tfstate) contain sensitive resource IDs and metadata. terraform.tfvars often holds secrets like access keys. None of these belong in a public repository.
- Save .gitignore.
- Initialize a git repository and create your first commit by running these commands in the VS Code terminal:
git init -b main
git add .
git commit -m "Initial commit"
What do these commands do?
- git init -b main creates a new Git repository with main as the default branch name.
- git add . stages all files (except those in .gitignore) for the commit.
- git commit -m "Initial commit" saves a snapshot of your project at this point.
You should see output listing the files committed, confirming your .tf files, README.md, and .gitignore are tracked.
- Open your browser and go to github.com.
- Click the + icon in the top-right corner, then click New repository.
- Enter terraform-portfolio as the repository name.
- Leave Public selected (so interviewers can see it).
- Do NOT check any of the initialization options (no README, no .gitignore, no license). You already have these locally.
- Click Create repository.
GitHub shows a page with setup instructions. Copy the HTTPS URL shown (it looks like https://github.com/YOUR-USERNAME/terraform-portfolio.git).
- Back in your VS Code terminal, link your local repo to GitHub and push by running these commands:
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR-USERNAME/terraform-portfolio.git"]]
git push -u origin main
What do these commands do?
- git remote add origin links your local repository to the GitHub repository you just created.
- git push -u origin main uploads all your committed files to GitHub and sets main as the default upstream branch.
- Refresh your GitHub repository page in the browser. You should see all your .tf files, README.md, and .gitignore listed.
- The README content renders automatically below the file list, showing your architecture notes and translation table.
Push rejected or authentication failed?
If you see an authentication error, GitHub now requires a personal access token instead of your password. Go to Settings > Developer settings > Personal access tokens > Tokens (classic) on GitHub, generate a new token with repo scope, and use it as your password when prompted.
Help me set up GitHub authentication
✔️ Awesome, I've got everything!
Great. Double check you have saved both README.md and .gitignore before moving on.
ⓧ I'd like to double check the full code
Here is the complete README.md:
# terraform-portfolio
A portfolio web page served by an EC2 instance inside a fully Terraform-provisioned VPC.
## Architecture
- VPC (10.0.0.0/16) with public subnet (10.0.1.0/24)
- Internet Gateway and route table for public access
- Security Group allowing HTTP (80) and SSH (22) inbound
- EC2 t2.micro instance running Apache httpd with portfolio HTML
## Commands
- `terraform init` - Initialize project and download providers
- `terraform plan` - Preview infrastructure changes
- `terraform apply` - Deploy infrastructure
- `terraform destroy` - Tear down all resources
## AWS to Azure Translation
| AWS | Azure Equivalent |
|-----|------------------|
| VPC | Virtual Network |
| Subnet | Subnet |
| Security Group | Network Security Group |
| EC2 | Virtual Machine |
| Internet Gateway | Internet routing (built into Azure VNets) |
| Terraform | Terraform or Bicep |
| AWS CLI | Azure CLI or PowerShell |
Here is the complete .gitignore:
.terraform/
*.tfstate
*.tfstate.backup
terraform.tfvars
.terraform.lock.hcl
Tear down all infrastructure with terraform destroy
Your EC2 instance costs approximately $0.0116/hour while running. Leaving it overnight would cost around $0.28, and a full month would be about $8.50. Terraform can reverse everything it created with a single command.
- Destroy all your AWS resources by running this command:
terraform destroy
What does terraform destroy do?
Terraform reads your state file to identify every resource it manages. It then deletes them in the correct dependency order (instance first, then security group, then subnet, then route table, then internet gateway, then VPC).
This is the equivalent of Bicep's delete operation in Azure, where removing a resource group destroys all resources inside it.
- Terraform displays a plan showing all resources marked for destruction. Type yes and press Enter to confirm.
- Wait for Terraform to finish. You should see a summary message confirming all resources have been destroyed.
What should I see?
Terraform outputs each resource as it is destroyed, ending with a summary that confirms all resources are gone. The VPC, subnet, internet gateway, route table, security group, and EC2 instance are all removed.
- Verify in the AWS Console that your resources are gone. Open the VPC service in the AWS Console and confirm the terraform-portfolio-vpc is no longer listed.
- Open the EC2 service and confirm no instances are running (or the instance shows as terminated).
Your bill is now $0
With all resources destroyed, no further charges accrue. The total cost for this project is typically under $0.05 if you destroyed promptly after completing the steps.
Destroy failed or timed out?
If the destroy fails on a dependency error, run terraform destroy again. Terraform retries and usually succeeds on the second attempt once the dependent resource finishes deleting.
If you still see resources in the AWS Console after a successful destroy, wait 1-2 minutes and refresh. AWS can take a moment to propagate the deletion.
Help me fix a failed terraform destroy
Secret mission
Add Cost Governance Tags and Auto-Shutdown
Cloud Architecture teams don't just deploy infrastructure. They govern it. Add cost allocation tags and an auto-shutdown schedule to prove you understand cloud cost management.
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 instance costs approximately $0.0116/hour while it runs, so leaving everything up will add up over time.
Cost warning
Your t2.micro instance costs roughly $8.50/month if left running continuously. Even if the auto-shutdown script stops it after 2 hours, EBS storage still incurs about $0.80/month.
Running terraform destroy drops your cost to $0. This is recommended after completing the project.
Resources you used:
- 1 EC2 instance (t2.micro) running Apache httpd.
- 1 VPC with a public subnet, internet gateway, and route table.
- 1 security group allowing HTTP and SSH traffic.
- 1 IAM user with programmatic access credentials.
- Local project folder (terraform-portfolio on your Desktop).
- GitHub repository.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep the web server accessible for a bit longer.
- Your EC2 instance will auto-shutdown after 2 hours thanks to the user data script you added.
- If you restart it later, it will continue to cost approximately $0.0116/hour.
- Your IAM user credentials remain active on your machine in the .aws folder.
- Terraform and AWS CLI remain installed on your system.
Pause - I'll come back to this later
Stop the EC2 instance to free up compute costs while keeping your infrastructure definition intact.
- Open the AWS Console and navigate to EC2 > Instances.
- Select your terraform-portfolio-web-server instance.
- Click Instance state > Stop instance.
- EBS storage still costs approximately $0.80/month for 8 GB, but compute charges stop immediately.
- To resume later, select the instance and click Instance state > Start instance.
Delete - I don't want to use this again
Remove all project resources and start fresh. This drops your AWS cost to $0.
- In your VS Code terminal, make sure you are inside the terraform-portfolio folder on your Desktop.
- Run the following command to destroy all AWS infrastructure:
terraform destroy
- Type yes when prompted to confirm.
- Wait for Terraform to finish removing all 6 resources (VPC, subnet, internet gateway, route table, security group, EC2 instance).
Next, delete the IAM user you created for this project.
- Open the AWS Console and navigate to IAM > Users.
- Select the checkbox next to the user you created in Step 1.
- Click Delete at the top of the page.
- Type the username in the confirmation field and click Delete again.
Remove the AWS CLI credentials stored on your machine.
- Delete the .aws folder in your user directory by running:
Remove-Item -Recurse -Force ~\.aws
Remove the local project files from your Desktop.
- Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
Remove-Item -Recurse -Force terraform-portfolio
Optionally, delete the GitHub repository.
- On GitHub, go to your repository page.
- Click Settings at the top of the repository.
- Scroll to the bottom and click Delete this repository.
- Follow the confirmation prompts to complete deletion.
Optionally, uninstall AWS CLI v2.
- Open Windows Settings > Apps > Installed apps.
- Find AWS Command Line Interface v2 and click Uninstall.
Optionally, remove Terraform.
- Delete the folder where you extracted terraform.exe (e.g., C:\terraform).
- Remove that folder from your system PATH in Environment Variables.
Nice Work!
Nice Work!
Nice work! You just deployed a live web server to AWS with every piece of infrastructure defined in code.
You've learned how to:
- Write modular Terraform HCL files defining VPC networking, security groups, and compute infrastructure from scratch.
- Deploy a live web server accessible via a public IP using the terraform plan/terraform apply/terraform destroy workflow, which maps directly to Bicep's what-if/deployment/delete pattern in Azure.
- Document AWS-to-Azure service translations and push a clean, professional Infrastructure as Code repository to GitHub.
- Secret Mission: Implement cost governance tagging and auto-shutdown scheduling to demonstrate cloud cost awareness.
Ready to quiz yourself?