CloudFormation is the core component of AWS for implementing Infrastructure as Code. It allows easy creation and management of related AWS resources by writing CloudFormation templates. Originally those templates were all written in JSON but nowadays there is also an option to use YAML (AWS CloudFormation Update – YAML, Cross-Stack References, Simplified Substitution). Good stuff! Surprisingly the vast majority of example templates is still written in JSON. So here is my take on why I think you should definitely give CloudFormation in YAML a try.

YAML is a superset of JSON. But instead of braces and brackets it uses indentation to denote structure (similar to Python, Ruby, Compose files…). So if you work on those templates as team you should agree on an indentation style or trouble is inevitable. YAML uses whitespace indentation, I usually go with indentation of 2 whitespaces per level. The YAML syntax is available as Reference Card but for writing CloudFormation templates in YAML you actually just need a small subset of that functionality.

So what are the main advantages of writing CloudFormation templates in YAML?

Inline Comments

This is something that always drives me crazy with JSON. There is no way to document the code with comments. Maintaining thousands of lines of configuration without comments can be a nightmare. YAML has inline comments so CloudFormation templates finally can be documented properly.

Short Form Notations

The representation of data structures with YAML is already more compact than with JSON because a lot of braces and brackets are omitted. But it can get even more compact: AWS introduced a lot of short form notations for common functions. These YAML tags can be found in the AWS CloudFormation documentation.

So lets say we want to add the tag “name” to a resource and that tag should have the value of a parameter “AppName” (i.e. “MyApp”) with the string “EC2Server” attached (i.e. “MyAppEC2Server”).

In JSON:

"Tags": [
  {
  "Key": "Name",
  "Value": {"Fn::Join": [ "", [ {"Ref": "AppName"}, "EC2Server" ] ]}
  }
]

In YAML:

Tags:
- Key: Name
  Value: Fn::Join: [ "", [ Ref: AppName, "EC2Server" ] ]

And in YAML short form:

Tags:
- Key: Name
  Value: !Join [ "", [ !Ref AppName, "EC2Server" ] ]

Especially with more complex operations this is much cleaner and easier to read.

UserData Scripts

When you initialize an EC2 instances you can pass a base64-encoded data block called UserData. This data block gets executed when the EC2 instance is launched for the first time.

Start the UserData with a bash shebang and you have a nice script to modify the EC2 instance to your needs in a reproducible easy to maintain fashion. The UserData is a bit similar to what a Dockerfile is for a Docker Image. For quite a few situations I found that the UserData was all I needed (compared to configuring the EC instance with custom AMIs, Chef, Ansible or similar).

Unfortunately creating that base64-encoded data block with JSON CloudFormation is cumbersome. The common way is to concatenate string fragments and then base64-encode these into the actual UserData:

"UserData": {
  "Fn::Base64": {
    "Fn::Join": [
      "",
      [
        "#!/bin/bash -xe\n",
        "timedatectl set-timezone Europe/Berlin\n",
        "apt-get update -y\n",
        "apt-get install python-pip -y\n",
        "pip install --upgrade pip\n",
        "pip install awscli awsebcli https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n",
        "echo \"Banner /etc/issue.net\" >> /etc/ssh/sshd_config\n",
        "echo -e \"",{"Fn::Join": [ "", [ "\n Stack: ", {"Ref": "AWS::StackName"}, "\n" ] ]},"\" >> /etc/issue.net\n",
        "/etc/init.d/ssh restart\n",
        "# Signal the status from cfn-init\n",
        "/usr/local/bin/cfn-signal -e $? ",
        "         --stack ", { "Ref" : "AWS::StackName" },
        "         --resource EC2Server ",
        "         --region ", { "Ref" : "AWS::Region" }, "\n"
      ]
    ]
  }
}

With YAML a new simplified substitution was introduced to CloudFormation templates. This Fn::Sub function basically adds a simple templating engine to strings. Combined with YAMLs literal blocks this gives us a very nice way to rewrite the UserData:

UserData:
  Fn::Base64: !Sub |
    #!/bin/bash -xe
    timedatectl set-timezone Europe/Berlin
    apt-get update -y
    apt-get install python-pip -y
    pip install --upgrade pip
    pip install awscli awsebcli https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
    echo "Banner /etc/issue.net" >> /etc/ssh/sshd_config
    echo -e "\n Stack: ${StackName}\n" >> /etc/issue.net
    /etc/init.d/ssh restart
    /usr/local/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Server --region ${AWS::Region}

This also makes it very easy to move parts of bash scripts in and out of CloudFormation. Maybe you want to try something in a temporary bash script on a server, then move it into the CloudFormation template… This works with Metadata scripts such as CloudInit scripts too.

Conclusion:

Writing CloudFormation templates in YAML is an interesting alternative to the JSON format. Finally we can use comments to document our templates inline. Also the more compact representation and the CloudFormation specific shortcuts make the code easier to read and understand.

Why I like CloudFormation templates in YAML syntax
Tagged on: