Deploy a Dynamic Website on AWS with CloudFormation

Eugene Miguel
30 min readMar 3, 2024

--

Leverage AWS CloudFormation to automate and streamline the deployment process.

Introduction

Reference Architecture

Hello and welcome back! We will learn how to use CloudFormation to deploy this E-commerce website on AWS using a LAMP stack and the reference architecture. You will also learn step by step how to build CloudFormation templates and create stack for various AWS services such as VPC with public and private subnets in 2 different availability zones, NAT Gateway, RDS, Application Load Balancer, Auto Scaling Group, and Route 53. Lastly, you can use the steps learned in this project to deploy any dynamic website on AWS using CloudFormation.

Prerequisite:

You must complete my projects below:

Host a Dynamic Ecommerce Website on AWS

Host a Dynamic Ecommerce Website on AWS with Terraform

How it works

AWS CloudFormation lets you model, provision, and manage AWS and third-party resources by treating infrastructure as code.

Photo by AWS on Google

Use cases

Manage infrastructure with DevOps

  • Automate, test, and deploy infrastructure templates with continuous integration and delivery (CI/CD) automations.

Scale production stacks

  • Run anything from a single Amazon Elastic Compute Cloud (EC2) instance to a complex multi-region application.

Share best practices

  • Define an Amazon Virtual Private Cloud (VPC) subnet or provisioning services like AWS OpsWorks or Amazon Elastic Container Service (ECS) with ease.

Set-up and Deploy Website

  1. Create VPC with Public and Private Subnets — Part I
  2. Create VPC with Public and Private Subnets — Part II
  3. Create NAT Gateway
  4. Create an RDS Database from DB Snapshot
  5. Create an Application Load Balancer
  6. Create an Auto Scaling Group
  7. Create a Record Set in Route-53
1Photo by Jimmy Conover on Unsplash

Create VPC with Public and Private Subnets — Part I

Reference Architecture

Important

Before you create a CloudFormation template of any AWS service, I strongly recommend that you know how to create that service using the management console, hence you can use the same steps to create that service using CloudFormation.

You can create a CloudFormation template using 2 different file format, YAML and JSON.

Open your preferred text editor (I’m using sublime text) and save our template as vpc.yaml.

Head over to template section under CloudFormation documentation on google. This is where you can find all the information that you need to create your template. Here’s my repository for this project.

Add # before Metadata and Output resources. This makes it a comment, CloudFormation will ignore it when its running this template. Since we won’t create any properties yet for our Metadata and Output, we don’t want it to cause any error.

Copy and paste the YAML-formatted template fragment in your text editor. Clean it up a little bit and it should look like this.

Format version — there is only one format version available under YAML.

Description — enables you to include comments about your template.

AWSTemplateFormatVersion: "2010-09-09"

Description: This template creates vpc with public and private subnets

Parameters — you can use the parameters section to customize your templates and in this project, we are going to use it to enter the CIDR block for our VPC and subnets (see our reference architecture above).

Parameters:
VpcCIDR:
Default: 10.0.0.0/16
Description: Please enter the IP range (CIDR notation) for this VPC
Type: String

PublicSubnet1CIDR:
Default: 10.0.0.0/24
Description: Please enter the IP range (CIDR notation) for the public subnet 1
Type: String

PublicSubnet2CIDR:
Default: 10.0.1.0/24
Description: Please enter the IP range (CIDR notation) for the public subnet 2
Type: String

PrivateSubnet1CIDR:
Default: 10.0.2.0/24
Description: Please enter the IP range (CIDR notation) for the private subnet 1
Type: String

PrivateSubnet2CIDR:
Default: 10.0.3.0/24
Description: Please enter the IP range (CIDR notation) for the private subnet 2
Type: String

PrivateSubnet3CIDR:
Default: 10.0.4.0/24
Description: Please enter the IP range (CIDR notation) for the private subnet 3
Type: String

PrivateSubnet4CIDR:
Default: 10.0.5.0/24
Description: Please enter the IP range (CIDR notation) for the private subnet 4
Type: String
Photo by C D-X on Unsplash

Creating an SSH Location parameter

This is going to be used to specify the IP address range that can SSH into the EC2 instance in our VPC (see properties under parameters section).

Allowed Pattern — we’ve specified the type of pattern that this parameter is going to accept — its going to accept digital value from 1 to 3\. and so on.

Save your file.
  SSHLocation:
AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})'
ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.
Default: 0.0.0.0/0
Description: The IP address range that can be used to access the web server using SSH.
MaxLength: '18'
MinLength: '9'
Type: String

Creating a VPC parameter

Resources (see resource and property reference under template reference) — This section contains reference information for all AWS resource and property types that are supported by AWS CloudFormation.

This is what the syntax looks like to create the AWS::EC2::VPC in YAML.

Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock:
EnableDnsHostnames:
EnableDnsSupport:
InstanceTenancy:
Tags:
- Key:
Value:

To reference parameters and other resources in your CloudFormation template, we need to use Intrinsic functions.

Go to Ref , copy the YAML syntax and enter our VpcCIDR

      CidrBlock: !Ref VpcCIDR

This is all you need to do to create your VPC resource.

Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCIDR
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: Default
Tags:
- Key: Name
Value: Test VPC

Creating an Internet Gateway Parameter

Reference Architecture

This is what the syntax looks like to create the AWS::EC2::InternetGateway in YAML.

  InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: Test IGW
Photo by Jack Prommel on Unsplash

Creating Attach Internet Gateway to VPC

Reference Architecture

This is what the syntax looks like to create the AWS::EC2::InternetGateway in YAML. To reference the VPC, we need to use Ref in Intrinsic functions.

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

Creating 2 Public Subnets

Reference Architecture

To reference an availability zone, use Fn::Select and Fn::GetAZs in Intrinsic functions. Let me explain what this syntax means. When you launch this template in any region, this !select is going to select a list of availability zones in that region. Once it asks for the list of AZ, you are telling it to select the first item in that list.

Lets revisit what I learnt in Python. If you have a list of availability zone in that region, when you have 0 in your template CloudFormation is going to select the first AZ of that list, this is how indexing works.

      AvailabilityZone: !Select [ 0, !GetAZs '' ]

This is what the syntax looks like to create our Public Subnet 1 and Public Subnet 2 in 2 different availability zones. Refer to AWS::EC2::Subnet for more information.

  PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, !GetAZs '' ]
CidrBlock: !Ref PublicSubnet1CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: Public Subnet 1
VpcId: !Ref VPC

PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 1, !GetAZs '' ]
CidrBlock: !Ref PublicSubnet2CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: Public Subnet 2
VpcId: !Ref VPC

Creating the Public Route Table, Route, and Public Subnet Associations

Reference Architecture

This is what the syntax looks like to create our public route table, its route, and associating 2 public subnets. Refer to AWS::EC2::Route and AWS::EC2::RouteTable for more information. You can review our reference architecture above to make sure you are on track.

  PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: Public Route Table
VpcId: !Ref VPC

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

PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet1

PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet2

Create VPC with Public and Private Subnets — Part II

Reference Architecture

Creating 4 Private Subnets

This is what the syntax looks like to create our private subnets. Refer to AWS::EC2::Subnet for more information. You can review our reference architecture above to make sure you are on track.

  PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, !GetAZs '' ]
CidrBlock: !Ref PrivateSubnet1CIDR
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: Private Subnet 1 | App Tier
VpcId: !Ref VPC

PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 1, !GetAZs '' ]
CidrBlock: !Ref PrivateSubnet2CIDR
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: Private Subnet 2 | App Tier
VpcId: !Ref VPC

PrivateSubnet3:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, !GetAZs '' ]
CidrBlock: !Ref PrivateSubnet3CIDR
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: Private Subnet 3 | Database Tier
VpcId: !Ref VPC

PrivateSubnet4:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 1, !GetAZs '' ]
CidrBlock: !Ref PrivateSubnet4CIDR
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: Private Subnet 4 | Database Tier
VpcId: !Ref VPC
Photo by Tim Gouw on Unsplash

Creating the Security Groups

This is what the syntax looks like to create 4 security groups for our 4 private subnets. Refer to AWS::EC2::SecurityGroup for more information.

The ALB security group will allow internet traffic on TCP protocol on port 80 and TCP protocol on port 443.

  ALBSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: Enable HTTP/HTTPS access on port 80/443
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
Tags:
- Key: Name
Value: ALB Security Group
VpcId: !Ref VPC

The SSH security group will allow traffic on TCP protocol on port 80. For CidrIp, we referenced it from the SSH location parameter that we created earlier.

  SSHSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: SSH Security Group
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref SSHLocation
Tags:
- Key: Name
Value: SSH Security Group
VpcId: !Ref VPC

The last security groups are for our EC2 instance — the Web Server and Database Security Groups.

To further understand how security groups control traffic and why we are doing it, you can complete few of my projects here;

Deploy a Static Website on AWS

Deploy a WordPress Website on AWS

Host a Dynamic Ecommerce Website on AWS

  WebServerSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: Enable HTTP/HTTPS access via port 80/443 locked down to the load balancer SG + SSH access via port 22 locked down SSH SG
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref ALBSecurityGroup
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref SSHSecurityGroup
Tags:
- Key: Name
Value: WebServer Security Group
VpcId: !Ref VPC
  DataBaseSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: Open database for access
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref WebServerSecurityGroup
Tags:
- Key: Name
Value: Database Security Group
VpcId: !Ref VPC
Photo by Josh Hild on Unsplash

These are all the resources that we need to create in our VPC. Go ahead and save your work we are going to use this template to create a stack in the management console. Go to CloudFormation and select Create stack.

Select Template is ready and Upload a template file. Upload your template here then click Next.

Provide your Stack name. Under Parameters, you can see all the parameters we created in our template.

Let’s say you want to use this template to create another VPC that has a different CIDR block, what you can do is change the default value here. This is one of the benefits of having parameters in your template.

Looking back at our template, we’ve specified our parameters by VPC CIDR, public subnet 1, public subnet 2 and so on but when we launch our CloudFormation template for this VPC, our parameters aren’t listed in the order that we have set it in our template. Imagine having a lot of disorganized parameters, this will make it difficult for the users who are using your template to find what they need.

To group our parameters together, we are going to use the Metadata section. Remove the # before Metadata. We are using AWS::CloudFormation::Interface Label to group our parameters into 3 labels.

Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: "VPC CIDR"
Parameters:
- VpcCIDR
-
Label:
default: "Subnet CIDR"
Parameters:
- PublicSubnet1CIDR
- PublicSubnet2CIDR
- PrivateSubnet1CIDR
- PrivateSubnet2CIDR
- PrivateSubnet3CIDR
- PrivateSubnet4CIDR
-
Label:
default: "SSH CIDR"
Parameters:
- SSHLocation

Save your template. Going back to Create stack page, reupload your template file.

This time its listed in the order we specified in our template.

Leave the Configure stack options page to default click Next. Review the template then click Submit.

Error!

Photo by Mahdi Bafande on Unsplash

I encountered an error while my CloudFormation was creating the stack. I clicked Detect root cause and it showed me that’s its likely the Instance Tenancy of my VPC resource. So I reviewed it and just replaced D with a d in Default deleted and recreated my stack and it worked!

CloudFormation has finished creating the stack.

Under Events, note that some resources may still show CREATE IN PROGRESS. You can review other labels such as Stack info, Resources, Outputs, Parameters, Template, and so on.

CloudFormation has finished creating all the resources in our template. Let’s go to our AWS account and verify if these resources are there.

Test VPC
Test VPC Details
2 public and 4 private Subnets in their respective VPC, availability zones, and CIDR blocks.
Public route table
Subnet associations of the public route
Internet gateway attached to Test VPC
Security groups with their inbound and outbound rules.
Photo by Rubén Bagüés on Unsplash

This is how you create a template for our VPC. Let’s go ahead and create our Output — we are using this to export some information from your VPC so you can reference it in another CloudFormation template. Delete your CloudFormation stack.

Review Output under Template anatomy.

Outputs:
Logical ID:
Description: Information about the value
Value: Value to return
Export:
Name: Name of resource to export

Remove the # before Output in your template. The values that we want to export from this CloudFormation template are our VPC ID, the ID for all of our subnets and security groups.

We are using !Sub intrinsic function and we are using a pseudo parameter that returns our stack name — VPC (since this output is for our VPC I like to keep everything consistent). Moreover, its going to take our stack name and join it with this VPC. Go to AWS: :StackName to find out more about pseudo parameters.

Outputs:
VPC:
Description: VPC ID
Export:
Name: !Sub ${AWS::StackName}-VPC
Value: !Ref VPC

This is how you create an output to export your VPC ID. The next resource were going to create an output for are the public and private subnets.

  PublicSubnet1:
Description: Public Subnet 1 ID
Export:
Name: !Sub ${AWS::StackName}-PublicSubnet1
Value: !Ref PublicSubnet1

PublicSubnet2:
Description: Public Subnet 2 ID
Export:
Name: !Sub ${AWS::StackName}-PublicSubnet2
Value: !Ref PublicSubnet2

PrivateSubnet1:
Description: Private Subnet 1 ID
Export:
Name: !Sub ${AWS::StackName}-PrivateSubnet1
Value: !Ref PrivateSubnet1

PrivateSubnet2:
Description: Private Subnet 2 ID
Export:
Name: !Sub ${AWS::StackName}-PrivateSubnet2
Value: !Ref PrivateSubnet2

PrivateSubnet3:
Description: Private Subnet 3 ID
Export:
Name: !Sub ${AWS::StackName}-PrivateSubnet3
Value: !Ref PrivateSubnet3

PrivateSubnet4:
Description: Private Subnet 4 ID
Export:
Name: !Sub ${AWS::StackName}-PrivateSubnet4
Value: !Ref PrivateSubnet4

This is how you create an output to export our public and private subnets. The last resource that were going to create an output for are the security groups.

  ALBSecurityGroup:
Description: Application Load Balancer Security Group ID
Export:
Name: !Sub ${AWS::StackName}-ALBSecurityGroup
Value: !Ref ALBSecurityGroup

SSHSecurityGroup:
Description: SSH Security Group ID
Export:
Name: !Sub ${AWS::StackName}-SSHSecurityGroup
Value: !Ref SSHSecurityGroup

WebServerSecurityGroup:
Description: Webserver Security Group ID
Export:
Name: !Sub ${AWS::StackName}-WebServerSecurityGroup
Value: !Ref WebServerSecurityGroup

DataBaseSecurityGroup:
Description: DataBase Security Group ID
Export:
Name: !Sub ${AWS::StackName}-DataBaseSecurityGroup
Value: !Ref DataBaseSecurityGroup

This is how we create an output for resources in our CloudFormation template. Review and save your work make sure everything is correct.

Photo by Alexander Andrews on Unsplash

Let’s go to our AWS console. Select Create stack, Upload our template file again, and click Submit.

It is now creating our stack. Give CloudFormation few minutes to create the rest of our resources.

Our VPC stack creation is now completed without errors.

Go to Outputs tab. We’re going to see all the outputs we exported from our template. Pay attention to Export name. When you are importing these values into another template, you are going to be importing it using this Export name (I’ll walk you through it later). The values that we are going to be exporting into another template are the Values here which is the ID for our ALBSecurityGroup, DataBaseSecurityGroup, and so on.

This is how we create a CloudFormation template for a VPC. Delete your CloudFormation stack.

Photo by Will on Unsplash

Create NAT Gateway

Reference Architecture

We will use CloudFormation to create NAT gateways in each public subnet to allow instances in the private subnet to have access to the internet.

We’ll also create 2 private route tables. Private route table 1 is going to be associated with private subnet 1 and 3 while private route table 2 is going to be associated with private subnet 2 and 4.

Before we start please create your VPC stack again in CloudFormation.

Once we are done creating our NAT gateway template, we’re going to create that stack on top of this VPC stack.

We’re going to use this YAML-formatted template fragment from AWS CLoudFormation documentation and remove the values that we don’t need.

{
"AWSTemplateFormatVersion" : "version date",

"Description" : "JSON string",

"Metadata" : {
template metadata
},

"Parameters" : {
set of parameters
},

"Rules" : {
set of rules
},

"Mappings" : {
set of mappings
},

"Conditions" : {
set of conditions
},

"Transform" : {
set of transforms
},

"Resources" : {
set of resources
},

"Outputs" : {
set of outputs
}
}

The values and its properties should look like this after updating them.

Description — I’ve entered the description this way to help me understand the type of value that this parameter was set. Upon reading this description, I know that I need to enter the name of the VPC stack. You can read more about Parameters and properties here.

AWSTemplateFormatVersion: 2010-09-09

Description: This template creates a nat gateway in each the public subnets

Parameters:
ExportVpcStackName:
Description: The name of the vpc stack that exports values
Type: String
Photo by Ryan Riggins on Unsplash

The next section that we’re going to create is our Resources section. The resource that we are trying to create is our NAT Gateway.

Reference Architecture

To create a NAT Gateway we are going to do these steps:

  1. Allocate Elastic IP Address
  2. Create Nat Gateway in each Public Subnets
  3. Create a Private Route Table
  4. Add a route to point internet-bound traffic to Nat Gateway
  5. Associate Private Subnets with Private Route Table

The Domain only takes 1 value, hence we are using VPC. Once we update the resource type and properties, our 2 elastic IP addresses templates should look like this:

Resources:
NatGateway1EIP:
Type: AWS::EC2::EIP
Properties:
Domain: VPC
Tags:
- Key: Name
Value: EIP 1

NatGateway2EIP:
Type: AWS::EC2::EIP
Properties:
Domain: VPC
Tags:
- Key: Name
Value: EIP 2

Next we need to create NAT gateway for each public subnets.

AllocationId — the ID of the elastic IP we created above. We are referencing it using !GetAtt Intrinsic functions.

SubnetID — to reference an output from another CloudFormation template, we’re going to use Fn::ImportValue Intrinsic functions.

The !Sub ${ExportVpcStackName}-PublicSubnet1 means we are going to substitute the name of the VPC that is exporting the values in here and the subnet we want to reference is our PublicSubnet1. Remember when we created our Output (vpc-PublicSubnet1) in the CloudFormation stack which is the same value we’re entering here.

Our updated template should look like this:

  NatGateway1:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGateway1EIP.AllocationId
SubnetId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-PublicSubnet1
Tags:
- Key: Name
Value: Nat Gateway Public Subnet 1

NatGateway2:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGateway2EIP.AllocationId
SubnetId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-PublicSubnet2
Tags:
- Key: Name
Value: Nat Gateway Public Subnet 2
Photo by Alessio Billeci on Unsplash

Let’s create the first private route table.

Reference Architecture

VpcID — to reference an output from another CloudFormation template, we’re going to use Fn::ImportValue Intrinsic functions.

  PrivateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: Private Route Table 1
VpcId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-VPC

Let’s add a route to this route table so it can route traffic to the internet through our NAT gateway 1.

  PrivateRoute1:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway1
RouteTableId: !Ref PrivateRouteTable1

Up next, we need to associate private subnets 1 and 3 with private route table 1.

  PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable1
SubnetId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-PrivateSubnet1

PrivateSubnet3RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable1
SubnetId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-PrivateSubnet3

Let’s create the second private route table.

  PrivateRouteTable2:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: Private Route Table 2
VpcId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-VPC

Add a route to this route table so it can route traffic to the internet through our NAT gateway 2.

  PrivateRoute2:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway2
RouteTableId: !Ref PrivateRouteTable2

Lastly, we’re going to associate private subnet and 4 with private route table 2.

  PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable2
SubnetId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-PrivateSubnet2

PrivateSubnet4RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable2
SubnetId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-PrivateSubnet4

This is all we need to do to create our NAT gateway template.

Photo by Juliana Malta on Unsplash

Save your work. Let’s go back to our AWS console to create our NAT gateway stack on top of our VPC stack. Select Create stack then With new resources (standard).

Choose Template is ready, Upload a template file, and upload our nat-gateway.yaml file.

Provide your Stack name. Under Parameters, we’re going to enter the name of the stack that exports the value that we referenced in this NAT gateway template. The name we gave that stack is VPC, click Next.

Click Next on Configure stack options then hit Submit.

After few minutes CloudFormation has finished creating all the resources in my NAT gateway template.

If we go back to our VPC we should be able to see all the resources.

2 elastic IP addresses associated with each NAT gateway
NAT gateway in public subnet 1
NAT gateway in public subnet 2
Public and private route tables
Route added to public route table
2 public subnets associated with our public route table
Route added to our private route table 1
The database and app tiers are associated with private route table 1
Route added to private route table 2
Private subnet 2 and 4 are associated with private route table 2

This is how you create a NAT gateway using CloudFormation. Allocating an elastic IP costs money, to prevent incurring cost you can delete your nat-gateway stack if you are not using it. You can leave the VPC running.

Whenever you create an EC2 instance in the private subnet that needs to access the internet then you can create your nat-gateway stack to allow those instance to have access to the internet. This is how you create NAT gateway using CloudFormation.

VPC Resource Map

The resource map shows relationships between resources inside a VPC and how traffic flows from subnets to NAT gateways, internet gateway and gateway endpoints.

You can use the resource map to understand the architecture of a VPC, see how many subnets it has in it, which subnets are associated with which route tables, and which route tables have routes to NAT gateways, internet gateways, and gateway endpoints.

You can also use the resource map to spot undesirable or incorrect configurations, such as private subnets disconnected from NAT gateways or private subnets with a route directly to the internet gateway. You can choose resources within the resource map, such as route tables, and edit the configurations for those resources.

Photo by D A V I D S O N L U N A on Unsplash

Create an RDS Database from DB Snapshot

Reference Architecture

We are going to create our RDS database using CloudFormation and during its creation, the template is going to create our RDS by restoring it from our database snapshot. Open your text editor save a new file rds.snapshot.yaml

These are the resources that we will need for this template:

Format version

Parameters

YAML-formatted template fragment

Amazon Relational Database Service resource type reference

Let’s break down a few values here:

AllowedPattern: ‘[a-zA-Z][a-zA-Z0–9]*’ The first character of this value has to be alphabetical afterwards you can have it alphanumeric.

Metadata — we are using this to group our parameters together. We are using 2 labels the Export VPC Stack Name and Database Parameters.

DatabaseSubnetGroup It allows you to specify the subnet where you want to launch your database in. We are putting our subnet in the private subnet 3 and private subnet 4.

SubnetIds These subnets are also in our VPC stack, similar to what we did previously we are using the import value Intrinsic functions to import those subnets into this RDS template.

VPCSecurityGroups: This security group is in our VPC stack and we’ve used the Fn::ImportValue Intrinsic functions to import this value from our VPC stack into this RDS template.

AWSTemplateFormatVersion: 2010-09-09

Description: This template creates an RDS DB instance that is restored from a DB snapshot

Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: Export VPC Stack Name
Parameters:
- ExportVpcStackName
-
Label:
default: Database Parameters
Parameters:
- DatabaseInstanceIdentifier
- DatabaseSnapshotIdentifier
- DatabaseInstanceClass
- MultiAZDatabase

Parameters:
ExportVpcStackName:
Description: The name of the vpc stack that exports values
Type: String

DatabaseInstanceIdentifier:
AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*'
ConstraintDescription: Must begin with a letter and contain only alphanumeric characters
Default: dev-rds-db
Description: Instance identifier name
MaxLength: 60
MinLength: 1
Type: String

DatabaseSnapshotIdentifier:
Description: The ARN of the DB snapshot that's used to restore the DB instance
Type: String

DatabaseInstanceClass:
AllowedValues:
- db.t1.micro
- db.t2.micro
- db.m1.small
- db.m1.medium
- db.m1.large
ConstraintDescription: Must select a valid database instance type
Default: db.t2.micro
Description: The database instance type
Type: String

MultiAZDatabase:
AllowedValues:
- true
- false
ConstraintDescription: Must be either true or false
Default: false
Description: Create a Multi-AZ MySQL Amazon RDS database instance
Type: String

Resources:
DatabaseSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS database
SubnetIds:
- Fn::ImportValue: !Sub ${ExportVpcStackName}-PrivateSubnet3
- Fn::ImportValue: !Sub ${ExportVpcStackName}-PrivateSubnet4
Tags:
- Key: Name
Value: database subnets

DatabaseInstance:
Type: AWS::RDS::DBInstance
Properties:
AvailabilityZone: !Select [ 0, !GetAZs '' ]
DBInstanceClass: !Ref DatabaseInstanceClass
DBInstanceIdentifier: !Ref DatabaseInstanceIdentifier
DBSnapshotIdentifier: !Ref DatabaseSnapshotIdentifier
DBSubnetGroupName: !Ref DatabaseSubnetGroup
Engine: MySQL
MultiAZ: !Ref MultiAZDatabase
VPCSecurityGroups:
- Fn::ImportValue: !Sub ${ExportVpcStackName}-DataBaseSecurityGroup

We’ve completed this template save your work. Let’s go back to our CloudFormation management console. Create your RDS stack by doing the same steps we did previously.

In DatabaseInstanceIdentifier under Parameters, this is where you will put the ARN of your snapshot you can find this in the Details section of your snapshot on RDS. Click Next and Create stack.

Error!

Photo by Mahdi Bafande on Unsplash

Another error i encountered. MyCloudFormation rds.yaml stack won’t accept my existing DatabaseInstanceIdentifier dev-rds-db

I got HTTP error 500 when I loaded my web app at the end of my project so I had to trace back my steps.

The reason is because of the AllowedPattern: '[a-ZA-Z][a-Za-Z0-9]*' constraint. It won’t let me use dev-rds-db which is my existing DB Instance Identifier from my RDS snapshot. Remember that your existing DB Instance Identifier from your snapshot should match with your RDS template otherwise your website/app won’t work.

So I had to delete this specific syntax, save my template and recreate my rds stack and it worked!

CloudFormation has successfully created our RDS stack.

Let’s go to our RDS dashboard and check if all of these resources are there.

CloudFormation has created a new DB and its using the same DB identifier from my RDS snapshot.
Our DB is created in our desired AZ, subnet in our Test VPC using database security group.

This is how we create an RDS instance using CloudFormation. You can delete your resources in this order to avoid it from running and incurring more cost:

  1. rds
  2. nat-gateway
  3. vpc
Photo by whereslugo on Unsplash

Create an Application Load Balancer

We are going to use CloudFormation to create an application load balancer for our project. Open your text editor and save a new file alb.yaml

These are the resources that we will need for this template:

Format version

Parameters

Metadata

YAML-formatted template fragment

AWS::ElasticLoadBalancingV2::LoadBalancer

Intrinsic functions

Before you create a CloudFormation template of any AWS service, I strongly recommend that you know how to create that service using the management console, hence you can use the same steps to create that service using CloudFormation.

Let’s break down a few values here:

AcmCertificate: we’re going to use this parameter to import the ARN of our certificate in AWS certificate manager.

SecurityGroups: this is how you enter the value to import your application load balancer security group we are using the ImportValue Intrinsic function.

ALBListenerNoSslCertificate: creates a listener on port 80

DefaultActions: we’re going to use this to redirect traffic from HTTP to HTTPS

AWSTemplateFormatVersion: 2010-09-09

Description: This template create an Application Load Balancer

Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: "Certificate Arn"
Parameters:
- AcmCertificate
-
Label:
default: "Export VPC Stack Name"
Parameters:
- ExportVpcStackName

Parameters:
AcmCertificate:
Description: The ARN of the AWS Certification Manager's certificate
Type: String

ExportVpcStackName:
Description: The name of the vpc stack that exports values
Type: String

Resources:
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: MyApplicationLoadBalancer
SecurityGroups:
- Fn::ImportValue: !Sub ${ExportVpcStackName}-ALBSecurityGroup
Subnets:
- Fn::ImportValue: !Sub ${ExportVpcStackName}-PublicSubnet1
- Fn::ImportValue: !Sub ${ExportVpcStackName}-PublicSubnet2

ALBListenerNoSslCertificate:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
DefaultActions:
- RedirectConfig:
Host: '#{host}'
Path: '/#{path}'
Port: '443'
Protocol: HTTPS
StatusCode: HTTP_301
Type: redirect
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP

ALBListenerSslCertificate:
Type : AWS::ElasticLoadBalancingV2::Listener
Properties:
Certificates:
- CertificateArn: !Ref AcmCertificate
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ALBTargetGroup
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 443
Protocol: HTTPS

ALBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
HealthCheckPath: /
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Matcher:
HttpCode: 200,302
Name: MyWebServers
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 5
VpcId:
Fn::ImportValue: !Sub ${ExportVpcStackName}-VPC

Outputs:
ALBTargetGroup:
Description: Webserver target group
Export:
Name: !Sub ${AWS::StackName}-ALBTargetGroup
Value: !Ref ALBTargetGroup

ApplicationLoadBalancerDnsName:
Description: Application Load Balancer DNS Name
Export:
Name: !Sub ${AWS::StackName}-ApplicationLoadBalancerDnsName
Value: !GetAtt ApplicationLoadBalancer.DNSName

ApplicationLoadBalancerZoneID:
Description: Application Load Balancer Canonical Hosted Zone ID
Export:
Name: !Sub ${AWS::StackName}-ApplicationLoadBalancerZoneID
Value: !GetAtt ApplicationLoadBalancer.CanonicalHostedZoneID

We are done creating our ALB template save your work. In your CloudFormation dashboard recreate your stack first before creating our ALB stack.

Under Specify stack details, enter your Stack name and Parameters.

This is my Certificate Arn (Certificate Manager) that I will need for my ALB stack.

Click the Next and Submit buttons.

Create in progress
The values we exported out of this template.

Let’s verify if these resources are created properly in our AWS account.

We have 1 load balancer — MyApplicationLoadBalancer
Listeners and rules tab we configured in our template for our load balancer.
Our MyWebServers target group in our custom VPC.
The health checks we configured in our template.

This wraps up using CloudFormation to create an application load balancer.

Photo by Anne Nygård on Unsplash

Create an Auto Scaling Group

Reference Architecture

We are going to use CloudFormation to create our auto scaling group. Open your text editor and save a new file asg.yaml

Before we start, let’s recreate our entire stack so that once we are done with this template we are going to create our auto scaling group stack.

Open your CloudFormation documentation to learn more about the resources that we will create for this template:

YAML-formatted template fragment

Format version

Parameters

Metadata

Intrinsic functions

Application Auto Scaling resource type reference

Let me discuss few parameters in our asg.yaml template:

Parameters

EC2KeyName It is going to select a key pair that’s in your account in that region so have it ready.

EC2ImageID The parameter that we are going to use to enter our image ID that I created in my last project (Fleet Cart AMI Version 2).

Metadata We will use this to group our parameters chronologically.

Resources We are using !Ref !Sub !GetATT intrinsic function to reference several parameters.

Our asg.yaml should look like this:

AWSTemplateFormatVersion: 2010-09-09 

Description: This template creates Auto Scaling Group.

Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: Export VPC Stack Name
Parameters:
- ExportVpcStackName
-
Label:
default: Export ALB Stack Name
Parameters:
- ExportAlbStackName
-
Label:
default: Email Address
Parameters:
- OperatorEMail
-
Label:
default: Image ID
Parameters:
- EC2ImageID
-
Label:
default: Launch Template Name
Parameters:
- WebServerLaunchTemplateName
-
Label:
default: Instance Type
Parameters:
- InstanceType
-
Label:
default: EC2 KeyPair
Parameters:
- EC2KeyName

Parameters:
ExportVpcStackName:
Description: The name of the vpc stack that exports values
Type: String

ExportAlbStackName:
Description: The name of the alb stack that exports values
Type: String

OperatorEMail:
Description: A valid EMail address to notify if there are any scaling operations
Type: String

EC2KeyName:
Description: Name of an EC2 KeyPair to enable SSH access to the instance.
Type: AWS::EC2::KeyPair::KeyName
ConstraintDescription: Must be the name of an existing EC2 KeyPair.

EC2ImageID:
Description: The ID of the custom AMI
Type: String

WebServerLaunchTemplateName:
AllowedPattern: '[a-zA-Z0-9\(\)\.\-/_]+'
ConstraintDescription: Must be unique to this account. Max 128 chars. No spaces or special characters like '&', '*', '@'.
Default: Lamp-Server-Launch-Template
Description: Name of launch template
Type: String

InstanceType:
Description: WebServers EC2 instance type
Type: String
Default: t2.micro
AllowedValues:
- t1.micro
- t2.nano
- t2.micro
- t2.small
ConstraintDescription: Must be a valid EC2 instance type.

Resources:
WebServerLaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: !Ref WebServerLaunchTemplateName
LaunchTemplateData:
ImageId: !Ref EC2ImageID
InstanceType: !Ref InstanceType
KeyName: !Ref EC2KeyName
Monitoring:
Enabled: true
SecurityGroupIds:
- Fn::ImportValue: !Sub ${ExportVpcStackName}-WebServerSecurityGroup

NotificationTopic:
Type: AWS::SNS::Topic
Properties:
Subscription:
- Endpoint: !Ref OperatorEMail
Protocol: email

WebServerAutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: Lamp Auto Scaling Group
VPCZoneIdentifier:
- Fn::ImportValue: !Sub ${ExportVpcStackName}-PrivateSubnet1
- Fn::ImportValue: !Sub ${ExportVpcStackName}-PrivateSubnet2
HealthCheckGracePeriod: 300
HealthCheckType: ELB
LaunchTemplate:
LaunchTemplateName: !Ref WebServerLaunchTemplateName
Version: !GetAtt WebServerLaunchTemplate.LatestVersionNumber
MinSize: 1
MaxSize: 4
DesiredCapacity: 2
Tags:
- Key: Name
Value: AS-WebServer
PropagateAtLaunch: true
TargetGroupARNs:
- Fn::ImportValue: !Sub ${ExportAlbStackName}-ALBTargetGroup
NotificationConfiguration:
TopicARN: !Ref NotificationTopic
NotificationTypes:
- 'autoscaling:EC2_INSTANCE_LAUNCH'
- 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR'
- 'autoscaling:EC2_INSTANCE_TERMINATE'
- 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR'

WebServerScaleUpPolicy:
Type: AWS::AutoScaling::ScalingPolicy
Properties:
AdjustmentType: ChangeInCapacity
AutoScalingGroupName: !Ref WebServerAutoScalingGroup
Cooldown: 60
ScalingAdjustment: 1

WebServerScaleDownPolicy:
Type: 'AWS::AutoScaling::ScalingPolicy'
Properties:
AdjustmentType: ChangeInCapacity
AutoScalingGroupName: !Ref WebServerAutoScalingGroup
Cooldown: 60
ScalingAdjustment: -1

CPUAlarmHigh:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmDescription: Scale-up if CPU > 90% for 10 minutes
MetricName: CPUUtilization
Namespace: AWS/EC2
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 90
AlarmActions:
- !Ref WebServerScaleUpPolicy
Dimensions:
- Name: AutoScalingGroupName
Value: !Ref WebServerAutoScalingGroup
ComparisonOperator: GreaterThanThreshold

CPUAlarmLow:
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: Scale-down if CPU < 70% for 10 minutes
MetricName: CPUUtilization
Namespace: AWS/EC2
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 70
AlarmActions:
- !Ref WebServerScaleDownPolicy
Dimensions:
- Name: AutoScalingGroupName
Value: !Ref WebServerAutoScalingGroup
ComparisonOperator: LessThanThreshold
Photo by Wilhelm Gunkel on Unsplash

We finished creating our asg.yaml template save your work. Under Specify stack details, enter your Stack name (asg) and Parameters.

The AMI that I want is Fleet Cart Version 2
Select your existing key pair.

CloudFormation has successfully created our asg.yaml stack let’s check our ASG dashboard to verify if these resources are there.

There will be cases where you will get a rollback error. In my case, I delete the last stack and recreate it and it worked fine.

Our Lamp Auto Scaling Group with 2 instances running, desired capacity of 2 in us-east 1a and 1b availability zones.
2 EC2 instances that our ASG have created.

This is how we create auto scaling group using CloudFormation. Up next, we are going to create our record set in Route-53 so we can access our web app.

Photo by Katrina Wright on Unsplash

Create a Record Set in Route-53

Reference Architecture

We are going to use CloudFormation to create our record set in Route 53. Open your text editor and save a new file route-53.yaml . Recreate all your stacks before proceeding.

Open your CloudFormation documentation to learn more about the resources that we will create for this template:

YAML-formatted template fragment

Format version

Parameters

Metadata

Let me discuss few parameters in our route-53.yaml template:

DomainName and Outputs Make sure to update this with your domain name.

This is how creating a record looks like in our AWS Route 53 account

Our template for our record set in Route 53 should look like this:

AWSTemplateFormatVersion: 2010-09-09

Description: This template creates a record in Route 53.

Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: Export ALB Stack Name
Parameters:
- ExportAlbStackName
-
Label:
default: Domain Name
Parameters:
- DomainName

Parameters:
ExportAlbStackName:
Description: The name of the alb stack that exports values
Type: String

DomainName:
Description: The main domain name of the E-Commerce site (e.g. fleetcart)
Type: String
Default: pinkastra.co.uk

Resources:
SiteDomainName:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: !Join ['', [!Ref DomainName, '.']]
RecordSets:
- Name: !Join
- ''
- - 'www.'
- !Ref DomainName
Type: A
AliasTarget:
HostedZoneId:
Fn::ImportValue: !Sub ${ExportAlbStackName}-ApplicationLoadBalancerZoneID
DNSName:
Fn::ImportValue: !Sub ${ExportAlbStackName}-ApplicationLoadBalancerDnsName

Outputs:
WebsiteURL:
Value: !Join
- ''
- - 'https://www.'
- 'pinkastra.co.uk'

This is all we need to create our template for creating a record set in Route-53 let’s go ahead and save our work then create our last stack in CloudFormation.

Enter the name of our ALB stack and your own Domain name. Click Next then Create stack.

It is creating our record set in Route 53.

CloudFormation has finished creating our route-53 stack.

The output that we exported this joined the https://www and (domain name).

Go to Output tab and click your domain name.

The web site/app is redirecting to https making it secured.

Congratulations!

Photo by Jason Goodman on Unsplash

Before you go!

  • Did you see what happens when you click and hold the clap 👏button?
  • Follow me and subscribe to the newsletter for more valuable content. Go to my Medium Page.
  • Leave a comment 💬 to let me know what you think about this solution.
  • Share 📢 this article with your network to help spread knowledge.

Thank you for following along. I hope you find this helpful in your Cloud journey. Stay tuned for my next project.

Build industry solutions with me here! Show your hiring Manager and organization that you are the right person for the job, help solve their problem and stand out from the crowd!

Connect with me on LinkedIn and GitHub.

--

--

Eugene Miguel

Cloud DevOps Engineer • AWS Certified Solutions Architect