Basic CloudFormation template structure and best practices

Basic CloudFormation template structure and best practices

What is an AWS CloudFormation template? Described in simple terms a CloudFormation template is a JSON formatted file that programmatically describes your AWS infrastructure. Through CloudFormation templates you can version control your infrastructure, specify infrastructure dependencies, and rapidly deploy services in a controlled manner.

Other than meta data (like comments, header data, etc.), a CloudFormation template consists of five sections:

  1. Parameters:
    The parameters section allows you to specify values that you can pass in to your template at runtime (when you create or update a stack). You can refer to your created parameters in alternative sections of the template.

        "Parameters" : {
            "ExampleInput" : {
                "Description" : "ExampleInput description.",
                "Type" : "String"
            }
        }
    
  2. Mappings:
    The mappings section allows you to specify a ‘map’ of key/value paries that you can use in alternative sections of the template. You can match a key to a corresponding value by using a special function.

        "Mappings" : {
            "ExampleMap" : {
                "Level1" : {
                    "Level2A" : "SomeValue",
                    "Level2B" : "SomeOtherValue"
                }
            }
        }
    

    A common map use case is to a map to define the right AMI values to use:

        "Mappings" : {
            "RegionMap" : {
                "us-east-1"      : { "32" : "ami-6411e20d", "64" : "ami-7a11e213" },
                "us-west-1"      : { "32" : "ami-c9c7978c", "64" : "ami-cfc7978a" },
                "eu-west-1"      : { "32" : "ami-37c2f643", "64" : "ami-31c2f645" },
                "ap-southeast-1" : { "32" : "ami-66f28c34", "64" : "ami-60f28c32" },
                "ap-northeast-1" : { "32" : "ami-9c03a89d", "64" : "ami-a003a8a1" }
            }
        }
    
  3. Conditions:
    The conditions section allows you to specify conditions that control whether certain resources are created or whether certain resource properties are assigned a value during stack creation or update. For example, you could conditionally create a resource that depends on whether the stack is in a certain region or account.

        "Conditions" : {
            "IsUsWest2" : {"Fn::Equals" : \[{ "Ref" : "AWS::Region" }, "us-west-2"\]}
        }
    
  4. Resources:
    The conditions section is where you actually specify the stack resources that you want CloudFormation to create. You can refer to the created resources in this section and the outputs section of the template.

        "Resources" : {
            "ELB" : {
                "Type" : "AWS::ElasticLoadBalancing::LoadBalancer"
                ...
            }
        }
    
  5. Outputs:
    The outputs section is used to describe and easily access the values that are returned from the resources section.

        "Outputs" : {
            "ELB" : {
                "Description" : "Example ELB Endpoint.",
                "Value" : {"Fn::GetAtt" : \[ "ELB" , "DNSName" \]}
            }
        }
    

Technically speaking the only required section is Resources, but it’s suggested to keep all the components there for easy configuration. Here’s an example of a very minimal template:

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "A bare bones example template.",
    "Parameters" : {},
    "Mappings" : {},
    "Conditions" : {},
    "Resources" : {
        ...
    },
    "Outputs" : {}
}

For practice we’re going to create an template for an Elastic Load Balancer. Working from our minimal template, we’re going to create a resource of type AWS::ElasticLoadBalancing::LoadBalancer. The only required property for an ELB is “Listeners”, which is an array of json listeners.

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "A bare bones example template.",
    "Parameters" : {},
    "Mappings" : {},
    "Conditions" : {},
    "Resources" : {
        "ELB" : {
            "Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
            "Properties" : {
                "Listeners" : \[ { "LoadBalancerPort" : "80", "InstancePort" : "80", "Protocol" : "HTTP" } \]
            }
        }
    },
    "Outputs" : {}
}

If you were to run this template through CloudFormation (either through the AWS Console or via the CLI with aws cloudformation create-stack --stack-name myteststack --template-body ./template.json) it would spawn an ELB.

That’s great, but what if I wanted several ELBs with different ports and protocols? That’s where parameters come in.

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "A bare bones example template.",
    "Parameters" : {
        "ElbPort" : {
            "Description" : "The port number that the ELB listens on.",
            "Type" : "String"
        },
        "InstancePort" : {
            "Description" : "The port number that the instance listens on.",
            "Type" : "Number",
            "Default" : "80"
        },
        "Protocol" : {
            "Description" : "The protocol being used by the ELB.",
            "Type" : "String",
            "Default" : "HTTP",
            "AllowedValues" : \["HTTP", "HTTPS", "TCP"\]
        }
    },
    "Mappings" : {},
    "Conditions" : {},
    "Resources" : {
        "ELB" : {
            "Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
            "Properties" : {
                "Listeners" : \[
                    {
                        "LoadBalancerPort" : { "Ref" : "ElbPort" },
                        "InstancePort" : { "Ref" : "InstancePort" },
                        "Protocol" : { "Ref" : "Protocol" }
                    }
                \]
            }
        }
    },
    "Outputs" : {}
}

Maybe we only want the ELB to be created in us-west-2, and don’t want staff to accidentally create the ELB in other regions. We can add a conditional in the “Conditions” section.

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "A bare bones example template.",
    "Parameters" : {
        "ElbPort" : {
            "Description" : "The port number that the ELB listens on.",
            "Type" : "String"
        },
        "InstancePort" : {
            "Description" : "The port number that the instance listens on.",
            "Type" : "Number",
            "Default" : "80"
        },
        "Protocol" : {
            "Description" : "The protocol being used by the ELB.",
            "Type" : "String",
            "Default" : "HTTP",
            "AllowedValues" : \["HTTP", "HTTPS", "TCP"\]
        }
    },
    "Mappings" : {},
    "Conditions" : {
        "IsUsWest2" : {"Fn::Equals" : \[{ "Ref" : "AWS::Region" }, "us-west-2"\]}
    },
    "Resources" : {
        "ELB" : {
            "Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
            "Condition" : "IsUsWest2",
            "Properties" : {
                "Listeners" : \[
                    {
                        "LoadBalancerPort" : { "Ref" : "ElbPort" },
                        "InstancePort" : { "Ref" : "InstancePort" },
                        "Protocol" : { "Ref" : "Protocol" }
                    }
                \]
            }
        }
    },
    "Outputs" : {}
}

By adding the meta data “Condition” to the resource, you can refer to a conditional that you’ve created in your “Conditions” section.

I think with that we’ll end it here - that covers the basics to get started. If you’re not using tools like CloudFormation to manage your infrastructure you should start - with CloudFormation templates you can apply version control to your infrastructure, repeat it in different regions (or accounts!), and audit your stack resources (think, removal / isolation of issues around mystery boxes).

I think in my one of my next posts I’ll cover CloudFormation Nested Stacks to show how you can make canned template snippets that you can inherit from a master. I’ll also cover masterless configuration management in the near future.

Make sure that you check out Amazon’s documented best practices here. You should also check out Amazon’s general resource documentation here.