circuit

A Simple Example of “Lambda-backed Custom Resource” in AWS CloudFormation

The minimal working snippets of the custom resource function.




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.

The architecture of custom resource in this example

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 function sendResponse() 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 as responsedata[“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.

Parameters send and received between CloudFormation and 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.




Continue Learning