Deploy a Web Server with Terraform
Use Terraform to provision a live EC2 web server on AWS from scratch.
Introduction
30 Second Summary
Every time you click through the AWS console to create a server, you are doing work that cannot be repeated, shared, or version-controlled. One wrong click and you are stuck debugging an invisible setting.
In this project, you will write Terraform configuration files from scratch to deploy a live web server on AWS. Every piece of your infrastructure will be defined in code that you can reuse, share, and tear down with a single command.
What You'll Build
A complete Terraform project that provisions a custom VPC, networking stack, and EC2 instance serving a web page on AWS.
By the end of this project, you'll have:
- A Terraform project with organized .tf configuration files that define your entire infrastructure.
- A custom VPC with a public subnet, internet gateway, route table, and security group allowing HTTP and SSH traffic.
- A live web server running on an EC2 instance that you can visit in your browser.
- Secret Mission: Refactor your configuration with Terraform variables to make it reusable across regions and instance types.
Are there any prerequisites?
You need an AWS account with IAM credentials that have permissions to create EC2 instances, VPCs, subnets, internet gateways, route tables, and security groups. Basic familiarity with what these AWS services are is helpful, but you do not need prior Terraform experience.
Install the AWS CLI and Terraform
To deploy your web server through code, you need two command-line tools installed on your machine. The AWS CLI connects your terminal to your AWS account so Terraform can create resources there.
In this step, you'll install both tools and create the project folder where your Terraform configuration files will live.
In this step, get ready to:
- Install and configure the AWS CLI to connect to your AWS account.
- Install the Terraform CLI on your machine.
- Create your Terraform project directory.
Install the AWS CLI
The AWS CLI is the official command-line tool for interacting with AWS services. Terraform uses the credentials you configure through the AWS CLI to authenticate with your account.
- Download and install the AWS CLI v2 by running these commands:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
What do these commands do?
- `curl` downloads the AWS CLI installer as a zip file.
- `unzip` extracts the installer files from the zip archive.
- `sudo ./aws/install` runs the installer with admin permissions so it can place the CLI in your system path.
- Verify the installation succeeded by running:
aws --version
You should see a version number starting with aws-cli/2.
Seeing 'command not found'?
- Make sure you ran all three install commands. If `unzip` is not installed, run `sudo apt install unzip` first, then rerun the three commands above.
- Try opening a new terminal window and running `aws --version` again. Some installs need a fresh session to update the path.
Help me troubleshoot why the aws command is not found after installing the AWS CLI on Linux.
Now that the CLI is installed, you need to connect it to your AWS account. The `aws configure` command stores your credentials locally so that Terraform can use them to create resources.
- Configure your AWS credentials by running this command:
aws configure
The command prompts you for four values one at a time. Enter them as follows:
- For AWS Access Key ID, paste your IAM user's Access Key ID.
- For AWS Secret Access Key, paste your IAM user's Secret Access Key.
- For Default region name, enter us-east-1.
- For Default output format, enter json.
What does aws configure do?
This command saves your credentials in a file at ~/.aws/credentials and your default settings in ~/.aws/config. Terraform reads these files automatically when it communicates with AWS.
After entering all four values, the command finishes and you return to your normal terminal prompt. This means your credentials were saved successfully.
Getting an 'invalid credentials' error later?
- Double check that you pasted the full Access Key ID and Secret Access Key without any extra spaces.
- Make sure your IAM user has permissions to create EC2 instances, VPCs, subnets, internet gateways, route tables, and security groups.
- You can re-run aws configure at any time to overwrite the stored values.
Help me troubleshoot my AWS CLI credentials configuration.
Install Terraform
Terraform is the tool that reads your configuration files and turns them into real AWS resources. You install it from HashiCorp's official package repository.
- Add the HashiCorp package repository and install Terraform by running the commands below.
- The final command may ask you to confirm the installation. Type y and press Enter when prompted.
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
What do these commands do?
- The first command downloads HashiCorp's GPG key so your system can verify the packages are authentic.
- The second command adds the HashiCorp package repository to your system's list of software sources.
- The third command refreshes the package list and installs Terraform from the new repository.
- Verify Terraform is installed by running:
terraform version
You should see Terraform v1.15.0 or a similar recent version number.
Seeing 'command not found' or a repository error?
- If `wget` is not found, install it first by running `sudo apt install wget` and then rerun the three commands above.
- If the repository command fails, make sure you are running Ubuntu or Debian. Other Linux distributions use a different installation method from the HashiCorp docs.
- Try opening a new terminal window and running `terraform version` again.
Help me troubleshoot my Terraform installation on Linux.
Set up the project directory
Every Terraform project lives in its own directory. Terraform treats the current directory as the workspace and reads all .tf files inside it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project directory and move into it by running these commands:
mkdir terraform-web-server
cd terraform-web-server
- Confirm you are in the new directory by running:
pwd
You should see a path ending in terraform-web-server.
Don't have a ~/Desktop folder?
Some Linux setups (especially servers) don't have a Desktop directory. Create it first by running:
mkdir -p ~/Desktop
Then run cd ~/Desktop again and continue with the mkdir step.
Your tools are installed and your project directory is ready. Next up, you'll write your first Terraform configuration files to set up the AWS provider and look up the right machine image for your server.
Configure the Terraform Provider and Data Source
Your tools are installed and your project directory is ready. Now it's time to write your first Terraform configuration.
Every Terraform project starts by declaring which cloud provider to use. You'll also create a data source that dynamically finds the right server image, so your code stays portable across regions.
In this step, get ready to:
- Create the provider configuration file that connects Terraform to AWS.
- Add a data source that dynamically finds the latest Amazon Linux 2023 AMI.
- Initialize the Terraform workspace to download the AWS provider plugin.
Create the provider configuration
The provider configuration tells Terraform which cloud platform to manage and which version of the provider plugin to use. It also sets region-level defaults for all resources in your project.
- Create a new file called terraform.tf in your terraform-web-server folder and add the following code:
# Define which providers and Terraform version this project requires
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.2"
}
# Configure the AWS provider to deploy resources in us-east-1
provider "aws" {
region = "us-east-1"
}
What does this code do?
- The terraform block declares that this project needs the AWS provider from HashiCorp, pinned to any 6.x version.
- required_version = ">= 1.2" ensures anyone running this code uses Terraform 1.2 or later.
- The provider "aws" block sets us-east-1 as the default region for every resource Terraform creates.
- Save terraform.tf.
- You won't see a change yet. The next substep creates the data source file.
✔️ Awesome, I've got everything!
Great. Double check you've saved terraform.tf before moving on.
ⓧ I'd like to double check the full code
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.2"
}
provider "aws" {
region = "us-east-1"
}
Create the AMI data source
A data source lets Terraform look up existing information from your cloud provider instead of you typing it in manually. You'll use one to dynamically find the latest AMI (Amazon Machine Image) for your server.
- Create a new file called main.tf in your terraform-web-server folder and add the following code:
# Dynamically look up the latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
# Match Amazon Linux 2023 images for x86_64 architecture
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
# Only select hardware virtual machine images
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
What does this code do?
- most_recent = true tells Terraform to grab the newest matching AMI instead of the oldest.
- owners = ["amazon"] ensures you only get official Amazon images.
- The first filter matches AMI names starting with al2023-ami-2023 for x86_64 architecture.
- The second filter limits results to HVM virtualization, which is required for t2.micro instances.
Why a data source instead of a hardcoded AMI ID?
AMI IDs change by region and over time. If you hardcode an ID like ami-0abcdef1234567890, your configuration breaks the moment that image is updated or you switch regions.
A data source queries AWS at runtime and always returns the latest match. Your code stays portable without any manual updates.
- Save main.tf.
- You won't see a change yet. In the next substep, you'll initialize Terraform and confirm both files are valid.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.tf before moving on.
ⓧ I'd like to double check the full code
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
Initialize the Terraform workspace
Before Terraform can manage any resources, it needs to download the provider plugin specified in your configuration. The terraform init command handles this setup.
- Initialize your Terraform workspace by running this command:
terraform init
You should see a message saying Terraform has been successfully initialized! at the bottom of the output.
What did terraform init create?
- A .terraform/ directory containing the downloaded AWS provider plugin.
- A .terraform.lock.hcl lock file that pins the exact provider version, similar to a package-lock.json in Node.js projects.
- Confirm both new items exist by running:
ls -a
You should see .terraform, .terraform.lock.hcl, main.tf, and terraform.tf in the output.
Seeing an error from terraform init?
- If you see Error: Failed to query available provider packages, check your internet connection. Terraform needs to download the AWS provider plugin from the registry.
- If you see a syntax error referencing terraform.tf or main.tf, open the file and check for missing braces or mismatched quotes.
- If you see Error: No configuration files, make sure you are inside the terraform-web-server directory where your .tf files live.
Help me debug my terraform init error.
Your Terraform workspace is initialized and ready to go. Next up, you'll define the networking infrastructure that your web server needs to be publicly accessible.
Define the Networking Infrastructure
Your Terraform workspace is initialized, and you have a data source ready to look up the latest Amazon Linux AMI. Now it's time to define the network your web server will live in.
An EC2 instance can't serve web traffic on its own. It needs a VPC to live in, a subnet with a public IP, an internet gateway for outside access, and a security group to control which ports are open.
In this step, get ready to:
- Create a VPC and public subnet for your web server.
- Set up an internet gateway and route table for public internet access.
- Define a security group with HTTP and SSH traffic rules.
Create the VPC and subnet
Every AWS resource lives inside a VPC. Your web server needs a VPC with a public subnet so it can receive traffic from the internet.
- In main.tf, add the following code below the existing data "aws_ami" block:
# Create a virtual private network for your web server
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "terraform-web-server-vpc"
}
}
# Create a public subnet inside the VPC
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
tags = {
Name = "terraform-web-server-public-subnet"
}
}
What does this code do?
- aws_vpc "main" creates a VPC with the address space 10.0.0.0/16, which gives you over 65,000 possible IP addresses. enable_dns_hostnames = true lets instances receive public DNS names.
- aws_subnet "public" carves out a smaller 10.0.1.0/24 range inside the VPC (256 addresses). map_public_ip_on_launch = true ensures any instance launched here automatically gets a public IP.
- aws_vpc.main.id is a resource reference. Terraform uses it to link the subnet to the VPC, and figures out the correct creation order automatically.
- Save main.tf.
You won't see any output yet. The next substep connects this subnet to the internet.
Create the internet gateway and route table
A subnet with a public IP isn't enough on its own. You also need an internet gateway and a route table to direct traffic between your VPC and the outside world.
- In main.tf, add the following code below the aws_subnet block you just created:
# Connect the VPC to the public internet
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-igw"
}
}
# Route all outbound traffic through the internet gateway
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"
}
}
# Apply the route table to the public subnet
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?
- aws_internet_gateway "main" attaches to your VPC and acts as the door between your private network and the public internet.
- aws_route_table "public" contains a route that sends all traffic (0.0.0.0/0) to the internet gateway. This is what makes the subnet "public."
- aws_route_table_association "public" links the route table to your subnet so the routing rules actually take effect.
- Save main.tf.
Still no visible output. The next substep adds the security group that controls which traffic can reach your instance.
Create the security group
A security group acts as a firewall for your instance. By default, all inbound traffic is blocked. You need to explicitly open the ports your server uses.
- In main.tf, add the following code below the aws_route_table_association block:
# Create a security group to control inbound and outbound traffic
resource "aws_security_group" "web_server" {
name = "terraform-web-server-sg"
description = "Allow HTTP and SSH inbound traffic"
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-sg"
}
}
# Allow inbound HTTP traffic on port 80
resource "aws_vpc_security_group_ingress_rule" "allow_http" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 80
to_port = 80
ip_protocol = "tcp"
}
# Allow inbound SSH traffic on port 22
resource "aws_vpc_security_group_ingress_rule" "allow_ssh" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 22
to_port = 22
ip_protocol = "tcp"
}
# Allow all outbound traffic so the instance can download packages and respond
resource "aws_vpc_security_group_egress_rule" "allow_all" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
What does this code do?
- aws_security_group "web_server" creates a firewall attached to your VPC. On its own, it blocks everything inbound.
- allow_http opens port 80 for web traffic from any IP address. This is how browsers will reach your web server.
- allow_ssh opens port 22 for SSH connections. This demonstrates multi-rule configuration in Terraform.
- allow_all is an egress rule that permits all outbound traffic. Your instance needs this to download packages and send HTTP responses.
Why are ingress rules separate resources?
The AWS provider recommends using standalone rule resources (aws_vpc_security_group_ingress_rule) instead of inline ingress blocks inside the security group. Standalone rules avoid conflicts when multiple parts of your configuration manage the same security group.
- Save main.tf.
- Confirm your Terraform configuration is valid by running:
terraform validate
- You should see Success! The configuration is valid.
Seeing a validation error?
- Check that every opening brace { has a matching closing brace }. A missing brace is the most common syntax error in HCL files.
- Make sure resource references like aws_vpc.main.id match the resource names you defined. A typo like aws_vpc.Main.id (capital M) causes a reference error.
- Verify that the data "aws_ami" block from the previous step is still at the top of main.tf. The new resources should appear below it, not replace it.
- Help me debug my Terraform validation error
✔️ Awesome, I've got everything!
Great. Make sure you've saved main.tf before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.tf file at this point in the project. Compare it with yours and fix any differences.
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "terraform-web-server-vpc"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
tags = {
Name = "terraform-web-server-public-subnet"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "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
}
resource "aws_security_group" "web_server" {
name = "terraform-web-server-sg"
description = "Allow HTTP and SSH inbound traffic"
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_http" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 80
to_port = 80
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_ingress_rule" "allow_ssh" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 22
to_port = 22
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_egress_rule" "allow_all" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
Your entire networking stack is defined in code. Next up, you'll add the EC2 instance itself and deploy everything to AWS with terraform apply.
Launch the EC2 Instance and View Your Web Server
Your networking infrastructure is fully defined in code. A VPC, subnet, internet gateway, route table, and security group are all ready to go.
Now it's time to put that network to use. In this step, you'll define an EC2 instance that automatically installs a web server when it boots, create Terraform outputs to display the server's address, and deploy everything to AWS with a single command.
In this step, get ready to:
- Define an EC2 instance with a startup script that installs a web server.
- Create Terraform outputs that display your server's public IP and URL.
- Deploy all your resources with terraform apply and verify your web page loads in a browser.
Define the EC2 instance
Your EC2 instance needs more than just a machine image and instance type. It also needs a startup script that runs the moment the instance boots.
Terraform's user_data argument lets you pass a bash script that EC2 executes on first launch. You'll use this to install Apache (the httpd package) and write a simple HTML page.
- In main.tf, scroll to the bottom of the file (after the aws_vpc_security_group_egress_rule resource).
- Add the following EC2 instance resource:
# Launch an EC2 instance as the web server
resource "aws_instance" "web_server" {
# Use the latest Amazon Linux 2023 AMI from the data source
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_server.id]
# Startup script that installs and configures Apache
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<html><head><title>Terraform Web Server</title></head><body><h1>Hello from Terraform!</h1><p>This web server was deployed using Infrastructure as Code.</p></body></html>" > /var/www/html/index.html
EOF
tags = {
Name = "terraform-web-server"
}
}
What does this code do?
- ami references the Amazon Linux 2023 image you looked up with the data.aws_ami.amazon_linux data source. No hardcoded AMI ID needed.
- instance_type is set to t2.micro, which is free-tier eligible for 750 hours per month in your first 12 months.
- subnet_id and vpc_security_group_ids place the instance inside the public subnet and attach the security group you created in the previous step.
- user_data is a bash script that EC2 runs on first boot. It installs Apache, starts the web server, enables it to survive reboots, and writes a simple HTML page.
- Save main.tf.
- You won't see a change yet. The deploy step later will create all your resources at once.
Worried about the heredoc syntax?
The <<-EOF ... EOF block is called a heredoc. Make sure the closing EOF is on its own line and indented at the same level as the resource's other arguments. If the indentation is off, Terraform may not parse it correctly.
Help me check if my user_data heredoc syntax is correct in main.tf.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.tf before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.tf file including all the networking resources from the previous step and the new EC2 instance at the bottom.
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "terraform-web-server-vpc"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
tags = {
Name = "terraform-web-server-public-subnet"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "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
}
resource "aws_security_group" "web_server" {
name = "terraform-web-server-sg"
description = "Allow HTTP and SSH inbound traffic"
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-web-server-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_http" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 80
to_port = 80
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_ingress_rule" "allow_ssh" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 22
to_port = 22
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_egress_rule" "allow_all" {
security_group_id = aws_security_group.web_server.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
resource "aws_instance" "web_server" {
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_server.id]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<html><head><title>Terraform Web Server</title></head><body><h1>Hello from Terraform!</h1><p>This web server was deployed using Infrastructure as Code.</p></body></html>" > /var/www/html/index.html
EOF
tags = {
Name = "terraform-web-server"
}
}
Create the outputs
Terraform outputs are values that get printed to the terminal after a successful terraform apply. They are the easiest way to surface important information from your infrastructure, like an IP address or URL.
- Create a new file named outputs.tf inside your terraform-web-server folder.
- Add these two output blocks to the file:
# Display the instance's public IP after deployment
output "instance_public_ip" {
description = "The public IP address of the web server"
value = aws_instance.web_server.public_ip
}
# Construct the full URL for easy browser access
output "web_server_url" {
description = "The URL to access the web server"
value = "http://${aws_instance.web_server.public_ip}"
}
What does this code do?
- instance_public_ip pulls the public IP address directly from the aws_instance.web_server resource. Terraform knows this value only after the instance is created.
- web_server_url uses string interpolation (${...}) to build a clickable HTTP URL from that IP. This saves you from manually typing http:// in front of the address.
- Save outputs.tf.
- You won't see a change yet. The output values only appear after you run terraform apply in the next substep.
✔️ Awesome, I've got everything!
Great. Double check you've saved outputs.tf before moving on.
ⓧ I'd like to double check the full code
Here is the complete outputs.tf file.
output "instance_public_ip" {
description = "The public IP address of the web server"
value = aws_instance.web_server.public_ip
}
output "web_server_url" {
description = "The URL to access the web server"
value = "http://${aws_instance.web_server.public_ip}"
}
Deploy and verify your web server
Everything is defined in code. Time to let Terraform create it all on AWS.
The Terraform workflow has two stages. First, terraform plan previews what will change. Then, terraform apply makes it happen.
- Preview all the resources Terraform will create by running this command:
terraform plan
What should I see?
Terraform prints a detailed preview of every resource it will create. You should see a list that includes the VPC, subnet, internet gateway, route table, route table association, security group, three security group rules, and the EC2 instance.
At the bottom, look for a summary line like Plan: 10 to add, 0 to change, 0 to destroy. This confirms that no existing resources will be modified or deleted.
- Deploy all your resources by running this command:
terraform apply
Terraform will ask for confirmation
Terraform shows the same plan preview and then prompts you with Do you want to perform these actions? Type yes and press Enter to confirm.
Terraform will take a minute or two to create all 10 resources. You'll see each resource's status update in real time as it progresses.
- Wait for the apply to finish. You should see Apply complete! followed by your two outputs: instance_public_ip and web_server_url.
- Copy the web_server_url value from the terminal output.
- Paste it into your browser's address bar and press Enter.
You should see a web page with the heading "Hello from Terraform!" and a paragraph that reads "This web server was deployed using Infrastructure as Code."
Why might it take a moment to load?
Even after Terraform reports the instance as created, the EC2 instance still needs time to boot, run yum update, install Apache, and start the web server. This can take 1-2 minutes. If the page does not load immediately, wait a moment and try again.
Page not loading at all?
- Make sure you are using http:// (not https://) in the URL. Your security group only allows traffic on port 80 (HTTP).
- Check that your security group's HTTP ingress rule allows port 80 from 0.0.0.0/0. Review the aws_vpc_security_group_ingress_rule block named allow_http in your main.tf.
- Verify the user_data script syntax. If the heredoc has a typo, the instance boots but Apache may not start. You can check the EC2 system log in the AWS console under EC2 > Instances > select your instance > Actions > Monitor and troubleshoot > Get system log.
My Terraform web server deployed but the page won't load in my browser. Help me debug the issue.
Your web server is live and publicly accessible, all from code you wrote yourself. Next up, you'll tackle the Secret Mission and make this configuration reusable with Terraform variables.
Secret mission
Make Your Configuration Reusable with Variables
Your Terraform code works, but every value is hardcoded. What if you wanted to deploy the same setup in a different region or with a larger instance type? Refactor your configuration to use Terraform variables and make it truly reusable.
Before you go
Clean Up Your Resources
This project created AWS resources that may incur charges if left running beyond the free tier. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Cost warning
Your EC2 instance (t2.micro) is free for 750 hours/month during the first 12 months of your AWS account. After that, it costs approximately $0.0116/hour in us-east-1.
If you are not sure whether you are still within the free tier, the safest option is to destroy all resources using terraform destroy.
Resources you used:
- EC2 instance (t2.micro) running Amazon Linux 2023 with Apache web server.
- VPC with DNS hostnames enabled.
- Public subnet with auto-assigned public IPs.
- Internet gateway attached to the VPC.
- Route table and route table association.
- Security group with HTTP and SSH ingress rules.
- Local Terraform state files and provider plugins in your project directory.
Keep everything running
No action needed. Choose this if you are still within the AWS free tier and want to keep experimenting with your web server.
- Your EC2 instance stays running and accessible at the public IP from your Terraform output.
- The VPC, subnet, internet gateway, route table, and security group have no ongoing cost.
- Monitor your usage on the AWS Billing and Cost Management dashboard to make sure you stay within the free tier.
Pause - I'll come back to this later
Stop the EC2 instance to halt compute charges while keeping your Terraform project files intact.
- Open the AWS Management Console and navigate to the EC2 dashboard.
- Select your instance named terraform-web-server.
- Click Instance state, then select Stop instance.
- Minimal EBS storage charges still apply while the instance is stopped.
- Keep your terraform-web-server project folder on your machine. You can run terraform apply again later to bring everything back up.
What about Terraform state drift?
Stopping the instance through the AWS console creates a difference between your Terraform state and the actual infrastructure. The next time you run terraform plan, Terraform will detect this drift and show you the changes needed to bring things back in sync.
Delete - I don't want to use this again
Remove all AWS resources and start fresh. This is the cleanest option and guarantees $0 cost going forward.
- Open your terminal and navigate to your terraform-web-server project folder.
- Destroy all AWS resources by running this command:
terraform destroy
- Terraform will show you a list of all resources it plans to remove. Type yes and press Enter to confirm.
- Wait for Terraform to finish removing all resources. You should see Destroy complete! with a count of resources destroyed.
- Optionally, delete the terraform-web-server folder from your machine if you no longer need the project files.
Mission accomplished
That's a Wrap!
Nice work! You just went from zero Terraform experience to deploying a live web server on AWS, entirely through code.
You've learned how to:
- Built a complete Terraform project from scratch that provisions a full networking stack on AWS, including a VPC, subnet, internet gateway, route table, and security group.
- Launched an EC2 instance with user data that automatically installed and configured an Apache web server, accessible from a public URL.
- Managed the full Infrastructure as Code lifecycle: initialized your workspace with terraform init, previewed changes with terraform plan, deployed with terraform apply, and tore everything down with terraform destroy.
- Secret Mission: Made your configuration reusable by refactoring hardcoded values into Terraform variables, so the same project can be deployed across different regions and instance types.
Ready to quiz yourself?