DEV Community

Cover image for How to deploy a high available and auto scale Apache Web Server on EC2 instances using AWS CloudFormation
lucianobm
lucianobm

Posted on

2

How to deploy a high available and auto scale Apache Web Server on EC2 instances using AWS CloudFormation

Image description

Introduction

This article will guide you deploy a high available and auto scale Apache Web Server on EC2 instances using AWS CloudFormation on both the AWS Console and AWS CLI.

To achieve this, we will create the following resources:

  • VPC with custom CIDR block

  • 3 Public Subnets in 3 different availability zones

  • Internet Gateway

  • Security Group

  • LaunchTemplate

  • Auto Scaling Group

  • Application Load Balancer

  • Scaling UP and DOWN policy


Let the fun begin!

According to AWS CloudFormation best practices documentation, it is recommended to break the templates into smaller manageable templates. So I splitted the deploy on 2 different template files vpc.yaml and asg_alb.yaml listed below.

Let’s go through each item of the template:

  • Description is a text string to describe the purpose of the template.

  • Parameters are used to pass custom values at runtime. It is how we can make templates reusable to customize our stacks when we create them.

On the example below (vpc.yaml), we are including the name of the environment (that will be used later to tag the resources), the IPv4 CIDR of the VPC and subnets.

    Parameters:
     EnvName:
       Description: Name that will be used on resources
       Type: String
       Default: LUIT

     VPCCIDR:
       Description: Please enter the IPv4 CIDR for this VPC
       Type: String
       Default: 10.10.0.0/16

     PublicSubnetACIDR:
       Description: Please enter the IPv4 CIDR for this Public Subnet A
       Type: String
       Default: 10.10.1.0/24
     PublicSubnetBCIDR:
       Description: Please enter the IPv4 CIDR for this Public Subnet B
       Type: String
       Default: 10.10.2.0/24
     PublicSubnetCCIDR:
       Description: Please enter the IPv4 CIDR for this Public Subnet C
       Type: String
       Default: 10.10.3.0/24
Enter fullscreen mode Exit fullscreen mode

  • Resources are the only required object of a template. We are including the VPC with the “DnsSupport” and “DnsHostnames” options enabled, an Internet Gateway that will be attached to the new VPC created, the 3 public subnets and the default route to 0.0.0.0/0 using the Internet Gateway created.
    Resources:
     VPC:
       Type: AWS::EC2::VPC
       Properties:
         CidrBlock: !Ref VPCCIDR
         EnableDnsSupport: true
         EnableDnsHostnames: true
         Tags:
          - Key: Name
            Value: !Ref EnvName

     InternetGateway:
       Type: AWS::EC2::InternetGateway
       Properties:
         Tags:
          - Key: Name
            Value: !Ref EnvName

     InternetGatewayAttachment:
       Type: AWS::EC2::VPCGatewayAttachment
       Properties:
         InternetGatewayId: !Ref InternetGateway
         VpcId: !Ref VPC

     PublicSubnetA:
       Type: AWS::EC2::Subnet
       Properties:
         VpcId: !Ref VPC
         CidrBlock: !Ref PublicSubnetACIDR
         MapPublicIpOnLaunch: true
         AvailabilityZone: "us-east-1a"
         #AvailabilityZone: !Select [ 0, !GetAZs '' ]
         Tags:
         - Key: Name
           Value: !Sub ${EnvName} Public Subnet A
     PublicSubnetB:
       Type: AWS::EC2::Subnet
       Properties:
         VpcId: !Ref VPC
         CidrBlock: !Ref PublicSubnetBCIDR
         MapPublicIpOnLaunch: true
         AvailabilityZone: "us-east-1b"
         #AvailabilityZone: !Select [ 1, !GetAZs '' ]
         Tags:
         - Key: Name
           Value: !Sub ${EnvName} Public Subnet B
     PublicSubnetC:
       Type: AWS::EC2::Subnet
       Properties:
         VpcId: !Ref VPC
         CidrBlock: !Ref PublicSubnetCCIDR
         MapPublicIpOnLaunch: true
         AvailabilityZone: "us-east-1c"
         #AvailabilityZone: !Select [ 2, !GetAZs '' ]
         Tags:
         - Key: Name
           Value: !Sub ${EnvName} Public Subnet C

     PublicRouteTable:
       Type: AWS::EC2::RouteTable
       Properties:
         Tags:
          - Key: Name
            Value: !Sub ${EnvName} Public Route Table
         VpcId: !Ref VPC

     DefaultPublicRoute:
       Type: AWS::EC2::Route
       DependsOn: InternetGatewayAttachment
       Properties:
         DestinationCidrBlock: 0.0.0.0/0
         GatewayId: !Ref InternetGateway
         RouteTableId: !Ref PublicRouteTable

     PublicSubnetARouteTableAssociation:
      Type: AWS::EC2::SubnetRouteTableAssociation
      Properties:
        RouteTableId: !Ref PublicRouteTable
        SubnetId: !Ref PublicSubnetA
     PublicSubnetBRouteTableAssociation:
      Type: AWS::EC2::SubnetRouteTableAssociation
      Properties:
        RouteTableId: !Ref PublicRouteTable
        SubnetId: !Ref PublicSubnetB
     PublicSubnetCRouteTableAssociation:
      Type: AWS::EC2::SubnetRouteTableAssociation
      Properties:
        RouteTableId: !Ref PublicRouteTable
        SubnetId: !Ref PublicSubnetC
Enter fullscreen mode Exit fullscreen mode

Lastly, we are including the Outputs section. It is optional but important to print the information of some resources that we might need to know or to use on any other stack.
On the example below, we are printing and exporting the ID of created VPC and subnets, and the CIDR of public subnets created.

    Outputs:
      VPC:
        Description: ID of created VPC
        Value: !Ref VPC
        Export:
          Name: VPC
      PublicSubnets:
        Description: CIDR of public subnets created
        Value: !Join [ ",", [ !Ref PublicSubnetACIDR, !Ref PublicSubnetBCIDR, !Ref PublicSubnetCCIDR ] ]
      PublicSubnetA:
        Description: ID of created public subnet A
        Value: !Ref PublicSubnetA
        Export:
          Name: PublicSubnetA
      PublicSubnetB:
        Description: ID of created public subnet B
        Value: !Ref PublicSubnetB
        Export:
          Name: PublicSubnetB
      PublicSubnetC:
        Description: ID of created public subnet C
        Value: !Ref PublicSubnetC
        Export:
          Name: PublicSubnetC
Enter fullscreen mode Exit fullscreen mode

Below is the full vpc.yaml template that we will use to create the stack.

AWSTemplateFormatVersion: '2010-09-09'
Description: This template deploys a VPC with 3 public subnets on 3 different availability zones and an Internet Gateway, with a default route on the public subnets.
Parameters:
EnvName:
Description: Name that will be used on resources
Type: String
Default: LUIT
VPCCIDR:
Description: Please enter the IPv4 CIDR for this VPC
Type: String
Default: 10.10.0.0/16
PublicSubnetACIDR:
Description: Please enter the IPv4 CIDR for this Public Subnet A
Type: String
Default: 10.10.1.0/24
PublicSubnetBCIDR:
Description: Please enter the IPv4 CIDR for this Public Subnet B
Type: String
Default: 10.10.2.0/24
PublicSubnetCCIDR:
Description: Please enter the IPv4 CIDR for this Public Subnet C
Type: String
Default: 10.10.3.0/24
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Ref EnvName
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Ref EnvName
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PublicSubnetACIDR
MapPublicIpOnLaunch: true
AvailabilityZone: "us-east-1a"
#AvailabilityZone: !Select [ 0, !GetAZs '' ]
Tags:
- Key: Name
Value: !Sub ${EnvName} Public Subnet A
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PublicSubnetBCIDR
MapPublicIpOnLaunch: true
AvailabilityZone: "us-east-1b"
#AvailabilityZone: !Select [ 1, !GetAZs '' ]
Tags:
- Key: Name
Value: !Sub ${EnvName} Public Subnet B
PublicSubnetC:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PublicSubnetCCIDR
MapPublicIpOnLaunch: true
AvailabilityZone: "us-east-1c"
#AvailabilityZone: !Select [ 2, !GetAZs '' ]
Tags:
- Key: Name
Value: !Sub ${EnvName} Public Subnet C
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: !Sub ${EnvName} Public Route Table
VpcId: !Ref VPC
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableId: !Ref PublicRouteTable
PublicSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnetA
PublicSubnetBRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnetB
PublicSubnetCRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnetC
Outputs:
VPC:
Description: ID of created VPC
Value: !Ref VPC
Export:
Name: VPC
PublicSubnets:
Description: CIDR of public subnets created
Value: !Join [ ",", [ !Ref PublicSubnetACIDR, !Ref PublicSubnetBCIDR, !Ref PublicSubnetCCIDR ] ]
PublicSubnetA:
Description: ID of created public subnet A
Value: !Ref PublicSubnetA
Export:
Name: PublicSubnetA
PublicSubnetB:
Description: ID of created public subnet B
Value: !Ref PublicSubnetB
Export:
Name: PublicSubnetB
PublicSubnetC:
Description: ID of created public subnet C
Value: !Ref PublicSubnetC
Export:
Name: PublicSubnetC
view raw vpc.yaml hosted with ❤ by GitHub

Now let’s go through each item of the asg_alb.yaml template:

We are including the following resources:

  • Security Group to allow inbound HTTP and SSH traffic from the Internet;

  • Launch Template with an Amazon Linux AMI, t2.micro instance type, my key pair “luciano” previous created, the security group created above and the UserData to install Apache Web Server;

  • Application Load Balancer, Target Group and Listener;

  • Auto Scaling Group with desired capacity of 2, minimum of 2 and maximum of 5.

  • Scale up and down policies with alarms to scale in case of the utilization of CPU goes above or beyond 50% for 10 minutes.

Also we are including an output section to print the DNS name of the Application Load Balancer.

Below is the full asg_alb.yaml template that we will use to create the stack.

AWSTemplateFormatVersion: '2010-09-09'
Description: This template deploys a Security Group that allows inbound traffic on SSH and HTTP, an Auto Scaling Group with a Launch Template that installs Apache on Amazon Linux and scaling up and down policies, and an Application Load Balancer.
Resources:
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow inbound HTTP and SSH traffic
GroupName: "String"
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
Tags:
- Key: Name
Value: WebServer
VpcId: !ImportValue VPC
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
ImageId: ami-0b5eea76982371e91
InstanceType: t2.micro
KeyName: luciano
Monitoring:
Enabled: true
SecurityGroupIds:
- !Ref SecurityGroup
UserData:
Fn::Base64: !Sub |
#! /bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
EC2AZ=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone)
echo '<center><h1>This Amazon EC2 instance is located in Availability Zone: AZID </h1></center>' > /var/www/html/index.txt
sed "s/AZID/$EC2AZ/" /var/www/html/index.txt > /var/www/html/index.html
LaunchTemplateName: LUIT-WebServer
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: ipv4
Name: LUIT
Scheme: internet-facing
SecurityGroups:
- !Ref SecurityGroup
Subnets:
- !ImportValue "PublicSubnetA"
- !ImportValue "PublicSubnetB"
- !ImportValue "PublicSubnetC"
Type: application
ApplicationLoadBalancerTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: LUIT
Port: 80
Protocol: HTTP
TargetType: instance
VpcId: !ImportValue VPC
ApplicationLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ApplicationLoadBalancerTargetGroup
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: LUIT-ASG
AvailabilityZones:
- us-east-1a
- us-east-1b
- us-east-1c
DesiredCapacity: 2
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
MaxSize: 5
MinSize: 2
VPCZoneIdentifier:
- !ImportValue 'PublicSubnetA'
- !ImportValue 'PublicSubnetB'
- !ImportValue 'PublicSubnetC'
TargetGroupARNs:
- !Ref ApplicationLoadBalancerTargetGroup
ScalingUPPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AdjustmentType: ChangeInCapacity
AutoScalingGroupName: !Ref AutoScalingGroup
Cooldown: 60
ScalingAdjustment: 1
ScalingDOWNPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AdjustmentType: ChangeInCapacity
AutoScalingGroupName: !Ref AutoScalingGroup
Cooldown: 60
ScalingAdjustment: -1
CPUAlarmHigh:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmActions:
- !Ref ScalingUPPolicy
AlarmDescription: Scale up if CPU > 50% for 10 minutes
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: AutoScalingGroupName
Value: !Ref AutoScalingGroup
MetricName: CPUUtilization
Namespace: AWS/EC2
Period: 300
EvaluationPeriods: 2
Statistic: Average
Threshold: 50
CPUAlarmDown:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmActions:
- !Ref ScalingDOWNPolicy
AlarmDescription: Scale down if CPU < 50% for 10 minutes
ComparisonOperator: LessThanThreshold
Dimensions:
- Name: AutoScalingGroupName
Value: !Ref AutoScalingGroup
MetricName: CPUUtilization
Namespace: AWS/EC2
Period: 300
EvaluationPeriods: 2
Statistic: Average
Threshold: 50
Outputs:
ApplicationLoadBalancer:
Description: The DNSName of the Application Load Balancer
Value: !GetAtt ApplicationLoadBalancer.DNSName
view raw asg_alb.yaml hosted with ❤ by GitHub

Now let’s deploy the stacks using both the AWS Console and AWS CLI:

Using AWS Console

After logging into your AWS account, search for CloudFormation.

CloudFormation service

On the CloudFormation dashboard, click on “Create stack” button.

Select “Upload a template file” and click on “Choose file” button to browse the vpc.yaml file on your computer and then click on “Next”.

Create stack

Enter a stack name, the environment name and the IPv4 CIDR blocks for the VPC and the subnets, and click on “Next”.

Specify stack details

On Configure stack options section, we will leave everything as default and click on “Next”.
Review your stack details and click on “Submit” if everything seems correct.

You can repeat this process to create another stack using the asg_alb.yaml file.


Using AWS CLI

To create the stacks using AWS CLI, follow the steps below:

Validate our templates

    aws cloudformation validate-template --template-body file://vpc.yaml

    aws cloudformation validate-template --template-body file://asg_alb.yaml
Enter fullscreen mode Exit fullscreen mode

Create stacks using our templates

    aws cloudformation create-stack --stack-name LUIT --template-body file://vpc.yaml 

    aws cloudformation create-stack --stack-name LUIT2 --template-body file://asg_alb.yaml
Enter fullscreen mode Exit fullscreen mode

Check if the stack we created via template is completed successfully

    aws cloudformation list-stack-resources --stack-name LUIT

    aws cloudformation list-stack-resources --stack-name LUIT2
Enter fullscreen mode Exit fullscreen mode

Describe stack and its resources to view its properties

    aws cloudformation describe-stacks --stack-name LUIT

    aws cloudformation describe-stacks --stack-name LUIT2


    aws cloudformation describe-stack-resources --stack-name LUIT

    aws cloudformation describe-stack-resources --stack-name LUIT2
Enter fullscreen mode Exit fullscreen mode

Check events for stack formation

    aws cloudformation describe-stack-events --stack-name LUIT

    aws cloudformation describe-stack-events --stack-name LUIT2
Enter fullscreen mode Exit fullscreen mode

Now you should check if the Status is “CREATE_COMPLETE” and check the resources created.

If everything was created successfully, we will be able to reach the Application Load Balancer DNS on our browser and see the Apache running.

ALB redirecting traffic to EC2 instance on us-east-1a

ALB redirecting traffic to EC2 instance on us-east-1c


Success!

We have successfully created 2 stacks using AWS CloudFormation to deploy a high available and auto scale Apache Web Server running on EC2 instances.


If this step-by-step guide was helpful to you, click the like button and drop a comment below! I can’t wait to hear from you!

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay