Introduction to AWS CloudFormation Custom Resource
Introduction
This note describes the AWS CloudFormation Custom resource.
A custom resource that fetches the API Key from a deployed AWS API Gateway is shown as an example to demonstrate how custom resources work and can be used.
CloudFormation Custom Resource
A Custom Resource has only one required property, the ServiceToken
. The
ServiceToken
can either be the ARN of a Lamda or an SNS. In the case of a
lambda the lambda will be called on create, delete or update of the custom
resource. When the lambda is called all properties of the custom resource can be
found in the ResourceProperty
of the request object.
A custom resource need to signal that deployment succeeded (or failed) and does
so by sending a response object to the response URL that can be found in the
ResponseURL
of the request object.
This is basically how a Custom Resource is defined in the template. In this
example, a CloudFormation parameter is passed in to the Custom Resource using
Fn::Ref
Intrinsic function and the parameter should contain the API Key Id.
Parameters:
ApiKeyIdParam:
Type: String
Resources:
GetApiKey:
Type: Custom::GetApiKey
Properties:
ServiceToken: !GetAtt GetApiKeyResourceHandler.Arn
ApiKeyId: !Ref ApiKeyIdParam
The Arn for the Lambda to be invoked is assigned to the ServiceToken
property,
in this example the Lambda resource is called GetApiKeyResourceHandler
. The
parameter ApiKeyId
passed in as a property will be read by the handler lambda
GetApiKeyResourceHandler
.
Resource handler Lambda
Any input needed by the logic implemented in the Custom Resource should be set
as properties on the custom resource. The properties will then be available in
the ResourceProperty
of the request object and can be read by the lambda
handler. The lambda handler can then e.g. use AWS SDK to read or manipulate
other resources.
GetApiKeyResourceHandler:
Type: AWS::Lambda::Function
Properties:
Description: Handler for GetApiKey Custom Resource
Handler: index.handler
Runtime: python3.9
Role: !GetAtt GetApiKeyResourceHandlerRole.Arn
Code:
ZipFile: |
import boto3
import json
import urllib.request
def handler(event, context):
response_status = "SUCCESS"
if event['RequestType'] != 'Create':
# Only care about stack Create opertations
send_response(event, response_status)
return
api_key_id = event['ResourceProperties'].get('ApiKeyId')
client = boto3.client('apigateway')
message = ''
data = {}
if api_key_id:
try:
r = client.get_api_key(apiKey = api_key_id, includeValue=True)
# Add output
data = { "ApiKey": r['value'] }
except Exception as e:
response_status = "FAILED"
print(f'Error: {e}')
message = f'Failed to deploy {str(e)}'
send_response(event, response_status, message, data)
def send_response(event, status, message='', data={}):
# Fill the required response properties
response = {
"Status": status,
"Reason": message,
"PhysicalResourceId": event.get('PhysicalResourceId') or event['LogicalResourceId'],
"StackId": event['StackId'],
"RequestId": event['RequestId'],
"LogicalResourceId": event['LogicalResourceId'],
"Data": data}
# Send the response
response_body = json.dumps(response).encode('utf-8')
req = urllib.request.Request(url=event['ResponseURL'], data=response_body, method="PUT")
with urllib.request.urlopen(req) as r:
print(r.read().decode('utf-8'))
The GetApiKeyResourceHandler
is a Lambda with inline python code. As defined
in the template the handler is the function named handler()
and starts by reading
the property that was passed in. Then the Lambda is getting the API key from the
API Gateway using the AWS Python SDK (called Boto3),
Finally a response is sent to the ResponseURL
that can be found in the request
object passed to the lambda. The response object should have Status: SUCCESS
to signal that the resource was successfull deployed.
Request Type
The request object also contains a RequestType
attribute which makes it
possible to check if the custom resource is created, updated or deleted.
Available RequestTypes
are Create, Update, Delete
.
Error handling
If there are errors in the logic, the Status
attribute in the response object
should be set to Status: FAILED
. Setting the status to FAILED will cause the
deploy to fail and the CloudFormation stack will rollback. You can also set a
helpful error message in the Reason
property: "Reason": "Failed to ...."
.
The error message will be shown in the CloudFormation event log.
Return value from a Custom Resource
The Custom Resource Handler may produce a value or some other result that need
to be propagated further. This is done by setting a Data
object in the
response object. In the example above the Data
object is set to:
data = { "ApiKey": r['value'] }
And then that value can be passed as an output (or passed to other resources) on
the CloudFormation stack using the GetAtt
intrinsic function.
Outputs:
ApiKey:
Description: 'API Key as Output from Custom Resource'
Value: !GetAtt GetApiKey.ApiKey
Permissions
Define the role referenced by the Custom Resource Handler. This role need to
have policies that allows the lambda to perform its logic. In this example the
logic need permission to perform apigateway:GET
action. Also the
AWSLambdaBasicExecutionRole
is set (which is only a policy allowing to write
CloudWatch logs).
GetApiKeyResourceHandlerRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: AllowApiGatewayGet
PolicyDocument:
Statement:
- Action: apigateway:GET
Effect: Allow
Resource: "*"
Version: "2012-10-17"
Problems and Caveats
It is possible to screw up a bit. Any errors in the Custom Resource handler code which makes the lambda crash before the response is sent will make CloudFormation wait until there is a timeout. The timeout is very long, like 30 minutes or so.
Note that this also happens on errors that you normally don’t handle in the code with exceptions e.g. syntax errors or importing unavailable modules.
The stack will stay in CREATE_IN_PROGRESS
or if the error happens when the
stack is deleted it will stay in DELETE_IN_PROGRESS
until CloudFormation times
out.
The problem causing the crash can probably be seen in the CloudWatch logs for
the Custom Resource handler. In this example they are found in
`CloudWatch/Log Groups/aws/lambda/
Summary
The CloudFormation template used in this example can be found on GitHub.
Deploy the template using AWS Console or using AWS CLI:
$ aws cloudformation create-stack --stack-name custom-resource-demo --template-body file://template.yaml --capabilities CAPABILITY_IAM --parameters ParameterKey=ApiKeyIdParam,ParameterValue=xxxyyyzzza