CloudFormation templates are text documents that declarativly express what infrastructure components that are part of a CloudFormation stack.A stack is a collection of AWS resources that you can manage as a single unit. CloudFormation templates must be in either JSON or YAML format. I prefer JSON. Here is what a skeleton of a JSON template looks like:
{
"AWSTemplateFormatVersion" : "version date",
"Description" : "JSON string",
"Metadata" : {
...
},
"Parameters" : {
...
},
"Mappings" : {
...
},
"Conditions" : {
...
},
"Transform" : {
...
},
"Resources" : {
...
},
"Outputs" : {
...
}
}
The skeleton has several different sections, which I shall describe below. Keep in mind that this is a very high level overview. These are all of the possible sections, but within the sections you can get far more detailed. The AWS::EC2::Instance resource type has dozens of potential properties, whereas my examples will only show a fraction.
Resources (required)
I am starting with the resources section because it is the only required one. This is the guts of your template, where you specify the actual elements of your stack, such as EC2 instances, SQS queues, etc.
When you specify a resource, you need to supply a Logical ID (an alphanumeric name that must be unique within the template), a resource type, and the properties of the resource. A simple resource definition for an EC2 instance might look something like this:
"Resources" : {
"myEC2Instance" : {
"Type" : "AWS::EC2::Instance",
"Properties" : {
"ImageID" : "ami-aaaaaaaaaaaaaaaaa",
"InstanceType" : "t2.large"
}
}
As noted previously, there are dozens of potential properties you can specify. If you have a custom volume you want to mount, you can do that using the “Volumes” key\value pair, if you want to enable enhanced monitoring or apply a security group to it, there are properties for that. For details on available properties, see the AWS documentation.
AWSTemplateFormatVersion (optional)
This is the easiest section to deal with – there is only one version, the date “2010-09-09”.
Description (optional)
This section is also pretty obvious. In the JSON template, there is no provision for comments, so do not skimp on the description.
Metadata (optional)
The MetaData section is the first non-trivial section. This is also an opportunity to self document the template\stack. For instance you could have an entry such as “S3prepocess” : {“Description” : “S3 bucket where files are placed before processing”}, “S3postpocess” : {“Description” : “S3 bucket where files are placed after processing”} .
There are also three CloudFormation-specific metadata keys:
-
AWS::CloudFormation::Init
:Used for providing data to the cfn-init helper script if you use that for initial configuration and set up of EC2 instances. -
AWS::CloudFormation::Interface
: This can help define how items are displayed in the CloudFormation Console -
AWS::CloudFormation::Designer
: Describes resource layout in the CloudFormation designer - AWS::CloudFormation::Authentication: Can specify credentials used fo
r AWS::CloudFormation::Init
Parameters (optional)
Now we are getting into some more interesting items. Parameters give your templates flexibility. As you might find in many programming languages, a parameter is an value that is used when you start processing. Parameters can have all sorts of restrictions, default values, etc. For example, lets say that you have an RDS instance that you want to define within a template. The RDS instance is to be either a db.m5.xlarge (default), a db.m5.2xlarge, or a db.m5.4xlarge. The parameter definition for this would look like the JSON below.
"Parameters" : {
"InstanceTypeParameter" : {
"Type" : "String",
"Default" : "db.m5.xlarge",
"AllowedValues" : ["db.m5.xlarge", "db.m5.2xlarge", "db.m5.4xlarge"],
"Description" : "RDS Instance Size. Enter db.m5.xlarge (default), db.m5.2xlarge, db.m5.4xlarge"
}
}
That is a fairly straight forward example, but you can get more complicated, using things like regex to restrict values where the parameter can not be limited to a few discrete values. Typical items where you may do this would be things like email address, or IP CIDR blocks. A parameter mapping for a VPC CIDR block might look something like this:
"Parameters" : {
"VPCCIDR" : {
"Type" : "String",
"Description" : "IP Address range for the VPN connected VPC",
"MinLength": "9",
"MaxLength": "18",
"Default": "10.1.0.0/16",
"AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
"ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x."
},
Mappings (optional)
Mappings are similar in some ways to parameters, but instead of selecting from a predetermined list of items such as the parameters section uses, there are a set of key\value pairs, and determine the result based on the mapping. A common use case is for mappings would be to specify a different AMI to use for an EC2 build based on what region it will be created in, or specifying different resources depending on whether the stack is for a production or test environment.
Because the mapping is based on the key\value pair, you need to reference it. The Fn::FindInMap function can be used to retrieve values. You can also nest several key\value pairs for more granularity. For example, if you have a stack that may be deployed in us-east-1 or us-east-2, and you have a standard AMI that you use for Windows Server 2016 and 2019, your mappings section would look something like this:
"Mappings" : {
"MapRegions" : {
"us-east-1" : {"Windows2019" : "ami-xxxxxxxxxxxxxxxxx", "Windows2016" : "ami-yyyyyyyyyyyyyyyyy"},
"us-east-2" : {"Windows2019" : "ami-aaaaaaaaaaaaaaaaa", "Windows2016" : "ami-bbbbbbbbbbbbbbbbb"},
}
}
To use the mapping, you use the Fn::FindInMap function:
"Resources" : {
"myEC2Instance" : {
"Type" : "AWS::EC2::Instance",
"Properties" : {
"ImageID" : { "Fn::FindInMap" : [ "MapRegions", { "Ref" : "AWS::Region" }, "Windows2019"]},
"InstanceType" : "t2.large"
}
}
Conditions (optional)
Conditions can be used to specify different elements for a stack depending on a logical value. In pseudo code, you can create a condition that states if Environment = production, use m5.8xlarge instance type, if Environment = test, use m5.large instance type. CloudFormation only builds resources based on a TRUE condition.
The condition section is dependent on a parameters section, and based on the condition, will build resources or create outputs.
The pseudo code above would actually look like this:
"Parameters" : {
"EnvType" : {
"Description" : "Environment type.",
"Default" : "test",
"Type" : "String",
"AllowedValues" : ["production", "test"],
"ConstraintDescription" : "must specify production or test."
}
},
"Conditions" : {
"CreateProdResources" : {"Fn::Equals" : [{"Ref" : "EnvType"}, "production"]},
"CreateTestResources" : {"Fn::Equals" : [{"Ref" : "EnvType"}, "test"]}
},
"Resources" : {
"EC2Instance" : {
"Type" : "AWS::EC2::Instance",
"Condition" : "CreateProdResources",
"Properties" : {
"ImageID" : "ami-aaaaaaaaaaaaaaaaa",
"InstanceType" : "m5.8xlarge"}
},
"EC2Instance" : {
"Type" : "AWS::EC2::Instance",
"Condition" : "CreateTestResources",
"Properties" : {
"ImageID" : "ami-aaaaaaaaaaaaaaaaa",
"InstanceType" : "m5.large"}
},
}
Outputs (optional)
As you might assume from the name, outputs can be used to output information from the stack. You can use outputs to document the items you have built (what is the instance ID of that new EC2 instance), or as input to another stack, if you use several stacks together.
References
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html
Mark Freedman has some helpful additional information here: https://markfreedman.com/aws-living-tip-sheet-sam/