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/-GetApiKeyResourceHandler

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

Resources

Custom Resource request

Custom Resource response