How do I update the AWS CloudFormation cfn-response module for AWS Lambda functions running on Python 2.7/3.6/3.7?

Last updated: 2021-02-16

I want to update the AWS CloudFormation cfn-response module for AWS Lambda functions running on Python 2.7/3.6/3.7.

Resolution

Note: The following steps are applicable only to Lambda functions running on Python 2.7/3.6/3.7. The following commands are applicable to Linux and macOS environments. Syntax may vary on Windows PowerShell.

Note: If you receive errors when running AWS Command Line Interface (AWS CLI) commands, make sure that you’re using the most recent AWS CLI version.

1.    To find the stacks that contain custom resources, run the following command:

aws cloudformation list-stacks --region us-east-1 | grep -oE 'arn:[^"]+' | while read arn; do aws cloudformation list-stack-resources --stack-name $arn --region us-east-1 | grep -E '(Custom::)|(::CustomResource)' | awk '{print $2}' | while read resource; do if [[ -n $resource ]]; then echo $arn; echo $resource; fi; done; done

You should see output similar to the following example output:

arn:aws:cloudformation:us-east-1:123456789012:stack/TestStack/3497b950-55f1-11eb-aad4-124a026c8667
"ResourceType": "AWS::CloudFormation::CustomResource",

2.    To find the Lambda function associated with the custom resource, run the following command to check the custom resource’s ServiceToken property from the stack’s template:

aws cloudformation get-template --stack-name TestStack | jq -r .TemplateBody

Note: The command in step 2 previews the stack’s template by using the jq option (from the jq website) to format the response.

You should see output similar to the following example output:

Resources:
  MyCustomResource:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !GetAtt MyFunction.Arn
      Name: "John"
  MyFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt MyRole.Arn
      Runtime: python3.7
      Code:
        ZipFile: |
          import cfnresponse
          def handler(event, context):
            responseData = {'Message': 'Hello {}!'.format(event['ResourceProperties']['Name'])}
            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CustomResourcePhysicalID")
  MyRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
Outputs:
  Result:
    Value: !GetAtt MyCustomResource.Message

Note: The template that you get from the output for step 2 is an example of a minimal template for a Lambda-backed custom resource. The property ServiceToken: !GetAtt MyFunction.Arn is in the MyCustomResource section. The value resolved by the !GetAtt MyFunction.Arn of the ServiceToken property is either the Amazon Resource Name (ARN) of the Amazon Simple Notification Service (Amazon SNS) topic or the Lambda function.

3.    In the template from step 2, identify where your Lambda function is defined.

If your Lambda function is in the same stack as the custom resource, skip to step 4. For example, the Fn::GetAtt function in step 2 shows that the Lambda function is defined in the same template as the custom resource.

If the ServiceToken property points to a hardcoded ARN, then the Lambda function could be in another stack. If the ServiceToken property is resolved through Fn::Import, then use the list-exports API in AWS CloudFormation to look up the value. For example:

aws cloudformation list-exports --region us-east-1
{
    "Exports": [
        {
            "ExportingStackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SomeOtherStack/481dc040-b283-11e9-b1bd-12d607a4fd1c",
            "Value": "arn:aws:lambda:us-east-1:123456789012:function:SomeOtherStack-MyFunction-5ZE2CQO8RAA9",
            "Name": "MyExport"
        }
    ]
}

Then, check for function tags located in a separate stack by using list-tags to locate the AWS CloudFormation stack ARN. For example:

aws lambda list-tags --resource arn:aws:lambda:us-east-1:123456789012:function:TestStack-MyFunction-5ZE2CQO8RAA9 | grep stack-id

You receive output similar to the following:

"aws:cloudformation:stack-id": "arn:aws:cloudformation:us-east-1:123456789012:stack/TestStack/3497b950-55f1-11eb-aad4-124a026c8667"

Note: You can also find function tags in the AWS Lambda console.

4.    To allow AWS CloudFormation to load the latest cfn-response module in your Lambda function, update the inline source code of your Lambda function. For example:

Code:
        ZipFile: |
          import cfnresponse
          def handler(event, context):
            responseData = {'Message': 'Hello {}!'.format(event['ResourceProperties']['Name'])}
            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CustomResourcePhysicalID")

Note: See step 2 for an example template that has a Lambda function with inline source code.

Now, the following cfn-response module code example is loaded by AWS CloudFormation into your Lambda function. For example:

from botocore.vendored import requests
import json
 
SUCCESS = "SUCCESS"
FAILED = "FAILED"
 
def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False):
    responseUrl = event['ResponseURL']
 
    print(responseUrl)
 
    responseBody = {}
    responseBody['Status'] = responseStatus
    responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name
    responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name
    responseBody['StackId'] = event['StackId']
    responseBody['RequestId'] = event['RequestId']
    responseBody['LogicalResourceId'] = event['LogicalResourceId']
    responseBody['NoEcho'] = noEcho
    responseBody['Data'] = responseData

Note: For more information, see the code examples in the "Module source code" section of the cfn-response module.

The cfn-response module code example uses botocore.requests in the Lambda function’s deployment package.

To update the cfn-response module to the latest version that uses urllib3, update the function’s inline code in the AWS CloudFormation template. Do this by adding a comment to the inline Lambda function's code. For example:

ZipFile: |
           import cfnresponse
           def handler(event, context):
+            # This comment was added to force an update on this function's code
             responseData = {'Message': 'Hello {}!'.format(event['ResourceProperties']['Name'])}
             cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CustomResourcePhysicalID")
   MyRole:

5.    Save any changes to the template that contains your Lambda function.

6.    Update your stack.

The cfn-response module is modified after the stack is finished updating.

Note: If your function's code resides in an Amazon Simple Storage Service (Amazon S3) bucket or Amazon Elastic Container Registry (Amazon ECR) image, you must update the module yourself to include the version with urllib3. To get the source code for the latest version of the cfn-response module, see cfn-response module.

Note: If a new Python or JavaScript runtime introduces a breaking change, you must update the cfn-response module. Instead of updating the ZipFile again, you can automatically attach the newest version of the cfn-response module whenever a function’s Runtime property is updated.


Did this article help?


Do you need billing or technical support?