Deploy an EC2 Instance with Terraform

Launch a secured EC2 instance in a VPC using Terraform and AWS Console.

Introduction

30 Second Summary

Every server you put on the internet needs a bouncer. Without one, anyone can knock on any door and walk right in. In the cloud, that bouncer is called a security group.

In this project, you will deploy an EC2 instance inside a VPC and protect it with a security group that controls exactly which traffic can reach it. You will build the infrastructure first through the AWS Console, then recreate it entirely as code with Terraform.

What You'll Build

A secured EC2 instance running Amazon Linux 2023 inside a VPC, protected by a security group that allows only SSH (port 22) and HTTP (port 80) inbound traffic.

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

  • A new AWS account with free-tier credits and an IAM user with EC2 and VPC permissions.
  • An EC2 instance launched through the console with a security group, verified via SSH connection.
  • A Terraform configuration that recreates the same secured infrastructure as code, using the recommended aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule resources.
  • Secret Mission: Add a Network ACL that layers subnet-level traffic control on top of your security group and observe how the two mechanisms interact.

Are there any prerequisites?

This project assumes you have completed the "Create a VPC with Terraform and AWS Console" project. You should be comfortable with Terraform basics, HCL syntax, and the terraform init/plan/apply/destroy workflow.

Before We Start

Before We Start...

Set Up Your AWS Account and Tools

Before you can launch an EC2 instance, you need an AWS account with the right permissions and a way to connect to your instance securely.

In this step, you will create a new AWS account with free-tier credits, set up an IAM user for programmatic access, and generate an SSH key pair. The SSH key pair is how you will prove your identity when connecting to EC2 instances later in this project.

In this step, get ready to:
  • Create an AWS account and IAM user with EC2 and VPC permissions.
  • Verify your Terraform and AWS CLI installations.
  • Generate an SSH key pair for connecting to EC2 instances.
Create your AWS account and IAM user

AWS requires an account before you can create any cloud resources. New accounts created after July 15, 2025 receive $100 in free-tier credits, which covers the resources in this project.

  • Open your browser and navigate to the AWS sign-up page:
https://portal.aws.amazon.com/billing/signup
  • Complete the sign-up process with your email, a strong password, and payment information.
  • Once your account is created, sign in to the AWS Management Console.

What is the AWS Free Tier?

For accounts created after July 15, 2025, AWS provides $100 in credits at sign-up plus up to $100 more by completing service activities.

The free plan lasts 6 months or until your credits are exhausted, whichever comes first. The t3.micro instance type you will use in this project is covered by these credits.

Now create an IAM user so you can interact with AWS from your terminal instead of only through the browser.

  • In the AWS Console, search for IAM in the top search bar and select IAM.
  • Click Users in the left sidebar.
  • Click Create user.
  • Enter a user name (e.g. terraform-user) and click Next.
  • Select Attach policies directly.
  • Search for and check the box next to AmazonEC2FullAccess.
  • Search for and check the box next to AmazonVPCFullAccess.
  • Click Next, then click Create user.

Why two policies?

AmazonEC2FullAccess covers launching instances, managing security groups, and key pairs. AmazonVPCFullAccess covers VPC, subnet, internet gateway, route table, and Network ACL resources.

Next, generate an access key so the AWS CLI can authenticate as this user.

  • Click on your newly created user name to open the user details.
  • Click the Security credentials tab.
  • Scroll down to Access keys and click Create access key.
  • Select Command Line Interface (CLI) as the use case.
  • Check the confirmation box at the bottom and click Next.
  • Click Create access key.
  • Copy both the Access key and Secret access key values to a safe place. You will not be able to see the secret key again after leaving this page.

Keep your keys safe

Never share your access keys, commit them to version control, or paste them into any online tool. Treat them like a password.

Now configure the AWS CLI with your new credentials.

  • Configure the AWS CLI with your new credentials by running this command:
aws configure
  • When prompted, enter your Access Key ID (the one you just copied).
  • Enter your Secret Access Key.
  • Enter us-east-1 for the default region.
  • Enter json for the default output format.

What does aws configure do?

This command stores your credentials locally so every future AWS CLI command authenticates automatically. It saves the values in ~/.aws/credentials and ~/.aws/config.

Seeing 'command not found' for aws?

Make sure the AWS CLI v2 is installed on your system. The stateBefore for this project assumes it is already available. If it is missing, install it from the official AWS docs before continuing.

Help me install the AWS CLI v2 on my system.

Verify your Terraform and AWS CLI installations

Both Terraform and the AWS CLI should already be installed on your system from the previous project. Confirm they are working and that your credentials are properly configured.

  • Confirm Terraform is installed by running this command:
terraform version

✔️ I see version 1.2 or higher

Terraform is ready. Move on to checking the AWS CLI.

ⓧ Command not found

Terraform is not installed. Install it from the official Terraform downloads page before continuing.

Help me install Terraform CLI on my Linux system.

  • Verify your AWS credentials are configured correctly by running this command:
aws sts get-caller-identity

✔️ I see my account details

You should see JSON output with your Account, UserId, and Arn. This confirms your CLI is authenticated. Move on to the next substep.

ⓧ I see an error about credentials

Run aws configure again and double-check that you pasted your Access Key ID and Secret Access Key correctly. Make sure there are no extra spaces before or after the values.

Help me fix my AWS CLI credentials configuration.

Generate an SSH key pair

An SSH key pair lets you connect securely to an EC2 instance without a password. You will upload the public key to AWS and keep the private key on your machine.

  • Generate an ED25519 SSH key pair by running this command:
ssh-keygen -t ed25519 -f ~/.ssh/ec2-key -N ""

What does this command do?

-t ed25519 specifies the key type (modern and secure). -f ~/.ssh/ec2-key sets the output file path. -N "" sets an empty passphrase so Terraform can use the key without prompting you.

  • Verify the key pair was created by running:
ls ~/.ssh/ec2-key*

You should see two files: ~/.ssh/ec2-key (your private key) and ~/.ssh/ec2-key.pub (your public key).

Seeing 'No such file or directory'?

If the ~/.ssh directory does not exist, create it first with mkdir -p ~/.ssh and then re-run the ssh-keygen command.

Help me troubleshoot my SSH key generation.

Your AWS account, CLI access, and SSH key pair are all ready. Next up, you will create a security group in the AWS Console and see how it controls traffic to your resources.

Create a Security Group in the AWS Console

Your EC2 instance will be exposed to the internet, which means any traffic could try to reach it. Before you launch anything, you need to define exactly which traffic is allowed in and which is blocked.

In this step, you will create a VPC with a public subnet, then create a security group that acts as a virtual firewall. You will configure it to allow only SSH and HTTP traffic.

In this step, get ready to:
  • Create a VPC with a public subnet using the console wizard.
  • Create a security group with SSH and HTTP inbound rules.
  • Explore how security group rules work and understand stateful behavior.
Create a VPC with a public subnet

Every AWS resource lives inside a VPC. You need a VPC with a public subnet so your EC2 instance can receive traffic from the internet.

  • In the AWS Management Console, use the search bar at the top to search for VPC and select the VPC service.
  • On the VPC dashboard, click Create VPC.
  • For Resources to create, select VPC and more.
  • Under Name tag auto-generation, enter console as the name. This automatically names your VPC console-vpc and your subnet console-subnet-public1.
  • For IPv4 CIDR block, enter 10.0.0.0/16.
  • For Number of Availability Zones, choose 1.
  • Expand Customize AZs and select us-east-1a.
  • For Number of public subnets, choose 1.
  • For Number of private subnets, choose 0.
  • For NAT gateways, keep the default value None.
  • For VPC endpoints, choose None.
  • For DNS options, keep both options selected.
  • Click Create VPC.

The wizard takes a few seconds to create your VPC, subnet, internet gateway, and route table. Wait for the green success banner to appear.

What did the wizard create?

The VPC and more wizard creates an entire networking stack in one go: a VPC with your CIDR block, a public subnet, an internet gateway, and a route table that sends internet-bound traffic through the gateway.

Without the internet gateway and route table, your subnet would be isolated from the internet. The wizard handles this wiring for you.

Don't see the Create VPC button?

Make sure you are in the us-east-1 region. Check the region selector in the top-right corner of the console. If you are signed in with the root account instead of your IAM user, you may not have the AmazonVPCFullAccess policy attached.

Help me troubleshoot the VPC console

Create a security group with inbound rules

A security group controls which traffic can reach your instance. You will create one that allows SSH connections (so you can log in) and HTTP traffic (so a web server could serve pages).

  • In the VPC console left navigation pane, click Security groups.
  • Click Create security group.
  • For Security group name, enter web-server-sg.
  • For Description, enter Allow SSH and HTTP inbound traffic.
  • For VPC, click the dropdown, remove the default VPC, and select your console-vpc.

Now add two inbound rules to allow SSH and HTTP traffic.

  • Under Inbound rules, click Add rule.
  • For Type, select SSH. The port automatically fills in as 22.
  • For Source, select Anywhere-IPv4 (0.0.0.0/0).
  • Click Add rule again to add a second rule.
  • For Type, select HTTP. The port automatically fills in as 80.
  • For Source, select Anywhere-IPv4 (0.0.0.0/0).
  • Leave the Outbound rules section unchanged. The default outbound rule allows all traffic to leave.
  • Click Create security group.

You should see a green success banner confirming the security group was created.

What is Anywhere-IPv4?

Setting the source to 0.0.0.0/0 means any IPv4 address on the internet can send traffic to that port. For a learning project this is fine, but in production you would restrict SSH access to your own IP address only.

Can't find your VPC in the dropdown?

Make sure you are still in the us-east-1 region. The security group VPC dropdown only shows VPCs in your current region. If you see only the default VPC, the wizard from the previous substep may not have completed successfully.

Help me find my VPC in the security group dropdown

Explore security group rules and stateful behavior

Now that you have a security group, take a moment to understand how the rules are evaluated. This knowledge will help you debug connectivity issues later.

  • In the security groups list, click on web-server-sg to open its details.
  • Click the Inbound rules tab. You should see your two rules: SSH on port 22 and HTTP on port 80, both with source 0.0.0.0/0.
  • Click the Outbound rules tab. You should see one rule allowing all traffic (All traffic) to all destinations (0.0.0.0/0).

What does stateful mean for security groups?

Security groups are stateful. This means if you send a request from your instance (outbound), the response traffic is automatically allowed back in, regardless of the inbound rules.

The same works in reverse. If an inbound rule allows traffic in (like an SSH connection), the response traffic is automatically allowed back out. You do not need to create matching outbound rules for return traffic.

This is different from Network ACLs, which are stateless and require explicit rules in both directions. You will see this difference in the Secret Mission.

Why allow only SSH and HTTP?

SSH (port 22) lets you securely connect to and manage your instance from a terminal. HTTP (port 80) lets a web server on the instance serve pages to the internet.

By only opening these two ports, you block all other traffic. If an attacker tries to connect on port 3306 (database) or port 443 (HTTPS), the security group silently drops their packets.

You now have a VPC with a public subnet and a security group that will protect your EC2 instance. Next up, you will launch an actual instance, attach this security group, and prove you can SSH in.

Launch and Connect to an EC2 Instance via the Console

Your VPC has a security group that controls exactly which traffic is allowed in and out. Now it is time to launch an actual server inside that protected network and prove you can connect to it.

In this step, you will launch a t3.micro EC2 instance through the AWS Console, connect to it via SSH, and then clean up all console resources so you can rebuild everything with Terraform in the next step.

In this step, get ready to:
  • Launch a t3.micro EC2 instance with Amazon Linux 2023 in your public subnet.
  • SSH into the running instance and verify you get a shell prompt.
  • Clean up all console-created resources (instance, security group, VPC).
Launch a t3.micro EC2 instance

An AMI (Amazon Machine Image) is a template containing the operating system and software your instance runs. You will use Amazon Linux 2023, which is free-tier eligible and maintained by AWS.

  • In the AWS Console, type EC2 in the top search bar and select **EC2** from the results.
  • Click **Launch instance** from the EC2 dashboard.
  • Under **Name and tags**, enter console-web-server as the instance name.
  • Under **Application and OS Images (Amazon Machine Image)**, confirm **Amazon Linux** is selected in the **Quick Start** tab.
  • Select the **Amazon Linux 2023 AMI** option. It is marked as "Free tier eligible".
  • Under **Instance type**, select t3.micro.
  • Under **Key pair (login)**, click **Create new key pair**.
  • Enter console-key as the key pair name.
  • Leave the type as **RSA** and format as .pem.
  • Click **Create key pair**. The console-key.pem file downloads automatically to your ~/Downloads folder.

Why a new key pair here?

You already have an SSH key pair at ~/.ssh/ec2-key from Step 1. That key is for Terraform later. The console launch wizard creates its own key pair and registers the public key with AWS automatically. This keeps the console exercise self-contained.

  • Under **Network settings**, click **Edit**.
  • In the **VPC** dropdown, select your console-vpc.
  • In the **Subnet** dropdown, select your public subnet in us-east-1a.
  • Set **Auto-assign public IP** to **Enable**. This is required because nondefault subnets disable public IPs by default.
  • Under **Firewall (security groups)**, select **Select existing security group**.
  • Choose web-server-sg from the dropdown.
  • Leave all other settings at their defaults.
  • In the **Summary** panel on the right, click **Launch instance**.

What did you just configure?

You placed a t3.micro instance running Amazon Linux 2023 inside your public subnet. The web-server-sg security group controls which traffic reaches it (only SSH on port 22 and HTTP on port 80).

Enabling Auto-assign public IP gives the instance a publicly routable address so you can reach it from your local machine over the internet.

You should see a success banner confirming your instance is launching.

Connect to the instance via SSH

Your instance needs a minute or two to start up. Once it reaches the "Running" state and has a public IP address, you can connect to it from your terminal.

  • Click **View all instances** (or navigate to **Instances** in the left sidebar).
  • Wait for the **Instance state** column to show **Running** and the **Status check** column to show **2/2 checks passed**.
  • Select your console-web-server instance by clicking its checkbox.
  • In the details pane below, find the **Public IPv4 address** field and copy the IP address.

Refresh if the IP is blank

If the Public IPv4 address field is empty, click the refresh icon at the top of the instances table. The address may take a moment to appear after the instance enters the Running state.

  • Set the correct permissions on your downloaded key file by running this command:
chmod 400 ~/Downloads/console-key.pem

Why chmod 400?

SSH refuses to use a private key file if other users on your system can read it. The chmod 400 command sets the file to read-only for your user and no access for anyone else.

  • SSH into your instance by running this command:
ssh -i ~/Downloads/console-key.pem ec2-user@[[EC2_PUBLIC_IP="your instance's public IPv4 address"]]
  • When prompted with "Are you sure you want to continue connecting?", type yes and press **Enter**.

What does this command do?

The -i flag tells SSH which private key to use for authentication. ec2-user is the default username on Amazon Linux instances. The public IP address routes your connection through the internet gateway, through the security group (which allows port 22), and into your instance.

You should see a shell prompt that looks similar to:

[ec2-user@ip-10-0-1-xxx ~]$

This confirms you are inside your EC2 instance. The security group allowed your SSH connection through on port 22.

  • Type exit and press **Enter** to disconnect from the instance and return to your local terminal.

Connection timed out or refused?

If you see a timeout, double-check that you enabled **Auto-assign public IP** during launch. An instance without a public IP is not reachable from the internet.

If you see a permission denied error, confirm you ran chmod 400 on the .pem file and that you are using the correct key file (console-key.pem, not ec2-key).

help me troubleshoot my SSH connection to EC2

Clean up all console resources

You have proven the architecture works. Now you will tear down the console resources so they do not consume your free-tier credits while you rebuild everything with Terraform.

  • Return to the **Instances** page in the EC2 console.
  • Select the checkbox next to console-web-server.
  • Click **Instance state** (top right), then select **Terminate (delete) instance**.
  • Confirm by clicking **Terminate (delete)**.
  • Wait for the **Instance state** column to show **Terminated**. This may take 1-2 minutes.

Why terminate before deleting other resources?

A security group cannot be deleted while an instance is using it. A VPC cannot be deleted while it has running instances. You must terminate the instance first to remove those dependencies.

  • Navigate to the VPC console by typing VPC in the top search bar and selecting **VPC**.
  • In the left sidebar, click **Your VPCs**.
  • Select the checkbox next to console-vpc.
  • Click **Actions**, then select **Delete VPC**.
  • The console shows a list of resources it will delete alongside the VPC (subnets, route tables, internet gateway, and security groups).
  • Type delete in the confirmation field, then click **Delete**.

What does Delete VPC remove?

When you delete a VPC through the console, AWS automatically removes the associated subnets, route tables, internet gateway, network ACLs, and security groups. You do not need to delete each one individually.

Your console-vpc, web-server-sg, subnet, and all associated networking resources are now gone. The only things remaining from this step are your local console-key.pem file and the key pair registered in EC2 (which has no ongoing cost).

VPC deletion failed?

If the deletion shows an error, the instance may not have fully terminated yet. Return to the EC2 Instances page, confirm the instance state is **Terminated**, and try the VPC deletion again.

help me delete my VPC in the AWS Console

You have proven the architecture works by hand. Next up, you will recreate this entire setup as code with Terraform.

Write the Terraform Configuration

You saw how creating an EC2 instance through the console involves clicking through multiple screens and manually configuring each resource. Now you will express that same infrastructure as code in a single Terraform file.

This means you can recreate, modify, or destroy the entire stack with one command. In this step, you will write a complete main.tf that defines a VPC, public subnet, security group, and EC2 instance.

In this step, get ready to:
  • Create a project directory and main.tf file with the provider block and VPC networking resources.
  • Add a security group with dedicated ingress and egress rule resources.
  • Add an SSH key pair, EC2 instance, and output block, then initialize and validate the configuration.
Create the project directory and VPC networking resources

Every Terraform project starts with a directory that holds your configuration files. You will create a terraform-ec2 folder on your Desktop and add the provider block plus all the VPC networking resources your instance needs.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir terraform-ec2
cd terraform-ec2
  • Confirm the folder exists by running:
ls ~/Desktop

You should see terraform-ec2 in the output.

  • Create the main.tf file by running:
touch main.tf
  • Open main.tf in your text editor.
  • Add the Terraform configuration block and provider block by pasting this code:
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  required_version = ">= 1.2"
}

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

What does this code do?

The terraform block tells Terraform which provider to download (the AWS provider, version 6.x) and which minimum Terraform version to require (1.2+).

The provider block configures that AWS provider to deploy resources in the us-east-1 region.

  • Below the provider block, add the VPC networking resources by pasting this code:
# --- VPC Networking ---

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

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

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

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

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

  tags = {
    Name = "my-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 = "my-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 five resources create the same networking foundation you built through the console earlier.

  • aws_vpc creates a VPC with the 10.0.0.0/16 address space and DNS enabled.
  • aws_subnet carves out a 10.0.1.0/24 public subnet that automatically assigns public IPs to instances.
  • aws_internet_gateway and aws_route_table connect the subnet to the internet via a 0.0.0.0/0 route.
  • aws_route_table_association links the route table to the subnet.
  • Save main.tf.

Seeing a syntax error in your editor?

Make sure you copied both blocks (the terraform/provider block and the VPC networking block) into the same file, one after the other. Check that every opening brace { has a matching closing brace }.

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

Add the security group and rule resources

In the console, you created a security group with inline inbound rules. In Terraform, the current best practice is to define the security group shell separately, then attach rules using dedicated rule resources.

  • In main.tf, add the security group and its rules below the VPC networking section by pasting this code:
# --- Security Group ---

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

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

resource "aws_vpc_security_group_ingress_rule" "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_ingress_rule" "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_egress_rule" "all_outbound" {
  security_group_id = aws_security_group.web_server.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

What does this code do?

The aws_security_group resource creates the security group shell inside your VPC. It has no inline rules.

  • aws_vpc_security_group_ingress_rule "ssh" allows inbound TCP traffic on port 22 (SSH) from any IP.
  • aws_vpc_security_group_ingress_rule "http" allows inbound TCP traffic on port 80 (HTTP) from any IP.
  • aws_vpc_security_group_egress_rule "all_outbound" allows all outbound traffic. The ip_protocol = "-1" means all protocols.

Why dedicated rule resources instead of inline rules?

The aws_security_group resource supports inline ingress and egress blocks, but the Terraform AWS provider documentation recommends against them. Inline rules struggle with managing multiple CIDR blocks and lack unique IDs for tags and descriptions.

Using aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule gives each rule its own lifecycle. You can add, remove, or modify a single rule without affecting others.

  • Save main.tf.

Not sure if your code is in the right order?

Your main.tf should now have the terraform/provider block at the top, then the five VPC networking resources, then the security group section you just added. The order matters because resources reference each other (the security group references aws_vpc.main.id).

Help me check that my Terraform security group resources are structured correctly

Add the key pair, EC2 instance, and output

The final piece is the EC2 instance itself. You will import your existing SSH public key, define the instance resource using SSM parameter resolution for the AMI, and add an output so Terraform prints the instance's public IP after deployment.

  • In main.tf, add the SSH key pair resource below the security group section by pasting this code:
# --- SSH Key Pair ---

resource "aws_key_pair" "deployer" {
  key_name   = "my-ec2-key"
  public_key = file("~/.ssh/ec2-key.pub")

  tags = {
    Name = "my-ec2-key"
  }
}

What does this code do?

The aws_key_pair resource uploads your local public key (~/.ssh/ec2-key.pub) to AWS so EC2 instances can recognize your private key during SSH connections. The file() function reads the file contents at plan time.

  • Below the key pair resource, add the EC2 instance and output block by pasting this code:
# --- EC2 Instance ---

resource "aws_instance" "web" {
  ami                    = "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web_server.id]
  key_name               = aws_key_pair.deployer.key_name

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

# --- Output ---

output "instance_public_ip" {
  description = "Public IP address of the EC2 instance"
  value       = aws_instance.web.public_ip
}

What does this code do?

The aws_instance resource creates a t3.micro EC2 instance in your public subnet, protected by your security group, and accessible with your SSH key.

  • The ami argument uses SSM parameter resolution to automatically fetch the latest Amazon Linux 2023 AMI ID. This avoids hardcoding AMI IDs that change over time and vary by region.
  • vpc_security_group_ids takes a list (note the square brackets) because an instance can belong to multiple security groups.
  • The output block tells Terraform to print the instance's public IP after deployment, so you know where to SSH.
  • Save main.tf.
  • Initialize the Terraform project by running this command:
terraform init

You should see a message ending with Terraform has been successfully initialized! confirming the AWS provider was downloaded.

Seeing a provider error during init?

If you see an error about the provider version, make sure your terraform block has version = "~> 6.0" and source = "hashicorp/aws" exactly as written above. Also confirm your terminal is inside the terraform-ec2 folder by running pwd.

Help me troubleshoot a terraform init error

  • Validate that your configuration has no syntax errors by running:
terraform validate

You should see: Success! The configuration is valid.

Seeing a validation error?

A validation error usually means a typo in a resource reference (like aws_vpc.main.id misspelled) or a missing required argument. Read the error message carefully. It tells you the exact file, line number, and what Terraform expected.

Help me fix my terraform validate error

✔️ Awesome, I've got everything!

Great. Double check you have saved main.tf and that terraform validate returns success.

ⓧ I'd like to double check the full code

Here is the complete main.tf file. Make sure yours matches exactly:

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

  required_version = ">= 1.2"
}

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

# --- VPC Networking ---

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

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

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

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

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

  tags = {
    Name = "my-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 = "my-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        = "web-server-sg"
  description = "Allow SSH and HTTP inbound traffic"
  vpc_id      = aws_vpc.main.id

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

resource "aws_vpc_security_group_ingress_rule" "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_ingress_rule" "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_egress_rule" "all_outbound" {
  security_group_id = aws_security_group.web_server.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

# --- SSH Key Pair ---

resource "aws_key_pair" "deployer" {
  key_name   = "my-ec2-key"
  public_key = file("~/.ssh/ec2-key.pub")

  tags = {
    Name = "my-ec2-key"
  }
}

# --- EC2 Instance ---

resource "aws_instance" "web" {
  ami                    = "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web_server.id]
  key_name               = aws_key_pair.deployer.key_name

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

# --- Output ---

output "instance_public_ip" {
  description = "Public IP address of the EC2 instance"
  value       = aws_instance.web.public_ip
}

Your Terraform configuration is complete and validated. Next up, you will deploy all of these resources with a single command and SSH into your Terraform-created instance.

Deploy with Terraform and Connect

In the previous step, you wrote and validated a complete Terraform configuration that describes your entire secured infrastructure. The configuration passed validation, but no real resources exist yet.

Now it's time to bring your infrastructure to life. You'll deploy everything with a single command, prove it works by connecting via SSH, and then tear it all down just as easily.

In this step, get ready to:
  • Run terraform plan and terraform apply to deploy all resources with one command.
  • SSH into your Terraform-created EC2 instance to verify connectivity.
  • Tear down everything with terraform destroy and confirm resources are gone.
Preview and deploy your infrastructure

Before creating real resources, Terraform lets you preview exactly what it will do. This is your safety check before spending any credits.

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

What should I see?

Terraform displays a detailed list of every resource it will create, including your VPC, subnet, internet gateway, route table, security group, security group rules, key pair, and EC2 instance.

At the bottom, you'll see a summary line like: Plan: 11 to add, 0 to change, 0 to destroy. This confirms everything is ready to go.

  • Deploy your infrastructure by running:
terraform apply
  • Terraform will show the same plan and ask you to confirm. Type yes and press Enter.

Terraform creates each resource one at a time, showing progress as it goes. This takes about 30-60 seconds.

What should I see when it finishes?

When deployment completes, you'll see a success message like: Apply complete! Resources: 11 added, 0 changed, 0 destroyed.

Below that, Terraform prints the instance_public_ip output you defined in your configuration. Copy this IP address. You'll need it in the next substep to connect via SSH.

Seeing an error during apply?

  • If you see a credentials error, verify your AWS CLI is configured correctly by running aws sts get-caller-identity. It should return your IAM user details.
  • If you see a "not authorized" error, check that your IAM user has both AmazonEC2FullAccess and AmazonVPCFullAccess policies attached.
  • If you see a file not found error for the SSH key, verify that ~/.ssh/ec2-key.pub exists by running ls ~/.ssh/ec2-key.pub.

Help me debug my terraform apply error.

Connect to your instance via SSH

Your instance is running and your security group allows SSH traffic on port 22. Time to prove it works by connecting from your local machine.

  • SSH into your new instance by running:
ssh -i ~/.ssh/ec2-key ec2-user@[[EC2_PUBLIC_IP="your instance public IP from the terraform apply output"]]
  • If prompted to confirm the host fingerprint, type yes and press Enter.

What should I see?

You should see a welcome message from Amazon Linux 2023 and a shell prompt that looks like [ec2-user@ip-10-0-1-xxx ~]$. This means you are now inside your EC2 instance.

Compare this to Step 3 where you connected through the console. The experience is identical, but this time your entire infrastructure was created from code in a single command.

  • Disconnect from the instance by typing exit and pressing Enter. This returns you to your local terminal.

Connection timed out or refused?

  • Make sure you copied the correct IP from the terraform apply output. Run terraform output instance_public_ip to see it again.
  • If you see a "Permission denied" error, check that you're using the correct key path (~/.ssh/ec2-key) and that the file permissions are correct (run chmod 400 ~/.ssh/ec2-key).
  • If the connection hangs, the instance may still be initializing. Wait 30 seconds and try again.

Help me troubleshoot my SSH connection to EC2.

Tear down all resources with terraform destroy

One of the biggest advantages of Infrastructure as Code is cleanup. Instead of manually deleting each resource in the right order, Terraform handles all of it.

  • Destroy all the resources Terraform created by running:
terraform destroy
  • Terraform shows every resource it will destroy. Type yes and press Enter to confirm.

What should I see?

Terraform deletes resources in the correct dependency order (instance first, then security group, then VPC networking). When finished, you'll see: Destroy complete! Resources: 11 destroyed.

  • Verify the resources are gone by switching to the AWS Console.
  • Navigate to the EC2 dashboard and confirm no running instances exist.
  • Navigate to the VPC dashboard and confirm my-terraform-vpc no longer appears.

Why is this better than manual cleanup?

In Step 3, you had to delete resources in a specific order: terminate the instance, wait, delete the security group, then delete the VPC. Terraform understands these dependencies and handles the ordering automatically.

This means no leftover resources and no surprise charges on your free-tier credits.

Destroy taking a long time or showing errors?

  • EC2 instance termination can take up to 2 minutes. Be patient and let Terraform wait for it.
  • If you see a dependency error, run terraform destroy again. Terraform will retry the remaining resources.
  • If you still have the SSH session open, exit it first. An active connection won't block destroy, but it's cleaner to disconnect.

Help me fix my terraform destroy error.

Secret mission

Add a Network ACL for Subnet-Level Security

Security groups control traffic at the instance level. Network ACLs add a second layer of defense at the subnet level. What happens when you add a Network ACL that conflicts with your security group?

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 consumes free-tier credits while running, but the VPC, subnet, security group, and other networking resources have no ongoing cost.

Cost warning

A running t3.micro instance consumes credits from your $100 AWS free-tier balance. If you plan to keep your instance running, be aware that credits deplete over time. Stop or terminate the instance to preserve credits for future projects.

Resources you used:

  • EC2 instance (t3.micro) in us-east-1. Consumes free-tier credits while running.
  • VPC, subnet, internet gateway, route table, security group, network ACL. No ongoing cost.
  • IAM user with programmatic access key. No ongoing cost.
  • SSH key pair at ~/.ssh/ec2-key (local file). No ongoing cost.
  • Local project folder at ~/Desktop/terraform-ec2. No ongoing cost.

Keep everything running

No action needed. Choose this if you plan to continue experimenting with your Terraform configuration or want to keep the infrastructure deployed.

  • Your EC2 instance will consume free-tier credits while it remains in a running state.
  • If you want to preserve credits but keep the option to redeploy, consider stopping the instance (see the Pause tab).
  • Your IAM user, SSH key pair, and local project folder remain on your machine for future use.

Pause - I'll come back to this later

Stop your EC2 instance to stop consuming compute credits. Your Terraform files and SSH keys stay on your machine so you can redeploy later.

  • If your instance is still running, open the EC2 console.
  • Select your instance, then click Instance state at the top of the page.
  • Click Stop instance.
  • A stopped instance does not consume compute credits. Minimal EBS storage charges may still apply but are negligible.
  • Your local files (~/Desktop/terraform-ec2, SSH keys) remain untouched for when you return.

Delete - I don't want to use this again

Remove all cloud resources, local project files, and optionally the IAM user. This gives you a completely clean slate.

If you already ran terraform destroy during Step 5 or the Secret Mission, your cloud resources are already gone. Follow the remaining steps to clean up local files.

  • Remove all cloud resources by running this from your project directory:
cd ~/Desktop/terraform-ec2
terraform destroy
  • Type yes when prompted to confirm. If the resources are already destroyed, Terraform will report that there is nothing to destroy.
  • Delete your local project folder by running:
rm -rf ~/Desktop/terraform-ec2
  • Remove the SSH key pair by running:
rm ~/.ssh/ec2-key ~/.ssh/ec2-key.pub
  • To delete the IAM user, open the IAM console in your browser.
  • In the left navigation pane, click Users.
  • Select the checkbox next to the IAM user you created for this project.
  • Click Delete at the top of the page.
  • In the confirmation dialog, type the username and click Delete to confirm.

Should I delete my IAM user?

If you plan to do more AWS projects (like others in this series), keep your IAM user. It has no ongoing cost and saves you from recreating credentials later.

Delete it only if you are done with AWS projects entirely and want a clean account.

Nice Work!

Nice Work!

You just deployed a secured EC2 instance inside a VPC, protected by a security group you built twice: once through the AWS Console and once as Terraform code.

You've learned how to:

  • Built a security group through the AWS Console with SSH and HTTP inbound rules, and learned that security groups are stateful (return traffic is automatically allowed).
  • Wrote a Terraform configuration using the current best practice of aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule resources instead of inline rules.
  • Deployed and destroyed a complete secured infrastructure (VPC + EC2 + Security Group) with single Terraform commands.
  • Secret Mission: Added a Network ACL to understand how subnet-level and instance-level security interact, and observed that a NACL deny rule overrides a security group allow rule.

Ready to quiz yourself?