CloudFormation: functions like ImportValue and GetAtt inside a Sub

Hello!

In CloudFormation, I think !Sub is the best way to generate strings that contain dynamic values. It’s better to interpolate, like this:

!Sub 'This is security group ${SG} in account ${AWS::AccountId}!'

Than to join, like this:

!Join ['', ['This is security group', !Ref SG, 'in account', !Ref 'AWS::AccountId']]

Both are common solutions, ${SG} resolves to the same value as !Ref SG, but I think interpolation is the right tool here. Join is better for other situations (like creating a comma delimited list from an array of strings).

It gets tricky if you need to render in your security group’s ID. You can’t !Ref that, you have to use !GetAtt, and the !Sub interpolation syntax just does a !Ref behind the scenes. You could still join:

!Join ['', ['This is security group', !GetAtt SG.GroupId, 'in account', !Ref 'AWS::AccountId']]

But that’s just as ugly as before. And, it doesn’t scale to more complex cases. I inherited a template, once, that used join to assemble a JSON string with half a dozen key/value pairs and it was unreadable. I’ll omit an example like that because it would be unreadable, but, if you’re struggling with join, read on! There is a solution.

!Sub has a second syntax that accepts a mapping, and with that and a little YAML sugar you can use it to render beautifully formatted, multi-line strings using values from any intrinsic function:

Parameter:
  Type: AWS::SSM::Parameter
  Properties:
    Name: /Example/SecurityGroupsJson
    Type: String
    Value: !Sub
      - |
        {
          "SecurityGroup1Id": "${SecurityGroup1Id}",
          "SecurityGroup2Id": "${SecurityGroup2Id}"
        }
      - SecurityGroup1Id: !ImportValue Stack1:SecurityGroupId
        SecurityGroup2Id: !GetAtt SecurityGroup.GroupId

That renders a JSON-formatted string:

{
  "SecurityGroup1Id": "sg-xxxxxxxx",
  "SecurityGroup2Id": "sg-yyyyyyyy"
}

Some details:

  • The ${} syntax now uses values from the map you pass in and from references to other resources in the template. I don’t know what happens if your map contains a key with the same name as a resource in that template because I’ve never tried that because that would be madness.
  • The vertical pipe is one of the YAML ways to make a multi-line string. There are a couple, but there’s an awesome website that explains them all.
  • Eagle-eyed readers may notice that the CF docs use a curly-bracket syntax for the map in the second item of the array, like this:
    !Sub
      - String
      - { Var1Name: Var1Value, Var2Name: Var2Value }
    

    That works, but my way is still valid CF YAML (tested ✅) and I think it’s a little cleaner.

Before I sign off, there’s one small piece of advice I’d like to leave you with: if this solves whatever problem you were facing, make sure you were solving the right problem. I haven’t seen a ton of cases in well-written CF templates where this was needed. It’s possible you’re trying to implement an anti-pattern and that’s why things got so complicated.

Happy automating!

Adam

If this was helpful and you want to save time by getting “copy and paste” patterns for Cloud DevOps in your inbox, subscribe here. If you don’t want to wait for the next one, check out these: