Example simple string replacement macro
The following example walks you through the process of using macros, from defining the macro in a template, to creating a Lambda function for the macro, and then to using the macro in a template.
In this example, we create a simple macro that inserts the specified string in place
of the specified target content in the processed template. And then we'll use it to
insert a blank WaitHandleCondition
in the specified location in the
processed template.
Creating a macro
Before using a macro, we first have to complete two things: create the Lambda function that performs the desired template processing, and then make that Lambda function available to CloudFormation by creating a macro definition.
The following sample template contains the definition for our example macro. To
make the macro available in a specific AWS account, create a stack from the
template. The macro definition specifies the macro name, a brief description, and
references the ARN of the Lambda function that CloudFormation invokes when this macro is
used in a template. (We haven't included a LogGroupName
or
LogRoleARN
property for error logging.)
In this example, assume that the stack created from this template is named
JavaMacroFunc
. Because the macro Name
property is set
to the stack name, the resulting macro is named JavaMacroFunc
as
well.
AWSTemplateFormatVersion: 2010-09-09 Resources: Macro: Type: AWS::CloudFormation::Macro Properties: Name:
!Sub '${AWS::StackName}'
Description:Adds a blank WaitConditionHandle named WaitHandle
FunctionName:'arn:aws:lambda:us-east-1:012345678910:function:JavaMacroFunc'
Using the macro
To use our macro, we include it in a template using the Fn::Transform
intrinsic function.
When we create a stack using the template below, CloudFormation calls our example
macro. The underlying Lambda function replaces one specified string with another
specified string. In this case, the result is a blank
AWS::CloudFormation::WaitConditionHandle
is inserted into the
processed template.
Parameters: ExampleParameter: Type: String Default: 'SampleMacro' Resources: 2a: Fn::Transform: Name: "JavaMacroFunc" Parameters: replacement: 'AWS::CloudFormation::WaitConditionHandle' target: '$$REPLACEMENT$$' Type: '$$REPLACEMENT$$'
-
The macro to invoke is specified as
JavaMacroFunc
, which is from the previous macro definition example. -
The macro is passed two parameters,
target
andreplacement
, which represent the target string and its desired replacement value. -
The macro can operate on the contents of the
Type
node becauseType
is a sibling of theFn::Transform
function referencing the macro. -
The resulting
AWS::CloudFormation::WaitConditionHandle
is named2a
. -
The template also contains a template parameter,
ExampleParameter
, which the macro also has access to (but doesn't use in this case).
Lambda input data
When CloudFormation processes our example template during stack creation, it passes
the following event mapping to the Lambda function referenced in the
JavaMacroFunc
macro definition.
-
region
:us-east-1
-
accountId
:012345678910
-
fragment
:{ "Type": "$$REPLACEMENT$$" }
-
transformId
:012345678910::JavaMacroFunc
-
params
:{ "replacement": "AWS::CloudFormation::WaitConditionHandle", "target": "$$REPLACEMENT$$" }
-
requestId
:5dba79b5-f117-4de0-9ce4-d40363bfb6ab
-
templateParameterValues
:{ "ExampleParameter": "SampleMacro" }
fragment
contains JSON representing the template fragment that the
macro can process. This fragment consists of the siblings of the
Fn::Transform
function call, but not the function call itself.
Also, params
contains JSON representing the macro parameters. In this
case, replacement and target. Similarly, templateParameterValues
contains JSON representing the parameters specified for the template as a
whole.
Lambda function code
Following is the actual code for the Lambda function underlying the
JavaMacroFunc
example macro. It iterates over the template fragment
included in the response (be it in string, list, or map format), looking for the
specified target string. If it finds the specified target string, the Lambda function
replaces the target string with the specified replacement string. If not, the
function leaves the template fragment unchanged. Then, the function returns a map of
the expected properties, discussed in detail below, to CloudFormation.
package com.macroexample.lambda.demo; import java.util.List; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; public class LambdaFunctionHandler implements RequestHandler<Map<String, Object>, Map<String, Object>> { private static final String REPLACEMENT = "replacement"; private static final String TARGET = "target"; private static final String PARAMS = "params"; private static final String FRAGMENT = "fragment"; private static final String REQUESTID = "requestId"; private static final String STATUS = "status"; private static final String SUCCESS = "SUCCESS"; private static final String FAILURE = "FAILURE"; @Override public Map<String, Object> handleRequest(Map<String, Object> event, Context context) { // TODO: implement your handler final Map<String, Object> responseMap = new HashMap<String, Object>(); responseMap.put(REQUESTID, event.get(REQUESTID)); responseMap.put(STATUS, FAILURE); try { if (!event.containsKey(PARAMS)) { throw new RuntimeException("Params are required"); } final Map<String, Object> params = (Map<String, Object>) event.get(PARAMS); if (!params.containsKey(REPLACEMENT) || !params.containsKey(TARGET)) { throw new RuntimeException("replacement or target under Params are required"); } final String replacement = (String) params.get(REPLACEMENT); final String target = (String) params.get(TARGET); final Object fragment = event.getOrDefault(FRAGMENT, new HashMap<String, Object>()); final Object retFragment; if (fragment instanceof String) { retFragment = iterateAndReplace(replacement, target, (String) fragment); } else if (fragment instanceof List) { retFragment = iterateAndReplace(replacement, target, (List<Object>) fragment); } else if (fragment instanceof Map) { retFragment = iterateAndReplace(replacement, target, (Map<String, Object>) fragment); } else { retFragment = fragment; } responseMap.put(STATUS, SUCCESS); responseMap.put(FRAGMENT, retFragment); return responseMap; } catch (Exception e) { e.printStackTrace(); context.getLogger().log(e.getMessage()); return responseMap; } } private Map<String, Object> iterateAndReplace(final String replacement, final String target, final Map<String, Object> fragment) { final Map<String, Object> retFragment = new HashMap<String, Object>(); final List<String> replacementKeys = new ArrayList<>(); fragment.forEach((k, v) -> { if (v instanceof String) { retFragment.put(k, iterateAndReplace(replacement, target, (String)v)); } else if (v instanceof List) { retFragment.put(k, iterateAndReplace(replacement, target, (List<Object>)v)); } else if (v instanceof Map ) { retFragment.put(k, iterateAndReplace(replacement, target, (Map<String, Object>) v)); } else { retFragment.put(k, v); } }); return retFragment; } private List<Object> iterateAndReplace(final String replacement, final String target, final List<Object> fragment) { final List<Object> retFragment = new ArrayList<>(); fragment.forEach(o -> { if (o instanceof String) { retFragment.add(iterateAndReplace(replacement, target, (String) o)); } else if (o instanceof List) { retFragment.add(iterateAndReplace(replacement, target, (List<Object>) o)); } else if (o instanceof Map) { retFragment.add(iterateAndReplace(replacement, target, (Map<String, Object>) o)); } else { retFragment.add(o); } }); return retFragment; } private String iterateAndReplace(final String replacement, final String target, final String fragment) { System.out.println(replacement + " == " + target + " == " + fragment ); if (fragment != null AND_AND fragment.equals(target)) return replacement; return fragment; } }
Lambda function response
Following is the mapping that the Lambda function returns to CloudFormation for processing.
-
requestId
:5dba79b5-f117-4de0-9ce4-d40363bfb6ab
-
status
:SUCCESS
-
fragment
:{ "Type": "AWS::CloudFormation::WaitConditionHandle" }
The requestId
matches that sent from CloudFormation, and a
status
value of SUCCESS
denotes that the Lambda
function successfully processed the template fragment included in the request. In
this response, fragment
contains JSON representing the content to
insert into the processed template in place of the original template snippet.
Resulting processed template
After CloudFormation receives a successful response from the Lambda function, it inserts the returned template fragment into the processed template.
Below is the resulting processed template for our example. The
Fn::Transform
intrinsic function call that referenced the
JavaMacroFunc
macro is no longer included. The template fragment
returned by the Lambda function is included in the appropriate location, with the
result that the content "Type": "$$REPLACEMENT$$"
has been replaced
with "Type": "AWS::CloudFormation::WaitConditionHandle"
.
{ "Parameters": { "ExampleParameter": { "Default": "SampleMacro", "Type": "String" } }, "Resources": { "2a": { "Type": "AWS::CloudFormation::WaitConditionHandle" } } }