Skip to content

Production-ready AWS infrastructure with EC2 Auto Scaling, Network Load Balancer, and multi-AZ deployment. Fully automated with Terraform.

Notifications You must be signed in to change notification settings

engabelal/simple-webapp-ec2-nlb-asg

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

31 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ Simple Web App on AWS EC2 Auto Scaling with NLB

Production-ready AWS infrastructure with EC2 Auto Scaling, Network Load Balancer, and multi-AZ high availability. Built with Terraform.

Terraform AWS Node.js Express Bash


⚑ Quick Start

# 1. Clone repository
git clone https://github.com/engabelal/simple-webapp-ec2-nlb-asg.git
cd simple-webapp-ec2-nlb-asg/infra-iac

# 2. Initialize Terraform
terraform init

# 3. Deploy to dev
terraform apply -var-file="../envs/dev.tfvars"

# 4. Get NLB URL
terraform output nlb_dns_name

🎯 Key Features

Feature Description
πŸ—οΈ Multi-AZ Architecture 2 Availability Zones for high availability
⚑ Auto Scaling Min: 2, Desired: 2, Max: 4 instances
πŸ”„ Network Load Balancer Layer 4 TCP load balancing
πŸ”’ Private Subnets EC2 instances isolated from internet
🌐 NAT Gateway per AZ Redundant outbound internet access
πŸ“¦ Multi-Environment Separate dev/prod configurations
πŸ›‘οΈ Security Groups Minimal permissions, port 3000 only

πŸ“Š Infrastructure Metrics

Component Count Configuration
Availability Zones 2 eu-north-1a, eu-north-1b
Public Subnets 2 10.0.1.0/24, 10.0.2.0/24
Private Subnets 2 10.0.3.0/24, 10.0.4.0/24
NAT Gateways 2 One per AZ (high availability)
EC2 Instances 2-4 Auto Scaling based on demand
Load Balancer 1 Network Load Balancer (NLB)

πŸ“Έ Screenshots

Web Application

Web Application Result Node.js/Express server displaying instance metadata (ID, AZ, Region) and infrastructure components


πŸ—οΈ Architecture

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   Internet  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                                     β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
β”‚              AWS VPC (10.0.0.0/16)                  β”‚
β”‚                                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚          Internet Gateway                  β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                   β”‚                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚      Public Subnets (Multi-AZ)             β”‚    β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚    β”‚
β”‚  β”‚  β”‚ Public-1     β”‚    β”‚ Public-2     β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ 10.0.1.0/24  β”‚    β”‚ 10.0.2.0/24  β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ (AZ-1a)      β”‚    β”‚ (AZ-1b)      β”‚     β”‚    β”‚
β”‚  β”‚  β”‚              β”‚    β”‚              β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚    β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ β”‚ NAT GW 1 β”‚ β”‚    β”‚ β”‚ NAT GW 2 β”‚ β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚    β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚     β”‚    β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                   β”‚                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚   Network Load Balancer (Internet-facing)  β”‚    β”‚
β”‚  β”‚            Port 3000 (TCP)                 β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                   β”‚                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚      Private Subnets (Multi-AZ)            β”‚    β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚    β”‚
β”‚  β”‚  β”‚ Private-1    β”‚    β”‚ Private-2    β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ 10.0.3.0/24  β”‚    β”‚ 10.0.4.0/24  β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ (AZ-1a)      β”‚    β”‚ (AZ-1b)      β”‚     β”‚    β”‚
β”‚  β”‚  β”‚              β”‚    β”‚              β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚    β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ β”‚   EC2    β”‚ β”‚    β”‚ β”‚   EC2    β”‚ β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ β”‚ Node.js  β”‚ β”‚    β”‚ β”‚ Node.js  β”‚ β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ β”‚ Port 3000β”‚ β”‚    β”‚ β”‚ Port 3000β”‚ β”‚     β”‚    β”‚
β”‚  β”‚  β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚    β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚     β”‚    β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚    β”‚
β”‚  β”‚         β”‚                   β”‚              β”‚    β”‚
β”‚  β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚    β”‚
β”‚  β”‚                     β”‚                      β”‚    β”‚
β”‚  β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚    β”‚
β”‚  β”‚         β”‚   Auto Scaling Group  β”‚          β”‚    β”‚
β”‚  β”‚         β”‚  Min: 2 | Max: 4      β”‚          β”‚    β”‚
β”‚  β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Traffic Flow

Inbound (User β†’ App):

Internet β†’ IGW β†’ NLB (Public Subnets) β†’ EC2 (Private Subnets)

Outbound (App β†’ Internet):

EC2 (Private Subnets) β†’ NAT Gateway (per AZ) β†’ IGW β†’ Internet

High Availability:

  • βœ… Each AZ has its own NAT Gateway
  • βœ… If one AZ fails, traffic routes to healthy AZ
  • βœ… No single point of failure

πŸ› οΈ Tech Stack

IaC Terraform 1.8+
Cloud Provider AWS (VPC, EC2, NLB, NAT Gateway, IGW)
Compute EC2 Auto Scaling Group (t3.micro)
Load Balancer Network Load Balancer (Layer 4)
Runtime Node.js 20.x
Framework Express.js 4.18
Scripting Bash (bootstrap.sh)
Networking Multi-AZ VPC with public/private subnets

πŸ“ Project Structure

simple-webapp-ec2-nlb-asg/
β”œβ”€β”€ infra-iac/
β”‚   β”œβ”€β”€ main.tf              # Main configuration
β”‚   β”œβ”€β”€ provider.tf          # AWS provider
β”‚   β”œβ”€β”€ variables.tf         # Input variables
β”‚   β”œβ”€β”€ outputs.tf           # Output values
β”‚   β”œβ”€β”€ networking.tf        # VPC, Subnets, IGW, NAT GW
β”‚   β”œβ”€β”€ security.tf          # Security Groups
β”‚   β”œβ”€β”€ compute.tf           # Launch Template, ASG
β”‚   └── loadbalancer.tf      # NLB, Target Group, Listener
β”œβ”€β”€ envs/
β”‚   β”œβ”€β”€ dev.tfvars           # Development config
β”‚   └── prod.tfvars          # Production config
β”œβ”€β”€ userdata/
β”‚   └── bootstrap.sh         # EC2 initialization script
β”œβ”€β”€ images/
β”‚   └── result.png           # Screenshot
└── README.md

βš™οΈ Setup Guide

Prerequisites

  • βœ… Terraform β‰₯ 1.8.0
  • βœ… AWS CLI configured
  • βœ… AWS Account with appropriate permissions
  • βœ… EC2 Key Pair (optional, for SSH access)

Step 1: Clone Repository

git clone https://github.com/engabelal/simple-webapp-ec2-nlb-asg.git
cd simple-webapp-ec2-nlb-asg

Step 2: Configure Variables

Edit envs/dev.tfvars:

project_name    = "simple-webapp"
environment     = "dev"
region          = "eu-north-1"
vpc_cidr        = "10.0.0.0/16"
public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.3.0/24", "10.0.4.0/24"]
azs             = ["eu-north-1a", "eu-north-1b"]
ami_id          = "ami-04c08fd8aa14af291"  # Amazon Linux 2023
key_pair_name   = "your-key-pair"          # Optional

Step 3: Initialize Terraform

cd infra-iac
terraform init

Step 4: Plan Deployment

terraform plan -var-file="../envs/dev.tfvars"

Step 5: Deploy Infrastructure

terraform apply -var-file="../envs/dev.tfvars"

Step 6: Get NLB URL

NLB_DNS=$(terraform output -raw nlb_dns_name)
echo "Application URL: http://$NLB_DNS"

πŸš€ Deployment Scenarios

Scenario 1: Development Environment

cd infra-iac
terraform init
terraform apply -var-file="../envs/dev.tfvars"

Result: 2 instances, t3.micro, minimal cost


Scenario 2: Production Environment

cd infra-iac
terraform init
terraform apply -var-file="../envs/prod.tfvars"

Result: 4 instances, t3.small, high availability


Scenario 3: Test Load Balancing

# Get NLB DNS
NLB_DNS=$(terraform output -raw nlb_dns_name)

# Multiple requests show different instances
for i in {1..10}; do
  curl http://$NLB_DNS | grep "Instance ID"
done

Scenario 4: Test Auto Scaling

# Terminate an instance
aws ec2 terminate-instances --instance-ids <instance-id>

# Watch ASG replace it automatically
watch -n 5 'aws autoscaling describe-auto-scaling-groups \
  --auto-scaling-group-names simple-webapp-dev-asg \
  --query "AutoScalingGroups[0].Instances[*].[InstanceId,HealthStatus]" \
  --output table'

Scenario 5: Destroy Infrastructure

cd infra-iac
terraform destroy -var-file="../envs/dev.tfvars"

πŸ’° Cost Breakdown

Development Environment (Monthly)

Component Quantity Unit Cost Total
EC2 (t3.micro) 2 $7.50 $15.00
Network Load Balancer 1 $16.20 $16.20
NAT Gateway 2 $32.40 $64.80
Data Transfer Variable - ~$5.00
EBS Storage 16 GB $0.08/GB $1.28
Total - - ~$102/month

Production Environment (Monthly)

Component Quantity Unit Cost Total
EC2 (t3.small) 4 $15.00 $60.00
Network Load Balancer 1 $16.20 $16.20
NAT Gateway 2 $32.40 $64.80
Data Transfer Variable - ~$10.00
EBS Storage 32 GB $0.08/GB $2.56
Total - - ~$153/month

πŸ’‘ Cost Optimization Tips:

  • Use Spot Instances for dev (save 70%)
  • Schedule scaling (scale down at night)
  • Use single NAT Gateway for dev (save $32)
  • Enable S3 VPC Endpoint (free data transfer)

πŸ”§ Troubleshooting

Issue Solution
NLB not accessible β€’ Check security group allows port 3000
β€’ Verify target health in AWS Console
β€’ Wait 2-3 minutes for NLB to be active
Instances unhealthy β€’ Check user data script logs: /var/log/user-data.log
β€’ Verify Node.js app is running: systemctl status nodejs-app
β€’ Check security group allows NLB traffic on port 3000
Terraform state locked terraform force-unlock <LOCK_ID>
AMI not found Update ami_id in tfvars for your region
NAT Gateway timeout β€’ Check route tables
β€’ Verify NAT GW has Elastic IP
β€’ Check NACL rules
Auto Scaling not working β€’ Check ASG health check settings
β€’ Verify launch template is valid
β€’ Check CloudWatch logs

πŸ§ͺ Testing

Test Load Balancing

NLB_DNS=$(terraform output -raw nlb_dns_name)

# Should show different instance IDs
for i in {1..20}; do
  curl -s http://$NLB_DNS | grep "Instance ID"
  sleep 1
done

Test High Availability

# Terminate instance in AZ-1a
aws ec2 terminate-instances --instance-ids <instance-id-az1a>

# Traffic should continue via AZ-1b
curl http://$NLB_DNS

Test Auto Scaling

# Watch ASG maintain desired count
watch -n 5 'aws autoscaling describe-auto-scaling-groups \
  --auto-scaling-group-names simple-webapp-dev-asg \
  --query "AutoScalingGroups[0].[DesiredCapacity,MinSize,MaxSize]" \
  --output table'

πŸ“š Advanced Topics

πŸ” Why NAT Gateway per AZ?

High Availability Benefits

Single NAT Gateway (Not Recommended):

AZ-1a: EC2 β†’ NAT GW (AZ-1a) β†’ Internet βœ…
AZ-1b: EC2 β†’ NAT GW (AZ-1a) β†’ Internet ❌ (Cross-AZ traffic)
  • ❌ Single point of failure
  • ❌ Cross-AZ data transfer costs
  • ❌ If AZ-1a fails, AZ-1b loses internet

NAT Gateway per AZ (Recommended):

AZ-1a: EC2 β†’ NAT GW (AZ-1a) β†’ Internet βœ…
AZ-1b: EC2 β†’ NAT GW (AZ-1b) β†’ Internet βœ…
  • βœ… No single point of failure
  • βœ… No cross-AZ data transfer
  • βœ… Full redundancy

Cost vs Reliability:

  • Extra cost: ~$32/month per NAT GW
  • Benefit: 99.99% availability
  • Production: Always use NAT GW per AZ
  • Development: Can use single NAT GW to save cost
πŸ—οΈ Infrastructure Components Explained

VPC & Networking

  • VPC: Isolated network (10.0.0.0/16)
  • Public Subnets: Host NLB and NAT Gateways
  • Private Subnets: Host EC2 instances (no direct internet)
  • Internet Gateway: Public internet access
  • NAT Gateways: Outbound internet for private instances
  • Route Tables: Traffic routing rules

Compute

  • Launch Template: EC2 configuration blueprint
  • Auto Scaling Group: Maintains instance count
  • User Data: Bootstrap script (installs Apache)

Load Balancing

  • Network Load Balancer: Layer 4 (TCP) load balancing
  • Target Group: Health checks and routing
  • Listener: Port 80 traffic forwarding

Security

  • Security Group: Firewall rules
    • Inbound: Port 3000 from anywhere
    • Outbound: All traffic allowed
πŸ”’ Security Best Practices

Implemented

  • βœ… EC2 instances in private subnets
  • βœ… No public IPs on instances
  • βœ… Security groups with minimal permissions (port 3000 only)
  • βœ… NAT Gateway for outbound only
  • βœ… Multi-AZ for high availability
  • βœ… Non-privileged port (3000) for Node.js
  • βœ… Systemd service for auto-restart

Recommended Additions

  • πŸ”„ Enable VPC Flow Logs
  • πŸ”„ Add WAF to NLB
  • πŸ”„ Implement HTTPS (ALB + ACM)
  • πŸ”„ Use AWS Secrets Manager
  • πŸ”„ Enable CloudTrail
  • πŸ”„ Add CloudWatch Alarms
πŸ“Š Monitoring & Alerts

CloudWatch Metrics to Monitor

  • EC2 CPU Utilization
  • Network In/Out
  • Target Health Count
  • NLB Active Connections
  • NAT Gateway Bytes Processed

Recommended Alarms

# High CPU alarm
aws cloudwatch put-metric-alarm \
  --alarm-name high-cpu \
  --metric-name CPUUtilization \
  --threshold 80 \
  --comparison-operator GreaterThanThreshold

# Unhealthy targets
aws cloudwatch put-metric-alarm \
  --alarm-name unhealthy-targets \
  --metric-name UnHealthyHostCount \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold

πŸŽ“ Learning Outcomes

After completing this project, you'll understand:

βœ… Multi-AZ Architecture - High availability design patterns
βœ… Auto Scaling - Automatic capacity management
βœ… Load Balancing - Traffic distribution strategies
βœ… VPC Networking - Public/private subnet design
βœ… NAT Gateway - Outbound internet for private resources
βœ… Security Groups - Network-level security
βœ… Infrastructure as Code - Terraform best practices
βœ… Cost Optimization - AWS pricing and optimization


🧹 Cleanup

Remove All Resources

cd infra-iac
terraform destroy -var-file="../envs/dev.tfvars"

Verify Cleanup

# Check no resources remain
aws ec2 describe-instances --filters "Name=tag:Project,Values=simple-webapp"
aws elbv2 describe-load-balancers --names simple-webapp-dev-nlb
aws ec2 describe-nat-gateways --filter "Name=tag:Project,Values=simple-webapp"

πŸ“ License

MIT License - Free to use for learning and production!


πŸ‘¨πŸ’» Author

Ahmed Belal - DevOps & Cloud Engineer

πŸ”— GitHub β€’ LinkedIn β€’ Email


🌟 Support

⭐ Star this repo if you found it helpful!

πŸ› Found a bug? Open an issue
πŸ’‘ Have suggestions? Create a pull request
πŸ“§ Questions? Reach out via email


Built with ❀️ by Ahmed Belal | ABCloudOps

About

Production-ready AWS infrastructure with EC2 Auto Scaling, Network Load Balancer, and multi-AZ deployment. Fully automated with Terraform.

Topics

Resources

Stars

Watchers

Forks