Motivation
When we use CloudFormation to create AWS resources, it sometimes happens we want to but cannot set dynamic parameters within CloudFormation syntax. For example, we cannot create parameters using for loop. Or we cannot use built-in functions in the nest. To achieve these complicated things, we can use the custom resource with the Lambda function.
In this post, I would share minimal working snippets of the custom resource function. Using a Lambda function with Node.js runtime, each arg in a parameter field is calculated, and all the results are sent back to CloudFormation. The result from the Lambda function is seen as the Output value of CloudFormation to check the result easily.
All codes are modified from the AWS tutorial.
Note: Use Region as you want but always be in the same region.
Note 2: If you use Terraform, it might be easier to use it because Terraform supports for-each.
Create S3 bucket
Create an S3 bucket to put Lambda resources. Name it as you want. This name is used in the following.
Create test-custom-resource.js
Create a file of the Lambda function. Zip this .js file and upload the zip file to your-s3-bucket. You can name js/zip files as you like but make sure these need to be matched to the parameters in yaml.
- At this time, we only modify the function of
export.hander
. The functionsendResponse()
and below are unchanged from the original tutorial (but required.) - In
**_function of export.hander_**
,param1
is sent from yaml. This is one string, therefore, needs to be split into a comma-separated array and then fed into a for_each loop. After through for loop, we need to convert the output array to one string again. The string is sent back asresponsedata[“output1”]
. - The calculation in for loop is putting awsAccountId and strings.
test-custom-resource.js
/**
* A sample Lambda function. param1 are from Cfn yaml, output1 is send back to Cfn yaml
**/
exports.handler = function (event, context) {
console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));
var responseData = {};
var myOutArray = [];
var param1 = event.ResourceProperties.ParamSendToLambda;
var awsAccountId = context.invokedFunctionArn.split(":")[4];
var myParamArray = param1.split(",");
myParamArray.forEach(function (value) {
myOutArray.push("arn:aws:iam::" + awsAccountId + ":user/" + value);
});
responseData["output1"] = myOutArray.toString();
sendResponse(event, context, "SUCCESS", responseData);
};
// Send response to the pre-signed S3 URL
function sendResponse(event, context, responseStatus, responseData) {
var responseBody = JSON.stringify({
Status: responseStatus,
Reason:
"See the details in CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData,
});
console.log("RESPONSE BODY:\n", responseBody);
var https = require("https");
var url = require("url");
var parsedUrl = url.parse(event.ResponseURL);
var options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.path,
method: "PUT",
headers: {
"content-type": "",
"content-length": responseBody.length,
},
};
console.log("SENDING RESPONSE...\n");
var request = https.request(options, function (response) {
console.log("STATUS: " + response.statusCode);
console.log("HEADERS: " + JSON.stringify(response.headers));
// Tell AWS Lambda that the function execution is done
context.done();
});
request.on("error", function (error) {
console.log("sendResponse Error:" + error);
// Tell AWS Lambda that the function execution is done
context.done();
});
// write data to request body
request.write(responseBody);
request.end();
}
zip and upload to S3 bucket
Create the zip file:
zip test-custom-resource.zip test-custom-resource.js
Upload to S3 bucket:
```aws s3api put-object --bucket <your-s3-bucket> --key test-custom-resource.zip --body test-custom-resource.zip
Your S3 bucket tree will be like this:
your-s3-bucket/
└── test-custom-resource.zip
└── test-custom-resource.js
Create CloudFormation Template
The yaml file for the CloudFormation stack is below.
- This template only creates the custom resource Lambda (and the Lambda function will be executed). Lambda permission is created accordingly.
- Enter the name of your S3 bucket in the “
your-s3-bucket
” field. - Note that the param “
arg1,arg2,arg3
” is sent to the Lambda function. Make sure there are no spaces between args or commas. CustomResource.output1
is the value that is sent back from the Lambda function.
custom-resource.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: >-
Sample of Custom Resource with Lambda function.
Modified from AWS doc.
Parameters:
S3Bucket:
Description: The name of the bucket that contains your packaged source
Type: String
Default: your-s3-bucket
S3Key:
Description: The name of the ZIP package
Type: String
Default: test-custom-resource.zip
ModuleName:
Description: The name of the Lambda file
Type: String
Default: test-custom-resource
Params:
Description: Params send to custom resource
Type: String
Default: "arg1,arg2,arg3"
Resources:
CustomResource:
Type: "Custom::CustomResource"
Properties:
ServiceToken: !GetAtt
- CustomInfoFunction
- Arn
Region: !Ref "AWS::Region"
ParamSendToLambda: !Ref Params
CustomInfoFunction:
Type: "AWS::Lambda::Function"
Properties:
Code:
S3Bucket: !Ref S3Bucket
S3Key: !Ref S3Key
Handler: !Join
- ""
- - !Ref ModuleName
- .handler
Role: !GetAtt
- LambdaExecutionRole
- Arn
Runtime: nodejs12.x
Timeout: 30
LambdaExecutionRole:
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:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: "arn:aws:logs:*:*:*"
- Effect: Allow
Action:
- "ec2:DescribeImages"
Resource: "*"
Outputs:
CustomDataResponse1:
Description: Data From Lambda
Value: !GetAtt CustomResource.output1
This diagram shows the names of the parameters sent and received between the CloudFormation template and the Lambda function. Notice that !Ref Params
in CloudFormation template is received as event.ResouceProperties.ParamSendToLambda
in the Lambda function.
Run CloudFormation
Run and create CloudFormation stack.
aws cloudformation create-stack --template-body file://custom-resource.yaml --capabilities CAPABILITY_NAMED_IAM --stack-name <your-cfn-stack-name>
You can get “OutputValue” via CLI.
aws cloudformation describe-stacks --stack-name <your-cfn-stack-name> | grep -o ‘“OutputValue”: “[^”]*’ | grep -o ‘[^”]*$’
The result would be like this.
arn:aws:iam::<AccountID>:user/arg1,arn:aws:iam::<AccountID>:user/arg2,arn:aws:iam::<AccountID>:user/arg3
Or you can see it on the CloudFormation console.
Summary
I showed a simple sample of creating a CloudFormation template with AWS Lambda-backed custom resources. By using custom resources, we can set dynamic parameters in CloudFormation for example, using for-loop.