In the Part 02 of this series, we setup AWS CLI and tested that we can access AWS resources using command line. Now we can use CloudFormation to provision our infrastructure.
Welcome to the fascinating world of Infrastructure as Code, IaC :-).
A new AWS account comes with some default networking components such as, a VPC, a Route table, a NACL, an Internet Gateway and 3 Subnets. However it is best to leave these default components to themselves and provision our own infrastructure from the scratch.
We will have 2 CloudFormation templates, one for the inception of networking and other bits and pieces and another one to deploy the host EC2 and its related components. I tweak this code whenever I get a chance, so please check the Github repository for the latest code.
Inception Template
In this template we export Security Group, Subnet IDs and R53 Hosted Zone ID from the stack so that we can use them for the host configuration using CloudFormation Fn::ImportValue function.
jayforweb.com is my test domain and the demo site is also live.
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the initial networking resources to deploy the Ghost blog host
Parameters:
VpcCidr:
Type: String
Default: 192.168.0.0/16
PublicSubnetCidr:
Type: String
Default: 192.168.0.0/20 # Have 16 subnets for us to go multi tier multi-az if needed.
HostedZone:
Type: String
Default: jayforweb.com
BucketName:
Type: String
Default: ghost.jayforweb.com
Resources:
MyGhostVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
Tags:
- Key: name
Value: Ghost Blog VPC
MyGhostSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref MyGhostVPC
CidrBlock: !Ref PublicSubnetCidr
Tags:
- Key: name
Value: Ghost Blog Subnet
MyGhostSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: SecurityGroup for the Ghost Blog
GroupName: MyGhostSG
VpcId: !Ref MyGhostVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- FromPort: '443'
IpProtocol: tcp
ToPort: '443'
CidrIp: 0.0.0.0/0
Tags:
- Key: name
Value: Ghost Blog SG
MyGhostIGW:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: name
Value: Ghost Blog InternetGateway
MyGhostVpcIGWAttachment:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
InternetGatewayId: !Ref MyGhostIGW
VpcId: !Ref MyGhostVPC
MyGhostRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref MyGhostVPC
Tags:
- Key: name
Value: Ghost Blog RouteTable
MyGhostRouteToIGW:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref MyGhostRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref MyGhostIGW
MyGhostSubnetToRoutTableAss:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
RouteTableId: !Ref MyGhostRouteTable
SubnetId: !Ref MyGhostSubnet
MyGhostS3Bucket:
Type: AWS::S3::Bucket
#DeletionPolicy: Retain
Properties:
BucketName: !Ref BucketName
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref MyGhostS3Bucket
PolicyDocument:
Statement:
-
Sid: GhostBucketAccess
Action:
- s3:*
Effect: Allow
Principal:
AWS:
- !Sub ${AWS::AccountId}
Resource:
- !Sub "arn:aws:s3:::${BucketName}"
MyGhostHostedZone:
Type: "AWS::Route53::HostedZone"
Properties:
HostedZoneConfig:
Comment: 'Hosted zone for jayforweb.com test domain'
Name: !Ref HostedZone
MyMailGunMxRecordSet:
Type: AWS::Route53::RecordSet
DependsOn: MyGhostHostedZone
Properties:
HostedZoneName : !Ref MyGhostHostedZone
Name: mg.jayforweb.com.
ResourceRecords:
- 10 mxa.mailgun.org.
- 10 mxb.mailgun.org.
TTL: '300'
Type: MX
MyMailGunOrgRecordSet:
Type: AWS::Route53::RecordSet
DependsOn: MyGhostHostedZone
Properties:
HostedZoneName : !Ref MyGhostHostedZone
Name: mg.jayforweb.com.
ResourceRecords:
- '"v=spf1 include:mailgun.org ~all"'
TTL: '300'
Type: TXT
MyMailGunDomainKeyRecordSet:
Type: AWS::Route53::RecordSet
DependsOn: MyGhostHostedZone
Properties:
HostedZoneName : !Ref MyGhostHostedZone
Name: smtp._domainkey.mg.jayforweb.com.
ResourceRecords:
- >-
"{YOUR VERY LONG DOMAIN KEY GOES HERE}"
TTL: '300'
Type: TXT
MyMailGunCNAMERecordSet:
Type: AWS::Route53::RecordSet
DependsOn: MyGhostHostedZone
Properties:
HostedZoneName : !Ref MyGhostHostedZone
Name: email.mg.jayforweb.com.
ResourceRecords:
- mailgun.org
TTL: '300'
Type: CNAME
Outputs:
MyGhostS3Bucket:
Description: S3 Bucket for config file storage and backups
Value: !Ref MyGhostS3Bucket
Export:
Name: PRIMARY-S3-BUCKET
MyHostedZone:
Description: Public hosted zone to hold DNS record sets
Value: !Ref MyGhostHostedZone
Export:
Name: PRIMARY-PUBLIC-HOSTED-ZONE
MyGhostSG:
Description: Security Group for the ghost host
Value: !Ref MyGhostSG
Export:
Name: PRIMARY-GHOST-SG
MyGhostSubnet:
Description: Subnet for the ghost host
Value: !Ref MyGhostSubnet
Export:
Name: PRIMARY-GHOST-SUBNET
I use a Makefile with few tasks so that its easier for us to execute AWS CLI commands with ease. The current Makefile looks like this.
REGION=ap-southeast-2
create-inception-stack:
aws cloudformation create-stack \
--region $(REGION) \
--profile blog-admin \
--stack-name $(STACK_NAME) \
--template-body file://cloudformation/templates/inception.cfn.yaml
update-inception-stack:
aws cloudformation update-stack \
--region $(REGION) \
--profile blog-admin \
--stack-name $(STACK_NAME) \
--template-body file://cloudformation/templates/inception.cfn.yaml
delete-stack:
aws cloudformation delete-stack \
--profile blog-admin \
--stack-name $(STACK_NAME)
Now its time to test it out. With make all we have to do now is to call the target create-inception-stack with the STACK_NAME=nameofmystack.
make create-inception-stack STACK_NAME=mystack

We can check the AWS console to see how it goes on that end.

This is great!. Now we have the basic setup in place for us to deploy the Ghost host EC2.
Ghost Blog Setup Template
In this template we are using the CloudFormation export values from the previous stack deployment. The Security Group and the Subnet ID values are imported using Fn::ImportValue function. One thing to keep in mind is that the CloudFormation Export values should be unique for the account. At work we follow ExportNamePrefix-ExportName-InstallationNumber pattern to keep everything in order.
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the Ghost blog host
# inception.cfn.yaml template should be deployed before this template
# as it creates the VPC and the rest of the networking resources.
Parameters:
# Fetch the latest AMI without hard-coding the image id
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
Resources:
# SSM to manage the Ghost host
MyGhostHostManagementRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
MyGhostHostInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref MyGhostHostManagementRole
MyGhostHostInstance:
Type: AWS::EC2::Instance
Properties:
IamInstanceProfile: !Ref MyGhostHostInstanceProfile
InstanceType: t2.micro
ImageId: !Ref LatestAmiId
NetworkInterfaces:
- AssociatePublicIpAddress: "true"
DeviceIndex: "0"
GroupSet:
- Fn::ImportValue: PRIMARY-GHOST-SG
SubnetId:
Fn::ImportValue: PRIMARY-GHOST-SUBNET
Tags:
- Key: Name
Value: Ghost Blog Host Instance
Now we have to update our Makefile with new targets to handle the ghost-blog-setup.cfn template.
REGION=ap-southeast-2
create-inception-stack:
aws cloudformation create-stack \
--region $(REGION) \
--profile blog-admin \
--stack-name $(STACK_NAME) \
--template-body file://cloudformation/templates/inception.cfn.yaml
create-blog-host-stack:
aws cloudformation create-stack \
--region $(REGION) \
--profile blog-admin \
--stack-name $(STACK_NAME) \
--capabilities CAPABILITY_NAMED_IAM \
--template-body file://cloudformation/templates/ghost-blog-setup.cfn.yaml
update-inception-stack:
aws cloudformation update-stack \
--region $(REGION) \
--profile blog-admin \
--stack-name $(STACK_NAME) \
--template-body file://cloudformation/templates/inception.cfn.yaml
update-blog-host-stack:
aws cloudformation update-stack \
--region $(REGION) \
--profile blog-admin \
--stack-name $(STACK_NAME) \
--capabilities CAPABILITY_NAMED_IAM \
--template-body file://cloudformation/templates/ghost-blog-setup.cfn.yaml
delete-stack:
aws cloudformation delete-stack \
--profile blog-admin \
--stack-name $(STACK_NAME)
Since we already deployed the inception stack we can now go ahead and deploy the ghost-blog-setup template.
This time the command should be
make create-blog-host-stack STACK_NAME=ghost-blog-test-1

No errors is a good news. From the console, we can check if everything is in place as expected.

Both stacks are in CREATE_COMPLETE state. We can test few more things. SSM is a must to maintain our instance. We will setup SSM from CLI at some point. For now, we can verify if we can get to our EC2 using SSM.
Go to AWS Systems Manager console and click on Managed Instances menu item from the left panel. You should see our new EC2 instance is listed there.

The beauty of CloudFormation shines when we want to delete our resources. If we did Click-Ops (create resources using the AWS console) then it will be a nightmare to keep track of all the resources and delete them one by one, like doing a treasure hunt. With CloudFormation, all we have to do is just delete the stack!
Once we have everything is in place, the project looks like below in the VSCode.

In case you want to try out the entire stack, have a look at my Github repository.
In the next part of this effort, let's update our Ghost host and configure Docker and other bits so that we can have a live blog running.
See you again!