CloudFormation Macros

Introduction

A little while ago, AWS released a Cloudformation feature, which enables developers to extend the flexibility of Cloudformation themselves. Cloudformation Macros can be used to run custom ‘Fn::Transform’ operations on Cloudformation templates using AWS Lambda. Furthermore, since it is using Lambda, these macros can run any arbitrary code while doing so.

Setup

As a semi-useful example I created a passphrase generator macro, which creates a random passphrase, stores it in SSM and then injects the passphrase into the resource definition.

Cloudformation Macros are Cloudformation resources as well. They are backed by a Lambda function, which needs to return a specifically structured JSON payload.

Description: >
  This stack sets up a Lambda function as a password generating CFN macro.
Resources:
  MacroFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: MacroFunctionRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LogWriter
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                Resource: !Join [":", ["arn", "aws", "logs", !Ref "AWS::Region", !Ref "AWS::AccountId", "*"]]
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - Logs:PutLogEvents
                Resource: !Join [":", ["arn", "aws", "logs", !Ref "AWS::Region", !Ref "AWS::AccountId", "log-group", "*"]]
              - Effect: Allow
                Action:
                  - ssm:PutParameter
                Resource: !Join [":", ["arn", "aws", "ssm", !Ref "AWS::Region", !Ref "AWS::AccountId", "*"]]
  MacroFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: >
          import string, random, boto3
          ssm = boto3.client('ssm')
          def handler(event, context):
            key = event['params']['Key']
            length = event['params']['Length']
            passphrase = ''.join(random.choices(string.ascii_letters + string.digits, k=length))
            ssm.put_parameter(
              Name=key,
              Value=passphrase,
              Type='SecureString',
              Description='Random passphrase generated by the "PasswordGenerator" Cloudformation macro'
            )
            return {'requestId': event['requestId'], 'status': 'success', 'fragment': passphrase}
      FunctionName: PasswordGeneratorMacro
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt MacroFunctionRole.Arn
      Runtime: 'python3.6'
      Timeout: 5
  Macro:
    Type: AWS::CloudFormation::Macro
    Properties:
      Description: >
        This Macro generates a random password, stores it in SSM and inserts it
        back into the calling template.
        It requires the parameter 'Length' to be set to a numeric value and the
        parameter 'Key' to specify an unused key in SSM to store the new
        password.
      FunctionName: !Ref MacroFunction
      Name: PasswordGenerator

Note the new Cloudformation resource AWS::Cloudformation::Macro, which makes the Lambda function accessible from other CloudFormation templates.

The macro can be deployed like this:

aws cloudformation create-stack --stack-name macro-password-generator --capabilities CAPABILITY_NAMED_IAM --template-body file://password-generator.yaml

That’s it! The macro can now be used from inside of other templates!

Usage

The newly created macro for automatic generation of passphrases can now be used. A common example could be the provisioning of an RDS database instance. Setting the password for the user can be insecure, if it is not handled correctly. If you were to put the password plain into the Cloudformation template, the template would still be uploaded to Cloudformation (that is S3) and could still be viewed or retrieved from the Cloudformation console or CLI. A better method of setting passwords would be to create a passphrase first, store it securely inside of SSM and then using dynamic references inside of the Cloudformation template. But you would still need to setup the passphrase in SSM first.

Here is a very simple resource definition of a small RDS instance using our newly generated password generator macro.

AWSTemplateFormatVersion: "2010-09-09"
Description: >
  This stack uses a previously setup Cloudformation macro.
Resources:
  Bucket:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: 20
      DBInstanceClass: db.t2.micro
      Engine: MySQL
      MasterUsername: dan
      MasterUserPassword:
        'Fn::Transform':
        - Name: PasswordGenerator
          Parameters:
            Length: 20
            Key: /rds/passwords/dan

This is using the same intrinsic ‘Fn::Transform’ function that powers other Cloudformation features, like AWS::Include.

You can deploy the RDS instance by instructing Cloudformation to generate a ChangeSet like so:

aws cloudformation deploy --stack-name database --template-file use-password-generator.yaml

That’s it. The newly generated password to the RDS master user should be the value behind /rds/passwords/dan in the SSM Parameter Store.

Written by Daniel Stamer on 19 October 2018
PGP EDAC0E3FCB1B3FEB