Using AWS CloudFormation Macros is also Plain and Simple

Cloud Migration Scenarios

Four scenarios to migrate to AWS – from infrastructure to ML

Recently I wrote a blog post on creating a custom resource for AWS CloudFormation to generate a random string. With the launch of Macro support, we can even do more. I build a similar function: a random string generator. Using Macros, the function will generate a new string every time you execute a CloudFormation deployment.

Btw, the Macro support is at the moment of publishing just GA for a few hours. I wanted to play around with this new feature and share my experience quickly. Probably lots of use cases will appear soon. Hopefully with this blog post you have your first Macro experiment done in minutes.

Deploy a Macro Lambda

First we have to deploy a Lambda function. Of course with the right policies and permissions, and new with Macro you have to add a AWS::CloudFormation::Transform resource.

AWSTemplateFormatVersion: 2010-09-09
Resources:
  TransformFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import traceback
          import string
          import random

          def random_string(size=6):
            return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(size))
          
          def handler(event, context):
              response = {
                  "requestId": event["requestId"],
                  "status": "success"
              }
              params = event.get("params", {})
              operation = params.get("Operation", "upper")
              number = int(params.get("Number", 6))
              no_param_string_funcs = ["upper", "lower"]

              rs = random_string(number)
              if operation == "upper":
                  response["fragment"] = rs.upper()
              elif operation == "lower":
                  response["fragment"] = rs.lower()
              else:
                  response["status"] = "failure"
              
              return response

      Handler: index.handler
      Runtime: python3.6
      Role: !GetAtt TransformExecutionRole.Arn
  TransformExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: [lambda.amazonaws.com]
            Action: ['sts:AssumeRole']
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: ['logs:*']
                Resource: 'arn:aws:logs:*:*:*'
  TransformFunctionPermissions:
    Type: AWS::Lambda::Permission
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt TransformFunction.Arn
      Principal: "cloudformation.amazonaws.com"
  Transform:
    Type: AWS::CloudFormation::Transform
    Properties:
      Name: !Sub "${AWS::AccountId}::RandomString"
      content: "Generates a random string"
      RoutingTable:
        '*': 0_1
      Versions:
        - VersionName: 0_1
          content: "Version 0_1 of the RandomString Generator"
          FunctionName: !GetAtt TransformFunction.Arn
      ExecutionPolicy:
        Version: 2012-10-17
        Id: AllowOtherAccountPolicy
        Statement:
          - Sid: AllowExecution
            Effect: Allow
            Principal:
              AWS: !Sub '${AWS::AccountId}'
            Action: "cloudformation:CreateChangeSet"
            Resource: !Sub "arn:*:cloudformation:${AWS::Region}:${AWS::AccountId}:transform/RandomString"

Deploy the stack:

aws cloudformation deploy \
    --stack-name cfn-macro-randomstring \
    --template-file template.yaml \
    --capabilities CAPABILITY_IAM

Use the Macro

Now we can test our CloudFormation Macro by creating and deploying the following template. Like I said, it will generate a new random string every time a deployment happens.

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      Tags:
        - Key: "join-test"
          Value: 
            'Fn::Join':
              - ''
              - - "mybucket-"
                - 'Fn::Transform':
                    Name: "RandomString"
                    Parameters:
                      Operation: "lower"
                      Number: "8"
aws cloudformation deploy \
    --stack-name test-the-macro \
    --template-file test.yaml

The Macro feature is plain and easy when you really want something to happen everytime you deploy the template. It could be handy when you are expirimenting and want a new resource, property value or attribute, everytime you change something in your stack. And of course you can think of many more use cases.

I tried to use the shorthand notation !Sub together with !Transform { Name : RandomString } without any parameters, but this is not supported yet. Although the documentation page says it does. Might be an easy bug and fixed quickly.

I’m also a big fan of cfn-lint. I automated this in my own (opinionated) cfn deployment cli. However, I now had to disable this check because cfn-lint is not up-to-date yet. And I think it will become a lot harder to enforce policies with all the powers of this Macro feature.

What I do like about the new Macro feature is that the lambda function is very small and simple. You just have to return a few values, instead of invoking a special CloudFormation endpoint with a lot of values, like Custom Resources require.

If you want to build your own Macro, also check out these examples.

Share this article: Tweet this post / Post on LinkedIn