Provisioning an Apache HTTP Server with AWS CloudFormation
From the AWS CloudFormation homepage:
"CloudFormation allows you to use a simple text file to model and provision, in an automated and secure manner, all the resources needed for your applications across all regions and accounts. This file serves as the single source of truth for your cloud environment."
CloudFormation is a web service that falls under the Infrastructure-as-Code (IaC) category. IaC allows users to define their infrastructure in a text file using a declarative approach to model your infrastructure. The text file is called "template" and is written in either JSON or YAML notation.
You may also enjoy: Using CloudFormation to Set Up Scalable Apps
In this post, I am going to share two templates file written in YAML which, when executed, provision an EC2 instance and install an Apache HTTP server without you logging in to the EC2 terminal. This can be used for beginners to get started with CloudFormation and add more configurations.
FAQs
Why Not Have Them All In One File?
It is true that you can declare all the resources in a single file, but as a best practice, it is always recommended to modularize your IaC. This helps in individual testing, deployment, the controlled impact of change, and most importantly, sharing. As an example, if there is an issue with my lambda deployment or EC2 deployment, then it helps and saves time to rollback or update only these components and leave the VPC and database as they are.
Why YAML?
To be honest, I have worked a lot on JSON (still using it in my current project), but after reading people's experience and trying both myself, I personally think YAML is suited better for a template. You don't see double-quotes and curly braces in YAML, which makes it more readable, plus you can add a lot of comments. Anyway, if you are comfortable with JSON, then go for it because YAML needs some learning (mine is still in progress).
So, let's get started. These template files are also available on GitHub for your reference.
Step 1
Create a base stack that consists of a VPC, RouteTable, NACL, IGW, and a Subnet. Based on your region, please provide the appropriate parameter values in the CloudFormation dashboard.
AWSTemplateFormatVersion: 2010-09-09
Description:
Sample template to create a VPC with IGW and public IP enabled.
You will be billed for the AWS resources used if you create a stack from this template.
After deleting stack, remember to delete the associated S3 bucket.
Parameters:
# CIDR of VPC
NetworkCIDR:
Description: CIDR of the new VPC
Type: String
Default: 10.0.0.0/16
# AZ Name where subnet will be created
AvailabilityZoneName:
Description: CIDR of the new VPC
Type: AWS::EC2::AvailabilityZone::Name
Default: ap-south-1a
# CIDR of new subnet within this VPC
SubnetCIDR:
Description: CIDR of the new subnet within this VPC
Type: String
Default: 10.0.1.0/24
Resources:
# create VPC
myVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref NetworkCIDR
EnableDnsHostnames: 'true'
EnableDnsSupport: 'true'
InstanceTenancy: default
Tags:
- Key: Name
Value: demo-vpc
- Key: Application
Value: !Ref 'AWS::StackName'
# create Internet Gateway
myIGW:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: demo-igw
- Key: Application
Value: !Ref 'AWS::StackName'
# attaching the IGW to my VPC
vpcToIgw:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref myVPC
InternetGatewayId: !Ref myIGW
# create a custom route table for demo vpc
myRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref myVPC
Tags:
- Key: Name
Value: demo-public-route-table
- Key: Application
Value: !Ref 'AWS::StackName'
# Add routes entries for public network through igw
myRoutes:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref myRouteTable
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref myIGW
# NACL
myPublicNACL:
Type: 'AWS::EC2::NetworkAcl'
Properties:
VpcId: !Ref myVPC
Tags:
- Key: Name
Value: demo-vpc-nacl
- Key: Application
Value: !Ref 'AWS::StackName'
# Allow all Incoming TCP traffic
myNaclRulesForInboundTCP:
Type: 'AWS::EC2::NetworkAclEntry'
Properties:
NetworkAclId: !Ref myPublicNACL
RuleNumber: '100'
Protocol: '6' # tcp
RuleAction: allow
Egress: 'false' # this rule applies to ingress traffic to the subnet
CidrBlock: 0.0.0.0/0 # any ip address
PortRange:
From: '0'
To: '65535'
# Allow all Outgoing TCP traffic
myNaclRulesForOutboundTCP:
Type: 'AWS::EC2::NetworkAclEntry'
Properties:
NetworkAclId: !Ref myPublicNACL
RuleNumber: '100'
Protocol: '6' # tcp
RuleAction: allow
Egress: 'true' # this rule applies to egress traffic from the subnet
CidrBlock: 0.0.0.0/0
PortRange:
From: '0' # client will be using ephemeral port, using 80 or 22 here will not work
To: '65535'
# creating a public subnet
myPublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref myVPC
AvailabilityZone: !Ref AvailabilityZoneName
CidrBlock: !Ref SubnetCIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value:
!Join
- ''
- - 'public-subnet-'
- !Ref AvailabilityZoneName
- Key: Application
Value: !Ref 'AWS::StackName'
# asscoiate subnet with our route table else by default it is asscoiated with main route table
mySubnetRouteTableAssociation:
Type: 'AWS::EC2::SubnetRouteTableAssociation'
Properties:
SubnetId: !Ref myPublicSubnet
RouteTableId: !Ref myRouteTable
# associate subnet with NACL else by default it is asscoiated with main NACLs
mySubnetNaclAssociation:
Type: 'AWS::EC2::SubnetNetworkAclAssociation'
Properties:
SubnetId: !Ref myPublicSubnet
NetworkAclId: !Ref myPublicNACL
# output key resources ids and export the values for cross-stack referencing
Outputs:
VpcID:
Description: ID of the newly created VPC
Value: !Ref myVPC
Export:
Name: !Sub "${AWS::StackName}-VPCID" # the name for cross referencing
PublicSubnetID:
Description: SubnetId of the public subnet
Value: !Ref myPublicSubnet
Export:
Name: !Sub "${AWS::StackName}-SUBNET"
Step 2
Create the second stack that consists of a Security Group, rules to allow HTTP and SSH, an EC2 instance, and a user-data to install the Apache HTTP server. This stack will reference the base stack. Please provide the correct image ID which is available in your region. The default value is from the Mumbai (ap-south) region.
AWSTemplateFormatVersion: 2010-09-09
Description:
Sample template to provision an EC2 Instance with public IP. Create a Security Group and associate with this EC2.
You will be billed for the AWS resources used if you create a stack from this template.
After deleting stack, remember to delete the associated S3 bucket.
# get the name of the base stack which is created first and has VPC details
Parameters:
VPCStackName:
Description: Name of the base VPC stack
Type: String
Default: BaseStack
KeyPairName:
Description: Name of an existing EC2 KeyPair to enable SSH access to the instance
Type: 'AWS::EC2::KeyPair::KeyName' # standard type
ConstraintDescription: must be the name of an existing EC2 KeyPair.
InstanceType:
Description: EC2 instance type
Type: String
Default: t2.micro
InstanceImageId:
Description: EC2 Image Id from this region
Type: AWS::EC2::Image::Id
Default: ami-0cb0e70f44e1a4bb5 # defaults for amazon linux in mumbai region
Resources:
# create a security group
mySG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable http(80) & ssh(22) access
GroupName: WebServer-SG
VpcId:
Fn::ImportValue: !Sub "${VPCStackName}-VPCID" # note here we are not using AWS::StackName
SecurityGroupIngress:
# allow http
- IpProtocol: tcp
FromPort: '80'
ToPort: '80'
CidrIp: 0.0.0.0/0 # any IP
# allow ssh
- IpProtocol: tcp
FromPort: '22'
ToPort: '22'
CidrIp: 0.0.0.0/0 # only for demo else use your IP or corporate gateway IP
Tags:
- Key: Name
Value: demo-sg
- Key: Application
Value:
Ref: "AWS::StackName"
# allow local traffic
SGBaseIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref mySG
IpProtocol: '-1'
FromPort: '-1'
ToPort: '-1'
SourceSecurityGroupId: !Ref mySG
# EC2 instance which will have access for http and ssh
EC2Instance:
Type: 'AWS::EC2::Instance'
Properties:
InstanceType: !Ref InstanceType
SubnetId:
Fn::ImportValue: !Sub "${VPCStackName}-SUBNET"
SecurityGroupIds:
- !Ref mySG
KeyName: !Ref KeyPairName
ImageId: !Ref InstanceImageId
UserData:
Fn::Base64: |
#!/bin/bash -xe
yum update -y # good practice to update existing packages
yum install -y httpd # install web server
systemctl start httpd
systemctl enable httpd
echo "Hello World" > /var/www/html/index.html
Tags:
- Key: Name
Value: demo-ec2
- Key: Application
Value:
Ref: "AWS::StackName"
# output important values for easy viewing in cloudformation dashboard
Outputs:
InstanceId:
Description: InstanceId of the first EC2 instance
Value: !Ref EC2Instance
PublicDNS:
Description: Public DNS Name of the EC2 instance
Value: !GetAtt
- EC2Instance
- PublicDnsName
PublicIP:
Description: Public IP address of the EC2 instance
Value: !GetAtt
- EC2Instance
- PublicIp
After provisioning, you can check the "Output" section of the CloudFormation dashboard to get the public IP and point your browser to this IP. Don't forget to delete the stack and the S3 bucket to avoid any cost.
Further Reading
Provision a Free AWS EC2 Instance in 5 Minutes
How to Secure an Apache Web Server