Welcome to a foundational project that will solidify your understanding of cloud architecture and Infrastructure as Code. In this guide, we are going to build a classic, scalable, and secure three tier web application from the ground up using AWS CloudFormation. Forget manual clicking; we are crafting a reusable blueprint that defines every component of our system. This project is a journey through core cloud services, touching on networking, security, databases, and compute. By the end, you will not only have a running application but also a powerful template and a deep appreciation for building robust systems on AWS. Let's get started!
Architecture Overview
Before we write a single line of code, let's visualize what we are building. A three tier architecture is a well established pattern for separating concerns in an application. It's like designing a well run restaurant.
- The Presentation Tier (The Host Stand): This is the public face of our application. We will use an Application Load Balancer (ALB) sitting in public subnets. The ALB's job is to greet incoming user traffic, check if the application is healthy, and direct requests to the right place. It's the only part of our system directly exposed to the internet.
- The Application Tier (The Kitchen): This is where the logic of our application runs. We will have an Auto Scaling Group of EC2 instances running our web server code. Crucially, these instances will live in private subnets, meaning they cannot be accessed directly from the internet. They only take orders from our trusted ALB.
- The Data Tier (The Pantry): This is where we store our precious data. A Relational Database Service (RDS) instance will hold our application's data. To keep it supremely secure, our database will also reside in the private subnets, isolated from everyone except our application servers.
All of this will be built inside a Virtual Private Cloud (VPC), which acts as our own private, isolated section of the AWS cloud. This architecture is a blueprint for excellence, providing security, scalability, and resilience.
The Network Foundation (VPC & Subnets)
First, we must build the digital real estate where our application will live. This involves creating the VPC and carving out our public and private subnets. The public subnets will have a path to the internet, while the private subnets will be shielded from it.
Here is the CloudFormation YAML that lays this network foundation.
AWSTemplateFormatVersion: '2010-09-09'
Description: A resilient three tier web application.
Parameters:
VpcCIDR:
Type: String
Default: 10.0.0.0/16
Description: CIDR block for the VPC.
PublicSubnet1CIDR:
Type: String
Default: 10.0.1.0/24
Description: CIDR block for the first Public Subnet.
PrivateSubnet1CIDR:
Type: String
Default: 10.0.10.0/24
Description: CIDR block for the first Private Subnet.
Resources:
# The main virtual network
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: MyWebApp-VPC
# Gateway to connect the VPC to the internet
InternetGateway:
Type: AWS::EC2::InternetGateway
# Attaches the Internet Gateway to the VPC
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# Subnets accessible from the internet (for our Load Balancer)
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PublicSubnet1CIDR
MapPublicIpOnLaunch: true # Instances here get a public IP
Tags:
- Key: Name
Value: MyWebApp-PublicSubnet1
# Subnets not accessible from the internet (for our App & DB)
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Ref PrivateSubnet1CIDR
MapPublicIpOnLaunch: false # Instances here are private
Tags:
- Key: Name
Value: MyWebApp-PrivateSubnet1
# Route table for public traffic
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
# Route that directs internet-bound traffic to the Internet Gateway
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
# Associate the public route table with our public subnet
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
Let's break this down. We start with Parameters to make our IP addressing flexible. The VPC resource creates our isolated network bubble. To give it an exit to the internet, we create an InternetGateway and attach it with VPCGatewayAttachment.
Next, we define our subnets. The PublicSubnet1 is special because it has a PublicRouteTable associated with it. This route table contains a PublicRoute that points all outbound traffic (0.0.0.0/0) to our InternetGateway. In contrast, the PrivateSubnet1 has no such route, effectively cutting it off from the public internet and making it a secure place for our application and database.
The Security Layer (Security Groups)
Now that we have our network, we need to add locks to the doors. Security Groups act as virtual firewalls, controlling traffic flow to and from our resources. We will create a chain of trust, where each layer only talks to the layers it is supposed to.
Load Balancer Security Group: This is our outermost gate. It needs to allow HTTP or HTTPS traffic from anywhere on the internet so that users can reach our application. The source will be
0.0.0.0/0.Application Security Group: This protects our application servers. It should only allow incoming traffic on our application's port (e.g., port 3000) from one specific source: the Load Balancer Security Group. This ensures that no one can bypass the load balancer and hit our application servers directly.
Database Security Group: This guards our most valuable asset, the data. It should only allow incoming traffic on the database port (e.g., MySQL's port 3306) from one source: the Application Security Group. The application servers are the only entities allowed to speak to the database.
This layered security model is a fundamental best practice for building secure applications in the cloud.
The Data Layer (RDS Database)
With our network and security in place, it’s time to provision our database. We use Amazon RDS for a managed database experience, which handles patching, backups, and failover.
First, we create an AWS::RDS::DBSubnetGroup. This resource tells RDS which of our private subnets it is allowed to place the database in, ensuring it remains isolated.
Next, we define the AWS::RDS::DBInstance resource. This is the actual database server. Here we specify properties like:
DBInstanceClass: The size and power of the database server (e.g.,
db.t3.micro).Engine: The database technology we want to use, such as "MySQL" or "PostgreSQL".
MasterUsername: The username for the admin account.
For the MasterUserPassword, a critical note on security: in a real world project, you must never hardcode a password in a template. The best practice is to use AWS Secrets Manager to generate and securely store the password, then reference it in your template.
The Application Layer (EC2 Auto Scaling Group)
This is the workhorse layer that runs our application code. We need it to be both resilient and scalable.
We start by defining an AWS::AutoScaling::LaunchTemplate. Think of this as the master blueprint or cookie cutter for our servers. It specifies everything a new EC2 instance needs to know to get started:
ImageId: The Amazon Machine Image (AMI) to use, which is the operating system and base software.
InstanceType: The server size (e.g.,
t3.micro).UserData: A powerful script that runs automatically when the instance first boots. You use this to install your application, download dependencies, and start your web server.
With the template ready, we create an AWS::AutoScaling::AutoScalingGroup. This resource is the brilliant manager of our application fleet. It uses the launch template to ensure a desired number of instances are always running. If an instance becomes unhealthy, the auto scaling group terminates it and launches a fresh one. If traffic spikes, it can automatically add more instances to handle the load. This is the key to a truly resilient and elastic application.
The Presentation Layer (Application Load Balancer)
Finally, we need to create the front door for our application. The Application Load Balancer provides a single, stable entry point for all user traffic.
AWS::ElasticLoadBalancingV2::LoadBalancer: This resource creates the ALB itself. We configure it to live in our public subnets, so it's reachable from the internet.AWS::ElasticLoadBalancingV2::TargetGroup: This creates a logical grouping of our application instances. The ALB forwards traffic to this group. The target group is also responsible for performing health checks on our EC2 instances, ensuring traffic is only sent to healthy servers.AWS::ElasticLoadBalancingV2::Listener: The listener is attached to the load balancer and "listens" for incoming connections on a specific port, like port 80 for HTTP. When it receives a connection, its rule is to forward the traffic to ourTargetGroup.
With the ALB in place, users connect to its stable DNS name, and the ALB handles the complex job of distributing traffic efficiently and reliably across our backend application servers.
Deployment Steps
With the complete CloudFormation template ready, deploying your entire infrastructure is as simple as running a few commands.
Save: Save the entire YAML code into a single file named
webapp-template.yml.Validate: Before deploying, it’s always a good idea to check your template for syntax errors using the AWS CLI.
aws cloudformation validate-template --template-body file://webapp-template.ymlDeploy: Use the deploy command to create your stack. The stack is the collection of all the resources defined in your template.
aws cloudformation deploy --template-file webapp-template.yml --stack-name MyWebAppStack --capabilities CAPABILITY_IAMClean Up: Once you are finished with the project, you can delete all the created resources with a single command to avoid incurring costs.
aws cloudformation delete-stack --stack-name MyWebAppStack
And there you have it! You've successfully defined and deployed a resilient, secure, and scalable three tier web application entirely as code. This project forms the bedrock of modern cloud application deployment.