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 now
Part 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:

  1. 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.
  2. 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.
  3. Resources: Resources are the most vital part of the CloudFormation template. Inside the resource section, you define and configure your AWS components.
  4. 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.

 

Mappings


"Mappings" : {
  "AWSInstanceType2Arch" : {
    "t1.micro" : { "Arch" : "64" },
    "m1.small" : { "Arch" : "32" },
    "m1.large" : { "Arch" : "64" },
    "m1.xlarge" : { "Arch" : "64" },
    "m2.xlarge" : { "Arch" : "64" },
    "m2.2xlarge" : { "Arch" : "64" },
    "m2.4xlarge" : { "Arch" : "64" },
    "c1.medium" : { "Arch" : "64" },
    "c1.xlarge" : { "Arch" : "64" },
    "cc1.4xlarge" : { "Arch" : "64" }
  },
    "AWSRegionArch2AMI" : {
    "us-east-1" : { "32" : "ami-ed65ba84", "64" : "ami-e565ba8c" }
  }
},

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" ] } ] },


Resources

AWS::IAM::User

"CfnUser" : {
  "Type" : "AWS::IAM::User",
  "Properties" : {
    "Path": "/",
    "Policies": [{
      "PolicyName": "root",
      "PolicyDocument": { "Statement":[{
        "Effect":"Allow",
        "Action":"*",
        "Resource":"*"
        }
      ]}
    }]   }
},


"Type" : "AWS::IAM::AccessKey",
"Properties" : {
  "UserName" : { "Ref": "CfnUser" }
}

We create the AWS IAM user and then create the AWS Access and Secret access keys for the IAM user which are used throughout the rest of the template. Access and Secret access keys are authentication keys used to authenticate to the AWS account.
AWS::SNS::Topic

"MySNSTopic" : {
  "Type" : "AWS::SNS::Topic",
  "Properties" : {
    "Subscription" : [ {
      "Endpoint" : { "Ref": "Email" },
      "Protocol" : "email"
    } ]   }
},

SNS is a highly available solution for sending notifications. In the Manatee infrastructure it is used for sending notifications to the development team.
AWS::Route53::RecordSetGroup

"JenkinsDNS" : {
  "Type" : "AWS::Route53::RecordSetGroup",
  "Properties" : {
    "HostedZoneName" : { "Fn::Join" : [ "", [ {"Ref" : "HostedZone"}, "." ]]},
    "RecordSets" : [{
      "Name" : { "Fn::Join" : ["", [ { "Ref" : "ApplicationName" }, ".", { "Ref" : "HostedZone" }, "." ]]},
      "Type" : "A",
      "TTL" : "900",
      "ResourceRecords" : [ { "Ref" : "IPAddress" } ]     }]   }
},

Route53 is a highly available DNS service. We use Route53 to create domains dynamically using the given HostedZone and ApplicationName parameters. If the parameters are not overriden, the domain jenkins.integratebutton.com will be created. We then reference the Elastic IP and associate it with the created domain. This way the jenkins.integratebutton.com domain will route to the created instance
AWS::EC2::Instance
EC2 gives access to on-demand compute resources. In this template, we allocate a new EC2 instance and configure it with a Keypair, Security Group, and Image ID (AMI). Then for provisioning the EC2 instance we use the UserData property. Inside UserData we run a set of bash commands along with cfn_init. The UserData script is run during instance creation.

"WebServer": {
  "Type": "AWS::EC2::Instance",
  "Metadata" : {
    "AWS::CloudFormation::Init" : {
      "config" : {
        "packages" : {
          "yum" : {
            "tomcat6" : [],
            "subversion" : [],
            "git" : [],
            "gcc" : [],
            "libxslt-devel" : [],
            "ruby-devel" : [],
            "httpd" : []           }
        },
        "sources" : {
          "/opt/aws/apitools/cfn" : { "Fn::Join" : ["", ["https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/resources/aws_tools/cfn-cli.tar.gz"]]},
          "/opt/aws/apitools/sns" : { "Fn::Join" : ["", ["https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/resources/aws_tools/sns-cli.tar.gz"]]}
        },
        "files" : {
          "/usr/share/tomcat6/webapps/jenkins.war" : {
            "source" : "http://mirrors.jenkins-ci.org/war/1.480/jenkins.war",
            "mode" : "000700",
            "owner" : "tomcat",
            "group" : "tomcat",
            "authentication" : "S3AccessCreds"
          },
          "/usr/share/tomcat6/webapps/nexus.war" : {
            "source" : "http://www.sonatype.org/downloads/nexus-2.0.3.war",
            "mode" : "000700",
            "owner" : "tomcat",
            "group" : "tomcat",
            "authentication" : "S3AccessCreds"
          },
          "/usr/share/tomcat6/.ssh/id_rsa" : {
            "source" : { "Fn::Join" : ["", ["https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/private/id_rsa"]]},
            "mode" : "000600",
            "owner" : "tomcat",
            "group" : "tomcat",
            "authentication" : "S3AccessCreds"
          },
          "/home/ec2-user/common-step-definitions-1.0.0.gem" : {
            "source" : { "Fn::Join" : ["", ["https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/gems/common-step-definitions-1.0.0.gem"]]},
            "mode" : "000700",
            "owner" : "root",
            "group" : "root",
            "authentication" : "S3AccessCreds"
          },
          "/etc/cron.hourly/jenkins_backup.sh" : {
            "source" : { "Fn::Join" : ["", ["https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/jenkins_backup.sh"]]},
            "mode" : "000500",
            "owner" : "root",
            "group" : "root",
            "authentication" : "S3AccessCreds"
          },
          "/etc/tomcat6/server.xml" : {
            "source" : { "Fn::Join" : ["", ["https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/server.xml"]]},
            "mode" : "000554",
            "owner" : "root",
            "group" : "root",
            "authentication" : "S3AccessCreds"
          },
          "/usr/share/tomcat6/aws_access" : {
            "content" : { "Fn::Join" : ["", [
              "AWSAccessKeyId=", { "Ref" : "HostKeys" }, "n",
              "AWSSecretKey=", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]}
            ]]},
            "mode" : "000400",
            "owner" : "tomcat",
            "group" : "tomcat",
            "authentication" : "S3AccessCreds"
          },
          "/opt/aws/aws.config" : {
            "content" : { "Fn::Join" : ["", [
              "AWS.config(n",
              ":access_key_id => "", { "Ref" : "HostKeys" }, "",n",
              ":secret_access_key => "", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]}, "")n"
            ]]},
            "mode" : "000500",
            "owner" : "tomcat",
            "group" : "tomcat"
          },
          "/etc/httpd/conf/httpd.conf2" : {
            "content" : { "Fn::Join" : ["", [
              "NameVirtualHost *:80n",
              "n",
              "ProxyPass /jenkins http://", { "Fn::Join" : ["", [{ "Ref" : "ApplicationName" }, ".", { "Ref" : "HostedZone" }]] }, ":8080/jenkinsn",
              "ProxyPassReverse /jenkins http://", { "Fn::Join" : ["", [{ "Ref" : "ApplicationName" }, ".", { "Ref" : "HostedZone" }]] }, ":8080/jenkinsn",
              "ProxyRequests Offn",
              "n",
              "Order deny,allown",
              "Allow from alln",
              "n",
              "RewriteEngine Onn",
              "RewriteRule ^/$ http://", { "Fn::Join" : ["", [{ "Ref" : "ApplicationName" }, ".", { "Ref" : "HostedZone" }]] }, ":8080/jenkins$1 [NC,P]n",
""
            ]]},
            "mode" : "000544",
            "owner" : "root",
            "group" : "root"
          },
          "/root/.ssh/config" : {
            "content" : { "Fn::Join" : ["", [
              "Host github.comn",
              "StrictHostKeyChecking non"
            ]]},
            "mode" : "000600",
            "owner" : "root",
            "group" : "root"
          },
          "/usr/share/tomcat6/.route53" : {
            "content" : { "Fn::Join" : ["", [
              "access_key: ", { "Ref" : "HostKeys" }, "n",
              "secret_key: ", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]}, "n",
              "api: '2012-02-29'n",
              "endpoint: https://route53.amazonaws.com/n",
              "default_ttl: '3600'"
            ]]},
            "mode" : "000700",
            "owner" : "tomcat",
            "group" : "tomcat"
          }
        }
      }
    },
    "AWS::CloudFormation::Authentication" : {
      "S3AccessCreds" : {
        "type" : "S3",
        "accessKeyId" : { "Ref" : "HostKeys" },
        "secretKey" : {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]},
        "buckets" : [ { "Ref" : "S3Bucket"} ]       }
    }
  },
  "Properties": {
    "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
    "InstanceType" : { "Ref" : "InstanceType" },
    "SecurityGroups" : [ {"Ref" : "FrontendGroup"} ],
    "KeyName" : { "Ref" : "KeyName" },
    "Tags": [ { "Key": "Name", "Value": "Jenkins" } ],
    "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
      "#!/bin/bash -vn",
      "yum -y install java-1.6.0-openjdk*n",
      "yum update -y aws-cfn-bootstrapn",
      "# Install packagesn",
      "/opt/aws/bin/cfn-init -s ", { "Ref" : "AWS::StackName" }, " -r WebServer ",
      " --access-key ", { "Ref" : "HostKeys" },
      " --secret-key ", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]},
      " --region ", { "Ref" : "AWS::Region" }, " || error_exit 'Failed to run cfn-init'n",
      "# Copy Github credentials to root ssh directoryn",
      "cp /usr/share/tomcat6/.ssh/* /root/.ssh/n",
      "# Installing Ruby 1.9.3 from RPMn",
      "wget -P /home/ec2-user/ https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/resources/rpm/ruby-1.9.3p0-2.amzn1.x86_64.rpmn",
      "rpm -Uvh /home/ec2-user/ruby-1.9.3p0-2.amzn1.x86_64.rpmn",
      "cat /etc/httpd/conf/httpd.conf2 >> /etc/httpd/conf/httpd.confn",
      "# Install S3 Gemsn",
      "gem install /home/ec2-user/common-step-definitions-1.0.0.gemn",
      "# Install Public Gemsn",
      "gem install bundler --version 1.1.4 --no-rdoc --no-rin",
      "gem install aws-sdk --version 1.5.6 --no-rdoc --no-rin",
      "gem install cucumber --version 1.2.1 --no-rdoc --no-rin",
      "gem install net-ssh --version 2.5.2 --no-rdoc --no-rin",
      "gem install capistrano --version 2.12.0 --no-rdoc --no-rin",
      "gem install route53 --version 0.2.1 --no-rdoc --no-rin",
      "gem install rspec --version 2.10.0 --no-rdoc --no-rin",
      "gem install trollop --version 2.0 --no-rdoc --no-rin",
      "# Update Jenkins with versioned configurationn",
      "rm -rf /usr/share/tomcat6/.jenkinsn",
      "git clone git@github.com:stelligent/continuous_delivery_open_platform_jenkins_configuration.git /usr/share/tomcat6/.jenkinsn",
      "# Get S3 bucket publisher from S3n",
      "wget -P /usr/share/tomcat6/.jenkins/ https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/hudson.plugins.s3.S3BucketPublisher.xmln",
      "wget -P /tmp/ https://raw.github.com/stelligent/continuous_delivery_open_platform/master/config/aws/cd_security_group.rbn",
      "ruby /tmp/cd_security_group --securityGroupName ", { "Ref" : "FrontendGroup" }, " --port 5432n",
      "# Update main Jenkins confign",
      "sed -i 's@.*@", { "Ref" : "HostKeys" }, "@' /usr/share/tomcat6/.jenkins/hudson.plugins.s3.S3BucketPublisher.xmln",
      "sed -i 's@.*@", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]}, "@' /usr/share/tomcat6/.jenkins/hudson.plugins.s3.S3BucketPublisher.xmln",
      "# Add AWS Credentials to Tomcatn",
      "echo "AWS_ACCESS_KEY=", { "Ref" : "HostKeys" }, "" >> /etc/sysconfig/tomcat6n",
      "echo "AWS_SECRET_ACCESS_KEY=", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]}, "" >> /etc/sysconfig/tomcat6n",
      "# Add AWS CLI Toolsn",
      "echo "export AWS_CLOUDFORMATION_HOME=/opt/aws/apitools/cfn" >> /etc/sysconfig/tomcat6n",
      "echo "export AWS_SNS_HOME=/opt/aws/apitools/sns" >> /etc/sysconfig/tomcat6n",
      "echo "export PATH=$PATH:/opt/aws/apitools/sns/bin:/opt/aws/apitools/cfn/bin" >> /etc/sysconfig/tomcat6n",
      "# Add Jenkins Environment Variablen",
      "echo "export SNS_TOPIC=", { "Ref" : "MySNSTopic" }, "" >> /etc/sysconfig/tomcat6n",
      "echo "export JENKINS_DOMAIN=", { "Fn::Join" : ["", ["http://", { "Ref" : "ApplicationName" }, ".", { "Ref" : "HostedZone" }]] }, "" >> /etc/sysconfig/tomcat6n",
      "echo "export JENKINS_ENVIRONMENT=", { "Ref" : "ApplicationName" }, "" >> /etc/sysconfig/tomcat6n",
      "wget -P /tmp/ https://raw.github.com/stelligent/continuous_delivery_open_platform/master/config/aws/showback_domain.rbn",
      "echo "export SGID=`ruby /tmp/showback_domain.rb --item properties --key SGID`" >> /etc/sysconfig/tomcat6n",
      "chown -R tomcat:tomcat /usr/share/tomcat6/n",
      "chmod +x /usr/share/tomcat6/scripts/aws/*n",
      "chmod +x /opt/aws/apitools/cfn/bin/*n",
      "service tomcat6 restartn",
      "service httpd restartn",
      "/opt/aws/bin/cfn-signal", " -e 0", " '", { "Ref" : "WaitHandle" }, "'"
    ]]}}
  }
},

Calling cfn init from UserData

"# Install packagesn",
"/opt/aws/bin/cfn-init -s ", { "Ref" : "AWS::StackName" }, " -r WebServer ",
" --access-key ", { "Ref" : "HostKeys" },
" --secret-key ", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]},
" --region ", { "Ref" : "AWS::Region" }, " || error_exit 'Failed to run cfn-init'n",
},

cfn_init is used to retrieve and interpret the resource metadata, installing packages, creating files and starting services. In the Manatee template we use cfn_init for easy access to other AWS resources, such as S3.

"/etc/tomcat6/server.xml" : {
  "source" : { "Fn::Join" : ["", ["https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/server.xml"]]},
  "mode" : "000554",
  "owner" : "root",
  "group" : "root",
  "authentication" : "S3AccessCreds"
},


"AWS::CloudFormation::Authentication" : {
  "S3AccessCreds" : {
    "type" : "S3",
    "accessKeyId" : { "Ref" : "HostKeys" },
    "secretKey" : {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]},
    "buckets" : [ { "Ref" : "S3Bucket"} ]   }
}

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" }
  }
},

In the snippets above, we create a new Elastic IP and then associate it with the EC2 instance created above. We do this so we can reference the Elastic IP when creating the Route53 Domain name.
AWS::CloudWatch::Alarm

"CPUAlarmLow": {
  "Type": "AWS::CloudWatch::Alarm",
  "Properties": {
    "AlarmDescription": "Scale-down if CPU < 70% for 10 minutes",
    "MetricName": "CPUUtilization",
    "Namespace": "AWS/EC2",
    "Statistic": "Average",
    "Period": "300",
    "EvaluationPeriods": "2",
    "Threshold": "70",
    "AlarmActions": [ { "Ref": "SNSTopic" } ],
    "Dimensions": [{
      "Name": "WebServerName",
      "Value": { "Ref": "WebServer" }
    }],
    "ComparisonOperator": "LessThanThreshold"
  }
},

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.

Outputs

Outputs are used to return information from what was created during the CloudFormaiton stack creation to the user. In order to return values, you define the Output name and then the resource you want to reference:

"Outputs" : {
  "Domain" : {
    "Value" : { "Fn::Join" : ["", ["http://", { "Ref" : "ApplicationName" }, ".", { "Ref" : "HostedZone" }]] },
    "Description" : "URL for newly created Jenkins app"
  },
  "NexusURL" : {
    "Value" : { "Fn::Join" : ["", ["http://", { "Ref" : "IPAddress" }, ":8080/nexus"]] },
    "Description" : "URL for newly created Nexus repository"
  },
  "InstanceIPAddress" : {
    "Value" : { "Ref" : "IPAddress" }
  }
}

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: