Deploy a High-Availability Web App using CloudFormation.
Salaudeen O. Abdulrasaq
Posted on July 8, 2023
INTRODUCTION
In this post, I am thrilled to share an exciting project that I had the opportunity to work on last year. Inspired by the comprehensive curriculum of the ALX Udacity Cloud DevOps course, I embarked on a journey that not only expanded my knowledge but also empowered me to apply cutting-edge cloud computing and DevOps practices. Specifically, this project delved into the realm of Infrastructure as Code with AWS CloudFormation, providing me with invaluable hands-on experience in building scalable cloud infrastructure.
If you're interested in exploring the code for the project, you can easily find it on my GitHub repository
STATEMENT OF PROBLEM
Scenario
Your company is creating an Instagram clone called Udagram.
Developers want to deploy a new application to the AWS infrastructure.
You have been tasked with provisioning the required infrastructure and deploying a dummy application, along with the necessary supporting software.
This needs to be automated so that the infrastructure can be discarded as soon as the testing team finishes their tests and gathers their results.
Optional - To add more challenge to the project, once the project is completed, you can try deploying sample website files located in a public S3 Bucket to the Apache Web Server running on an EC2 instance.
Server specs
Launch Configuration was created for application servers in order to deploy four servers, two located in each of your private subnets. The launch configuration was used by an auto-scaling group. Two vCPUs were used with 4GB of RAM. The Operating System used is Ubuntu 18. An Instance size and Machine Image (AMI) that best fits this spec was chosen.
MY SOLUTION:
Architecture Diagram
Below is the content of the parameter files and configuration files for the network infrastructure, s3 buckets, and servers(EC2 instances)
Network Parameters
network.json
[
{
"ParameterKey": "EnvironmentName",
"ParameterValue": "UdacityProject"
},
{
"ParameterKey": "VPCCIDR",
"ParameterValue": "10.0.0.0/16"
},
{
"ParameterKey": "PubSubnet1CIDR",
"ParameterValue": "10.0.1.0/24"
},
{
"ParameterKey": "PubSubnet2CIDR",
"ParameterValue": "10.0.2.0/24"
},
{
"ParameterKey": "PrivSubnet1CIDR",
"ParameterValue": "10.0.3.0/24"
},
{
"ParameterKey": "PrivSubnet2CIDR",
"ParameterValue": "10.0.4.0/24"
}
]
S3 bucket Parameters
s3bucket.json
[{
"ParameterKey": "EnvironmentName",
"ParameterValue": "UdacityProject"
},
{
"ParameterKey": "S3BucketName",
"ParameterValue": "udacityprojects3webserverbucket"
}
]
Server (EC2 Instance) Parameters
servers.json
[
{
"ParameterKey": "EnvironmentName",
"ParameterValue": "UdacityProject"
}
]
Network Configuration
The network.yaml file below creates a network infrastructure with public and private subnets, routing, and internet access. It includes parameters for customizing the environment, VPC CIDR, and subnet CIDR blocks. The code creates resources such as VPC, internet gateway, subnets (public and private), NAT gateways, and route tables. Outputs are defined to export important values like VPC ID, route table IDs, and subnet IDs. This CloudFormation template enables the creation of a network setup suitable for routing internet traffic to both public and private subnets.
network.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Creates the required network infrastructure for public and private routing with internet access
Parameters:
EnvironmentName:
Description: An Environment name that will be prefixed to resources
Type: String
VPCCIDR:
Type: String
PrivSubnet1CIDR:
Type: String
PrivSubnet2CIDR:
Type: String
PubSubnet1CIDR:
Type: String
PubSubnet2CIDR:
Type: String
Resources:
myVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsHostnames: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: "MainVPC"
# Create Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Ref EnvironmentName
# Attached the internet Gateway to myVPC
InternetGatewayAttached:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref myVPC
# Creating Public and Private Subnets in the same availability zone(us-east-1a).
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: "us-east-1a"
CidrBlock: !Ref PubSubnet1CIDR
VpcId:
Ref: myVPC
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Subnet (AZ1)
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: myVPC
CidrBlock: !Ref PrivSubnet1CIDR
MapPublicIpOnLaunch: false
AvailabilityZone: "us-east-1a"
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Private Subnet (AZ1)
# Creating Public and Private Subnets in the same availability zone(us-east-1b).
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: "us-east-1b"
CidrBlock: !Ref PubSubnet2CIDR
VpcId:
Ref: myVPC
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Subnet (AZ2)
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: myVPC
CidrBlock: !Ref PrivSubnet2CIDR
MapPublicIpOnLaunch: false
AvailabilityZone: "us-east-1b"
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Private Subnet (AZ2)
# Elastic IP for the NATGateway in Subnet1
EIP1:
Type: AWS::EC2::EIP
DependsOn: InternetGatewayAttached
Properties:
Domain: myVPC
Tags:
- Key: Name
Value: "Elastic IP for our NATGateway1"
EIP2:
Type: AWS::EC2::EIP
DependsOn: InternetGatewayAttached
Properties:
Domain: myVPC
Tags:
- Key: Name
Value: "Elastic IP for our NATGateway2"
# Creating NAT gateway in publicsubnet1
NAT1:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt:
- EIP1
- AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: "NAT to be used by servers in the private subnet"
NAT2:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt:
- EIP2
- AllocationId
SubnetId: !Ref PublicSubnet2
Tags:
- Key: Name
Value: "NAT to be used by servers in the private subnet"
#_______PUBLIC SUBNET________
# # Route Table for Public subnet
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref myVPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Public Routes
# Create Route for public Subnet 1 & 2
PublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttached
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGateway
# Associate Route Table to Public subnet 1 & 2
AssociatePublicRoute:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicRouteTable
SubnetId: !Ref PublicSubnet1
AssociatePublicRoute:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicRouteTable
SubnetId: !Ref PublicSubnet2
# ______PRIVATE SUBNET 1______
# Route Table for Private subnet1
PrivateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref myVPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Private Routes (AZ1)
# Create Route for Private subnet1
PrivateRoute1:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable1
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId:
Ref: NAT1
# Private Route Table1 Association to Private Subnet1
AssociatePrivateRoute:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable1
SubnetId: !Ref PrivateSubnet1
# _____PRIVATE SUBNET 2_____
# Route Table for Private subnet2
PrivateRouteTable2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref myVPC
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} Private Routes (AZ2)
# Create Route for Private subnet2
PrivateRoute2:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable2
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId:
Ref: NAT2
# Private Route Table2 Association to Private Subnet2
AssociatePrivateRoute:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable2
SubnetId: !Ref PrivateSubnet2
Outputs:
myVPC:
Description: The VPC created for this project
Value: !Ref myVPC
Export:
Name: !Sub ${EnvironmentName}-VPCID
PublicRouteTable:
Description: Public Route Table
Value: !Ref PublicRouteTable
Export:
Name: !Sub ${EnvironmentName}-PUB-RT
PrivateRouteTable1:
Description: Private route Table1
Value: !Ref PrivateRouteTable1
Export:
Name: !Sub ${EnvironmentName}-PRI-RT1
PrivateRouteTable2:
Description: Private route Table2
Value: !Ref PrivateRouteTable2
Export:
Name: !Sub ${EnvironmentName}-PRI-RT2
PublicSubnets:
Description: A list of the public subnets
Value: !Join [ ",", [ !Ref PublicSubnet1, !Ref PublicSubnet2 ]]
Export:
Name: !Sub ${EnvironmentName}-PUB-SUBNETS
PublicSubnet1:
Description: public subnet 1 in "us-east-1a"
Value: !Ref PublicSubnet1
Export:
Name: !Sub ${EnvironmentName}-PUB-SUB1
PublicSubnet2:
Description: public subnet 2 in us-east-1b
Value: !Ref PublicSubnet2
Export:
Name: !Sub ${EnvironmentName}-PUB-SUB2
PrivateSubnets:
Description: A list of the private subnets
Value: !Join [ ",", [ !Ref PrivateSubnet1, !Ref PrivateSubnet2 ]]
Export:
Name: !Sub ${EnvironmentName}-PRIV-SUBNETS
PrivateSubnet1:
Description: private subnet 1 in us-east-1a
Value: !Ref PrivateSubnet1
Export:
Name: !Sub ${EnvironmentName}-PRIV-SUB1
PrivateSubnet2:
Description: private subnet 1 in us-east-1b
Value: !Ref PrivateSubnet2
Export:
Name: !Sub ${EnvironmentName}-PRIV-SUB2
VPCdefaultSecurityGroup:
Description: Returns the default security group of the created VPC
Value: !GetAtt myVPC.DefaultSecurityGroup
Export:
Name: !Sub ${EnvironmentName}-myVPC-SG
S3 Bucket Configuration
The s3bucket.yaml file below creates an S3 bucket for deploying a high-availability web app. The bucket is configured with public read access, an index document, and an error document. A bucket policy allows all actions on the bucket, and an IAM role with AmazonS3FullAccess policy is created to enable EC2 instances to manage the web app. Outputs include the IAM role, website URL, and secure website URL. This CloudFormation template facilitates the setup of an S3 bucket for hosting a high-availability web app with appropriate permissions and URL accessibility.
s3bucket.yaml
Description: >
Create an S3 bucket for deploying a high-availability web-app.
Parameters:
EnvironmentName:
Description: An environment name that will be prefixed to resource names.
Type: String
S3BucketName:
Description: S3 bucket name.
Type: String
Resources:
S3WebServer:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref S3BucketName
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
Tags:
- Key: Name
Value: !Sub ${EnvironmentName} s3webserver bucket
DeletionPolicy: Delete
S3WebAppPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3WebServer
PolicyDocument:
Statement:
- Effect: Allow
Action: s3:*
Resource: !Join ['', ['arn:aws:s3:::', !Ref 'S3WebServer', '/*']]
Principal:
AWS: '*'
WebServerIAMRole:
Type: 'AWS::IAM::Role'
Properties:
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AmazonS3FullAccess'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Service:
- 'ec2.amazonaws.com'
Action:
- 'sts:AssumeRole'
Path: '/'
MyInstanceProfile:
Type: "AWS::IAM::InstanceProfile"
Properties:
Path: "/"
Roles:
-
Ref: "WebServerIAMRole"
Outputs:
WebServerIAMRole:
Description: 'Allow EC2 instances to manage Web App S3'
Value: !Ref MyInstanceProfile
Export:
Name: !Sub ${EnvironmentName}-IAM-NAME
# WebServerIAMRole:
# Description: Iam Instance Profile Arn
# Value: !GetAtt WebServerIAMRole.Arn
# Export:
# Name: !Sub ${EnvironmentName}-IAM-ARN
WebsiteURL:
Value: !GetAtt [S3WebServer, WebsiteURL]
Description: URL for website hosted on S3
WebsiteSecureURL:
Value: !Join ['', ['https://', !GetAtt [S3WebServer, DomainName]]]
Description: Secure URL for website hosted on S3
Server (EC2 instance) Configuration
The servers.yaml file below creates a network infrastructure with servers for hosting a high-availability web app.
The code defines several resources, including security groups, launch configuration, auto scaling group, load balancer, listener, target group, scaling policies, and outputs. These resources enable the setup of a load-balanced environment for the web app.
The code provides an output called LoadBalancerEndpoint, which represents the endpoint for reaching the load balancer.
Overall, this CloudFormation template facilitates the creation of a load-balanced infrastructure with auto scaling capabilities for hosting web servers.
servers.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Creates the required servers in the network infrastructure defined by "network.yml"
Parameters:
EnvironmentName:
Description: An Environment name that will be prefixed to resources
Type: String
InstanceType:
Description: Amazon EC2 instance type for the instances
Type: String
AllowedValues:
- t2.micro
- t3.micro
- t3.small
- t3.medium
Default: t2.micro
Mappings:
WebServerRegion:
us-east-1:
HVM64: ami-052efd3df9dad4825
Resources:
#Loadbalancer security group.
LBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow http request to loadbalancer
VpcId:
Fn::ImportValue:
!Sub "${EnvironmentName}-VPCID"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
# Web Server security group.
InstanceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow http to Webserver
VpcId:
Fn::ImportValue:
!Sub "${EnvironmentName}-VPCID"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 0
ToPort: 65535
CidrIp: 0.0.0.0/0
WebServerLaunchConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
IamInstanceProfile:
Fn::ImportValue: !Sub '${EnvironmentName}-IAM-NAME'
UserData:
# wget -P /var/www/html https://project2udacity.s3-us-west-2.amazonaws.com/index.html
Fn::Base64: !Sub |
#!/bin/bash
apt-get update -y
apt-get install unzip awscli -y
apt-get install apache2 -y
systemctl start apache2.service
sudo rm /var/www/html/index.html
sudo aws s3 cp s3://udacityprojects3webserverbucket/udagram.zip /var/www/html
sudo unzip /var/www/html/udagram.zip -d /var/www/html
sudo rm /var/www/html/udagram.zip
systemctl restart apache2.service
ImageId: !FindInMap [WebServerRegion, !Ref 'AWS::Region', HVM64]
SecurityGroups:
- !Ref InstanceSecurityGroup
InstanceType:
!Ref InstanceType
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
VolumeSize: '10'
VolumeType: 'gp2'
WebServerASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
VPCZoneIdentifier:
- Fn::ImportValue: !Sub "${EnvironmentName}-PRIV-SUB1"
- Fn::ImportValue: !Sub "${EnvironmentName}-PRIV-SUB2"
LaunchConfigurationName: !Ref WebServerLaunchConfig
MaxSize: '4'
MinSize: '4'
DesiredCapacity: '4'
TargetGroupARNs:
- Ref: WebServerTargetGroup
WebServerloadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Subnets:
- Fn::ImportValue: !Sub "${EnvironmentName}-PUB-SUB1"
- Fn::ImportValue: !Sub "${EnvironmentName}-PUB-SUB2"
SecurityGroups:
- Ref: LBSecurityGroup
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn:
Ref: WebServerTargetGroup
LoadBalancerArn:
Ref: WebServerloadBalancer
Port: '80'
Protocol: HTTP
ALBListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- Type: forward
TargetGroupArn:
Ref: WebServerTargetGroup
Conditions:
- Field: path-pattern
Values: [/]
ListenerArn:
Ref: Listener
Priority: 1
WebServerTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 5
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 4
HealthyThresholdCount: 3
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 3
VpcId:
Fn::ImportValue:
Fn::Sub: "${EnvironmentName}-VPCID"
# Scaling Policy Description (Up)
WebServerScaleDown:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AdjustmentType: ChangeInCapacity
AutoScalingGroupName: !Ref WebServerASG
Cooldown: 300
ScalingAdjustment: 1
# Scaling Policy Description (Down)
WebServerScaleDown:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AdjustmentType: ChangeInCapacity
AutoScalingGroupName: !Ref WebServerASG
Cooldown: 300
ScalingAdjustment: -1
Outputs:
LoadBanlancerEndpoint:
Description: this endpoint is used to reach the loadbalancer.
Value: !Join [ "", [ 'http://', !GetAtt WebServerloadBalancer.DNSName ]]
Export:
Name: !Sub ${EnvironmentName}-LBENDPOINT
Script Usage
This Repository contains some scripts that will be used to create the CloudFormation stack.
Usage:
Create:
./create.sh (stackName) (script.yml) (parameters.json) (profile)
Example:
./create.sh Udagram infrastructure/network.yaml parameters/network.json udacity_user
Below is the content of the create.sh script
aws cloudformation create-stack --stack-name $1 --template-body file://$2 --parameters file://$3 --capabilities "CAPABILITY_IAM" "CAPABILITY_NAMED_IAM" --region=us-east-1 --profile=$4
Update:
./update.sh (stackName) (script.yml) (parameters.json) (profile)
Example:
./update.sh Udagram infrastructure/network.yaml parameters/network.json udacity_user
Below is the content of the update.sh script
aws cloudformation update-stack --stack-name $1 --template-body file://$2 --parameters file://$3 --capabilities "CAPABILITY_IAM" "CAPABILITY_NAMED_IAM" --region=us-east-1 --profile=$4
Posted on July 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.