How to Cloud - Instantly Set up a VPC for public and private traffic

July 10, 2022

In this article, we'll create a new VPC from scratch. We'll divvy up the network into two sub-networks or subnets, one public and one private in two different availability zones. Our public subnet will be open to internet traffic. On the other hand, our private subnet will filter out internet traffic. You can host sensitive code, a database or any resource that should be hidden from internet traffic. We'll accomplish this using the AWS console as well as Terraform, so you're aware of both methods. Let's get building.

Here's an architecture diagram of my setup.

VPC Architecture

VPC basics

A Virtual Private Cloud, or VPC, is an internal network that is programmatically cordoned off from the rest of the larger AWS network. Inside a VPC, we add servers, databases or containers, any infrastructure our application needs. Creating versatile networks to suit any business need is an advantage of VPCs.

VPCs can operate subnets in different availability zones, control sources of traffic and carve out private sub-networks for internal or external traffic. This makes our networks resiliant and secure, which are table-stakes for running a production application today.

Code

The entire code is available at this repo.

Note: All code snippets and screenshots in this article are up-to-date at the time of publishing. Software, languages and frameworks are in constant flux. If you notice obsolete or deprecated code, do tell.

Launching a VPC from the AWS Console

Every new AWS account comes with a default VPC that has been pre-configured by AWS. Log into your VPC dashboard of your AWS console and you'll see atleast one VPC, several subnets, a NACL (Network Access Control List), an Internet Gateway, a Route table and a Security Group. A VPC, in it's default state, is open to all incoming and outgoing traffic.

First, head to your AWS console and navigate to the VPC section. If you haven't touched this section before, it should look something like this.

Initial AWS VPC Dashboard

Step 1: Create a new VPC from scratch.

Create a new VPC with a default tenency and choose an IpV4 range of 10.0.0.0/24. This CIDR block gives us about 253 usable IPs for all our resources, enough for a simple application. Give your VPC a name you'll remember, it's easier to associate the rest of the network components with the right VPC, especially when you have more than one VPC

Create a VPC

Step 2: Attach a new Internet Gateway

A VPC requires an Internet Gateway to communicate with the internet. Click on Internet Gateway on the sidebar of the console, and on the gateway admin, create a new Gateway. When it's complete, attach the gateway to your new VPC

Create a new Gateway

Attach to a VPC

Click on the dropdown to attach it to your VPC

Attached to a VPC

Select the VPC you just created.

Step 3: Route table accepts Internet Traffic

A Route table, as the name suggests, is a table of routes that directs network traffic to and from a VPC. Every VPC comes with a main or default route table. You may stick with the one assigned or create a new table. We are going to locate the one attached to our VPC and edit the routes to allow internet traffic to and from the gateway we recently added.

Default Route Table

Click "Edit Routes" to add the gateway

Route table with gateway

Step 4: Create a NACL and add Traffic Rules

A Network Access Control List, or NACL, is a firewall for a subnet. You can choose the types of traffic (HTTP, SSH, MYSQL) you want and deny the rest.

Adding individual NACLs to each subnet gives us fine-grained control to different sections of our network. The first NACL we create will pass through SSH and Web traffic and drop everything else. You should restrict SSH traffic to and from your home or office IP.

Public NACL

A second NACL for our private subnet will allow only Mysql traffic for now. This is a placeholder rule and we can tweak it later.

Private NACL

Step 5: Create a private and public Subnet.

Next, create two subnets in different availability zones. Name them so that its easier to identify which zones they belong to. Edit your NACL list so that the NACLs are associated with the correct subnet.

Public Subnet

Public and Private Subnets

Public and Private Subnets

Associate your subnets with the right NACL

Step 6: Allow web and SSH traffic in your public security group.

Now, add a new security group to permit SSH and web traffic through to all servers that we'll add to the public subnet. Remember, security groups are firewalls that are applied to EC2 instances. They are different from NACLs, which are applied to the entire subnet.

Security Group for public Servers

Step 7: Launch an EC2 instance in your public subnet.

And finally, launch an EC2 instance in your public subnet that can accept web traffic. More importantly, to ensure that your private subnet is truly private, launch another instance in the closed subnet. No internet traffic should get through.

Create an EC2 instance 1 Create an EC2 instance 2


Launching a VPC with Terraform

Now that you understand how a network is assembled step-by-step via the console, let's take it up a notch and launch the same network using Terraform.

First, our VPC

# Create a new VPC with `default` tenency. 
# Choose an IpV4 range of `10.0.0.0/24` which 
# gives us 253 usable IPs

resource "aws_vpc" "vpc_spgl" {
  cidr_block       = "10.0.0.0/24"
  instance_tenancy = "default"

  tags = {
    Name        = "vpc-${var.project_name}-1"
    environment = var.tag_environment
  }
}

output "show_vpc" {
  value = aws_vpc.vpc_spgl.arn
}

Next, the Internet Gateway

Unlike the console, we directly attach the gateway to the VPC.

# internet_gateway
resource "aws_internet_gateway" "ig_spgl" {
  vpc_id = aws_vpc.vpc_spgl.id

  tags = {
    Name        = "ig-${var.project_name}-1"
    environment = var.tag_environment
  }
}

output "show_internet_gateway_id" {
  value = aws_internet_gateway.ig_spgl.id
}

Routes in our route-table

First, we identify the default route table in our new VPC. Then, we add a route to connect our gateway to bring in internet traffic.

# Route Table

resource "aws_default_route_table" "rt_spgl" {
  default_route_table_id = aws_vpc.vpc_spgl.default_route_table_id

  tags = {
    Name        = "rt-${var.project_name}-1"
    environment = var.tag_environment
  }
}

# Add a route to the route_table permitting internet traffic from the gateway.
resource "aws_route" "route_ig_rt" {
  route_table_id         = aws_default_route_table.rt_spgl.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.ig_spgl.id
}

A public and private NACL to protect our subnets

egress and ingress resources represents rules for outbound and inbound traffic.

# Network ACLs
# Create two NACLs for each subnet of your VPC. 
# One NACL allows web and SSH traffic. 
# The second NACL allows only SSH traffic, keeping it somewhat private. 

# Nacl 1
resource "aws_network_acl" "nacl_public" {
  vpc_id = aws_vpc.vpc_spgl.id

  # Specify your outbound rules here
  egress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  # Specify your inbound rules here

  # Allow HTTP traffic
  ingress {
    protocol   = "tcp"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 80 
    to_port    = 80
  }

  # Allow HTTPS traffic
  ingress {
    protocol   = "tcp"
    rule_no    = 200
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 443
    to_port    = 443
  }

  # Allow SSH traffic from your IP. 
  ingress {
    protocol   = "tcp"
    rule_no    = 300
    action     = "allow"
    cidr_block = var.ssh_source_ip
    from_port  = 22
    to_port    = 22
  }

  tags = {
    Name        = "nacl-${var.project_name}-public"
    environment = var.tag_environment
  }
}

output "show_nacl_public" {
  value = aws_vpc.vpc_spgl.arn
}

# Nacl 2
resource "aws_network_acl" "nacl_private" {
  vpc_id = aws_vpc.vpc_spgl.id

  ingress {
    protocol   = "tcp"
    rule_no    = 100
    action     = "allow"
    cidr_block = var.ssh_source_ip
    from_port  = 22
    to_port    = 22
  }

  tags = {
    Name        = "nacl-${var.project_name}-private"
    environment = var.tag_environment
  }
}

Subnets in different availability zones

First, create two subnets and associate them with the appropriate NACL.

# Subnets
# Create 2 subnets and associate them with the right NACL. 

resource "aws_subnet" "subnet_public" {
  vpc_id            = aws_vpc.vpc_spgl.id
  cidr_block        = "10.0.0.0/25"
  availability_zone = "us-east-1a"

  tags = {
    Name        = "subnet-${var.project_name}-public"
    environment = var.tag_environment
  }
}

resource "aws_subnet" "subnet_private" {
  vpc_id            = aws_vpc.vpc_spgl.id
  cidr_block        = "10.0.0.128/25"
  availability_zone = "us-east-1b"

  tags = {
    Name        = "subnet-${var.project_name}-private"
    environment = var.tag_environment
  }
}

resource "aws_network_acl_association" "nacl_public_1" {
  network_acl_id = aws_network_acl.nacl_public.id
  subnet_id      = aws_subnet.subnet_public.id
}

resource "aws_network_acl_association" "nacl_private_1" {
  network_acl_id = aws_network_acl.nacl_private.id
  subnet_id      = aws_subnet.subnet_private.id
}

Launch EC2 instances in your subnet.

Launch an instance in your public subnet. To SSH into your instance, before you launch an instance, generate an SSH keypair using the ssh-keygen command on a NIX terminal, or PUTTY on windows. Paste the contents of the public key into the Terraform variable file or tfvars file like I've done in the code. Refer to the full code library on my github repo.

#  EC2 instance

# Specify the KeyPair you will use to SSH into the instance. 
resource "aws_key_pair" "ec2_key_pair" {
  key_name   = var.ec2_key_name
  public_key = var.ec2_public_key
}


resource "aws_instance" "ec2_public" {
  ami               = var.ec2_ami_id
  instance_type     = "t3.micro"
  key_name          = aws_key_pair.ec2_key_pair.key_name
  availability_zone = "us-east-1a"
  # When spinning an ec2 VM in a VPC, use vpc_security_group_ids instead. 
  vpc_security_group_ids      = [aws_security_group.sg_public.id]
  subnet_id                   = aws_subnet.subnet_public.id
  associate_public_ip_address = true

  tags = {
    Name        = "ec2-${var.project_name}-public"
    environment = var.tag_environment
  }
}

# Print the Public IP of the EC2 instance 
# to the terminal. 
output "ec2_public_ip" {
  value = aws_instance.ec2_public.public_ip
}


At this point, you should be able to SSH into your instance with the keypair you attached like this

ssh -i <your SSH private Key> ec2-user@ec2-ip

That's it. Your you now have a VPC to host a web-service or an internal application. You can slice and section your network in many ways, so don't hesitate to customize this set up for your needs.

As always if you have any questions, For questions or suggestions, please DM me @sandeep_gopal

Share