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.