As a follow up to my last post An Introduction to CloudFormation Tool from AWS, this post covers the basics of a CloudFormation templates.
A CloudFormation template consists of 6 sections – Description, Parameters, Mappings, Conditions, Resources and Outputs. Only the Resources section is required. However, as a good practice, we highly recommend using all the sections of a template. Each template section is separated by a comma. A template is written in JSON and always starts with an open brace { and always closes with a closed brace }.
Here’s a sample template from AWS website:
{
"AWSTemplateFormatVersion" : "version date",
"Description" : "Valid JSON strings",
"Parameters" : {
set of parameters
},
"Mappings" : {
set of mappings
},
"Conditions" : {
set of conditions
},
"Resources" : {
set of resources
},
"Outputs" : {
set of outputs
}
}
Now let us take a look at a CloudFormation template that creates a bastion host (a jump box) to connect to instances in a VPC.
The template Description enables you to provide arbitrary comments about your template. It is a literal JSON string that is between 0 and 1024 bytes in length; its value cannot be based on a parameter or function.
The optional Parameters section enables you to pass values into your template at stack creation time. Parameters let you create templates that can be customized for each stack deployment. When you create a stack from a template containing parameters, you can specify values for those parameters. Within the template, you can use the “Ref” intrinsic function to specify those parameter values in properties values for resources. For example, you can pass parameters in the AWS CLI or you can type the values in the AWS console while creating an instance.
Here’s the Parameters section defined in a CloudFormation template:
"Parameters" : {
"KeyName" : {
"Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances",
"Type" : "String",
"MinLength": "1",
"MaxLength": "64",
"AllowedPattern" : "[-_ a-zA-Z0-9]*",
"ConstraintDescription" : "can contain only alphanumeric characters, spaces, dashes and underscores."
},
"BastionKeyName" : {
"Description" : "Name of the EC2 KeyPair we will create internally to access instances in our VPC",
"Type" : "String",
"MinLength": "1",
"MaxLength": "64",
"AllowedPattern" : "[-_ a-zA-Z0-9]*",
"ConstraintDescription" : "can contain only alphanumeric characters, spaces, dashes and underscores."
},
"VpcId" : {
"Type" : "String",
"Description" : "VpcId of your existing Virtual Private Cloud (VPC)"
},
"SubnetId" : {
"Type" : "String",
"Description" : "SubnetId of an existing Public facing subnet in your Virtual Private Cloud (VPC)"
}
},
More information on the Parameters section can be found at the following URL: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
"Mappings" : {
"AWSInstanceType2Arch" : {
"t1.micro" : { "Arch" : "64" },
"m1.small" : { "Arch" : "64" },
"m1.medium" : { "Arch" : "64" },
"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" }
},
"AWSRegionArch2AMI" : {
"us-east-1" : { "64" : "ami-35792c5c" },
"us-west-1" : { "64" : "ami-687b4f2d" },
"us-west-2" : { "64" : "ami-d03ea1e0" },
"eu-west-1" : { "64" : "ami-149f7863" },
"sa-east-1" : { "64" : "ami-9f6ec982" },
"ap-southeast-1" : { "64" : "ami-14f2b946" },
"ap-southeast-2" : { "64" : "ami-a148d59b" },
"ap-northeast-1" : { "64" : "ami-3561fe34" }
}
},
"Resources" : {
"CfnUser": {
"Type": "AWS::IAM::User",
"Properties": {
"Path": "/",
"Policies": [
{
"PolicyName": "root",
"PolicyDocument": {
"Statement": [
{
"Effect" : "Allow",
"Action": [
"ec2:CreateKeyPair",
"ec2:DescribeKeyPairs",
"ec2:DescribeRegions",
"ec2:ImportKeyPair"
],
"Resource" : "*"
},
{
"Effect": "Allow",
"Action": "cloudformation:DescribeStackResource",
"Resource": "*"
}]
}
}
]
}
},
"CfnKeys": {
"Type": "AWS::IAM::AccessKey",
"Properties": {
"UserName": {
"Ref": "CfnUser"
}
}
},
"IPAddress" : {
"Type" : "AWS::EC2::EIP",
"Properties" : {
"Domain" : "vpc",
"InstanceId" : { "Ref" : "BastionHost" }
}
},
"BastionSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"VpcId" : { "Ref" : "VpcId" },
"GroupDescription" : "Enable SSH access via port 22",
"SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : "0.0.0.0/0" } ]
}
},
"BastionHost" : {
"Type" : "AWS::EC2::Instance",
"Metadata": {
"AWS::CloudFormation::Init": {
"config": {
"packages": {
"yum": {
"python-boto": []
}
},
"files": {
"/home/ec2-user/create-keypair" : {
"content" : {
"Fn::Join" : ["", ["#!/usr/bin/pythonn",
"import stringn",
"import randomn",
"import boto.ec2n",
"kp_name = '",{ "Ref" : "BastionKeyName" },"'n",
"ec2 = boto.ec2.connect_to_region('", {"Ref" : "AWS::Region" }, "')n",
"keypair = ec2.create_key_pair(kp_name)n",
"keypair.save('/home/ec2-user/.ssh/')n",
"print 'Created keypair: %s' % kp_namen"]]
},
"mode" : "000750",
"owner" : "ec2-user",
"group" : "ec2-user"
},
"/home/ec2-user/.boto": {
"content": {
"Fn::Join": ["", [ "[Credentials]n",
"aws_access_key_id = ", { "Ref": "CfnKeys" }, "n",
"aws_secret_access_key = ", { "Fn::GetAtt": ["CfnKeys", "SecretAccessKey"] }, "n",
"[Boto]n",
"ec2_region_name = ", { "Ref" : "AWS::Region" }, "n",
"ec2_region_endpoint = ec2.", { "Ref" : "AWS::Region" }, ".amazonaws.comn"]]
},
"mode": "000600",
"owner": "ec2-user",
"group": "ec2-user"
}
},
"commands" : {
"00create-keypair" : {
"command" : ["su", "ec2-user", "-c", "python create-keypair"],
"cwd" : "/home/ec2-user"
}
}
}
}
},
"Properties" : {
"InstanceType" : "m1.small",
"ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, "64" ]},
"SecurityGroupIds" : [{ "Ref" : "BastionSecurityGroup" }],
"SubnetId" : { "Ref" : "SubnetId" },
"KeyName" : { "Ref" : "KeyName" },
"UserData": {
"Fn::Base64" : { "Fn::Join" : ["", [
"#!/bin/bash -vn",
"yum update -y aws-cfn-bootstrapn",
"# Helper functionn",
"function error_exitn",
"{n",
" /opt/aws/bin/cfn-signal -e 1 -r "$1" '", { "Ref" : "BastionHostHandle" }, "'n",
" exit 1n",
"}n",
"/opt/aws/bin/cfn-init -s ", { "Ref" : "AWS::StackName" }, " -r BastionHost ",
" --access-key ", { "Ref" : "CfnKeys" },
" --secret-key ", {"Fn::GetAtt": ["CfnKeys", "SecretAccessKey"]},
" --region ", { "Ref" : "AWS::Region" }, " || error_exit 'Failed to run cfn-init'n",
"# All is well so signal successn",
"/opt/aws/bin/cfn-signal -e 0 -r "Server setup complete" '", { "Ref" : "BastionHostHandle" }, "'n"
]
]
}
}
}
},
"BastionHostHandle" : {
"Type" : "AWS::CloudFormation::WaitConditionHandle"
},
"ControllerCondition" : {
"Type" : "AWS::CloudFormation::WaitCondition",
"DependsOn" : "BastionHost",
"Properties" : {
"Handle" : { "Ref" : "BastionHostHandle" },
"Timeout" : "900"
}
}
}
"Outputs" : {
"InstanceID" : {
"Value" : {"Ref": "BastionHost"},
"Description" : "Bastion Instance ID"
},
"IPAddress" : {
"Value" : { "Ref" : "IPAddress" },
"Description" : "Public IP address of instance"
},
"BastionKeyName" : {
"Value" : { "Ref" : "BastionKeyName" },
"Description" : "The internal bastion KeyPair name"
}
}
}