Automate application deployment with AWS CloudFormation Part 3 - Launch Configuration

This blog is part of a multi-blog series on AWS CloudFormation. 

Part 1 of this blog series provided a brief CloudFormation introduction, introduced the CloudFormation template, and then discussed the "Parameters" section of the CloudFormation template.

Part 2 discussed the "Resources" section of the CloudFormation template.

The CloudFormation template used in the running example is for my personal website which I recently migrated into the AWS cloud.

Click here to download the entire CloudFormation template for the www.robhughes.net (RHDN for short) website. I recommend opening up the RHDN CloudFormation template in a separate window/tab so you can quickly refer to the contents throughout this series of blogs. 

Most CloudFormation templates I have viewed come with an embedded warning which I will dutifully repeat here:

WARNING: This template creates one or more Amazon EC2 instances, an Elastic Load Balancer and an Amazon RDS DB instance. You will be billed for the AWS resources used if you create a stack from this template.

If you use the RHDN template to create a CloudFormation stack you could incur charges! See the What is the AWS Free Usage Tier regarding how you can get started using AWS for free.

Figure 1 below contains a representation of the various stages in the lifecycle of a CloudFormation resource stack.

Figure 1. CloudFormation Stack Lifecycle

Figure 1\. Cloudformation Stack Lifecycle

This blog will focus on stage three "Launch Config". The Part 2 blog provided an overview of the "Resources" section of the CloudFormation template. Most resources defined within the "Resources" section of the template drive the creation of resources such as load balancers, RDS DB instances, security groups, autoscale groups, autoscaling policies, alarms, etc. Those type of resources tend to be "one time" resources and come into existence at the time the stack is created and live for the lifetime of the stack. Other resources are dynamic in that they come into existence after the "one time" resources and are destroyed and replaced by other dynamic resources of the lifetime of the stack. The best example of this is EC2 instances within an autoscaling group. Multiple EC2 instances may need to be provisioned at stack creation to meet the initial minimum/desired number of instances for the associated autoscaling group. Over the lifetime of the stack the number of EC2 instances may grow or shrink depending on demand.

Each EC2 instance may require configuration or application/software components to be installed before the EC2 instance is ready to be used as part of an autoscaling group.

CloudFormation provides the powerful CloudInit capability, along with the cfn-init tool, to customize the build-out of EC2 instances.

The hooks for this capability start with special resources in the Resources section of the CloudFormation template where the value of the "Type" key is of type  "AWS::AutoScaling::LaunchConfiguration" or "AWS::EC2::Instance". Figure 2 shows the AWS::AutoScaling::LaunchConfiguration type and the list of possible properties for the type as well.

Figure 2: AWS::AutoScaling::LaunchConfiguration Type

{
   "Type" : "AWS::AutoScaling::LaunchConfiguration",
   "Properties" : {
      "AssociatePublicIpAddress" : Boolean,
      "BlockDeviceMappings" : [ BlockDeviceMapping, ... ],
      "EbsOptimized" : Boolean,
      "IamInstanceProfile" : String,
      "ImageId" : String,
      "InstanceId" : String,
      "InstanceMonitoring" : Boolean,
      "InstanceType" : String,
      "KernelId" : String,
      "KeyName" : String,
      "RamDiskId" : String,
      "SecurityGroups" : [ SecurityGroup, ... ],
      "SpotPrice" : String,
      "UserData" : String
   }
} 

Some of the more interesting properties are the "InstanceType", "KeyName", and "SecurityGroups" properties. The "InstanceType" key is used to configure the EC2 instance type of all EC2 instances created using this launch configuration. The "KeyName" key is used to configure the SSH key that will be permitted ssh access to the running EC2 instance. The "SecurityGroups" key allows configuration of the security groups in which each EC2 instance will be placed. These are the same properties that can be configured when creating EC2 instances via the AWS console, command line (CLI) tools, or software development kits (SDKs). CloudFormation uses resources with the AWS::AutoScaling::LaunchConfiguration and AWS::EC2::Instance type to perform this configuration.

One additional property, the "UserData" property, provides the ability to customize the configuration of EC2 instances after they are created. More on that in a bit.

The RHDN CloudFormation template declares an autoscaling group resource with logical ID "WebServerGroup" and type "AWS::AutoScaling::AutoScalingGroup". Predictably, when the stack is created an autoscaling group resource will be created as a result of this declaration:

    "WebServerGroup" : {
      "Type" : "AWS::AutoScaling::AutoScalingGroup",
      "Properties" : {
        "AvailabilityZones" : { "Fn::GetAZs" : "" },
        "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
        "MinSize" : "1",
        "MaxSize" : "5",
        "DesiredCapacity" : { "Ref" : "WebServerCapacity" },
        "LoadBalancerNames" : [ { "Ref" : "ElasticLoadBalancer" } ]
      }
    },

One property of the autoscaling group resource is the key with name "LaunchConfigurationName". The value of this key points to a special resource of type AWS::AutoScaling::LaunchConfiguration. In this case the CloudFormation intrinsic Ref function is used to look up the value of the resource with logical ID "LaunchConfig". For RHDN this resource is declared as:

"LaunchConfig": {
  "Type" : "AWS::AutoScaling::LaunchConfiguration",
  "Metadata" : {
    "Comment1" : "Configure the bootstrap helpers to install Apache httpd and the Rails application.",
    "Comment2" : "The application is cloned from gitlab.",
    "AWS::CloudFormation::Init": {
      <snip>
    }
  },
  "Properties" : {
    <snip>
    "UserData": {
      <snip>
    }
  }
}

Resources of type "AWS::AutoScaling::LaunchConfiguration" or "AWS::EC2::Instance", resources which drive creation of EC2 instance, can take advantage of the powerful CloudInit tool to customize EC2 instances. There is a lot going on in this resource declaration. Brush up on your JSON reading skills and peruse the entire LaunchConfig resource within the RHDN template here. The remainder of this blog will highlight two important sections within this resource declaration. First the "UserData" property within the "Properties" section provides a means to pass user data to each running EC2 instance. The AWS::CloudFormation::Init metadata attribute, used by cfn-init, within the Metadata key for the resource declaration provides an additional method to further customize the bootstrap of applications running on the EC2 instance.

The value of the "UserData" key must be a Base64 encoded string. The AWS Linux AMI (used for RHDN) and Ubuntu AMIs come with the CloudInit tool pre-installed. The CloudInit tool is a able to interpret the user data string passed to the running EC2 instance and act accordingly depending on the content. If the string starts with "#!" then some extra magic happens. In this case CloudInit will interpret the content as a shell script and write the string to a file on the running EC2 instance and then execute the script as as the root user. This simple capability provides a very useful hook to begin the application bootstrap process for an EC2 instance. This script can take advantage of native operating system commands to install packages, install ruby gems, compile code, etc. However it does have a drawback. The value of the UserData key within the resource declaration must conform to the structure of a JSON document. In addition it is likely you will want to make use of various CloudFormation intrinsic functions to retrieve the value of parameters and other resources within the CloudFormation template. Creating a script within this string can be tedious with all the escaping that is needed to make the string conform to JSON syntax. Because of this you may want to do as little as possible with this script. Basically just enough to be able to invoke the cfn-init script. The script provided in the UserData section for the LaunchConfig resource invokes the cfn-init script and passes in a reference to the RHDN CloudFormation stack ID and a reference to the Logical ID of the special LaunchConfig resource within that stack. That is enough information for cfn-init to take over and continue the process of bootstrapping the application on the running EC2 instance.

cfn-init is one of a number of complementary scripts provided by the aws-cfn-bootstrap package on the AWS Linux AMI.

The cfn-­init helper script reads template metadata from the AWS::CloudFormation::Init key and acts accordingly to:

  • Fetch and parse metadata from CloudFormation.
  • Retrieve files stored external to the EC2 instance.
  • Install packages.
  • Write files to disk.
  • Enable/disable and start/stop services, etc.

You can use the  AWS::CloudFormation::Init type to include bootstrap related configuration metadata used by the cfn-­init helper script.  The cfn-init script looks for resource metadata rooted in the AWS::CloudFormation::Init metadata key. As mentioned above you pass in a reference to the resource that contains the Metadata attributes which contain the AWS::CloudFormation::Init key which helps cfn-init discover this information.

A barebones use of the "AWS::CloudFormation::Init" key is included below to help show the types of things that can be configured:

"Resources": {
  "MyInstance": {
    "Type": "AWS::AutoScaling::LaunchConfiguration",
    "Metadata" : {
      "AWS::CloudFormation::Init" : {
        "config" : {
          "packages" : {
            :
          },
          "groups" : {
            :
          },
          "users" : {
            :
          },
          "sources" : {
            :
          },
          "files" : {
            :
          },
          "commands" : {
            :
          },
          "services" : {
            :
          }
        }
      }
    },
    "Properties": {
      :
    }
  }
}

The cfn-init helper script processes these configuration sections in the following order: packages, groups, users, sources, files, commands, and then services. 

See the AWS::CloudFormation::Init documentation for in-depth information and sample use of each of the above configuration sections.

The RHDN website takes advantage of a number of the available configuration sections.

The "packages" section is used to install Apache httpd and the ruby and rubygem packages.

The "sources" section is used to retrieve the CloudFormationRailsRHDN.zip archive file stored in an S3 bucket. The archive file is expanded into the /home/ec2-user/rhdnzip directory on the server and contains the bootstrap.sh script and a pre-compiled Passenger module for Apache httpd. I found that compiling the Passenger module on a t1.micro instance could take around 20 minutes. Since this would delay the amount of time it takes an EC2 instance to be provisioned and become a usable member of the autoscaling group I instead opted to compile the module once and store all the pre-compiled bits in the CloudFormationRailsRHDN.zip file and then just retrieve them when an EC2 instance is created. The bootstrap.sh file does the heavy lifting of bootstrapping the RHDN web application.  Alternatively this logic could have been placed in the script in the UserData section of the LaunchConfig resource but that would have been tedious to write and painful to debug. Crack open the CloudFormationRailsRHDN.zip archive file if you want view the contents of the bootstrap.sh script.

The "files" section was handy to generate the database.yml file used by the Ruby On Rails application to connect to the database instance. This section suffers from the same pitfall as it requires embedding YAML logic within a JSON document. However it is less tedious to get right as you can copy and paste logic from freely able code snippets online.

The use of the "UserData" section within "AWS::AutoScaling::LaunchConfiguration" or "AWS::EC2::Instance" resource types and the  "AWS::CloudFormation::Init" metadata attribute within the Metadata section in conjunction with the cfn-init script provide a rich set of options to bootstrap applications/services running on EC2 instances.

The approach used in the running RHDN example uses a very dynamic approach in that it starts with a barebones AWS Linux AMI and installs/configures all the necessary application components after the EC2 instance is created. It is a little bit of a hybrid approach in that the Apache httpd Passenger module is pre-compiled and pulled down to the instance to shorten provisioning time. At the other extreme is a more static approach where you build out an EC2 instance with all the necessary application components then take a snapshot which becomes the gold image AMI from which your EC2 instances are created. This approach tends to allow EC2 instances to start up faster but does require additional configuration management to manage versions of AMIs for newer versions of the application when updates occur.

Speaking of updates, CloudFormation templates can be updated after the stack is created and those updates will be applied to the running stack. Existing EC2 instances will not be updated but newly provisioned EC2 instances will use the updated CloudFormation template.

To summarize: CloudFormation templates allow you create configuration driven recipes to automate creation of entire software stacks. Copies of an application/service can be stamped out in cookie cutter fashion using AWS management console, various CLI tools, and SDKs. Common configuration settings can be factored out as CloudFormation parameters to allow stacks to be customized at creation time.