Deploy a Web Server with Terraform
Provision a full AWS networking stack and EC2 instance using Terraform code.
Introduction
30 Second Summary
Every time you set up a cloud server by clicking through a web console, that work disappears the moment you close your browser tab. There is no way to repeat it, share it, or undo a mistake without starting over.
In this project, you will use Terraform to write code that defines and deploys a complete AWS server. Instead of clicking buttons, you will describe your infrastructure in files that you can version, share, and reuse.
What You'll Build
A complete AWS infrastructure stack defined in Terraform configuration files, including networking, a firewall, and a live server you can reach on the public internet.
By the end of this project, you'll have:
- A working Terraform project with proper file structure (main.tf, variables.tf, outputs.tf).
- A full VPC networking stack deployed to AWS, including a subnet, internet gateway, and route table.
- A live EC2 instance running on the public internet, deployed entirely through code with zero console clicks.
- Secret Mission: Auto-deploy an Nginx web server using EC2 user data scripts, so your instance serves a live web page without any manual setup.
Are there any prerequisites?
You need an AWS account (free tier eligible) to complete this project. No prior Terraform experience is required.
Install Terraform and Configure AWS CLI
To deploy infrastructure as code, you need two tools talking to each other: Terraform to define your resources, and the AWS CLI to authenticate with your AWS account.
In this step, you will install both tools on your Mac and connect the AWS CLI to your account using an access key.
In this step, get ready to:
- Install Terraform via Homebrew.
- Install AWS CLI v2.
- Configure AWS credentials for programmatic access.
Install Terraform
Terraform is a command-line tool that reads your infrastructure code and provisions real cloud resources from it.
- Open your terminal by pressing Cmd+Space (macOS) to open Spotlight, typing Terminal, and pressing Enter.
- Check if Terraform is already installed by running this command:
terraform --version
✔️ I see version 1.15 or higher
Your output starts with something like Terraform v1.15. You are good to go. Skip ahead to the next substep (Install AWS CLI v2).
ⓧ I see an older version
You have Terraform installed, but it may be an outdated version. The core Homebrew terraform formula is disabled at version 1.5.7. You need to install the latest version from the official HashiCorp tap.
- Remove the old formula and install from the HashiCorp tap by running these commands:
brew uninstall terraform
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
- Verify the new version by running:
terraform --version
You should now see a version starting with 1.15.
ⓧ Command not found
Terraform is not installed yet. You will install it using Homebrew, the macOS package manager.
What is Homebrew?
Homebrew is a package manager for macOS that lets you install command-line tools with a single command. If you do not have Homebrew installed, visit brew.sh and follow the one-line install instructions first.
- Add the official HashiCorp tap and install Terraform by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
Why the HashiCorp tap?
The default Homebrew terraform formula is disabled and stuck at version 1.5.7 due to a license change. The official hashicorp/tap provides the latest release (1.15.2).
- Verify the installation by running:
terraform --version
You should see output starting with Terraform v1.15 followed by your platform name.
Still seeing command not found?
Make sure Homebrew itself is installed and on your PATH. Try running brew --version to confirm. If that also fails, install Homebrew from brew.sh first, then retry the Terraform install commands.
Help me fix my PATH so the terminal can find Terraform.
Install AWS CLI v2
The AWS CLI is the command-line interface that authenticates your local machine with your AWS account. Terraform uses these stored credentials to provision resources.
- Check if AWS CLI is already installed by running this command:
aws --version
✔️ I see version 2.x
Your output starts with aws-cli/2. followed by a version number. You are good to go. Skip ahead to the next substep (Configure AWS credentials).
ⓧ I see version 1.x
You have an older version of the AWS CLI. Version 2 is required for this project.
- Download and install the official macOS package by running:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
- Verify the new version by running:
aws --version
You should now see output starting with aws-cli/2. confirming version 2 is active.
ⓧ Command not found
AWS CLI is not installed yet. You will install the official macOS package.
- Download and install AWS CLI v2 by running these commands:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
What do these commands do?
The first command downloads the official AWS CLI installer package. The second command runs the macOS package installer, which places the aws binary on your system PATH.
You will be prompted for your Mac password because sudo runs the installer with administrator privileges.
- Verify the installation by running:
aws --version
You should see output starting with aws-cli/2. followed by additional version details.
Command still not found after install?
Close your terminal window and open a new one. The installer adds aws to /usr/local/bin, which requires a fresh shell session to detect.
Help me fix my AWS CLI installation.
Configure AWS credentials
Terraform needs permission to create resources in your AWS account. You will create an IAM user with an access key and store those credentials locally using the AWS CLI.
- Open the AWS Management Console in your browser and sign in to your account.
- Search for IAM in the top search bar and select IAM from the results.
- In the left navigation pane, click Users.
- Click Create user.
- On the Specify user details page, enter terraform-user as the User name.
- Click Next.
- On the Set permissions page, select Attach policies directly.
- Search for AdministratorAccess and select the checkbox next to it.
- Click Next.
- Click Create user.
Why AdministratorAccess?
For this learning project, full admin access keeps things simple. In a production environment, you would create a custom policy granting only the specific permissions needed (EC2, VPC, etc.).
Now create an access key for this user so the AWS CLI can authenticate as them.
- Click the terraform-user link in the Users list to open the user details page.
- Click the Security credentials tab.
- Scroll down to the Access keys section and click Create access key.
- On the Access key best practices & alternatives page, select Command Line Interface (CLI).
- Check the confirmation checkbox at the bottom.
- Click Next.
- Click Create access key.
- Click Download .csv file to save your credentials. Store this file somewhere safe. This is the only time you can view the secret access key.
- Click Done.
Save your credentials now
AWS only shows the secret access key once at creation time. If you lose it, you will need to delete the key and create a new one. Download the CSV and keep it in a safe place.
Now configure the AWS CLI with the access key you just created.
- Run the configuration command in your terminal:
aws configure
- When prompted, enter your AWS Access Key ID from the downloaded CSV file.
- Enter your AWS Secret Access Key from the CSV file.
- For Default region name, type us-east-1.
- For Default output format, type json.
What does aws configure do?
This command stores your credentials in ~/.aws/credentials and your region/output preferences in ~/.aws/config. Terraform automatically reads these files when it needs to authenticate with AWS.
- Verify your credentials are working by running:
aws sts get-caller-identity
You should see a JSON response containing your account ID, user ARN, and the username terraform-user.
Seeing an authentication error?
Double-check that you copied the Access Key ID and Secret Access Key exactly from the CSV file. There should be no extra spaces or line breaks. If the key is wrong, run aws configure again to re-enter them.
Help me fix my AWS CLI credentials.
Your machine is now fully equipped to talk to AWS through code. Next up, you will create the Terraform project structure and start writing your first infrastructure definitions.
Set Up the Terraform Project Structure
You have Terraform and the AWS CLI installed and configured. Now it's time to create the project that will hold your entire infrastructure definition as code.
Every Terraform project uses a clear file structure to separate concerns. You will create a dedicated folder with three files that divide your configuration into inputs, resources, and outputs.
In this step, get ready to:
- Create the project directory and three configuration files.
- Define input variables in variables.tf.
- Configure the AWS provider in main.tf.
Create the project directory and files
Terraform reads .tf files from a single directory to build your infrastructure plan. You need a dedicated folder to keep everything organized.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir terraform-web-server
cd terraform-web-server
- Create the three Terraform configuration files by running:
touch main.tf variables.tf outputs.tf
- Confirm the files exist by running:
ls
You should see main.tf, outputs.tf, and variables.tf listed in the output.
Don't see all three files?
Make sure you ran the touch command from inside the terraform-web-server directory. Run pwd to check your current location.
If you see No such file or directory errors, make sure the mkdir command succeeded before running cd. help me figure out why my files weren't created
.
- Open your code editor by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the terraform-web-server folder, then click Open.
Why three separate files?
Terraform loads all .tf files in a directory as one configuration. Splitting into main.tf (resources), variables.tf (inputs), and outputs.tf (outputs) is a community convention that keeps your project readable as it grows.
Define your input variables
Variables let you change values like the AWS region or network size without editing the resource definitions directly. This makes your configuration reusable across environments.
- In VS Code, open variables.tf from the file explorer sidebar and paste in the following code:
variable "aws_region" {
description = "AWS region to deploy resources in"
type = string
default = "us-east-1"
}
variable "vpc_cidr_block" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidr" {
description = "CIDR block for the public subnet"
type = string
default = "10.0.1.0/24"
}
What does this code do?
Each variable block declares an input that can be overridden at runtime. The three fields inside each block serve specific purposes:
- description documents what the variable is for (shows up in terraform plan output).
- type enforces that only strings can be passed in, catching typos early.
- default provides a fallback value so you can run terraform apply without passing values manually.
The vpc_cidr_block and public_subnet_cidr variables define the IP address ranges for your VPC networking. You will use these in the next step.
- Save variables.tf by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Seeing red squiggly lines in VS Code?
VS Code may not recognize .tf files by default. This does not affect Terraform's ability to read them. If you want syntax highlighting, install the HashiCorp Terraform extension from the VS Code marketplace.
If you see actual syntax errors, make sure every opening brace { has a matching closing brace } and that string values are wrapped in double quotes. Help me fix syntax errors in my variables.tf
.
✔️ Awesome, I've got everything!
Great. Make sure you have saved variables.tf before moving on.
ⓧ I'd like to double check the full code
Here is the complete variables.tf file:
variable "aws_region" {
description = "AWS region to deploy resources in"
type = string
default = "us-east-1"
}
variable "vpc_cidr_block" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidr" {
description = "CIDR block for the public subnet"
type = string
default = "10.0.1.0/24"
}
Configure the AWS provider
The terraform block tells Terraform which provider plugins to download. The provider block configures how that plugin connects to AWS.
- Open main.tf in VS Code and paste in the following code:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.15.0"
}
provider "aws" {
region = var.aws_region
}
What does this code do?
- required_providers declares that this project needs the AWS provider from HashiCorp's registry. The ~> 6.0 version constraint allows any 6.x release but blocks future major version changes that could break your code.
- required_version ensures anyone running this project has Terraform 1.15.0 or higher installed.
- provider "aws" configures the AWS plugin. By referencing var.aws_region, it reads the region from your variables.tf file instead of hardcoding it.
- Save main.tf by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Switch to your terminal. Make sure you are still inside the terraform-web-server directory, then run:
terraform validate
You should see a message confirming the configuration is valid. This proves Terraform can read both files and that your provider reference is correct.
Seeing a validation error?
If Terraform reports an error about missing required providers, you may need to run terraform init first. That is covered in Step 5, so for now a successful validate confirms your syntax is correct.
If you see a version constraint error, double check that your required_version line reads >= 1.15.0 with the quotes included. Help me fix my Terraform validation error
.
✔️ Awesome, I've got everything!
Great. Make sure you have saved main.tf before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.tf file so far:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.15.0"
}
provider "aws" {
region = var.aws_region
}
Your project skeleton is in place. Next up, you will fill main.tf with real AWS resources by defining a VPC, subnet, internet gateway, and route table.
Define the VPC and Networking
Your Terraform project structure is ready with properly separated files for resources, variables, and outputs. Now it is time to define the actual cloud infrastructure.
Every server needs a network to live in. On AWS, that network is a VPC (Virtual Private Cloud). In this step, you will build a complete networking stack: a VPC, an internet gateway, a public subnet, and a route table that connects everything to the public internet.
In this step, get ready to:
- Create a VPC and internet gateway for public internet access.
- Add a public subnet that automatically assigns public IPs to instances.
- Wire up a route table that directs outbound traffic through the internet gateway.
Add the VPC and internet gateway
A VPC is an isolated virtual network where your AWS resources live. On its own, a VPC has no internet access. An Internet Gateway is the bridge that connects your VPC to the public internet.
- In main.tf, find the closing brace of your provider "aws" block. Add the following code below it:
# --- Networking ---
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr_block
tags = {
Name = "terraform-web-server-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-igw"
}
}
What does this code do?
The aws_vpc resource creates your isolated network. The cidr_block defines the IP address range available inside the VPC. You are pulling this value from the variable you defined in variables.tf (10.0.0.0/16, which gives you over 65,000 possible addresses).
The aws_internet_gateway resource attaches to the VPC via aws_vpc.main.id. This is how Terraform resources reference each other. Terraform uses these references to figure out the correct order to create resources.
- Save main.tf.
You will not see output yet. The subnet and route table you add next will complete this network.
Seeing red squiggles in your editor?
If your editor shows errors on var.vpc_cidr_block or aws_vpc.main.id, confirm that your variables.tf file exists in the same folder and contains the vpc_cidr_block variable.
Check that every opening brace { has a matching closing brace }. Missing braces are the most common HCL syntax error.
Why is my VPC code showing errors in main.tf?
Add the public subnet with a dynamic availability zone
A subnet is a smaller slice of your VPC's IP range. You need at least one subnet to place resources like EC2 instances. Instead of hardcoding which availability zone to use, you will use a data source to ask AWS which zones are available in your region.
- In main.tf, add this code below the internet gateway resource:
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = {
Name = "terraform-web-server-public-subnet"
}
}
What does this code do?
The data "aws_availability_zones" block is a data source. Unlike resources (which create things), data sources look up existing information from AWS. This one fetches the list of availability zones in your region that are currently available.
The subnet uses data.aws_availability_zones.available.names[0] to pick the first zone from that list. The map_public_ip_on_launch = true setting means any instance launched in this subnet will automatically receive a public IP address.
- Save main.tf.
The route table in the next substep will finish connecting this subnet to the internet.
Alignment looks off?
HCL uses spaces for alignment (not tabs). If your indentation looks inconsistent, you can run this command in your terminal from the terraform-web-server folder to auto-format your files:
terraform fmt
This command fixes indentation and spacing without changing any logic.
My Terraform code formatting looks wrong. How do I fix it?
Add the route table and association
A subnet with a public IP is not enough on its own. AWS also needs a route table that tells traffic where to go. Without a route pointing to the internet gateway, your subnet's traffic has no path out.
- In main.tf, add this code below the subnet resource:
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 = "terraform-web-server-public-rt"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
What does this code do?
The route table contains a single route: 0.0.0.0/0 (meaning "all traffic") should flow through the internet gateway. This is what makes the subnet truly public.
The aws_route_table_association links the route table to your subnet. Without this association, the subnet would use the VPC's default route table (which has no internet route).
- Save main.tf.
- Confirm your formatting is clean by running this in your terminal:
terraform fmt
If the command prints a filename, it means it fixed formatting in that file. If it prints nothing, your formatting was already correct.
Seeing an error from terraform fmt?
If you see an error mentioning a syntax problem, check for mismatched braces. Each resource block needs an opening { and closing }, and nested blocks like route { } and tags { } need their own matching pairs.
Also confirm you are running the command from inside the terraform-web-server directory.
Help me fix a syntax error in my Terraform networking code.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.tf with all the networking resources.
ⓧ I'd like to double check the full code
Here is the complete main.tf as it should look at the end of this step:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.15.0"
}
provider "aws" {
region = var.aws_region
}
# --- Networking ---
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr_block
tags = {
Name = "terraform-web-server-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-igw"
}
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = {
Name = "terraform-web-server-public-subnet"
}
}
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 = "terraform-web-server-public-rt"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
Your networking stack is complete. You have a VPC with internet access, a public subnet, and a route table wiring it all together. Next up, you will define the security group that controls which traffic can reach your server, then launch the EC2 instance itself.
Define the Security Group and EC2 Instance
Your network is ready. You have a VPC, a public subnet, an internet gateway, and a route table that sends traffic to the internet.
Now you need two more things before your server can go live: a firewall that controls which traffic can reach it, and the server itself. In this step, you will define a security group (the firewall) and an EC2 instance (the server).
In this step, get ready to:
- Create a security group that allows SSH and HTTP traffic.
- Add a data source that finds the latest Amazon Linux 2023 AMI.
- Define the EC2 instance that will become your web server.
Add the security group
An AWS security group acts as a virtual firewall for your instance. It defines which traffic is allowed in (ingress) and which traffic is allowed out (egress).
Your web server needs two inbound rules: SSH on port 22 so you can connect to it, and HTTP on port 80 so browsers can reach it.
- In main.tf, add the following code below the aws_route_table_association resource:
# --- Security Group ---
resource "aws_security_group" "web_server" {
name = "terraform-web-server-sg"
description = "Allow SSH and HTTP inbound traffic"
vpc_id = aws_vpc.main.id
ingress {
description = "SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
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 = "terraform-web-server-sg"
}
}
What does this code do?
The first ingress block opens port 22 for SSH connections. The second opens port 80 for HTTP web traffic. Both allow traffic from any IP address (0.0.0.0/0).
The egress block with protocol = "-1" means all protocols are allowed outbound. This lets your server download packages and respond to requests.
Why allow SSH from anywhere?
Opening SSH to 0.0.0.0/0 is fine for a learning project. In production, you would restrict this to your specific IP address to prevent unauthorized access.
- Save main.tf.
- You won't see a change yet. The next substep adds the AMI data source that your EC2 instance will reference.
Seeing syntax errors in your editor?
Make sure every opening brace { has a matching closing brace }. The security group has nested blocks (ingress, egress) inside the main resource block, so count your braces carefully.
Also check that the vpc_id = aws_vpc.main.id line references the VPC you defined in the previous step. The name must match exactly.
Help me fix a syntax error in my Terraform security group resource.
Add the AMI data source
An AMI (Amazon Machine Image) is the template for your server's operating system. Instead of hardcoding an AMI ID (which changes per region and over time), you will use a data source to find the latest one automatically.
- In main.tf, add the following code below the security group's closing brace:
# --- 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"]
}
}
What does this code do?
This is a data source. Unlike a resource block that creates something, a data block looks up existing information in AWS.
The filters narrow results to Amazon Linux 2023 images for x86_64 architecture using HVM virtualization. Setting most_recent = true ensures you always get the latest patched version.
- Save main.tf.
- You won't see a change yet. The next substep creates the EC2 instance that uses this AMI.
Not sure about the filter syntax?
Each filter block requires a name field (the AWS attribute to filter on) and a values field (a list of acceptable values). Make sure values uses square brackets even for a single item.
Help me understand the aws_ami data source filter syntax in Terraform.
Add the EC2 instance
This is the actual server. The instance resource ties everything together: it uses the AMI you just looked up, places itself in your public subnet, and attaches the security group for firewall rules.
- In main.tf, add the following code directly below the aws_ami data source:
resource "aws_instance" "web_server" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web_server.id]
tags = {
Name = "terraform-web-server"
}
}
What does this code do?
The ami argument references the data source you just created. Terraform resolves data.aws_ami.amazon_linux_2023.id to the latest matching AMI ID at plan time.
The t3.micro instance type is part of the AWS Free Tier (750 hours per month for the first 12 months). vpc_security_group_ids takes a list, so you wrap the single security group ID in square brackets.
- Save main.tf.
- You have now defined all the infrastructure resources for your web server. In the next step, you will add outputs and deploy everything with Terraform.
Unsure if your references are correct?
Double check that aws_subnet.public.id matches the subnet resource name you defined in the previous step. The pattern is always resource_type.resource_name.attribute.
If your editor shows a warning about vpc_security_group_ids, make sure you are using square brackets around the value. This argument expects a list, not a single string.
Help me check my EC2 instance resource references are correct.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.tf with all three new blocks: the security group, the AMI data source, and the EC2 instance.
ⓧ I'd like to double check the full code
Here is the complete main.tf file as it should look now:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.15.0"
}
provider "aws" {
region = var.aws_region
}
# --- Networking ---
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr_block
tags = {
Name = "terraform-web-server-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-igw"
}
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = {
Name = "terraform-web-server-public-subnet"
}
}
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 = "terraform-web-server-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_server" {
name = "terraform-web-server-sg"
description = "Allow SSH and HTTP inbound traffic"
vpc_id = aws_vpc.main.id
ingress {
description = "SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
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 = "terraform-web-server-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_server" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web_server.id]
tags = {
Name = "terraform-web-server"
}
}
Your entire infrastructure is now defined in code. Next up, you will add output values and deploy everything to AWS with a single command.
Add Outputs and Deploy
Your infrastructure code is complete. Every resource is defined, from the VPC down to the EC2 instance.
Now you need two things: a way to retrieve your server's public IP address after deployment, and a way to actually bring these resources to life. In this step, you will write Terraform outputs and run the full deploy workflow.
In this step, get ready to:
- Define output values that display your instance's public IP and ID after deployment.
- Initialize Terraform and preview the plan.
- Deploy all resources to AWS and verify the instance is running.
Define the output values
Terraform outputs let you extract specific values from your deployed infrastructure. Without outputs, you would need to log into the AWS console to find your instance's IP address every time you deploy.
- Open outputs.tf in your editor and add the following code:
output "instance_public_ip" {
description = "Public IP address of the EC2 instance"
value = aws_instance.web_server.public_ip
}
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web_server.id
}
What does this code do?
Each output block declares a value that Terraform will print to the terminal after a successful terraform apply.
- instance_public_ip pulls the public IP address from the EC2 instance. You will use this IP to connect to your server.
- instance_id pulls the unique instance identifier that AWS assigns. This is useful for referencing the instance in other commands or scripts.
- Save outputs.tf.
✔️ Awesome, I've got everything!
Great. Double check you have saved outputs.tf before moving on.
ⓧ I'd like to double check the full code
output "instance_public_ip" {
description = "Public IP address of the EC2 instance"
value = aws_instance.web_server.public_ip
}
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web_server.id
}
Initialize Terraform and preview the plan
Before Terraform can create any resources, it needs to download the AWS provider plugin that knows how to talk to the AWS API. This is what terraform init does.
- Make sure your terminal is inside the terraform-web-server folder.
- Download the AWS provider by running this command:
terraform init
What does this command do?
terraform init reads your required_providers block, downloads the hashicorp/aws provider plugin into a hidden .terraform directory, and creates a .terraform.lock.hcl lock file to pin the exact provider version.
- You should see a message ending with "Terraform has been successfully initialized!" in your terminal.
Seeing an error during init?
- If you see a provider version error, check that your main.tf has version = "~> 6.0" in the required_providers block.
- If you see a credentials error, run aws configure again and verify your Access Key ID and Secret Access Key are correct.
Why is Terraform failing to initialize?
Now preview what Terraform will create. This is the planning step where Terraform compares your code to reality (nothing exists yet) and shows you what it intends to do.
- Preview the infrastructure plan by running:
terraform plan
What does this command do?
terraform plan reads all your .tf files, checks the current state (empty, since nothing exists), and shows exactly which resources it will create. It does not make any changes.
Look for the summary line at the bottom. It should say something like "Plan: 7 to add, 0 to change, 0 to destroy." This tells you Terraform will create 7 resources and delete nothing.
- Scroll through the plan output. You should see each resource listed with a + symbol, meaning it will be created.
Seeing errors in the plan?
- A "Could not retrieve the list of available versions" error means your internet connection or AWS credentials are not working. Verify with aws sts get-caller-identity.
- A reference error (like "Reference to undeclared resource") means a variable or resource name in your code has a typo. Compare your main.tf against the full code tab above.
Why is terraform plan showing errors?
Deploy the infrastructure with terraform apply
The plan looks good. Time to make it real. The terraform apply command will create all 7 resources on AWS.
- Deploy your infrastructure by running:
terraform apply
- Terraform will show the plan one more time and ask you to confirm. Type yes and press **Enter**.
What happens during apply?
Terraform creates resources in dependency order. It starts with the VPC (which has no dependencies), then the internet gateway and subnet (which depend on the VPC), then the route table and security group, and finally the EC2 instance (which depends on the subnet and security group).
At the end, Terraform prints your output values. Look for instance_public_ip in the output. This is the public IP address of your new server.
- Wait for Terraform to finish. You should see a message like "Apply complete! Resources: 7 added, 0 changed, 0 destroyed." followed by your output values.
- Note the instance_public_ip value printed in the outputs. You will use this IP in the next step.
Apply failed with an error?
- If you see "UnauthorizedOperation", your IAM user does not have enough permissions. Make sure your IAM user has the AdministratorAccess policy attached.
- If you see a VPC limit error, your account may have reached the maximum number of VPCs in us-east-1. Delete unused VPCs in the AWS console or try a different region by changing the aws_region default in variables.tf.
- If apply partially succeeded and then failed, run terraform destroy to clean up, fix the issue, and try terraform apply again.
Why did terraform apply fail?
- Verify your instance is running by opening the AWS console, navigating to **EC2**, then **Instances**. You should see an instance named terraform-web-server with a status of **Running**.
Your infrastructure is live. You just deployed a full networking stack and a running server without clicking a single button in the AWS console. Next up, you will verify the deployment and learn how to tear it all down.
Verify and Destroy
Your infrastructure is live. In the previous step, you deployed a full AWS networking stack and EC2 instance with a single command.
Now it is time to close the loop. You will verify that everything landed correctly in the AWS console, peek inside the state file that Terraform uses to track your resources, and then tear it all down with one command.
In this step, get ready to:
- Confirm your EC2 instance is running in the correct VPC and subnet.
- Review how Terraform state tracks your real-world resources.
- Destroy all infrastructure with a single command.
Confirm your instance in the AWS console
The terraform apply output told you your instance was created. The AWS console lets you visually confirm that the instance is running inside the correct network with the right firewall rules.
- Open the AWS Management Console in your browser.
- Make sure you are in the us-east-1 region. Check the region selector in the top-right corner of the console.
- Search for EC2 in the top search bar and open the EC2 dashboard.
- In the left navigation pane, click Instances.
- Find the instance named terraform-web-server. Its Instance state should show Running.
- Click on the instance to open its details.
- Select the Networking tab. Confirm the VPC ID matches your terraform-web-server-vpc and the Subnet ID matches terraform-web-server-public-subnet.
- Select the Security tab. Confirm that terraform-web-server-sg is listed with inbound rules for ports 22 and 80.
What should I see?
Your instance details show it running in the VPC and subnet you defined in code. The security group shows the exact SSH and HTTP rules you wrote in your main.tf. Every piece of infrastructure matches what Terraform created from your configuration.
Review the Terraform state file
Terraform needs a way to remember which real AWS resources belong to which blocks in your code. It stores this mapping in a file called terraform.tfstate.
- Open terraform.tfstate in your code editor. You will find it in the root of your terraform-web-server folder.
- Scroll through the file and notice the structure.
What is Terraform state?
The state file is a JSON document that maps every resource block in your code to a real AWS resource. It contains IDs, ARNs, and attributes for every resource Terraform manages.
Terraform uses this file to know what already exists so it can calculate the difference between your desired state (code) and the actual state (AWS). Without it, Terraform would not know which resources to update or destroy.
Never edit state manually
The state file is managed by Terraform. Editing it by hand can corrupt the mapping between your code and real resources. If you need to make changes, always use terraform plan and terraform apply.
Destroy all resources
The power of Infrastructure as Code is that teardown is just as automated as creation. One command removes everything in reverse dependency order.
- Destroy all the AWS resources Terraform created by running this command:
terraform destroy
What does this command do?
Terraform reads the state file to identify every resource it manages. It then calculates the correct deletion order based on dependencies. For example, it terminates the EC2 instance before deleting the subnet, and deletes the subnet before removing the VPC.
- Terraform will show you a plan of everything it is about to destroy. Review the list of resources.
- Type yes when prompted and press Enter to confirm.
- Wait for Terraform to finish deleting all resources. You will see each resource marked as destroyed in the terminal output.
What should I see?
Terraform outputs a line for each resource as it is destroyed. The final line reads something like Destroy complete! Resources: 7 destroyed. The number may vary slightly depending on your configuration.
- Go back to the AWS console EC2 dashboard.
- Click Instances in the left navigation pane.
- Confirm that the terraform-web-server instance is no longer running. It will show as Terminated or disappear entirely.
Destroy command failing?
Make sure you are running terraform destroy from inside your terraform-web-server directory where the terraform.tfstate file lives.
If you see a credentials error, run aws configure again to make sure your access keys are still set.
Why is terraform destroy failing for me?
Secret mission
Auto-Install Nginx with User Data
Your infrastructure deploys a blank server. What if it could configure itself on first boot? Add a user data script that installs Nginx and serves a custom web page, so your instance is ready to go the moment it launches.
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. The t3.micro instance is covered by the AWS Free Tier (750 hours/month for the first 12 months), but if you have exceeded your free tier window, leaving it running costs approximately $0.0104/hour.
Cost warning
If your AWS account is past its 12-month Free Tier period, your running EC2 instance incurs charges. Run terraform destroy when you are done to ensure $0 ongoing costs.
Resources you used:
- AWS resources: 1x VPC, 1x Subnet, 1x Internet Gateway, 1x Route Table, 1x Security Group, 1x EC2 Instance (t3.micro).
- Local project files: the terraform-web-server directory on your Desktop.
- Terraform CLI (installed via Homebrew).
- AWS CLI v2 (installed via macOS pkg).
- AWS credentials stored in ~/.aws/.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with Terraform and AWS.
- Your EC2 instance will continue running and consuming Free Tier hours.
- Terraform, AWS CLI, and your project files remain on your machine for future use.
- Your AWS credentials in ~/.aws/ remain configured for other projects.
Pause - I'll come back to this later
Remove the AWS resources to stop any potential charges, but keep your local project files and tools installed so you can redeploy later.
- Navigate to your project directory and run the following command to destroy all AWS resources:
- Type yes when prompted to confirm.
cd ~/Desktop/terraform-web-server
terraform destroy
- Your local terraform-web-server folder, Terraform CLI, and AWS CLI remain on your machine.
- To redeploy later, run terraform apply from the same directory.
Delete - I don't want to use this again
Remove all AWS resources, local project files, and optionally the tools you installed.
First, destroy all AWS resources:
- Navigate to your project directory and run:
cd ~/Desktop/terraform-web-server
terraform destroy
- Type yes when prompted to confirm. Terraform removes all six resources in reverse dependency order.
Next, delete your local project files:
- Remove the project directory by running:
cd ~/Desktop
rm -rf terraform-web-server
Optionally, uninstall Terraform:
- Uninstall the Terraform formula by running:
brew uninstall hashicorp/tap/terraform
Optionally, uninstall AWS CLI v2:
- Remove the AWS CLI symlinks and installation directory by running:
sudo rm /usr/local/bin/aws
sudo rm /usr/local/bin/aws_completer
sudo rm -rf /usr/local/aws-cli
Optionally, remove your stored AWS credentials:
- Delete the ~/.aws/ directory. This removes credentials used by all AWS tools on your machine.
rm -rf ~/.aws/
- Verify in the AWS console that no resources remain by opening the EC2 dashboard in us-east-1 and confirming zero running instances.
Nice Work!
Nice Work!
You just deployed real cloud infrastructure without clicking a single button in the AWS console. Every resource, from the VPC to the running EC2 instance, was defined in code and provisioned through your terminal.
You've learned how to:
- Write a complete Terraform project with proper file structure (main.tf, variables.tf, outputs.tf).
- Provision a full VPC networking stack on AWS: VPC, subnet, internet gateway, route table, and security group.
- Practice the complete Terraform workflow: init, plan, apply, and destroy.
- Auto-deploy a web server using EC2 user data scripts to serve a live page without any manual setup.
Ready to quiz yourself?