Author: Karen
Categories: TECH
Tags: Terraform AWS

A good starting point for creating EC2 instances is to create them in the default VPC and subnet provided by AWS.

Deploying an Amazon EC2 instance within the default VPC subnet significantly reduces setup complexity while still providing a secure, scalable, and reliable infrastructure. The default VPC is automatically configured with essential components such as subnets in each Availability Zone, route tables, security groups, network ACLs, and an internet gateway. This allows to launch EC2 instances quickly without needing deep networking expertise, making it ideal for rapid production deployments. 

Additionally, using the default VPC subnet ensures built-in connectivity and high availability while following AWS best practices. Instances launched in a default subnet can access the internet immediately (when assigned a public IP). As workloads grow, these deployments in the default VPC can seamlessly integrate with load balancers, auto scaling groups, and managed services.

In this article we will go over the steps of setting up a minimal production-ready EC2 environment with Terraform which can be used for deploying your application. Before diving into the actual implementation, it helps to understand a few core AWS networking concepts.

Virtual Public Cloud (VPC)

A VPC is your own private network inside AWS. You can think of it as your company’s private “internet neighborhood,” isolated from others. Inside the VPC you define IP ranges, create subnets, and control all networking.

Subnet

A subnet is a smaller network segment inside your VPC.
Subnets can be:

  • Public: can reach the internet (through an Internet Gateway)

  • Private: cannot directly reach the internet

In the default VPC, all subnets are public because they have routes to the Internet Gateway.

Route Table

A route table contains rules that determine how network traffic is directed:

  • Internet-bound traffic is routed through the Internet Gateway

  • Internal traffic stays within the VPC

The default VPC includes a default route table that already has a 0.0.0.0/0 route to the internet gateway, making all its subnets public.

Internet Gateway (IGW)

An IGW allows resources inside your VPC to connect to the internet. Default VPC has an IGW attached.

Security Groups

Security groups are virtual firewalls attached to EC2 instances or other resources, defining what inbound and outbound traffic is allowed.

Terraform template

We select AWS as a provider, then fetch default vpc and subnet. Existing resources are referenced using the data keyword, while new resources are created using the resource keyword.

For the security group, we open port 80 and 22 for web traffic and ssh access repsectively, and outbound traffic is allowed to anywhere.

We then create a t2.micro EC2 instance, referencing correct subnet, security group and key_pair. The key pair is used for SSH access from your local machine.

To generate a key pair if one does not already exist, run:

ssh-keygen -t rsa -b 4096 -f ~/.ssh/my-key (passphrases may be skipped).

this command generates my-key and my-key.pub files in the ssh directory (you can give it any other name).

The public key is uploaded to EC2. You can verify this in the AWS console under:

Instance -> Details -> Key pair assigned at launch and it should list reference my-key

or by connecting to the instance and checking:

cat ~/.ssh/authorized_keys.

To ssh into you instance:

ssh -i ~\.ssh\my-key [user]@[public_ip_of_instance] (Bash)

or 

ssh -i $env:USERPROFILE\.ssh\my-key [user]@[public_ip_of_instance] (Powershell)

The corresponding terraform template is the following:

provider "aws" {
  region = "eu-west-2"  # Choose the AWS region to deploy into
}

# Fetch the region's default VPC
data "aws_vpc" "default" {
  default = true  # Tells AWS: use the default VPC
}

# Get all default subnets inside the default VPC
data "aws_subnets" "default" {
  filter {
    name   = "vpc-id"               # Filter subnets by VPC ID
    values = [data.aws_vpc.default.id]  # Use the default VPC's ID
  }
}

# Security group for the web instance
resource "aws_security_group" "web" {
  name        = "web-sg"        # Security group name
  description = "Allow HTTP and SSH"  # What this SG is for
  vpc_id      = data.aws_vpc.default.id  # Attach SG to default VPC

  ingress {
    description = "HTTP"           # Allow web traffic
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"            # HTTP uses TCP
    cidr_blocks = ["0.0.0.0/0"]    # Allow from anywhere
  }

  ingress {
    description = "SSH"            # Allow SSH access
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"            # SSH uses TCP
    cidr_blocks = ["0.0.0.0/0"]    # Allow SSH from anywhere (not ideal for prod)
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"             # -1 = all protocols
    cidr_blocks = ["0.0.0.0/0"]    # Allow all outbound traffic
  }
}

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


# EC2 instance running the web app
resource "aws_instance" "web" {
  ami                    = "ami-03a725ae7d906005d" # OS image (update for your region)
  instance_type          = "t2.micro"               # Instance size
  subnet_id              = data.aws_subnets.default.ids[0]  # Put instance in a default subnet
  vpc_security_group_ids = [aws_security_group.web.id] # Attach security group
  associate_public_ip_address = true  # Give the instance a public IP
  key_name = aws_key_pair.host_key.key_name

  tags = {
    Name = "web"  # Tag for identifying the instance
  }
}

IP Restriction

Opening SSH to all IPs (0.0.0.0/0) is not recommended, as it exposes the instance to brute-force attempts. A safer approach is to restrict SSH access to your own IP address. brute force attempts to connect to your instance, and even with key auth, there are risks involved. Let's replace cidr_blocks = ["0.0.0.0/0"] in

  ingress {
    description = "SSH"            # Allow SSH access
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"            # SSH uses TCP
    cidr_blocks = ["0.0.0.0/0"]    # Allow SSH from anywhere (not ideal for prod)
  }

with a variable-based configuration

variable "ssh_allowed_ips" {
  type = list(string)
}

ingress {
    description = "SSH"            # Allow SSH access
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"            # SSH uses TCP
    cidr_blocks = var.ssh_allowed_ips
  }

Then define ssh_allowed_ips variable in terraform.tfvars. terraform.tfvars file is located in the same directory as your main.tf.

ssh_allowed_ips = [
  "203.0.113.42/32"
]

Check your computer IP (can be checked with What Is My IP Address - See Your Public Address - IPv4 & IPv6) and adjust  ssh_allowed_ips to match your own public address.

This method, however, may not be ideal if your PC’s IP address changes frequently. In that case, configuring an IP range instead of a single IP, using a VPN, or leveraging AWS Systems Manager may be better options.

If everything was done correctly, you should have an up and running EC2 instance. You should also be able to SSH to the instance from your PC only.

 Notes

  • Route tables belong to a VPC but are associated with subnets
  • Internet Gateways are VPC-level resources
  • A public EC2 instance cannot reside in a private subnet