Dynamic resource generation of resources using CloudFormation Macros.
One of the things you can do with this new feature is generate and deploy a number of resources based in a parameter value or the resulting value of the describing the number of AZs in order to create a subnet for each AZ for example.
In this example I’m defining a dynamic number of IAM users (depending on the int I provide in the parameter) that will have the same custom EC2 policy for EC2 resources tagged with Owner:devteam. The user will be prompted to reset their passwords on next sign-in. Also an EC2 instance will be created for each user simulating a classroom.
How to work with Macros:
In order to use this feature you need two things:
-One stack containing a AWS::CloudFormation::Macro resource along a “AWS::Lambda::Function”.
-The stack that will use the Macro which will process a section or the whole template by using on it the Fn::Transform function or a Transform section to transforming the whole template.
Biggest Caveat:
- Your Function will receive the whole template minus transform sections.
Take care of how you send the Macro Response in your lambda function.
You need to send the proper “Fragment”, this is the the processed template CFN receive from the lambda function.
def lambda_handler(event, context): FinalFragment= event[“fragment”] …loops adding new objects to the template.
…. …. … … …. response[“requestId”] = event[“requestId”]
response[“status”] = “success”
response[“fragment”] = FinalFragmentprint (FinalFragment[“Resources”])return response
In the sample attached I’m using python and just looping to create the number of resources I need in the final template. However you can go further with your function and implement more complex logics using the SDK to do API describes etc…
You can keep several Macros in your account doing different types of processing and using them whenever you need unlimited power to process your template suiting to a huge amount of use cases.
Outcome:

Macro Template:
AWSTemplateFormatVersion: 2010-09-09
Resources:
Macro:
Type: AWS::CloudFormation::Macro
Properties:
Name: !Sub '${AWS::StackName}'
Description: Transform the original template performing a for loop that adds the requested number of Users resources.
FunctionName: !Ref MacroProcessorMacroProcessor:
Type: "AWS::Lambda::Function"
Properties:
FunctionName: MacroProcessor
Runtime: "python3.6"
Description: IAM Users Macro processor function
Handler: "index.lambda_handler"
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: |
import json
def lambda_handler(event, context):FinalFragment= event["fragment"]
Number = event["templateParameterValues"]["NumberOfUsers"]
#I declare a new dictionary to have the resources object
NewUser = {}
NewUser = FinalFragment["Resources"]for i in range(int(Number)):
#I modify the IAM User resource logical id in order to add it later to "Resources" object the number of times requestedNewUser["EC2Instance"+str(i+1)] = NewUser["EC2Instance"].copy()FinalFragment["Resources"].update(NewUser)
response = {}
response["requestId"] = event["requestId"]
response["status"] = "success"
response["fragment"] = FinalFragmentprint (FinalFragment["Resources"])return responseTimeout: "100"
MemorySize: 128
Role: !GetAtt LambdaExecutionRole.ArnLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub MacroProcessor-lambda-role-${AWS::Region}
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: /LambdaPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub MacroProcessor-lambda-policy-${AWS::Region}
PolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Action: "cloudformation:*"
Resource: "*"
-
Effect: Allow
Action: "logs:*"
Resource: "*"
Roles:
-
!Ref LambdaExecutionRole
Template to be processed:
AWSTemplateFormatVersion: 2010-09-09
Description: This template generates the requested number of IAM users that will have the same custom EC2 policy for EC2 resources tagged with Owner:devteam. The user will be prompted to reset their passwords on next sign-in. Also an EC2 instance will be created for each user simulating a classroom.
Transform: "Macro"
Parameters:
NumberOfUsers:
Type: String
Description: Enter the number of users to create for the Dev Team
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH access to the web server
Type: 'AWS::EC2::KeyPair::KeyName'
ConstraintDescription: must be the name of an existing EC2 KeyPair.
InstanceType:
Description: WebServer EC2 instance type
Type: String
Default: t2.small
AllowedValues:
- t1.micro
- t2.nano
- t2.micro
- t2.small
- t2.medium
- t2.large
ConstraintDescription: must be a T2 EC2 instance type.
SSHLocation:
Description: >-
Lockdown SSH access to the bastion host (default can be accessed from
anywhere)
Type: String
MinLength: '9'
MaxLength: '18'
Default: 0.0.0.0/0
AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})'
ConstraintDescription: must be a valid CIDR range of the form x.x.x.x/x.Mappings:
AWSRegion2AMI:
us-east-1:
AMI: "ami-97785bed"
us-west-2:
AMI: "ami-f2d3638a"
us-west-1:
AMI: "ami-824c4ee2"
eu-west-1:
AMI: "ami-d834aba1"
eu-west-2:
AMI: "ami-403e2524"
eu-west-3:
AMI: "ami-8ee056f3"
eu-central-1:
AMI: "ami-5652ce39"
ap-northeast-1:
AMI: "ami-ceafcba8"
ap-northeast-2:
AMI: "ami-863090e8"
ap-northeast-3:
AMI: "ami-83444afe"
ap-southeast-1:
AMI: "ami-68097514"
ap-southeast-2:
AMI: "ami-942dd1f6"
ap-south-1:
AMI: "ami-531a4c3c"
us-east-2:
AMI: "ami-f63b1193"
ca-central-1:
AMI: "ami-a954d1cd"
sa-east-1:
AMI: "ami-84175ae8"
cn-north-1:
AMI: "ami-cb19c4a6"
cn-northwest-1:
AMI: "ami-3e60745c"Resources:
myuser:
Type: AWS::IAM::User
Properties:
Path: "/"
LoginProfile:
Password: myP@ssW0rd
PasswordResetRequired: yes
ManagedPolicyArns:
- !Ref DevTeamEC2PolicyEC2Instance:
Type: 'AWS::EC2::Instance'
Properties:
KeyName: !Ref KeyName
InstanceType: !Ref InstanceType
ImageId: !FindInMap [ AWSRegion2AMI, !Ref "AWS::Region", "AMI" ]
SecurityGroups:
- !Ref EC2SecurityGroup
Tags:
- Key: Owner
Value: devteamEC2SecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: SSH access
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: '22'
ToPort: '22'
CidrIp: !Ref SSHLocationDevTeamEC2Policy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: "Policy for EC2"
Path: "/"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ec2:StartInstances
- ec2:StopInstances
- ec2:Describe*
- ec2:Get*
Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
Condition:
StringEquals:
ec2:ResourceTag/Owner: "devteam"

