AWS Builder Center
From EC2 to a Production-Ready AWS Application: VPC, Load Balancer, Auto Scaling, Private Subnets and VPN

From EC2 to a Production-Ready AWS Application: VPC, Load Balancer, Auto Scaling, Private Subnets and VPN

During my AWS Solutions Architect learning journey, one of the most valuable hands-on exercises was understanding how individual AWS services come together to form a complete application architecture. Instead of simply launching an EC2 instance, we built the architecture around network isolation, load balancing, auto scaling, private application servers, health checks and secure connectivity. Understand a real-world flow: Internet → Load Balancer → Target Group → Private EC2 → Backend Services

Cloud Data Engineer
During my AWS Solutions Architect learning journey, one of the most valuable hands-on exercises was understanding how individual AWS services come together to form a complete application architecture.
Instead of simply launching an EC2 instance, we built the architecture around network isolation, load balancing, auto scaling, private application servers, health checks and secure connectivity.
The goal was to understand a real-world flow:
Internet → Load Balancer → Target Group → Private EC2 → Backend Services
while also understanding how private resources can communicate with external networks through controlled networking paths.
This article explains the architecture step by step and how I would organize the same project in GitHub using Infrastructure as Code.

1. The Architecture We Are Building

The simplified production-style architecture is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
INTERNET
|
v
+----------------------+
| Application Load |
| Balancer (ALB) |
| Public Subnets |
+----------+-----------+
|
Target Group
|
+-------------+-------------+
| |
v v
+-------------+ +-------------+
| Frontend EC2| | Frontend EC2|
| Private AZ-1| | Private AZ-2|
+------+------+ +------+------+
| |
+-------------+-------------+
|
Backend Service
|
Private EC2 / DB
For secure outbound connectivity:
1
2
3
4
5
6
7
8
9
10
11
12
13
Private EC2
|
v
Private Route Table
|
v
NAT Gateway
|
v
Internet Gateway
|
v
Internet
For private connectivity with an office/data center:
1
2
3
4
5
6
7
8
9
10
11
On-Premises Network
|
Customer Gateway
|
Encrypted VPN Tunnels
|
AWS VPN Gateway / Transit Gateway
|
AWS VPC
|
Private Resources
These are two different networking concepts:
  • NAT Gateway → allows private AWS resources to initiate outbound internet connections.
  • AWS Site-to-Site VPN → securely connects an on-premises/remote network with AWS.
A private EC2 instance does not use a Site-to-Site VPN simply to browse the public internet. For IPv4 outbound internet access from a private subnet, the normal pattern is a NAT Gateway in a public subnet. (AWS Documentation )

2. Why We Should Not Put Everything in a Public Subnet

A beginner-friendly architecture might look like:
1
2
3
4
5
Internet
|
+---- EC2
+---- EC2
+---- Database
This works for experimentation, but it exposes too many resources.
A better architecture separates the application into layers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Internet
|
v
Public Subnet
|
v
ALB
|
v
Private Subnet
|
v
Application
|
v
Private Backend
A public subnet has a route to an Internet Gateway, while a private subnet does not have a direct route to an Internet Gateway. (AWS Documentation )
This separation is one of the fundamental ideas behind secure AWS network architecture.

3. Step 1 — Create the VPC

Let's start with a VPC.
For example:
1
2
VPC CIDR:
10.0.0.0/16
We can divide it into multiple subnets:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VPC: 10.0.0.0/16

AZ-1
├── Public Subnet
│ 10.0.1.0/24
│
└── Private Subnet
10.0.11.0/24

AZ-2
├── Public Subnet
│ 10.0.2.0/24
│
└── Private Subnet
10.0.12.0/24
Each subnet exists entirely within one Availability Zone. Using multiple Availability Zones helps protect an application from a failure affecting a single Availability Zone. (AWS Documentation )

4. Step 2 — Internet Gateway

The public subnets need internet connectivity.
We attach an Internet Gateway to the VPC:
1
2
3
VPC
|
+--- Internet Gateway
The public route table contains:
1
2
3
4
Destination Target
--------------------------------
10.0.0.0/16 local
0.0.0.0/0 Internet Gateway
The 0.0.0.0/0 route represents the default IPv4 route to the internet. (AWS Documentation )

5. Step 3 — Private Subnet and NAT Gateway

Our application EC2 instances should not have public IP addresses.
But they may still need to:
  • Download Linux packages
  • Pull application dependencies
  • Access external APIs
  • Download updates
  • Communicate with approved internet services
This is where NAT Gateway comes in.
Architecture:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Private EC2
|
v
Private Route Table
|
| 0.0.0.0/0
v
NAT Gateway
|
v
Internet Gateway
|
v
Internet
The private route table contains:
1
2
3
4
Destination Target
--------------------------------
10.0.0.0/16 local
0.0.0.0/0 NAT Gateway
A NAT Gateway allows resources in private subnets to initiate outbound internet connections without allowing unsolicited inbound connections from the internet. (AWS Documentation )
For production workloads, AWS recommends considering a NAT Gateway in each active Availability Zone where private resources require internet access. (AWS Documentation )

6. Step 4 — Security Groups

Now we define who can talk to whom.
A good security model is:
1
2
3
4
5
6
7
8
9
Internet
|
| HTTP/HTTPS
v
ALB Security Group
|
| Application Port
v
EC2 Security Group
For example:

ALB Security Group

1
2
3
4
Inbound

HTTP 80 Internet
HTTPS 443 Internet

Application Security Group

Instead of:
1
Port 80 → 0.0.0.0/0
we can use:
1
Port 80 → ALB Security Group
This means the application EC2 instances accept traffic from the load balancer rather than directly from the whole internet.
This is a much better security boundary.

7. Step 5 — Create the Launch Template

Now we create the EC2 Launch Template.
Think of a launch template as the blueprint for our application servers.
It can define:
  • AMI
  • Instance type
  • Key pair
  • Security group
  • IAM role
  • EBS configuration
  • User Data
  • Tags
  • Network configuration
AWS documents launch templates specifically as configuration templates that can be used by Auto Scaling Groups. (AWS Documentation )
For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Launch Template
|
+-- Ubuntu AMI
|
+-- Instance Type
|
+-- Application Security Group
|
+-- IAM Role
|
+-- EBS
|
+-- User Data
|
+-- Tags
The biggest advantage is consistency.
If Auto Scaling launches ten EC2 instances, they can all be created using the same configuration.

8. Step 6 — Configure the Application

For example, our Ubuntu EC2 instance could install Nginx automatically using User Data:
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

apt-get update -y
apt-get install -y nginx

systemctl enable nginx
systemctl start nginx

cat > /var/www/html/index.html <<'EOF'
<h1>AWS Production Architecture</h1>
<p>Application running on a private EC2 instance.</p>
EOF
Now every EC2 instance launched from the template can automatically prepare the application environment.
This is one reason User Data and Launch Templates work well with Auto Scaling.

9. Step 7 — Create the Target Group

The next component is the Target Group.
The target group represents the EC2 instances that can receive application traffic.
For our web application:
1
2
3
4
5
Target Type: Instances
Protocol: HTTP
Port: 80
Protocol Version: HTTP/1.1
VPC: Application VPC
We also configure health checks.
For example:
1
2
3
Protocol: HTTP
Port: Traffic Port
Path: /
The Application Load Balancer continuously checks the health of targets in the target group and routes traffic to healthy targets. (AWS Documentation )

10. Step 8 — Create the Application Load Balancer

Now we create an Application Load Balancer.
Configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Load Balancer Type:
Application Load Balancer

Scheme:
Internet-facing

IP Address Type:
IPv4

VPC:
Application VPC

Availability Zones:
AZ-1
AZ-2
The ALB is placed in public subnets because it needs to receive traffic from the internet.
The application servers remain private.
The resulting flow is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
User
|
| HTTP/HTTPS
v
Internet-facing ALB
|
| Forward
v
Target Group
|
+---- EC2-1
|
+---- EC2-2
|
+---- EC2-3
An Application Load Balancer operates at the application layer and can use listener rules to determine how HTTP/HTTPS requests are routed to target groups. (AWS Documentation )

11. Step 9 — Listener Configuration

For a simple application:
1
2
Listener:
HTTP : 80
Default action:
1
2
Forward to:
Frontend Target Group
For a real production application, I would normally prefer:
1
2
3
4
5
6
7
HTTPS : 443
|
v
TLS Certificate
|
v
Target Group
This protects client-to-load-balancer communication.

12. Step 10 — Create the Auto Scaling Group

Now we create the Auto Scaling Group.
The ASG uses our Launch Template.
Example:
1
2
3
Minimum capacity: 2
Desired capacity: 2
Maximum capacity: 6
We select private subnets across multiple Availability Zones.
1
2
3
4
5
6
7
8
9
ASG
|
+--- Private AZ-1
| |
| EC2
|
+--- Private AZ-2
|
EC2
AWS provides a documented pattern for registering an Auto Scaling Group with Elastic Load Balancing so traffic can be distributed across healthy EC2 instances. (AWS Documentation )

13. What Happens When Traffic Increases?

Imagine our application currently has:
1
2
EC2-1
EC2-2
Suddenly traffic increases.
The Auto Scaling policy can launch another instance:
1
2
3
EC2-1
EC2-2
EC2-3
The new instance is created using the Launch Template.
Then it becomes available to the target group after registration and health checks succeed.
The ALB can then distribute traffic across healthy targets.
1
2
3
4
5
ALB
/ | \
/ | \
v v v
EC2-1 EC2-2 EC2-3
This is the real power of combining Launch Templates + Auto Scaling + Target Groups + ALB.

14. What Happens When an EC2 Instance Fails?

Suppose:
1
EC2-2 → Unhealthy
The target group health check detects the problem.
The ALB stops routing normal traffic to that unhealthy target. (AWS Documentation )
If the Auto Scaling Group determines that the instance needs replacement, it can terminate and replace it according to the configured health checks and scaling behavior.
The new instance comes from:
1
2
3
4
5
6
7
8
9
10
11
12
13
Launch Template
|
v
New EC2
|
v
Target Group
|
v
Health Check
|
v
Healthy
This reduces the need for manual intervention.

15. Frontend and Backend Separation

Now we can make the architecture closer to the diagram I used during the learning exercise.
Instead of having only one application layer:
1
ALB → EC2
we can create:
1
2
3
4
5
6
7
8
9
10
11
12
13
Internet
|
v
ALB
|
v
Frontend Service
|
v
Backend Service
|
v
Database
The frontend can communicate with the backend using a private address or an internal load balancer.
For example:
1
2
3
4
5
Frontend SG
|
| TCP 8080
v
Backend SG
The backend security group should allow port 8080 only from the frontend security group.
This is much better than opening:
1
8080 → 0.0.0.0/0

16. How Private Machines Communicate

One of the most important concepts I learned is that private EC2 instances do not need public IP addresses to communicate with other resources inside the VPC.
For example:
1
2
3
4
5
6
7
8
9
10
11
Frontend EC2
Private IP:
10.0.11.10

|
| private VPC networking
v

Backend EC2
Private IP:
10.0.12.10
The VPC's local routing enables communication between resources using their private addresses when security groups and network ACLs permit it. AWS route tables include a local route for communication within the VPC CIDR. (AWS Documentation )

17. How Does the Private EC2 Reach the Internet?

This is an important distinction.
A private EC2 instance:
1
2
does NOT have
Public IPv4
but needs to download:
1
2
3
4
apt packages
GitHub dependencies
external APIs
security updates
The traffic can follow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Private EC2
|
v
Private Route Table
|
| 0.0.0.0/0
v
NAT Gateway
|
v
Internet Gateway
|
v
Internet
The internet sees the NAT Gateway's public address rather than directly exposing the private EC2 instance.
AWS recommends NAT Gateways over NAT instances for most use cases because they are managed by AWS and require less administration. (AWS Documentation )

18. Where Does VPN Come Into the Architecture?

VPN solves a different problem.
Imagine our company has an office:
1
2
Company Office
10.20.0.0/16
and AWS has:
1
2
AWS VPC
10.0.0.0/16
We want the office network to communicate with private AWS resources.
We can create:
1
2
3
4
5
6
7
8
9
10
11
Office
|
Customer Gateway
|
Encrypted VPN Tunnels
|
AWS Virtual Private Gateway
|
VPC
|
Private EC2
AWS Site-to-Site VPN provides two VPN tunnels between the AWS-side gateway and the customer gateway for high availability. (AWS Documentation )
The route table then needs routes for the remote network.
For example:
1
2
3
4
Destination Target
--------------------------------
10.0.0.0/16 local
10.20.0.0/16 VPN Gateway
Now:
1
2
3
4
5
6
7
8
9
10
Office Server
10.20.0.10
|
| Encrypted VPN
v
AWS VPC
|
v
Private EC2
10.0.11.10
This is how a corporate network can securely communicate with private AWS resources.

19. VPN Is Not the Same as NAT

This distinction is extremely important.

NAT Gateway

Purpose:
Private AWS resources → Internet
1
2
3
4
5
Private EC2
↓
NAT Gateway
↓
Internet

Site-to-Site VPN

Purpose:
AWS VPC ↔ On-Premises Network
1
2
3
4
5
AWS VPC
↕
Encrypted VPN
↕
Company Network
So a production architecture may use both.

20. GitHub Project Structure

If I were building this as a portfolio project, I would keep the infrastructure in GitHub using Terraform.
A clean repository could look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
aws-production-architecture/
│
├── README.md
│
├── architecture/
│ └── architecture.png
│
├── terraform/
│ │
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── providers.tf
│ │
│ ├── vpc.tf
│ ├── security-groups.tf
│ ├── nat.tf
│ ├── launch-template.tf
│ ├── target-group.tf
│ ├── alb.tf
│ ├── autoscaling.tf
│ └── vpn.tf
│
├── application/
│ ├── frontend/
│ └── backend/
│
├── scripts/
│ ├── install-nginx.sh
│ └── deploy.sh
│
└── .gitignore
This makes the project much easier for another engineer to understand and reproduce.

21. Terraform Architecture

The Terraform dependency should conceptually be:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
VPC
|
+-- Subnets
|
+-- Route Tables
|
+-- Internet Gateway
|
+-- NAT Gateway
|
+-- Security Groups
|
+-- Launch Template
|
+-- Target Group
|
+-- Application Load Balancer
|
+-- Auto Scaling Group
|
+-- Optional VPN
Terraform allows us to describe the infrastructure as code instead of creating every resource manually through the AWS Console.

22. Example Terraform: Launch Template

A simplified example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
resource "aws_launch_template" "app" {
name_prefix = "production-app-"
image_id = var.ami_id
instance_type = "t3.micro"

vpc_security_group_ids = [
aws_security_group.app.id
]

user_data = base64encode(file("${path.module}/../scripts/install-nginx.sh"))

tag_specifications {
resource_type = "instance"

tags = {
Name = "production-app"
Environment = "production"
}
}
}
The important concept is that the launch template becomes the reusable EC2 definition.

23. Example Target Group

1
2
3
4
5
6
7
8
9
10
11
12
13
resource "aws_lb_target_group" "app" {
name = "production-app-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id

health_check {
enabled = true
protocol = "HTTP"
path = "/"
port = "traffic-port"
}
}
The target group is responsible for defining how the ALB communicates with the application targets and how their health is evaluated. (AWS Documentation )

24. Example Application Load Balancer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
resource "aws_lb" "app" {
name = "production-app-alb"
internal = false
load_balancer_type = "application"

security_groups = [
aws_security_group.alb.id
]

subnets = [
aws_subnet.public_a.id,
aws_subnet.public_b.id
]
}
And the listener:
1
2
3
4
5
6
7
8
9
10
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.app.arn
port = 80
protocol = "HTTP"

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}

25. Example Auto Scaling Group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
resource "aws_autoscaling_group" "app" {

min_size = 2
desired_capacity = 2
max_size = 6

vpc_zone_identifier = [
aws_subnet.private_a.id,
aws_subnet.private_b.id
]

launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}

target_group_arns = [
aws_lb_target_group.app.arn
]
}
Now the relationship is:
1
2
3
4
5
6
7
8
9
10
11
12
13
Auto Scaling Group
|
v
Launch Template
|
v
EC2 Instances
|
v
Target Group
|
v
Application Load Balancer

26. Production Improvements

The architecture above is a strong foundation, but I would not call the basic lab configuration fully production-ready without additional controls.
For a real production deployment, I would add:

Security

  • HTTPS/TLS
  • AWS WAF
  • Least-privilege IAM
  • Secrets Manager
  • Private EC2 instances
  • Restricted security groups
  • VPC endpoints where appropriate

Availability

  • Multiple Availability Zones
  • NAT Gateway per active AZ where required
  • Multiple application instances
  • ALB across multiple AZs
  • Auto Scaling

Observability

  • Amazon CloudWatch
  • ALB access logs
  • VPC Flow Logs
  • Application logs
  • Alarms
  • Dashboards

Deployment

  • GitHub
  • Terraform
  • CI/CD
  • Automated testing
  • Infrastructure validation
  • Separate environments
For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GitHub
|
v
CI/CD
|
v
Terraform
|
v
AWS VPC
|
+---- ALB
|
+---- Auto Scaling
|
+---- EC2
|
+---- Backend
|
+---- Monitoring

27. Why This Architecture Is Used in Real Applications

This pattern solves several common production problems.

Problem 1 — One EC2 Instance Fails

Solution:
Multiple instances + Auto Scaling.

Problem 2 — Too Much Traffic

Solution:
ALB + Auto Scaling.

Problem 3 — EC2 Should Not Be Public

Solution:
Private subnets + public ALB.

Problem 4 — Application Needs Internet Access

Solution:
NAT Gateway.

Problem 5 — Company Network Needs Private AWS Access

Solution:
Site-to-Site VPN or, depending on requirements, Direct Connect/Transit Gateway.

Problem 6 — New Instances Need Consistent Configuration

Solution:
Launch Template.

Problem 7 — Failed Instances Should Not Receive Traffic

Solution:
Target Group health checks + ALB.

28. Final Architecture

Putting everything together:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
INTERNET
|
v
+---------------------------+
| Application Load Balancer |
| Public Subnets |
+-------------+-------------+
|
HTTP/HTTPS
|
v
+---------------+
| Target Group |
| Health Checks |
+-------+-------+
|
+--------------+--------------+
| |
v v
+-------------+ +-------------+
| Frontend EC2| | Frontend EC2|
| Private AZ-1| | Private AZ-2|
+------+------+ +------+------+
| |
+-------------+---------------+
|
v
Backend Service
|
v
Private Backend
|
v
Database

Private EC2
|
+---- NAT Gateway ----> Internet
|
+---- VPN Gateway ----> On-Premises Network

Conclusion

This exercise changed the way I look at an EC2-based application.
An EC2 instance by itself is only a compute resource. The real architecture comes from understanding how VPC, subnets, route tables, security groups, Launch Templates, Target Groups, Application Load Balancers, Auto Scaling and VPN connectivity work together.
The complete request flow becomes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
User
↓
Internet
↓
Application Load Balancer
↓
Target Group
↓
Healthy Private EC2
↓
Frontend
↓
Backend
↓
Database
And for outbound or private network communication:
1
2
3
4
Private EC2
├──→ NAT Gateway → Internet
│
└──→ VPN → Corporate Network
The biggest lesson for me was that AWS architecture is not about launching services individually; it is about designing the communication, security, availability and scaling between those services.
This is the kind of architecture pattern I want to continue building and improving throughout my AWS Solutions Architect journey.

Official AWS References

One important correction to the diagram: if the goal is private EC2 → public Internet, use the NAT Gateway path. If the goal is private EC2/VPC ↔ your company/remote network, use the VPN path. Keeping those two paths separate is an important production networking concept. (AWS Documentation )
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article