Continuous Delivery in the Cloud: CloudFormation (Part 3 of 6)

In part 1 of this series, I introduced the Continuous Delivery (CD) pipeline for the Manatee Tracking application. In part 2 I went over how we use this CD pipeline to deliver software from checkin to production. A list of topics for each of the articles is summarized below.Part 1: Introduction – introduction to continuous delivery in the cloud and the rest of the articles;Part 2: CD Pipeline – In-depth look at the CD Pipeline
Part 3: CloudFormation – What you’re reading nowPart 4: Dynamic Configuration – “Property file less” infrastructure;Part 5: Deployment Automation – Scripted deployment orchestration;Part 6: Infrastructure Automation – Scripted environment provisioning (Infrastructure Automation)
In this part of the series, I am going to explain how we use CloudFormation to script our AWS infrastructure and provision our Jenkins environment.What is CloudFormation?CloudFormation is an AWS offering for scripting AWS virtual resource allocation. A CloudFormation template is a JSON script which references various AWS resources that you want to use. When the template runs, it will allocate the AWS resources accordingly.
A CloudFormation template is split up into four sections:

Parameters: Parameters are values that you define in the template. When creating the stack through the AWS console, you will be prompted to enter in values for the Parameters. If the value for the parameter generally stays the same, you can set a default value. Default values can be overridden when creating the stack. The parameter can be used throughout the template by using the “Ref” function.

Mappings: Mappings are for specifying conditional parameter values in your template. For instance you might want to use a different AMI depending on the region your instance is running on. Mappings will enable you to switch AMIs depending on the region the instance is being created in.

Resources: Resources are the most vital part of the CloudFormation template. Inside the resource section, you define and configure your AWS components.

Outputs: After the stack resources are created successfully, you may want to have it return values such as the IP address or the domain of the created instance. You use Outputs for this. Outputs will return the values to the AWS console or command line depending on which medium you use for creating a stack.

CloudFormation parameters, and resources can be referenced throughout the template. You do this using intrinsic functions, Ref, Fn::Base64, Fn::FindInMap, Fn::GetAtt, Fn::GetAZs and Fn::Join. These functions enable you to pass properties and resource outputs throughout your template – reducing the need for most hardcoded properties (something I will discuss in part 4 of this series, Dynamic Configuration).How do you run a CloudFormation template?
You can create a CloudFormation stack using either the AWS Console, CloudFormation CLI tools or the CloudFormation API.Why do we use CloudFormation?
We use CloudFormation in order to have a fully scripted, versioned infrastructure. From the application to the virtual resources, everything is created from a script and is checked into version control. This gives us complete control over our AWS infrastructure which can be recreated whenever necessary.CloudFormation for Manatees
In the Manatee Infrastructure, we use CloudFormation for setting up the Jenkins CD environment. I am going to go through each part of the jenkins template and explain its use and purpose. In template’s lifecycle, the user launches the stack using the jenkins.template and enters in the Parameters. The template then starts to work:
1. IAM User with AWS Access keys is created
2. SNS Topic is created
3. CloudWatch Alarm is created and SNS topic is used for sending alarm notifications
4. Security Group is created
5. Wait Condition created
6. Jenkins EC2 Instance is created with the Security Group from step #4. This security group is used for port configuration. It also uses AWSInstanceType2Arch and AWSRegionArch2AMI to decide what AMI and OS type to use
7. Jenkins EC2 Instance runs UserData script and executes cfn_init.
8. Wait Condition waits for Jenkins EC2 instance to finish UserData script
9. Elastic IP is allocated and associated with Jenkins EC2 instance
10. Route53 domain name created and associated with Jenkins Elastic IP
11. If everything creates successfully, the stack signals complete and outputs are displayed
Now that we know at a high level what is being done, lets take a deeper look at what’s going on inside the jenkins.template.

Parameters

Email: Email address that SNS notifications will be sent. When we create or deploy to target environments, we use SNS to notify us of their status.

ApplicationName: Name of A Record created by Route53. Inside the template, we dynamically create a domain with A record for easy access to the instance after creation. Example: jenkins.integratebutton.com, jenkins is the ApplicationName

HostedZone: Name of Domain used Route53. Inside the template, we dynamically create a domain with A record for easy access to the instance after creation. Example: jenkins.integratebutton.com, integratebutton.com is the HostedZone.

KeyName: EC2 SSH Keypair to create the Instance with. This is the key you use to ssh into the Jenkins instance after creation.

InstanceType: Size of the EC2 instance. Example: t1.micro, c1.medium

S3Bucket: We use a S3 bucket for containing the resources for the Jenkins template to use, this parameter specifies the name of the bucket to use for this.

These Mappings are used to define what type of operating system architecture and AWS AMI (Amazon Machine Image) ID to use to use based upon the Instance size. The instance size is specified using the Parameter InstanceType
The conditional logic to interact with the Mappings is done inside the EC2 instance.
"ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },

When possible, we try to use cfn_init rather than UserData bash commands because it stores a detailed log of Cfn events on the instance.AWS::EC2::SecurityGroup
When creating a Jenkins instance, we only want certain ports to be open and only open to certain users. For this we use Security Groups. Security groups are firewall rules defined at the AWS level. You can use them to set which ports, or range of ports to be opened. In addition to defining which ports are to be open, you can define who they should be open to using CIDR.
"FrontendGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"GroupDescription" : "Enable SSH and access to Apache and Tomcat",
"SecurityGroupIngress" : [
{"IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : "0.0.0.0/0"},
{"IpProtocol" : "tcp", "FromPort" : "8080", "ToPort" : "8080", "CidrIp" : "0.0.0.0/0"},
{"IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"}
]
}
},

In this security group we are opening ports 22, 80 and 8080. Since we are opening 8080, we are able to access Jenkins at the completion of the template. By default, ports on an instance are closed, meaning these are necessary to be specified in order to have access to Jenkins.AWS::EC2::EIP
When an instance is created, it is given a public DNS name similar to: ec2-107-20-139-148.compute-1.amazonaws.com. By using Elastic IPs, you can associate your instance an IP rather than a DNS.
"IPAddress" : {
"Type" : "AWS::EC2::EIP"
},
"IPAssoc" : {
"Type" : "AWS::EC2::EIPAssociation",
"Properties" : {
"InstanceId" : { "Ref" : "WebServer" },
"EIP" : { "Ref" : "IPAddress" }
}
},

There are many reasons an instance can become unavailable. CloudWatch is used to monitor instance usage and performance. CloudWatch can be set to notify specified individuals if the instance experiences higher than normal CPU utilization, disk usage, network usage, etc. In the Manatee infrastructure we use CloudWatch to monitor disk utilization and notify team members if it reaches 90 percent.
If the Jenkins instance goes down, our CD pipeline becomes temporarily unavailable. This presents a problem as the development team is temporarily blocked from testing their code. CloudWatch helps notify us if this is an impending problem..AWS::CloudFormation::WaitConditionHandle, AWS::CloudFormation::WaitCondition
Wait Conditions are used to wait for all of the resources in a template to be completed before signally template success.
"WaitHandle" : {
"Type" : "AWS::CloudFormation::WaitConditionHandle"
},
"WaitCondition" : {
"Type" : "AWS::CloudFormation::WaitCondition",
"DependsOn" : "WebServer",
"Properties" : {
"Handle" : { "Ref" : "WaitHandle" },
"Timeout" : "990"
}
}

When creating the instance, if a wait condition is not used, CloudFormation won’t wait for the completion of the UserData script. It will signal success if the EC2 instance is allocated successfully rather than waiting for the UserData script to run and signal success.

For instance with the InstanceIPAddress, we are refernceing the IPAddress resource which happens to be the Elastic IP. This will return the Elastic IP address to the CloudFormation console.
CloudFormation allows us to completely script and version our infrastructure. This enables our infrastructure to be recreated the same way every time by just running the CloudFormation template. Because of this, your environments can be run in a Continuous integration cycle, rebuilding with every change in the script.
In the next part of our series – which is all about Dynamic Configuration – we’ll go through building your infrastructure to only require a minimal amount of hard coded properties if any. In this next article, you’ll see how you can use CloudFormation to build “property file less” infrastructure.
Resources: