Hello!
It takes a few pieces to assemble a working CloudFormation Custom Resource. I like to start from a simple example and build up to what I need. Here’s the code I use as a starting point.
First, a few notes:
- My custom resources are usually small, often only a few dozen lines (more than that is usually a signal that I’m implementing an anti-pattern). Because my resources are small, I just drop the code into CloudFormation’s ZipFile. That saves me from building a package and from porting my own version of cfn-response. In more complex cases you may want to expand your lambda function resource.
- When I tested the code in this article, the current version of AWS’s cfn-response module threw this warning into logs:
DeprecationWarning: You are using the put() function from 'botocore.vendored.requests'
The vendored version of requests in botocore was never really meant for use outside of botocore itself, and it was recently deprecated. AWS knows about this, but they haven’t updated their cfn-response code yet.
- One of the most common problems I see in custom resource development is unhandled exceptions. Read more in my article here.
- This example focuses on the minimum resources, permissions, and code for a healthy custom resource. I skipped some of the usual good practices like template descriptions and parameterized config.
- I put the CloudWatch Logs Log Group in the same template as the custom resource so it was easy to see for this example. Usually I put them in a separate template because they don’t share the same lifecycle. I don’t want rollbacks or reprovisions to delete my logs.
- The custom resource
Type
can have several different values. Check out the details here.
Now, the code:
--- AWSTemplateFormatVersion: '2010-09-09' Resources: Logs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/custom-resource RetentionInDays: 30 ExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: '/' Policies: - PolicyName: custom-resource-execution-role PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:DescribeLogGroup - logs:PutLogEvents Resource: !GetAtt Logs.Arn Function: Type: AWS::Lambda::Function Properties: Code: ZipFile: | # https://operatingops.com/2018/10/13/cloudformation-custom-resources-avoiding-the-two-hour-exception-timeout/ import logging import cfnresponse def handler(event, context): logger = logging.getLogger() logger.setLevel(logging.INFO) try: if event['RequestType'] == 'Delete': logger.info('Deleted!') cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) return logger.info('It worked!') cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except Exception: logger.exception('Signaling failure to CloudFormation.') cfnresponse.send(event, context, cfnresponse.FAILED, {}) FunctionName: custom-resource Handler: index.handler Role: !GetAtt ExecutionRole.Arn Runtime: python3.7 Timeout: 30 CustomResource: Type: Custom::Function Properties: ServiceToken: !GetAtt Function.Arn
With a few parameters and a little extra code this pattern almost always solves my problem. Hope it helps!
Happy automating,
Adam
Need more than just this article? We’re available to consult.
You might also want to check out these related articles: