Create Lambda functions to evaluate resources for Lambda Hooks
AWS CloudFormation Lambda Hooks allows you to evaluate CloudFormation and AWS Cloud Control API operations against your own custom code. Your Hook can block an operation from proceeding, or issue a warning to the caller and allow the operation to proceed. When you create a Lambda Hook, you can configure it to intercept and evaluate the following CloudFormation operations:
-
Resource operations
-
Stack operations
-
Change set operations
Topics
Developing a Lambda Hook
When Hooks invoke your Lambda it will wait up to 30 seconds for the Lambda to evaluate the input. The Lambda will return a JSON response that indicates whether the Hook succeeded or failed.
Request input
The input passed to your Lambda function depends on the Hook target operation (examples: stack, resource, or change set).
Response input
In order to communicate to Hooks if your request succeeded or failed, your Lambda function needs to return a JSON response.
The following is an example shape of the response Hooks expects:
{ "hookStatus": "SUCCESS" or "FAILED" or "IN_PROGRESS", "errorCode": None or "NonCompliant" or "InternalFailure" "message": String, "clientRequestToken": String "callbackContext": None, "callbackDelaySeconds": Integer, }
- hookStatus
-
The status of the Hook. This is a required field.
Valid values: (
SUCCESS
|FAILED
|IN_PROGRESS
)Note
A Hook can return
IN_PROGRESS
3 times. If no result is returned, the Hook will fail. For a Lambda Hook, this means your Lambda function can be invoked up to 3 times. - errorCode
-
Shows whether the operation was evaluated and determined to be invalid, or if errors occurred within the Hook, preventing the evaluation. This field is required if the Hook fails.
Valid values: (
NonCompliant
|InternalFailure
) - message
-
The message to the caller that states why the Hook succeeded or failed.
Note
When evaluating CloudFormation operations, this field is truncated to 4096 characters.
When evaluating Cloud Control API operations, this field is truncated to 1024 characters.
- clientRequestToken
-
The request token that was provided as an input to the Hook request. This is a required field.
- callbackContext
-
If you indicate that the
hookStatus
isIN_PROGRESS
you pass an additional context that's provided as input when the Lambda function is reinvoked. - callbackDelaySeconds
-
How long Hooks should wait to invoke this Hook again.
Examples
The following is an example of a successful response:
{ "hookStatus": "SUCCESS", "message": "compliant", "clientRequestToken": "123avjdjk31" }
The following is an example of a failed response:
{ "hookStatus": "FAILED", "errorCode": "NON_COMPLIANT", "message": "S3 Bucket Versioning must be enabled.", "clientRequestToken": "123avjdjk31" }
Evaluating resource operations with Lambda Hooks
Any time you create, update, or delete a resource, that's considered a resource
operation. As an example, if you run update a CloudFormation stack that creates a new
resource, you have completed a resource operation. When you create, update or delete
a resource using Cloud Control API, that is also considered a resource operation. You can
configure your CloudFormation Lambda Hook to target RESOURCE
and
CLOUD_CONTROL
operations in the Hook
TargetOperations
configuration.
Note
The delete
Hook handler is only invoked when a resource
is deleted using an operation trigger from cloud-control
delete-resource
or cloudformation delete-stack
.
Topics
Lambda Hook resource input syntax
When your Lambda is invoked for a resource operation, you'll receive a JSON input containing the resource properties, proposed properties, and the context around the Hook invocation.
The following is an example shape of the JSON input:
{ "awsAccountId": String, "stackId": String, "changeSetId": String, "hookTypeName": String, "hookTypeVersion": String, "hookModel": { "LambdaFunction": String }, "actionInvocationPoint": "CREATE_PRE_PROVISION" or "UPDATE_PRE_PROVISION" or "DELETE_PRE_PROVISION" "requestData": { "targetName": String, "targetType": String, "targetLogicalId": String, "targetModel": { "resourceProperties": {...}, "previousResourceProperties": {...} } }, "requestContext": { "invocation": 1, "callbackContext": null } }
awsAccountId
-
The ID of the AWS account containing the resource being evaluated.
stackId
-
The stack ID of the CloudFormation stack this operation is a part of. This field is empty if the caller is Cloud Control API.
changeSetId
-
The ID of the change set that initiated the Hook invocation. This value is empty if the resource change was initiated by Cloud Control API, or the
create-stack
,update-stack
, ordelete-stack
operations. hookTypeName
-
The name of the Hook that's running.
hookTypeVersion
-
The version of the Hook that's running.
hookModel
-
LambdaFunction
-
The current Lambda ARN invoked by the Hook.
actionInvocationPoint
-
The exact point in the provisioning logic where the Hook runs.
Valid values: (
CREATE_PRE_PROVISION
|UPDATE_PRE_PROVISION
|DELETE_PRE_PROVISION
) requestData
-
targetName
-
The name of the target resource being created.
targetType
-
The target type being created, for example
AWS::S3::Bucket
. targetLogicalId
-
The logical ID of the resource being evaluated. If the origin of the Hook invocation is CloudFormation, this will be the logical resource ID defined in your CloudFormation template. If the origin of this Hook invocation is Cloud Control API, this will be will be a constructed value.
targetModel
-
resourceProperties
-
The proposed properties of the resource being modified. If the resource is being deleted, this value will be empty.
previousResourceProperties
-
The properties that are currently associated with the resource being modified. If the resource is being created, this value will be empty.
requestContext
-
- invocation
-
The current attempt at executing the Hook.
- callbackContext
-
If the Hookwas set to
IN_PROGRESS
, andcallbackContext
was returned, it will be here after reinvocation.
Example Lambda Hook resource change input
In following example input, the Guard Hook will receive
the definition of the AWS::DynamoDB::Table
resource being changed.
The ProvisionedThroughput
and ReadCapacityUnits
parameters are being updated from 3 to 10.
For more information on the available properties for the resource, see AWS::DynamoDB::Table .
{ "awsAccountId": "123456789", "stackId": "arn:aws:cloudformation:eu-central-1:123456789:stack/test-stack/123456abcd", "hookTypeName": "my::lambda::resourcehookfunction", "hookTypeVersion": "00000008", "hookModel": { "LambdaFunction": "arn:aws:lambda:eu-central-1:123456789:function:resourcehookfunction" }, "actionInvocationPoint": "UPDATE_PRE_PROVISION", "requestData": { "targetName": "AWS::DynamoDB::Table", "targetType": "AWS::DynamoDB::Table", "targetLogicalId": "DDBTable", "targetModel": { "resourceProperties": { "AttributeDefinitions": [ { "AttributeType": "S", "AttributeName": "Album" }, { "AttributeType": "S", "AttributeName": "Artist" } ], "ProvisionedThroughput": { "WriteCapacityUnits": 5, "ReadCapacityUnits": 10 }, "KeySchema": [ { "KeyType": "HASH", "AttributeName": "Album" }, { "KeyType": "RANGE", "AttributeName": "Artist" } ] }, "previousResourceProperties": { "AttributeDefinitions": [ { "AttributeType": "S", "AttributeName": "Album" }, { "AttributeType": "S", "AttributeName": "Artist" } ], "ProvisionedThroughput": { "WriteCapacityUnits": 5, "ReadCapacityUnits": 5 }, "KeySchema": [ { "KeyType": "HASH", "AttributeName": "Album" }, { "KeyType": "RANGE", "AttributeName": "Artist" } ] } } }, "requestContext": { "invocation": 1, "callbackContext": null } }
Example Lambda function for resource operations
The following example Lambda Hook targets Node.js
. This
is a simple function that fails any resource update to DynamoDB, which tries to set
the ProvisionedThroughput ReadCapacity
to something larger than 10.
If the Hook succeeds, the message, "ReadCapacity is correctly
configured," will display to the caller. If the request fails validation, the
Hook will fail with the status, "ReadCapacity cannot be more than
10."
export const handler = async (event, context) => { var targetModel = event?.requestData?.targetModel; var targetName = event?.requestData?.targetName; var response = { "hookStatus": "SUCCESS", "message": "ReadCapacity is correctly configured.", "clientRequestToken": event.clientRequestToken }; if (targetName == "AWS::DynamoDB::Table") { var readCapacity = targetModel?.resourceProperties?.ProvisionedThroughput?.ReadCapacityUnits; if (readCapacity > 10) { response.hookStatus = "FAILED"; response.errorCode = "NonCompliant"; response.message = "ReadCapacity must be cannot be more than 10."; } } return response; };
Evaluating stack operations with Lambda Hooks
Any time you create, update, or delete a stack with a new template, you can
configure your CloudFormation Lambda Hook to start by evaluating the new
template and potentially block the stack operation from proceeding. You can
configure your CloudFormation Lambda Hook to target STACK
operations in the Hook TargetOperations
configuration.
Topics
Lambda Hook stack input syntax
When your Lambda is invoked for a stack operation, you'll receive a JSON
request containing the Hook invocation context,
actionInvocationPoint
, and request context. Due to the size of
CloudFormation templates, and the limited input size accepted by Lambda functions,
the actual templates are stored in an Amazon S3 object. The input of the
requestData
includes an Amazon S3 resigned URL to another object,
which contains the current and previous template version.
The following is an example shape of the JSON input:
{ "clientRequesttoken": String, "awsAccountId": String, "stackID": String, "changeSetId": String, "hookTypeName": String, "hookTypeVersion": String, "hookModel": { "LambdaFunction":String }, "actionInvocationPoint": "CREATE_PRE_PROVISION" or "UPDATE_PRE_PROVISION" or "DELETE_PRE_PROVISION" "requestData": { "targetName": "STACK", "targetType": "STACK", "targetLogicalId": String, "payload": String (S3 Presigned URL) }, "requestContext": { "invocation": Integer, "callbackContext": String } }
clientRequesttoken
-
The request token that was provided as an input to the Hook request. This is a required field.
awsAccountId
-
The ID of the AWS account containing the stack being evaluated.
stackID
-
The stack ID of the CloudFormation stack.
changeSetId
-
The ID of the change set that initiated the Hook invocation. This value is empty if the stack change was initiated by Cloud Control API, or the
create-stack
,update-stack
, ordelete-stack
operations. hookTypeName
-
The name of the Hook that's running.
hookTypeVersion
-
The version of the Hook that's running.
hookModel
-
LambdaFunction
-
The current Lambda ARN invoked by the Hook.
actionInvocationPoint
-
The exact point in the provisioning logic where the Hook runs.
Valid values: (
CREATE_PRE_PROVISION
|UPDATE_PRE_PROVISION
|DELETE_PRE_PROVISION
) requestData
-
targetName
-
This value will be
STACK
. targetType
-
This value will be
STACK
. targetLogicalId
-
The stack name.
payload
-
The Amazon S3 presigned URL containing a JSON object with the current and previous template definitions.
requestContext
-
If the Hook is being reinvoked, this object will be set.
invocation
-
The current attempt at executing the Hook.
callbackContext
-
If the Hook was set to
IN_PROGRESS
andcallbackContext
was returned, it will be here upon reinvocation.
The payload
property in the request data is a URL that your code
needs to fetch. Once it has received the URL, you get an object with the
following schema:
{ "template": String, "previousTemplate": String }
template
-
The full CloudFormation template that was provided to
create-stack
orupdate-stack
. It can be a JSON or YAML string depending on what was provided to CloudFormation.In
delete-stack
operations, this value will be empty. previousTemplate
-
The previous CloudFormation template. It can be a JSON or YAML string depending on what was provided to CloudFormation.
In
delete-stack
operations, this value will be empty.
Example Lambda Hook stack change input
The following is an example stack change input. The Hook is
evaluating a change which updates the ObjectLockEnabled
to true,
and adds an Amazon SQS queue:
{ "clientRequestToken": "f8da6d11-b23f-48f4-814c-0fb6a667f50e", "awsAccountId": "123456789", "stackId": "arn:aws:cloudformation:eu-central-1:123456789:stack/david-ddb-test-stack/400b40f0-8e72-11ef-80ab-02f2902f0df1", "changeSetId": null, "hookTypeName": "my::lambda::stackhook", "hookTypeVersion": "00000008", "hookModel": { "LambdaFunction": "arn:aws:lambda:eu-central-1:123456789:function:stackhookfunction" }, "actionInvocationPoint": "UPDATE_PRE_PROVISION", "requestData": { "targetName": "STACK", "targetType": "STACK", "targetLogicalId": "my-cloudformation-stack", "payload": "https://s3......" }, "requestContext": { "invocation": 1, "callbackContext": null } }
This is an example payload
of the
requestData
:
{ "template": "{\"Resources\":{\"S3Bucket\":{\"Type\":\"AWS::S3::Bucket\",\"Properties\":{\"ObjectLockEnabled\":true}},\"SQSQueue\":{\"Type\":\"AWS::SQS::Queue\",\"Properties\":{\"QueueName\":\"NewQueue\"}}}}", "previousTemplate": "{\"Resources\":{\"S3Bucket\":{\"Type\":\"AWS::S3::Bucket\",\"Properties\":{\"ObjectLockEnabled\":false}}}}" }
Example Lambda function for stack operations
The following example Lambda Hook is targeting Node.js
.
It's a simple function that downloads the stack operation payload, parses the
template JSON, and returns SUCCESS
.
export const handler = async (event, context) => { var targetType = event?.requestData?.targetType; var payloadUrl = event?.requestData?.payload; var response = { "hookStatus": "SUCCESS", "message": "Stack update is compliant", "clientRequestToken": event.clientRequestToken }; try { const templateHookPayloadRequest = await fetch(payloadUrl); const templateHookPayload = await templateHookPayloadRequest.json() if (templateHookPayload.template) { // Do something with the template templateHookPayload.template // JSON or YAML } if (templateHookPayload.previousTemplate) { // Do something with the template templateHookPayload.previousTemplate // JSON or YAML } } catch (error) { console.log(error); response.hookStatus = "FAILED"; response.message = "Failed to evaluate stack operation."; response.errorCode = "InternalFailure"; } return response; };
Evaluating change set operations with Lambda Hooks
Any time you create a change set, you can configure your CloudFormation Lambda
Hook to first evaluate the new change set and potentially block its
execution. You can configure your CloudFormation Lambda Hook to target
CHANGE_SET
operations in the Hook
TargetOperations
configuration.
Topics
Lambda Hook change set input syntax
The input for change set operations is similar to stack operations, but the
payload of the requestData
also includes a list of resource changes
introduced by the change set.
The following is an example shape of the JSON input:
{ "clientRequesttoken": String, "awsAccountId": String, "stackID": String, "changeSetId": String, "hookTypeName": String, "hookTypeVersion": String, "hookModel": { "LambdaFunction":String }, "requestData": { "targetName": "CHANGE_SET", "targetType": "CHANGE_SET", "targetLogicalId": String, "payload": String (S3 Presigned URL) }, "requestContext": { "invocation": Integer, "callbackContext": String } }
clientRequesttoken
-
The request token that was provided as an input to the Hook request. This is a required field.
awsAccountId
-
The ID of the AWS account containing the stack being evaluated.
stackID
-
The stack ID of the CloudFormation stack.
changeSetId
-
The ID of the change set that initiated the Hook invocation.
hookTypeName
-
The name of the Hook that's running.
hookTypeVersion
-
The version of the Hook that's running.
hookModel
-
LambdaFunction
-
The current Lambda ARN invoked by the Hook.
requestData
-
targetName
-
This value will be
CHANGE_SET
. targetType
-
This value will be
CHANGE_SET
. targetLogicalId
-
The change set ARN..
payload
-
The Amazon S3 presigned URL containing a JSON object with the current template, as well as a list of changes introduced by this change set.
requestContext
-
If the Hook is being reinvoked, this object will be set.
invocation
-
The current attempt at executing the Hook.
callbackContext
-
If the Hook was set to
IN_PROGRESS
andcallbackContext
was returned, it will be here upon reinvocation.
The payload
property in the request data is a URL that your code
needs to fetch. Once it has received the URL, you get an object with the
following schema:
{ "template": String, "changedResources": [ { "action": String, "beforeContext": JSON String, "afterContext": JSON String, "lineNumber": Integer, "logicalResourceId": String, "resourceType": String } ] }
template
-
The full CloudFormation template that was provided to
create-stack
orupdate-stack
. It can be a JSON or YAML string depending on what was provided to CloudFormation. changedResources
-
A list of changed resources.
action
-
The type of change applied to the resource.
Valid values: (
CREATE
|UPDATE
|DELETE
) beforeContext
-
A JSON string of the resource properties before the change. This value is null when the resource is being created. All boolean and number values in this JSON string are STRINGS.
afterContext
-
A JSON string of the resources properties if this change set is executed. This value is null when the resource is being deleted. All boolean and number values in this JSON string are STRINGS.
lineNumber
-
The line number in the template that caused this change. If the action is
DELETE
this value will be null. logicalResourceId
-
The logical resource ID of the resource being changed.
resourceType
-
The resource type that’s being changed.
Example Lambda Hook change set change input
The following is an example change set change input. In the following example,
you can see the changes introduced by the change set. The first change is
deleting a queue called CoolQueue
. The second change is adding a
new queue called NewCoolQueue
. The last change is an update to the
DynamoDBTable
.
{ "clientRequestToken": "f8da6d11-b23f-48f4-814c-0fb6a667f50e", "awsAccountId": "123456789", "stackId": "arn:aws:cloudformation:eu-central-1:123456789:stack/david-ddb-test-stack/400b40f0-8e72-11ef-80ab-02f2902f0df1", "changeSetId": "arn:aws:cloudformation:eu-central-1:123456789:changeSet/davids-change-set/59ebd63c-7c89-4771-a576-74c3047c15c6", "hookTypeName": "my::lambda::changesethook", "hookTypeVersion": "00000008", "hookModel": { "LambdaFunction": "arn:aws:lambda:eu-central-1:123456789:function:changesethookfunction" }, "actionInvocationPoint": "CREATE_PRE_PROVISION", "requestData": { "targetName": "CHANGE_SET", "targetType": "CHANGE_SET", "targetLogicalId": "arn:aws:cloudformation:eu-central-1:123456789:changeSet/davids-change-set/59ebd63c-7c89-4771-a576-74c3047c15c6", "payload": "https://s3......" }, "requestContext": { "invocation": 1, "callbackContext": null } }
This is an example payload
of the
requestData.payload
:
{ template: 'Resources:\n' + ' DynamoDBTable:\n' + ' Type: AWS::DynamoDB::Table\n' + ' Properties:\n' + ' AttributeDefinitions:\n' + ' - AttributeName: "PK"\n' + ' AttributeType: "S"\n' + ' BillingMode: "PAY_PER_REQUEST"\n' + ' KeySchema:\n' + ' - AttributeName: "PK"\n' + ' KeyType: "HASH"\n' + ' PointInTimeRecoverySpecification:\n' + ' PointInTimeRecoveryEnabled: false\n' + ' NewSQSQueue:\n' + ' Type: AWS::SQS::Queue\n' + ' Properties:\n' + ' QueueName: "NewCoolQueue"', changedResources: [ { logicalResourceId: 'SQSQueue', resourceType: 'AWS::SQS::Queue', action: 'DELETE', lineNumber: null, beforeContext: '{"Properties":{"QueueName":"CoolQueue"}}', afterContext: null }, { logicalResourceId: 'NewSQSQueue', resourceType: 'AWS::SQS::Queue', action: 'CREATE', lineNumber: 14, beforeContext: null, afterContext: '{"Properties":{"QueueName":"NewCoolQueue"}}' }, { logicalResourceId: 'DynamoDBTable', resourceType: 'AWS::DynamoDB::Table', action: 'UPDATE', lineNumber: 2, beforeContext: '{"Properties":{"BillingMode":"PAY_PER_REQUEST","AttributeDefinitions":[{"AttributeType":"S","AttributeName":"PK"}],"KeySchema":[{"KeyType":"HASH","AttributeName":"PK"}]}}', afterContext: '{"Properties":{"BillingMode":"PAY_PER_REQUEST","PointInTimeRecoverySpecification":{"PointInTimeRecoveryEnabled":"false"},"AttributeDefinitions":[{"AttributeType":"S","AttributeName":"PK"}],"KeySchema":[{"KeyType":"HASH","AttributeName":"PK"}]}}' } ] }
Example Lambda function for change set operations
The following example Lambda Hook is targeting Node.js
.
It's a simple function that downloads the change set operation payload, loops
through each change, and then prints out the before and after properties before
it returns a SUCCESS
.
export const handler = async (event, context) => { var payloadUrl = event?.requestData?.payload; var response = { "hookStatus": "SUCCESS", "message": "Change set changes are compliant", "clientRequestToken": event.clientRequestToken }; try { const changeSetHookPayloadRequest = await fetch(payloadUrl); const changeSetHookPayload = await changeSetHookPayloadRequest.json(); const changes = changeSetHookPayload.changedResources || []; for(const change of changes) { var beforeContext = {}; var afterContext = {}; if(change.beforeContext) { beforeContext = JSON.parse(change.beforeContext); } if(change.afterContext) { afterContext = JSON.parse(change.afterContext); } console.log(beforeContext) console.log(afterContext) // Evaluate Change here } } catch (error) { console.log(error); response.hookStatus = "FAILED"; response.message = "Failed to evaluate change set operation."; response.errorCode = "InternalFailure"; } return response; };