Building a Secure and Scalable Three-Tier Architecture on AWS using CloudFormation
Guide for building secure, scalable three-tier architecture on AWS using CloudFormation, detailing components, benefits, and implementation steps for enterprise applications.
Building a Secure and Scalable Three-Tier Architecture on AWS
In today's cloud-first world, designing resilient, secure, and scalable architectures is essential for businesses of all sizes. In this article, we'll explore a production-ready three-tier high availability architecture implemented with AWS CloudFormation, breaking down its components, benefits, and implementation details.
What is a Three-Tier Architecture?
A three-tier architecture separates an application into three logical and physical computing tiers:
- Presentation Tier (Web): Handles user interface and client requests
- Application Tier (App): Contains the business logic and processing
- Data Tier (Database): Manages data storage and access
This separation of concerns improves security, scalability, and maintainability by isolating each component.
Architecture Overview:
Our implementation uses AWS CloudFormation to provision a complete three-tier architecture with high availability features:
The architecture includes:
- Network Layer: VPC with public, private, and isolated subnets across two availability zones
- Security Layer: WAF, security groups, and network ACLs
- Web Tier: Application Load Balancer in public subnets
- Application Tier: Auto Scaling EC2 instances in private subnets
- Data Tier: Aurora MySQL cluster in isolated subnets
- Management: Bastion host for secure administrative access
**Key Components
Network Infrastructure**
The foundation of our architecture is a well-designed VPC with multiple subnet types:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCIDR
EnableDnsHostnames: true
EnableDnsSupport: true
We create three types of subnets across two availability zones:
- Public Subnets (10.0.1.0/24, 10.0.2.0/24): Host internet-facing resources
- Private Subnets (10.0.10.0/24, 10.0.11.0/24): Host application servers
- Isolated Subnets (10.0.20.0/24, 10.0.21.0/24): Host database resources
This design implements the principle of least privilege at the network level, with each tier having only the connectivity it requires.
Security Controls
Multiple security layers protect our infrastructure:
- AWS WAF: Protects against common web exploits and attacks
WebACL:
Type: AWS::WAFv2::WebACL
Properties:
Name: !Sub ${EnvironmentName}-WebACL
Scope: REGIONAL
Rules:
- Name: AWSManagedRulesCommonRuleSet
Priority: 1
- Security Groups: Implement fine-grained access control
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
- Network Isolation: Each tier is isolated in its own subnet type
High Availability Features
The architecture implements multiple high availability features:
1.*** Multi-AZ Deployment:*** All components are deployed across two availability zones 2. Auto Scaling: Application tier automatically adjusts capacity
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
VPCZoneIdentifier:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
MinSize: '2'
MaxSize: '6'
DesiredCapacity: '2'
- Load Balancing: Distributes traffic and provides health checks
- Database Redundancy: Aurora MySQL with primary and replica instances
Application Tier
The application tier uses EC2 instances in an Auto Scaling group:
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
ImageId: !Ref LatestAmiId
InstanceType: !Ref InstanceType
SecurityGroupIds:
- !Ref EC2SecurityGroup
The instances are bootstrapped with a simple web server for demonstration purposes, but in a real-world scenario, you would deploy your application code here.
Data Tier
The data tier uses Amazon Aurora MySQL, a fully managed database service:
DBCluster:
Type: AWS::RDS::DBCluster
Properties:
Engine: aurora-mysql
DatabaseName: !Ref DBName
DBSubnetGroupName: !Ref DBSubnetGroup
VpcSecurityGroupIds:
- !Ref DBSecurityGroup
Aurora provides built-in high availability with automatic failover capabilities.
Setting Up the Architecture
Prerequisites
Before deploying this architecture, you'll need:
- An AWS account with appropriate permissions
- AWS CLI installed and configured or console access
- An EC2 key pair for SSH access
- A secure password for the database
Deployment Steps
- Save the CloudFormation template to a file named three-tier-ha.yaml
- Deploy the stack using AWS CLI:
aws cloudformation create-stack \ --stack-name three-tier-ha \ --template-body file://three-tier-ha.yaml \ --parameters \ ParameterKey=KeyPairName,ParameterValue=your-key-pair \ ParameterKey=DBPassword,ParameterValue=your-secure-password \ --capabilities CAPABILITY_IAM
- Monitor the deployment in the AWS CloudFormation console
- Access the outputs once deployment is complete:
aws cloudformation describe-stacks \ --stack-name three-tier-ha \ --query "Stacks[0].Outputs"
- Connect to the bastion host to access resources in private subnets:
ssh -i your-key.pem ec2-user@<BastionPublicIP>
Benefits of This Architecture
Security
- **Defense in Depth: **Multiple security layers protect your application
- Network Isolation: Each tier has its own security controls
- Least Privilege: Components only have the access they need
- ** WAF Protection:** Guards against common web vulnerabilities
Scalability
- Horizontal Scaling: Auto Scaling groups adjust to demand
- Vertical Scaling: Instance types can be changed as needed
- **Database Scaling: **Aurora scales compute and storage independently
Reliability
- **Multi-AZ Deployment: **Resilient to availability zone failures
- **Auto Recovery: **Failed instances are automatically replaced
- Load Distribution: Traffic is balanced across healthy instances
- **Database Redundancy: **Automatic failover for database instances
Maintainability
- Infrastructure as Code: Entire architecture defined in CloudFormation
- Parameterized Deployment: Easily customize for different environments
- Separation of Concerns: Each tier can be updated independently
- Bastion Host: Secure administrative access
Cost Optimization
While this architecture provides enterprise-grade features, you can optimize costs by:
- Right-sizing instances: Start with smaller instance types and scale as needed
- Auto Scaling policies: Scale down during low-traffic periods
- Reserved Instances: Purchase reserved instances for predictable workloads
- Aurora Serverless: Consider Aurora Serverless for variable workloads
Enhancements and Next Steps This architecture provides a solid foundation, but consider these enhancements for production use:
- **HTTPS Support: **Add SSL/TLS certificates and configure HTTPS listeners
- Monitoring: Implement CloudWatch dashboards and alarms
- Backup Strategy: Configure automated backups and test restoration
- CI/CD Pipeline: Automate application deployment *** Session Management:** Add ElastiCache for session storage
- Content Delivery: Integrate CloudFront for static content
Conclusion
The three-tier high availability architecture presented in this article provides a robust foundation for deploying applications on AWS. By leveraging CloudFormation, you can consistently deploy this architecture across multiple environments while maintaining security, scalability, and reliability. This architecture follows AWS best practices and can be adapted to a wide range of applications, from simple websites to complex enterprise systems. The separation of tiers, combined with high availability features, ensures your application can handle failures gracefully while providing a consistent experience to your users. Whether you're building a new application or migrating an existing one to AWS, this architecture provides a proven pattern for success in the cloud.
Full file: three-tier-ha.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 3-Tier HA Architecture with WAF, ALB, EC2 Auto Scaling, and Aurora MySQL
Parameters:
LatestAmiId:
Type: String
Default: ami-05ffe3c48a9991133
Description: AMI ID for the EC2 instances
EnvironmentName:
Type: String
Default: ThreeTierHA
VpcCIDR:
Type: String
Default: 10.0.0.0/16
KeyPairName:
Type: AWS::EC2::KeyPair::KeyName
Description: Name of an existing EC2 KeyPair
InstanceType:
Type: String
Default: t3.xlarge
AllowedValues:
- t3.large
- t3.xlarge
- t3.2xlarge
DBInstanceType:
Type: String
Default: db.r5.large
DBName:
Type: String
Default: appdb
DBUsername:
Type: String
Default: admin
NoEcho: true
DBPassword:
Type: String
NoEcho: true
MinLength: '8'
Mappings:
SubnetConfig:
VPC:
CIDR: 10.0.0.0/16
Public1:
CIDR: 10.0.1.0/24
Public2:
CIDR: 10.0.2.0/24
Private1:
CIDR: 10.0.10.0/24
Private2:
CIDR: 10.0.11.0/24
Isolated1:
CIDR: 10.0.20.0/24
Isolated2:
CIDR: 10.0.21.0/24
Resources:
# VPC and Network Configuration
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCIDR
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-VPC
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-IGW
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
# Public Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs ''
CidrBlock: !FindInMap
- SubnetConfig
- Public1
- CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PublicSubnet1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 1
- !GetAZs ''
CidrBlock: !FindInMap
- SubnetConfig
- Public2
- CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PublicSubnet2
# Private Subnets
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs ''
CidrBlock: !FindInMap
- SubnetConfig
- Private1
- CIDR
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PrivateSubnet1
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 1
- !GetAZs ''
CidrBlock: !FindInMap
- SubnetConfig
- Private2
- CIDR
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PrivateSubnet2
# Isolated Subnets for Database
IsolatedSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 0
- !GetAZs ''
CidrBlock: !FindInMap
- SubnetConfig
- Isolated1
- CIDR
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-IsolatedSubnet1
IsolatedSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select
- 1
- !GetAZs ''
CidrBlock: !FindInMap
- SubnetConfig
- Isolated2
- CIDR
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-IsolatedSubnet2
# NAT Gateways
NATGateway1:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NATGateway1EIP.AllocationId
SubnetId: !Ref PublicSubnet1
NATGateway2:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NATGateway2EIP.AllocationId
SubnetId: !Ref PublicSubnet2
NATGateway1EIP:
Type: AWS::EC2::EIP
DependsOn: InternetGatewayAttachment
Properties:
Domain: vpc
NATGateway2EIP:
Type: AWS::EC2::EIP
DependsOn: InternetGatewayAttachment
Properties:
Domain: vpc
# Route Tables and Associations
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PublicRoutes
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet1
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet2
PrivateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PrivateRoutes1
DefaultPrivateRoute1:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable1
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway1
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable1
SubnetId: !Ref PrivateSubnet1
PrivateRouteTable2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-PrivateRoutes2
DefaultPrivateRoute2:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable2
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway2
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable2
SubnetId: !Ref PrivateSubnet2
IsolatedRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-IsolatedRoutes
IsolatedSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref IsolatedRouteTable
SubnetId: !Ref IsolatedSubnet1
IsolatedSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref IsolatedRouteTable
SubnetId: !Ref IsolatedSubnet2
# Security Groups
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ALB Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
BastionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Bastion Host Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EC2 Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref BastionSecurityGroup
DBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Aurora Security Group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref EC2SecurityGroup
# Bastion Host
BastionHost:
Type: AWS::EC2::Instance
Properties:
InstanceType: t3.micro
ImageId: !Ref LatestAmiId
KeyName: !Ref KeyPairName
SubnetId: !Ref PublicSubnet1
SecurityGroupIds:
- !Ref BastionSecurityGroup
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-Bastion
# WAF Web ACL
WebACL:
Type: AWS::WAFv2::WebACL
Properties:
Name: !Sub ${EnvironmentName}-WebACL
Scope: REGIONAL
DefaultAction:
Allow: {}
Rules:
- Name: AWSManagedRulesCommonRuleSet
Priority: 1
OverrideAction:
None: {}
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesCommonRuleSet
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: AWSManagedRulesCommonRuleSetMetric
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: !Sub ${EnvironmentName}-WebACLMetric
# Application Load Balancer
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
SecurityGroups:
- !Ref ALBSecurityGroup
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
Type: application
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ALBTargetGroup
ALBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckPath: /
HealthCheckIntervalSeconds: 30
Port: 80
Protocol: HTTP
VpcId: !Ref VPC
# Launch Template
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
ImageId: !Ref LatestAmiId
InstanceType: !Ref InstanceType
KeyName: !Ref KeyPairName
SecurityGroupIds:
- !Ref EC2SecurityGroup
TagSpecifications:
- ResourceType: instance
Tags:
- Key: Name
Value: !Sub ${EnvironmentName}-Int
UserData: !Base64
Fn::Sub: |
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
# Get IMDSv2 token
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
# Use token to get metadata
INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/instance-id)
AVAILABILITY_ZONE=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/placement/availability-zone)
PRIVATE_IP=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/local-ipv4)
cat <<EOF > /var/www/html/index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to My Web Server</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
background-color: #f0f0f0;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to ${EnvironmentName}</h1>
<p>This server is running on Amazon Linux 2</p>
<p>Instance ID: $INSTANCE_ID</p>
<p>Availability Zone: $AVAILABILITY_ZONE</p>
<p>Private IP: $PRIVATE_IP</p>
<p>Timestamp: $(date)</p>
</div>
</body>
</html>
EOF
# Set proper permissions
chmod 644 /var/www/html/index.html
# Auto Scaling Group
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
VPCZoneIdentifier:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
MinSize: '2'
MaxSize: '6'
DesiredCapacity: '2'
CapacityRebalance: true
NewInstancesProtectedFromScaleIn: false
TargetGroupARNs:
- !Ref ALBTargetGroup
HealthCheckType: ELB
HealthCheckGracePeriod: 300
# Aurora DB Cluster
DBCluster:
Type: AWS::RDS::DBCluster
Properties:
Engine: aurora-mysql
DatabaseName: !Ref DBName
MasterUsername: !Ref DBUsername
MasterUserPassword: !Ref DBPassword
DBSubnetGroupName: !Ref DBSubnetGroup
VpcSecurityGroupIds:
- !Ref DBSecurityGroup
AvailabilityZones:
- !Select
- 0
- !GetAZs ''
- !Select
- 1
- !GetAZs ''
DBInstance1:
Type: AWS::RDS::DBInstance
Properties:
Engine: aurora-mysql
DBClusterIdentifier: !Ref DBCluster
DBInstanceClass: !Ref DBInstanceType
DBSubnetGroupName: !Ref DBSubnetGroup
DBInstance2:
Type: AWS::RDS::DBInstance
Properties:
Engine: aurora-mysql
DBClusterIdentifier: !Ref DBCluster
DBInstanceClass: !Ref DBInstanceType
DBSubnetGroupName: !Ref DBSubnetGroup
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for Aurora DB
SubnetIds:
- !Ref IsolatedSubnet1
- !Ref IsolatedSubnet2
Outputs:
VPC:
Description: VPC ID
Value: !Ref VPC
ALBDNSName:
Description: ALB DNS Name
Value: !GetAtt ApplicationLoadBalancer.DNSName
DBEndpoint:
Description: Aurora Cluster Endpoint
Value: !GetAtt DBCluster.Endpoint.Address
WebACLArn:
Description: WAF Web ACL ARN
Value: !GetAtt WebACL.Arn
BastionPublicIP:
Description: Bastion Host Public IP
Value: !GetAtt BastionHost.PublicIp
Relevant content
AWS OFFICIALUpdated 3 years ago