Sitemap

Creating a VPC with an EC2 Instance: Terraform for Beginners

7 min readApr 7, 2026

--

I recently purchased W J Pearce’s eBook ‘Tech One Twenty’. Within it are what he refers to as ‘micro projects’.

The first on the list was a Terraform project. As someone just getting into it, I wasn’t just interested in making it work. I wanted to understand why it works.

Even though he gives you the code in the book, you still have to challenge yourself on understanding what it’s actually doing.

What we will do:

  • Create a VPC with a CIDR block of 10.0.0.0/16.
  • Add a subnet to the VPC in the ap-southeast-2 availability zone with a CIDR block of 10.0.1.0/24.
  • Set up a security group within the VPC that allows SSH and HTTP from any IP address, and allows all egress traffic.
  • Launch an EC2 within this subnet and security group using a specified AMI.

These are the relevant Terraform files we’ll create:

  • main.tf
  • outputs.tf — Terraform output values let you export structured data about your resources. You can use this data to configure other parts of your infrastructure with automation tools, or as a data source for another Terraform workspace.
  • providers.tf
  • variables.tf
  • versions.tf

CREATE THE TERRAFORM FILES

Find a directory you want to hold the Terraform files in.

Create the files:

touch providers.tf variables.tf main.tf outputs.tf versions.tf

Edit the files:

providers.tf:

provider "aws" {
region = var.aws_region
}

variables.tf:

variable "aws_region" {
default = "ap-southeast-2"
description = "The AWS region to create resources in."
}

variable "instance_type" {
default = "t2.micro"
description = "The instance type of the EC2 instance."
}

variable "ami_id" {
description = "The AMI ID to use for the EC2 instance."
}
Press enter or click to view image in full size

main.tf:

resource "aws_vpc" "techone_vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "techone_vpc"
}
}

resource "aws_subnet" "example_subnet" {
vpc_id = aws_vpc.techone_vpc.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
availability_zone = "ap-southeast-2a"
tags = {
Name = "example-subnet"
}
}

resource "aws_security_group" "techonetwenty_sg" {
name = "techonetwenty-security-group"
description = "Allow SSH and HTTP"
vpc_id = aws_vpc.techone_vpc.id

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
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 = "techonetwenty_sg"
}
}

resource "aws_instance" "example_instance" {
ami = var.ami_id
instance_type = var.instance_type
subnet_id = aws_subnet.example_subnet.id
vpc_security_group_ids = [aws_security_group.techonetwenty_sg.id]
tags = {
Name = "example-instance"
}
}

outputs.tf:

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

output "instance_public_ip" {
description = "The public IP of the EC2 instance."
value = aws_instance.example_instance.public_ip
}

versions.tf:

terraform {
required_version = ">= 0.12"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
Press enter or click to view image in full size

OPTIONAL — CHANGE THE SECURITY GROUPS FOR TERRAFORM V5.0

Read over main.tf if you want to learn more. Basically ingress and egress rules have their own resource called aws_vpc_security_group_[ ingress | egress ]_rule. This is optional however we’ll stick with Terraform V3.0 in this demo.

versions.tf:

terraform {
required_version = ">= 0.12"

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

main.tf:

resource "aws_vpc" "techone_vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "techone_vpc"
}
}

resource "aws_subnet" "example_subnet" {
vpc_id = aws_vpc.techone_vpc.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
availability_zone = "ap-southeast-2a"
tags = {
Name = "example-subnet"
}
}

resource "aws_security_group" "techonetwenty_sg" {
name = "techonetwenty-security-group"
description = "Allow SSH and HTTP"
vpc_id = aws_vpc.techone_vpc.id

# ingress {
# from_port = 22
# to_port = 22
# protocol = "tcp"
# cidr_blocks = ["0.0.0.0/0"]
# }
# ingress {
# 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 = "techonetwenty_sg"
# }
}

## Added as a replacement of the ingress rules above ##

resource "aws_vpc_security_group_ingress_rule" "ssh" {
security_group_id = aws_security_group.techonetwenty_sg.id
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
from_port = 22
to_port = 22
}

## Added as a replacement of the ingress rules above ##

resource "aws_vpc_security_group_ingress_rule" "http" {
security_group_id = aws_security_group.techonetwenty_sg.id
ip_protocol = "tcp"
cidr_ipv4 = "0.0.0.0/0"
from_port = 80
to_port = 80
}

## Added as a replacement of the ingress rules above ##

resource "aws_vpc_security_group_ingress_rule" "allow_ping" {
security_group_id = aws_security_group.techonetwenty_sg.id
ip_protocol = "icmp"
cidr_ipv4 = "0.0.0.0/0"
from_port = -1
to_port = -1
}

## Added as a replacement of the egress rule above ##

resource "aws_vpc_security_group_egress_rule" "allow_all_outbound_traffic" {
security_group_id = aws_security_group.techonetwenty_sg.id
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
from_port = 0
to_port = 0
}

resource "aws_instance" "example_instance" {
ami = var.ami_id
instance_type = var.instance_type
subnet_id = aws_subnet.example_subnet.id
vpc_security_group_ids = [aws_security_group.techonetwenty_sg.id]
tags = {
Name = "example-instance"
}
}

For help with the ICMP rule I used this post.

APPLYING THE CONFIGURATION TO AWS

Authenticate to AWS with either of these two commands:

Authenticate with Access Keys:

aws configure

Or authenticate with a web page:

aws login

If the aws command doesn’t work for you, it means you need to install AWS CLI. Choose from the list here to download it.

Get Lumenyx’s stories in your inbox

Join Medium for free to get updates from this writer.

Now we can initialise Terraform:

terraform init
Press enter or click to view image in full size

Now we can plan our configuration. What this simply shows us is what Terraform will create (or change) without actually committing to that change.

terraform plan

It then prompted me for an AMI (since we didn’t define it in the variables.tf file).

The AMI is used was ami-0a11f7293cd9a562e.

NOTE: This could different depending on your region.

I got my AMI ID from EC2 > Instances > Launch an Instance > Choose my AMI and copy the ID:

Press enter or click to view image in full size

Now we can apply the changes to AWS:

terraform apply
Press enter or click to view image in full size

CONFIRMING IT’S ACTUALLY ON AWS

Go to the EC2 management console and check out your running EC2 instances.

You’ll notice a new instance:

Press enter or click to view image in full size

What about the VPC?

Go to the VPC management console and take a look:

Press enter or click to view image in full size
Yep it’s here.

Security groups?

Yep right here:

Press enter or click to view image in full size

Can we use EC2 Instance Connect to SSH into the EC2 instance?

No. It needs an Internet Gateway:

Press enter or click to view image in full size

Let’s quickly add one!

ADD AN INTERNET GATEWAY AND A ROUTING TABLE

Append this to the main.tf file:

resource "aws_internet_gateway" "tech120-igw" {
vpc_id = aws_vpc.techone_vpc.id

tags = {
Name = "techone_vpc"
}
}

resource "aws_route_table" "tech120-rt" {
vpc_id = aws_vpc.techone_vpc.id

route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.tech120-igw.id
}
tags = {
Name = "techone_vpc"
}
}

resource "aws_route_table_association" "rt-association" {
subnet_id = aws_subnet.example_subnet.id
route_table_id = aws_route_table.tech120-rt.id
}
Press enter or click to view image in full size
Press enter or click to view image in full size

And we’re in!

Press enter or click to view image in full size

DELETE YOUR RESOURCES — SO YOU DON’T GET CHARGED

Now we want to destroy (delete) all the resources we just made.

You can do that by following this command:

terraform destroy
Press enter or click to view image in full size

Let’s confirm that everything is deleted within the AWS management console.

NOTE: There are default VPCs along with default subnets/route tables/security groups etc that are there by default. Do not delete these. I distinguish them by noting if they’re in the 172.16.X.X range (that means they’re default), and then checking the resource map to see what’s connected. If they’re connected then they’re likely to be there by default.

Check these to be sure everything is deleted:

  • VPC
  • Subnet
  • Route tables
  • Security groups
  • Internet Gateway

--

--