This walkthrough show you the resource configuration of a Lambda-backed custom resource using a sample template, and how to launch a Lambda-backed custom resource using the same sample template. The sample custom resource template creates a Lambda-backed custom resource that creates a delay mechanism.
Note
Be aware of the following:
CloudFormation is a free service; however, you are charged for the AWS resources, such as
the Lambda function and EC2 instance, that you include in your stacks at the current rate for
each. For more information about AWS pricing, see the detail page for each product at http://aws.amazon.com
A Lambda-backed custom resource used to be the recommended method for retrieving AMI IDs. Rather than creating a custom resource and Lambda function to retried AMI IDs, you can use AWS Systems Manager parameters in your template to retrieve the latest AMI ID value stored in a Systems Manager parameter. This makes your templates more reusable and easier to maintain. For more information, see Specify existing resources at runtime with CloudFormation-supplied parameter types.
Topics
Overview
The following steps provide an overview of this implementation.
-
Save a sample template containing the Lambda function code as a file on your machine with the name
samplelambdabackedcustomresource.template
. -
Use the sample template to create your stack with a custom resource, a Lambda function, and an IAM role that uses Lambda to write logs to CloudWatch.
-
The custom resource implements a delay mechanism. The Lambda function sleeps for the specified duration during create and update operations. The resource will only be invoked for an update operation if the properties are modified.
-
The template
Outputs
exports two values,TimeWaited
and theWaiterId
.TimeWaited
is the length of time the resource waited, andWaiterId
is the unique ID generated during the execution.
Sample template
You can see the Lambda-backed custom resource sample template with the delay mechanism below:
JSON
{
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"LambdaExecutionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
},
"Path": "/",
"Policies": [
{
"PolicyName": "AllowLogs",
"PolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:*"
],
"Resource": "*"
}
]
}
}
]
}
},
"CFNWaiter": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "index.handler",
"Runtime": "python3.9",
"Timeout": 900,
"Role": {
"Fn::GetAtt": [
"LambdaExecutionRole",
"Arn"
]
},
"Code": {
"ZipFile": {
"Fn::Sub": "from time import sleep\nimport json\nimport cfnresponse\nimport uuid\n\ndef handler(event, context):\n wait_seconds = 0\n id = str(uuid.uuid1())\n if event[\"RequestType\"] in [\"Create\", \"Update\"]:\n wait_seconds = int(event[\"ResourceProperties\"].get(\"WaitSeconds\", 0))\n sleep(wait_seconds)\n response = {\n \"TimeWaited\": wait_seconds,\n \"Id\": id \n }\n cfnresponse.send(event, context, cfnresponse.SUCCESS, response, \"Waiter-\"+id)"
}
}
}
},
"CFNWaiterCustomResource": {
"Type": "AWS::CloudFormation::CustomResource",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"CFNWaiter",
"Arn"
]
},
"WaitSeconds": 60
}
}
},
"Outputs": {
"TimeWaited": {
"Value": {
"Fn::GetAtt": [
"CFNWaiterCustomResource",
"TimeWaited"
]
},
"Export": {
"Name": "TimeWaited"
}
},
"WaiterId": {
"Value": {
"Fn::GetAtt": [
"CFNWaiterCustomResource",
"Id"
]
},
"Export": {
"Name": "WaiterId"
}
}
}
}
YAML
AWSTemplateFormatVersion: "2010-09-09"
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
Policies:
- PolicyName: "AllowLogs"
PolicyDocument:
Statement:
- Effect: "Allow"
Action:
- "logs:*"
Resource: "*"
CFNWaiter:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Runtime: python3.9
Timeout: 900
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile:
!Sub |
from time import sleep
import json
import cfnresponse
import uuid
def handler(event, context):
wait_seconds = 0
id = str(uuid.uuid1())
if event["RequestType"] in ["Create", "Update"]:
wait_seconds = int(event["ResourceProperties"].get("WaitSeconds", 0))
sleep(wait_seconds)
response = {
"TimeWaited": wait_seconds,
"Id": id
}
cfnresponse.send(event, context, cfnresponse.SUCCESS, response, "Waiter-"+id)
CFNWaiterCustomResource:
Type: "AWS::CloudFormation::CustomResource"
Properties:
ServiceToken: !GetAtt CFNWaiter.Arn
WaitSeconds: 60
Outputs:
TimeWaited:
Value: !GetAtt CFNWaiterCustomResource.TimeWaited
Export:
Name: TimeWaited
WaiterId:
Value: !GetAtt CFNWaiterCustomResource.Id
Export:
Name: WaiterId
Sample template walkthrough
The following snippets explain relevant parts of the sample template to help you understand how the Lambda function is associated with a custom resource and understand the output.
- AWS::Lambda::Function resource
CFNWaiter
-
The
AWS::Lambda::Function
resource specifies the function's source code, handler name, runtime environment, and execution role Amazon Resource Name (ARN).The
Handler
property is set toindex.handler
since it uses a Python source code. For more information on accepted handler identifiers when using inline function source codes, see AWS::Lambda::Function Code.The
Runtime
is specified aspython3.9
since the source file is a Python code.The
Timeout
is set to 900 seconds.The
Role
property uses theFn::GetAtt
function to get the ARN of theLambdaExecutionRole
execution role that's declared in theAWS::IAM::Role
resource in the template.The
Code
property defines the function code inline using a Python function. The Python function in the sample template does the following:-
Create a unique ID using the UUID
-
Check if the request is a create or update request
-
Sleep for the duration specified for
WaitSeconds
duringCreate
orUpdate
requests -
Return the wait time and unique ID
-
JSON
...
"CFNWaiter": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "index.handler",
"Runtime": "python3.9",
"Timeout": 900,
"Role": {
"Fn::GetAtt": [
"LambdaExecutionRole",
"Arn"
]
},
"Code": {
"ZipFile": {
"Fn::Sub": "from time import sleep\nimport json\nimport cfnresponse\nimport uuid\n\ndef handler(event, context):\n wait_seconds = 0\n id = str(uuid.uuid1())\n if event[\"RequestType\"] in [\"Create\", \"Update\"]:\n wait_seconds = int(event[\"ResourceProperties\"].get(\"WaitSeconds\", 0))\n sleep(wait_seconds)\n response = {\n \"TimeWaited\": wait_seconds,\n \"Id\": id \n }\n cfnresponse.send(event, context, cfnresponse.SUCCESS, response, \"Waiter-\"+id)"
}
}
}
},
...
YAML
...
CFNWaiter:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Runtime: python3.9
Timeout: 900
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile:
!Sub |
from time import sleep
import json
import cfnresponse
import uuid
def handler(event, context):
wait_seconds = 0
id = str(uuid.uuid1())
if event["RequestType"] in ["Create", "Update"]:
wait_seconds = int(event["ResourceProperties"].get("WaitSeconds", 0))
sleep(wait_seconds)
response = {
"TimeWaited": wait_seconds,
"Id": id
}
cfnresponse.send(event, context, cfnresponse.SUCCESS, response, "Waiter-"+id)
...
- AWS::IAM::Role
resource
LambdaExecutionRole
-
The
AWS::IAM:Role
resource creates an execution role for the Lambda function, which includes an assume role policy which allows Lambda to use it. It also contains a policy allowing CloudWatch Logs access.
JSON
...
"LambdaExecutionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
},
"Path": "/",
"Policies": [
{
"PolicyName": "AllowLogs",
"PolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:*"
],
"Resource": "*"
}
]
}
}
]
}
},
...
YAML
...
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
Policies:
- PolicyName: "AllowLogs"
PolicyDocument:
Statement:
- Effect: "Allow"
Action:
- "logs:*"
Resource: "*"
...
- AWS::CloudFormation::CustomResource
resource
CFNWaiterCustomResource
-
The custom resource links to the Lambda function with its ARN using
!GetAtt CFNWaiter.Arn
. It will implement a 60 second wait time for create and update operations, as set inWaitSeconds
. The resource will only be invoked for an update operation if the properties are modified.
JSON
...
"CFNWaiterCustomResource": {
"Type": "AWS::CloudFormation::CustomResource",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"CFNWaiter",
"Arn"
]
},
"WaitSeconds": 60
}
}
},
...
YAML
...
CFNWaiterCustomResource:
Type: "AWS::CloudFormation::CustomResource"
Properties:
ServiceToken: !GetAtt CFNWaiter.Arn
WaitSeconds: 60
...
Outputs
-
The
Output
of this template are theTimeWaited
and theWaiterId
. TheTimeWaited
value uses aFn::GetAtt
function to provide the amount of time the waiter resource actually waited. TheWaiterId
uses aFn::GetAtt
function to provide the unique ID that was generated and associated with the execution.
JSON
...
"Outputs": {
"TimeWaited": {
"Value": {
"Fn::GetAtt": [
"CFNWaiterCustomResource",
"TimeWaited"
]
},
"Export": {
"Name": "TimeWaited"
}
},
"WaiterId": {
"Value": {
"Fn::GetAtt": [
"CFNWaiterCustomResource",
"Id"
]
},
"Export": {
"Name": "WaiterId"
}
}
}
...
YAML
...
Outputs:
TimeWaited:
Value: !GetAtt CFNWaiterCustomResource.TimeWaited
Export:
Name: TimeWaited
WaiterId:
Value: !GetAtt CFNWaiterCustomResource.Id
Export:
Name: WaiterId
...
Prerequisites
You must have IAM permissions to use all the corresponding services, such as Lambda and CloudFormation.
Launching the stack
To create the stack
-
Find the template of your preference (YAML or JSON) from the Sample template section and save it to your machine with the name
samplelambdabackedcustomresource.template
. -
Open the CloudFormation console at https://console.aws.amazon.com/cloudformation/
. -
From the Stacks page, choose Create stack at top right, and then choose With new resources (standard).
For Prerequisite - Prepare template, choose Choose an existing template.
-
For Specify template, choose Upload a template file, and then choose Choose file.
-
Select the
samplelambdabackedcustomresource.template
template file you saved earlier. -
Choose Next.
-
For Stack name, type
SampleCustomResourceStack
. and choose Next. -
For this walkthrough, you don't need to add tags or specify advanced settings, so choose Next.
-
Ensure that the stack name looks correct, and then choose Create.
It might take several minutes for CloudFormation to create your stack. To monitor progress, view the stack events. For more information, see View stack information from the CloudFormation console.
If stack creation succeeds, all resources in the stack, such as the Lambda function and custom resource, were created. You have successfully used a Lambda function and custom resource.
If the Lambda function returns an error, view the function's logs in the CloudWatch Logs console
Cleaning up resources
Delete the stack to clean up all the stack resources that you created so that you aren't charged for unnecessary resources.
To delete the stack
-
From the CloudFormation console, choose the SampleCustomResourceStack stack.
-
Choose Actions and then Delete Stack.
-
In the confirmation message, choose Yes, Delete.
All the resources that you created are deleted.
Now that you understand how to create and use Lambda-backed custom resource, you can use the sample template and code from this walkthrough to build and experiment with other stacks and functions.