Last updated: 14th October, 2025
Attaching Private Backend Instances to Internet-Facing Load Balancer
How to attach backend instances with private IP addresses to internet-facing load balancer
This project was developed within a lab environment that automatically terminates all resources when lab timer lapses. To ensure continuity, I created an Infrastructure as Code (IaC) solution using CloudFormation. This allowed me to quickly redeploy the entire stack when the lab is restarted and pick up from where I left off without any manual reconfiguration.
Introduction: Initial Setup & Prerequisites
When developing an architecture for an application on AWS, it is highly recommended in cloud practices to provision application instances into private subnets with no direct access to the internet. This ensures the safety of the application. However, the application still needs to respond to user requests, hence the need for an interface to between the private instances an the users. This interface can be established with an Elastic Load Balancer.
Throughout this document, we would create the architecture below step by step to fufill this.

Prerequisites
- • AWS Account with appropriate IAM users and permissions
- • Familiarity with AWS Management Console
- • A Command Line Interface (CLI) tool
- • Familiarity with basic Linux Commands
- • Familiarity with AWS CloudFormation (optional but recommended)
I will be using a WSL terminal
Step 1: Creating VPC and Subnets
We will first create an isoloated network using a VPC and create 3 subnets inside itTwo public and one private
• In the Management Console search for VPC and open the VPC Dashboard.
• Create VPC.
• "VPC only" and provide a name ("my-vpc" in my case).
I will be using this name format for the entire project for simplicity, you can use any format that works for you• Set an IPv4 CIDR block of 10.0.0.0/16
• Review and Create VPC

Now to create subnets,
If you're wondering why create subnets by the way, they're used to sectionalize the VPC. They allow us to easily apply rules that determine which sections of the network have access to the internet and which do not.
Expand the VPC Dashboard sidebar if closed and go to Subnets
Create Subnet
Choose the VPC created earlier ("my-vpc").
We will create Public Subnets first
• So name it "public-subnet-1" since it's the only public subnet
• Set the Availability Zone to your preference (e.g., us-west-2a) and note it somewhere
becasue the second public subnet must be in a different AZ• Set the IPv4 subnet CIDR block to 10.0.1.0/28
• Review and click Create Subnet

We will follow similar procedure to create the second public subnet
• Name: public-subnet-2
• Different AZ from prublic-subnet-1
• CIDR: 10.0.2.0/28

And then a private subnet
• Name: private-subnet
• Same AZ as public-subnet-1
• CIDR: 10.0.3.0/28

Cloudformation Code for Step 1 - (VPC and Subnets)
The part of the Cloudformation code that creates vpc and subnets as we discussed above
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
This template creates a loadbalancer for 2 instances in a private network
Parameters:
VPCName:
Description: The name of the VPC being created.
Type: String
Default: my-vpc
AL2023AMI:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Mappings:
SubnetConfig:
VPC:
CIDR: 10.0.0.0/16
Public1:
CIDR: 10.0.1.0/28
Public2:
CIDR: 10.0.2.0/28
Private:
CIDR: 10.0.3.0/28
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: "true"
EnableDnsHostnames: "true"
CidrBlock: !FindInMap
- SubnetConfig
- VPC
- CIDR
Tags:
- Key: Name
Value: !Ref VPCName
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs
CidrBlock: !FindInMap
- SubnetConfig
- Public1
- CIDR
Tags:
- Key: Name
Value: public-subnet-1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 1
- !GetAZs
CidrBlock: !FindInMap
- SubnetConfig
- Public2
- CIDR
Tags:
- Key: Name
Value: public-subnet-2
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs
CidrBlock: !FindInMap
- SubnetConfig
- Private
- CIDR
Tags:
- Key: Name
Value: private-subnet
Step 2: Routing
While still in the VPC Dashboard we'll configure route tables.
The purpose of routing is to control the traffic flow between the subnets, or between the subnets and the internet
What we'll create:
• A Route Table for the Public Subnets with routes to an Internet Gateway to enable internet access.
• A Route Table for the Private Subnet with a route to a NAT Gateway to ensure they remain isolated from direct internet access.
Public route table will connect an internet gateway to all public subnets associated with it so they can have access to internet
Private route table on the other hand needs a NAT gateway to provide a restricted route to the internet for the private subnet
So let's create the two before we move on
Choose "Internet gateways" from the sidebar and click on Create Internet Gateway.
• Name it "my-igw"
andCreate internet gateway.
Select Attach to VPC from the green pop-up and select the VPC ("my-vpc").

Now go to "NAT Gateways" and Create NAT Gateway.
• Name: "my-ngw"
• Choose the first public subnet ("public-subnet-1")
A NAT Gateway must always be in a public subnet• Allocate an Elastic IP
A elastic ip gives the NAT Gateway a static IP address• Review andCreate NAT gateway

Now that we have both Internet Gateway and NAT Gateway created, we can create the route tables Open "Route Tables" from the side menu
• Create Route Table.
• We'll name it "public-rtb" and associate it with the VPC.
• Create route table

You'll be redirected to the dashboard of the just created route table
There, select the "Routes" tab. Click on Edit routes and then Add route.
• Set the destination to 0.0.0.0/0 and the target to the Internet Gateway("my-igw")
Save changes

We need to associate this route table with the public subnets so they're reachable from the internet.
• Select the "Subnet associations" tab and Edit subnet associations.
• Select the public subnets ("public-subnet-1", "public-subnet-2")
Save associations

We repeat the process to create a route table for the private subnet.
• We'll name it "private-rtb" and associate it with the VPC.
• In the "Routes" tab, add a route with destination 0.0.0.0/0 and target the NAT Gateway ("my-ngw").
• In the "Subnet associations" tab, associate the private subnet ("private-subnet) with this route table.
If everything is done correctly the VPC resource map should look like this.
The additional route table was created by default by the lab environment I used. It doesn't affect anything

Key Points / Best Practices
- A NAT Gateway must always be in a public subnet
- Private subnets should not have a direct route to the Internet Gateway, they only interact with the internet for their needs through the NAT Gateway
- Ensure that the route tables are correctly associated with their respective subnets
- Double-check the CIDR blocks to avoid overlaps and ensure proper segmentation
Cloudformation Code for Step 2 - (Routing)
The part of the Cloudformation code that creates the IGW, NAT Gateway and Route Tables as discussed above
Uploading only this part to Cloudformation will fail to create unless the VPC and Subnets from Step 1 are already created
Append this code to the code from Step 1 to make it work, and ensure the indentations are correct
# Resources: ...
# VPC and Subnets section
# ....
# Routing section
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: my-igw
GatewayToInternet:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
ElasticIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: ngw-eip
NATGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt ElasticIP.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: my-ngw
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: public-rtb
PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayToInternet
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: private-rtb
PrivateRouteToNAT:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway
PrivateSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet
RouteTableId: !Ref PrivateRouteTable
Step 3: Security Groups
In this step we will set up Security Groups to control access to the instances and load balancer we will provision
Security Groups act as virtual firewalls that regulate inbound and outbound traffic. We will create rules to allow only necessary traffic, enhancing the security of our application.
What we'll create:
• A Security Group for the Classic Load Balancer to allow ssh and http.
• A Security Group for Instances to allow traffic from the Load Balancer group only
We'll start with the Classic Load Balancer Group
• Go to Security in the VPC Dashboard.
• Create Security Group.
• We'll name it "LoadBalancerSG"
• Add a description
• Select VPC (my-vpc)
For Inbound rules:
• Add a rule for SSH access from anywhere
• Choose "SSH" from the dropdown
• Set the Source to "Anywhere IPv4"
• Add another rule for HTTP also with Source "Anywhere IPv4"
• Review andCreate security group

For Instance Security Group.
• We'll name it "InstanceSG"
• Add inbound rules:
• SSH - Source: Custom - "LoadBalancerSG" (search "sg" and select LoadBalancerSG from the dropdown)
• HTTP - Source: Custom - also from "LoadBalancerSG"

Key Points / Best Practices
- The instances security should allow traffic from only the load balancer security group
- Ensure to use descriptive names and comments for rules to make management easier
Cloudformation Code for Step 3 - (Security Groups)
The part of the Cloudformation code that creates the Security Groups as dicussed above
Uploading only this part to Cloudformation will fail to create unless the VPC and Subnets from Step 1 are already created
Append this code to the code from Step 1 and Step 2 to make it work, and ensure the indentations are correct
# Resources: ...
# VPC and Subnets section
# ....
# Routing section
# ....
# Security groups
LoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable SSH and HTTP access from anywhere
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: LoadBalancerSG
InstanceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable HTTP and SSH access from Load Balancer Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
Tags:
- Key: Name
Value: InstanceSG
Step 4: EC2 Instances
In this step, we will launch two EC2 instances, configure them with InstanceSG security group created in the previous step, and set up user data scripts to automate the installation of necessary software.

• Search for "EC2" in the Search bar and open the EC2 Dashboard.
• Click on "Launch Instance".
• Set number of instances to 2 (find this in the panel on the right, hopefully aws had not moved it somewhere else at the time you read this document)
• We're creating 2 instances at a go so we won't set name tags yet.
• Choose an Amazon Machine Image (AMI). Select "Amazon Linux 2023".
• Choose an Instance Type. Select "t2.micro" (free tier).
• Select a key pair for SSH access. If you don't have one, create a new key pair and download the ".pem" file
will be required to connect to the instances later when we test the deployment.

Edit Network Settings:
- Select the VPC (my-vpc)
- Select the the subnet (private-subnet)
- Disable Auto-assign Public IP
the instances are private and don't require a public IP.- Under Security Group, "Select an existing security group" and select the Instance security group (InstanceSG)

• Leave everything else as default and scroll to Advanced Details
• In the User Data section, copy and paste the code below:
Instance User Data
#!/bin/bash
set -euxo pipefail
sudo yum update -y
sudo yum install -y httpd
sudo systemctl start httpd
sudo systemctl enable httpd
sudo echo "$HOSTNAME" > /var/www/html/index.html

• Review andLaunch instance
Once the instances finish creating, we'll go to the dashboard and assign them names so we can easily identify them later
- Name them "Instance1" and "Instance2"
Cloudformation Code for Step 4 - (EC2 Instances)
The part of the Cloudformation code that creates the EC2 Instances as dicussed above
Uploading only this part to Cloudformation will fail to create unless you combine with the codes from the previous steps, and ensure the indentations are correct
# Resources: ...
# VPC and Subnets section
# ....
# Routing section
# ....
# Security groups
# ....
# EC2 Instances
Instance1:
Type: AWS::EC2::Instance
Properties:
InstanceType: t2.micro
KeyName: vockey
ImageId: !Ref AL2023AMI
NetworkInterfaces:
- AssociatePublicIpAddress: false
DeviceIndex: 0
SubnetId: !Ref PrivateSubnet
GroupSet:
- !Ref InstanceSecurityGroup
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -euxo pipefail
sudo yum update -y
sudo yum install -y httpd
sudo systemctl start httpd
sudo systemctl enable httpd
sudo echo "$HOSTNAME" > /var/www/html/index.html
Tags:
- Key: Name
Value: Instance1
Instance2:
Type: AWS::EC2::Instance
Properties:
InstanceType: t2.micro
KeyName: vockey
ImageId: !Ref AL2023AMI
NetworkInterfaces:
- AssociatePublicIpAddress: false
DeviceIndex: 0
SubnetId: !Ref PrivateSubnet
GroupSet:
- !Ref InstanceSecurityGroup
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -euxo pipefail
sudo yum update -y
sudo yum install -y httpd
sudo systemctl start httpd
sudo systemctl enable httpd
sudo echo "$HOSTNAME" > /var/www/html/index.html
Tags:
- Key: Name
Value: Instance2
Step 5: Load Balancer
In this section we are going to a create The Load Balancer which will forward user requests to the instances
Locate Load Balancers on the EC2 sidebar under "Load Balancing"
• Click on Create load balancer
• Choose Classic Load Balancer
• We'll name it "my-alb"
• We'll make it internet-facing
• Select the VPC
• Check the 2 AZs and select the public subnet in each
• Choose "LoadBalancerSG" as the security group

Inside listeners and routing:
-Set an HTTP listener on port 80
-Set another listener for SSH (tcp on port 22)

Add instances

- Instance1 and Instance2 if you named them so
Leave everything else as-is andCreate load balancer
It will take a few minutes to set up
Once it's done, copy the ALB DNS name and note it somewhere

Cloudformation Code for Step 5 - (Load Balancer)
The part of the Cloudformation code that creates the Load Balancer dicussed above
Uploading only this part to Cloudformation will fail to create unless the VPC and Subnets from Step 1 are already created
Append this code to the code from Step 1 through Step 4 to make it work, and ensure the indentations are correct
# Resources: ...
# VPC and Subnets section
# ....
# Routing section
# ....
# Security groups
# ....
# EC2 Instances
# ....
# Load Balancer
LoadBalancer:
Type: AWS::ElasticLoadBalancing::LoadBalancer
Properties:
Scheme: internet-facing
LoadBalancerName: my-alb
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
Listeners:
- LoadBalancerPort: 80
InstancePort: 80
Protocol: HTTP
InstanceProtocol: HTTP
- LoadBalancerPort: 22
InstancePort: 22
Protocol: TCP
InstanceProtocol: TCP
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
HealthCheck:
Target: HTTP:80/index.html
HealthyThreshold: '3'
UnhealthyThreshold: '5'
Interval: '30'
Timeout: '5'
Instances:
- !Ref Instance1
- !Ref Instance2
Step 6: Testing
In this final step, we will test the entire setup to ensure that all components are functioning correctly. We will verify that the Load Balancer can respond to requests, and also test if it can connect to the instances via ssh
Make sure your key pair file has the right permissions set.
Run this command to do so:
Open a browser and run http://ALB-DNS-NAME/ and you see the hostname of the instance the Load balancer routed the request to
Note that if you run https insted of http it will not work because of the rules in the secuirty

Refresh the page a few more times until the Hostname changes to that of the second instance

Confirmed! the Load Balancer is working as expected.
Let's try to ssh into either of the instances through the Load Balancer
ssh -i YOUR-KEY-PAIR.pem ec2-user@ALB-DNS-NAME

We have confirmed the whole setup is working as expected!
You can run further tests on your own to verify if the security groups are working as intended
For example try to send http requests to the site on a different port (eg, 54322) to see if it will respond or not
You can also try to ssh into the instances directly using their private IPs to see if it will work or not
Congratulations!
That's all about it, we've been able to successfully complete the design
Thank You
Thank you for following this project on establishing a connection between a load balancer and application backend private instances. I hope you found it informative and helpful. If you have any questions or need further assistance, feel free to reach out!
Get In Touch
Have questions about this project or want to collaborate? I'd love to hear from you!

Dickson Ankamah
Cloud Practitioner
I'm always open to discussing new opportunities, interesting projects, or just having a chat about technology. Feel free to reach out through any of the channels above!