CloudFormation Join: Use Sub Instead

Hello!

In CloudFormation, it’s common to construct strings with the !Join function. Like this example from AWS’s cfn-init docs:

UserData: !Base64
  'Fn::Join':
    - ''
    - - |
        #!/bin/bash -xe
      - |
        # Install the files and packages from the metadata
      - '/opt/aws/bin/cfn-init -v '
      - '         --stack '
      - !Ref 'AWS::StackName'
      - '         --resource WebServerInstance '
      - '         --configsets InstallAndRun '
      - '         --region '
      - !Ref 'AWS::Region'
      - |+

Here’s the script this renders into:

#!/bin/bash -xe
# Install the files and packages from the metadata
/opt/aws/bin/cfn-init -v          --stack test         --resource WebServerInstance          --configsets InstallAndRun          --region us-west-2

To me, both are messy and hard to read. It abuses multiline string declarations (|) to create single line breaks. Spaces are all over the place. There are YAML - and ' characters everywhere.

I use the !Sub function with one multi-line string instead:

UserData:
  Fn::Base64: !Sub |
    #!/bin/bash -xe
    # Install the files and packages from the metadata
    /opt/aws/bin/cfn-init -v \
      --stack ${AWS::StackName} \
      --resource WebServerInstance \
      --configsets InstallAndRun \
      --region ${AWS::Region}

Fewer lines, no YAML syntax scattered around the script. It reads like a normal shell script except we can use ${} wherever we’d have used a !Ref. It renders like this:

#!/bin/bash -xe
# Install the files and packages from the metadata
/opt/aws/bin/cfn-init -v \
  --stack test \
  --resource WebServerInstance \
  --configsets InstallAndRun \
  --region us-west-2

I think both are much easier to read.

Some details:

  • I used Fn::Base64 instead of !Base64 because you can’t mix the long and short form in this case.
  • If your string needs values from other functions, like !GetAtt or !ImportValue, check out this.
  • Every new line in the sub version is a new line in the rendered script. Like any multiline shell command it has to break lines between the arguments with \. The join version renders the cfn-init command into one long line with a ton of spaces between the arguments, and a side effect is they don’t need the multiline command syntax.
  • The ${thing} syntax of !Sub is also a type shell variable expansion. Make sure you only use it for CloudFormation references. No problem for me because I only use CloudFormation to render super simple scripts that basically just call cfn-init. Shell’s $thing syntax is all I need. If your script is complex enough that this isn’t enough, I recommend reconsidering your approach. It’s usually an anti-pattern to use CloudFormation for those cases.

I almost always use !Sub instead of !Join. It lets you write strings like the strings they are, rather than polluting them with a bunch of YAML syntax.

Happy automating!

Adam

Need more than just this article? I’m available to consult.

You might also want to check out these related articles: