Going through the internals of the template
The template is the main building block of CloudFormation. We can consider the template as a declarative instruction for CloudFormation—for example, what to create and how many.
The template file, whether in JSON or YAML, consists of several elements, which we will describe in the rest of this section.
AWSTemplateFormatVersion
The AWSTemplateFormatVersion section is responsible for instructing CloudFormation as to what version of the template we are going to supply it with.
Not to be confused with the API version of CloudFormation, AWSTemplateFormatVersion is about the structure of the template.
When AWS has added this block to resolve potential breaking changes to CloudFormation, it can only have one value: "2010-09-09". This version is there from the very beginning of CloudFormation, but there is always a chance that AWS will add newer versions for template formatting. Until then, this section is optional, and if it is not provided, CloudFormation will assume that your template has the default format version mentioned previously.
Description
The description section is optional and serves only one purpose—to explain what your template is and what it is going to create if you push it to CloudFormation. It doesn't allow you to refer to parameters, and supports single-line or multiline values.
Your description can be like this:
Description: Core network template consisting of VPC, IGW, RouteTables, and Subnets
It can also be like this:
Description: >
Core network template. Consists of:
- VPC
- IGW
- Subnets
- RouteTables
There are no specific rules, but keep in mind that unlike comments in your template, the description is also seen in CloudFormation services if you access it via the API, ASW CLI, or AWS Console.
All in all, the best practice for the description is to store useful information for CloudFormation users.
Metadata
While the metadata section might not seem not useful, it adds additional configuration capabilities.
You can use metadata in conjunction with CloudFormation-specific resources, such as the following:
- AWS::CloudFormation::Init
- AWS::CloudFormation::Interface
- AWS::CloudFormation::Designer
We will cover these in more detail later.
Parameters
One of the most important, yet optional, sections, Parameters, allows us to make our template reusable. Think of parameters as variables for your stack: CIDR ranges for your VPC and subnets, instance types for your EC2 and RDS instances, and so on.
The use of parameters differs in different cases. One of the most popular ways of using parameters is to allow the creation of different stacks without changing the template resources.
For example, look at the following code:
Parameters:
InstanceType:
Type: String
Resources:
Ec2Instance:
Type: AWS::EC2::Instance
Properties:
# …
InstanceType: !Ref InstanceType
# …
In this case, you define the InstanceType for your EC2 separately from the actual template, meaning that you can choose different types for different environments (development, testing, production) or other cases.
It is important to know that parameters have the following properties: Default, AllowedValues, and AllowedPattern.
These properties are not mandatory, but it is a best practice to use them.
For example, say that you want to specify the Environment tag as a parameter and you have the environments dev, test, and prod. You will likely want to prevent the template user from having to specify the tag that should not be there (such as foo or bar). For this, you will want to use AllowedValue:
Parameters:
Environment:
Type: String
AllowedValues: [dev, test, prod]
AllowedPattern should be used when you want to match your parameter within a specific regular expression. For a case when you need to supply an IP address or a CIDR range, you want to make sure that you will pass exactly the following:
Parameters:
SubnetCidr:
Type: String
AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}'
Important note
You have to make sure that you follow the Java regular expression syntax!
Default is a property that helps you to ensure that your parameter always has a value. By design, when you try to create a stack and you don't specify a parameter value, the validation of the template (in the precreation step) will fail.
In this example, we pass a Docker Image tag as a parameter for the ECS Task Definition – Container Definition. To make sure there is an image, we set the default value to latest in any case:
Parameters:
DockerImageVersion:
Type: String
Default: latest
On the other hand, sometimes you need to double check that the template user always knows what they are doing, so it is up to you as a template developer to decide whether you want to have the default values.
Needless to say, parameters have types. The list of types is available on the CloudFormation documentation page at https://docs.aws.amazon.com/en_pv/AWSCloudFormation/latest/UserGuide/parameters-section-structure.
One of the most frequently used types is String, because most of the property values of Resources require this data type, but we can also specify arrays and numbers.
AWS-specific parameter types
It is handy to use AWS-specific parameter types, such as AWS::EC2::AvailabilityZone::Name, AWS::EC2::Image::Id, AWS::EC2::Instance::Id, AWS::EC2::KeyPair::KeyName, and AWS::EC2::VPC::Id. The benefit here is that CloudFormation will check whether the value supplied to this specific parameter type is valid for this kind of resource during validation.
For example, if you want to specify the subnet ID as a parameter, you will have to write a correct regular expression for it, while AWS::EC2::Subnet::Id already has this check built in.
Mappings
Mappings are similar to parameters, but are provided in a dictionary format.
Mappings intrinsic functions
Unlike parameters, whose values are obtained with Fn::Ref, values for mappings can be referenced using the Fn::FindInMap function.
The most common usage for mappings is when you want to specify the AMI ID for different regions:
Mappings:
RegionMap:
us-east-1:
HVM64: ami-0ff8a91507f77f867
HVMG2: ami-0a584ac55a7631c0c
us-west-1:
HVM64: ami-0bdb828fd58c52235
HVMG2: ami-066ee5fd4a9ef77f1
eu-west-1:
HVM64: ami-047bb4163c506cd98
HVMG2: ami-0a7c483d527806435
ap-northeast-1:
HVM64: ami-06cd52961ce9f0d85
HVMG2: ami-053cdd503598e4a9d
ap-southeast-1:
HVM64: ami-08569b978cc4dfa10
HVMG2: ami-0be9df32ae9f92309
Resources:
Ec2Instance:
Type: "AWS::EC2::Instance"
Properties:
ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", HVM64]
Note that maintaining mappings can be troublesome, so the best way of using them is to store constant values that are rarely changed.
Conditions
Conditions are similar to parameters, but their value is either true or false. Conditions are declared in a separate block in the template and are referred to in the resource declaration.
Important note
Note that Conditions in a declaration cannot be set to true or false manually. The Boolean value is obtained only from a result of intrinsic functions, such as Fn::Equals, Fn::If, Fn::Not, Fn::And, and Fn::Or!
For example, look at the following code:
---
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
Env:
Default: dev
Description: Define the environment (dev, test or prod)
Type: String
AllowedValues: [dev, test, prod]
Conditions:
IsProd: !Equals [!Ref Env, 'prod']
Resources:
Bucket:
Type: "AWS::S3::Bucket"
Condition: IsProd
In this case, we have to manually provide the Env parameter, which must equal prod; otherwise, the resource will not be created.
Conditions work along with specific Boolean intrinsic functions, but those functions also work outside of the conditional block. We will cover these in greater detail when we start the development of our template.
Transform
The transform block is declared when we want to run CloudFormation-specific macros in our template. This is a deep topic that we will cover in the last part of this book, Extending CloudFormation
Resources
This is the main and only required block in the template file. The resources section provides, as is indicated by its name, resources that we want to provision.
The resources that we create can vary. Some think that any AWS resource can be created by CloudFormation, but there are multiple services (such as AWS Organizations, Control Tower, and many others) that do not support CloudFormation.
Before starting to create the stack, you must first review the resource and property reference in the CloudFormation documentation to make sure that the services you want to use are supported by CloudFormation. For more information, go to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html.
Resources have multiple attributes, listed as follows:
- Type: The type of the resource (instance, subnet, DynamoDB table, lambda function, and so on).
- Properties: Configuration of the resource.
- DependsOn: Used for adding indirect dependencies.
- CreationPolicy: Used for notifying CloudFormation to wait for the signal of completion (we will cover this when we create AutoScaling groups).
- DeletionPolicy: Used to instruct CloudFormation what to do with the resource, when we delete it from the stack (or with the stack)—for example, from the state. Used for important resources, such as storage backends.
- UpdatePolicy: Used to instruct CloudFormation how to handle indirect updates of AutoScaling groups, ElastiCache replication groups, and Lambda function aliases.
- UpdateReplacePolicy: Similar to DeletionPolicy, UpdateReplacePolicy is used to manage leftover resources when they have to be replaced with a new one. It works within the same logical resource and default policy as Delete.
Outputs
Outputs are the values we export from the stack after its creation.
On its own, the outputs do not bring many benefits, but in the previous chapter, we used outputs to automatically get the ARN of the IAM role we created.
Outputs can retrieve the physical name or ID of the resource or its attributes.
For example, we can retrieve the API key and secret for an IAM user when we create it and supply our end users with it:
Resources:
Bob:
Type: AWS::IAM::User
Properties:
UserName: Bob
BobApiKey:
Type: AWS::IAM::AccessKey
Properties:
UserName: !Ref Bob
Outputs:
BobKey:
Value: !Ref BobApiKey
BobSecret:
Value: !GetAttBobApiKey.SecretAccessKey
As you can see, we retrieved Bob's API key and secret and exposed it to the outputs.
Important note
Note that outputs are accessible to anyone who is authorized to read the stacks' description, so exposing security credentials is a risky operation. In case you need to use outputs for that, make sure that only authorized users will have access to your stacks, even if they are read-only.
Outputs also have a powerful feature called exports. Exports allow the template developer to refer to the existing resources, created in another stack. Using exports is a best practice for large workloads where you often have a shared stack (resources used by many applications) and application-specific stacks.
Now, let's start developing our template. Before proceeding, make sure that you understand all the preceding sections because we will rely on them heavily in the coming sections.