Building a CFT for a CORs Enabled API Gateway and Lambda Integration

 

API Gateway combined with AWS Lambdas are simple to get running, but it’s also very easy to make breaking changes that can be difficult to track. AWS CloudFormation is a great tool to build out your infrastructure as code and is more maintainable than using the AWS Console to make changes. Once you have a functioning CloudFormation Template (CFT), you can rebuild your infrastructure with stability, take in params to create multiple environments, and include your infrastructure in your continuous deployment pipeline.

This blog doesn’t explain how to use Lambdas or API gateway, except for what is necessary to explain the CFT. The AWS console abstracts some of the complexities, which can be helpful for getting something functioning quickly, but I encourage you to build out a CFT for those resources, as it requires you to understand a higher level of the options and what is actually going on behind the scenes in AWS. One example of this is enabling CORs via the console with a single click. With the CFT, you have to manually input each of the headers that get added behind the scenes as well as the OPTIONS request. Overall, building resources this way forces you to understand what you’re working with.

The entire CFT can be found here. Note: You’ll need to swap out your Lambda Execution Role to use it.

The Lambda

def lambda_handler(event, context):
  print(event)
  return {
      "status" : "success"
  }
 

This is a basic Python Lambda that prints a log event. It returns a success message, with no details. Basically the Lambda takes in data and prints it to the console.

Lambda Resource:

  Type: 'AWS::Lambda::Function'
  Properties:
    Handler: 'index.lambda_handler'
    FunctionName: 'log-event-example'
    Role: !Join
      - ''
      - - 'arn:aws:iam::'
        - !Ref 'AWS::AccountId'
        - ':role/LambdaBasicExecution'
    Code:
      ZipFile: |
        def lambda_handler(event, context):
          print(event)
          return {
              "status" : "success"
          }
    Runtime: python3.6
    Timeout: '25'
    TracingConfig:
      Mode: Active
    MemorySize: 128

Everything in our CFT will be a child of Resources. Our resource named “LogEventLambda” contains all of the information needed to build the Lambda. In the console, there is a dropdown to select a role for your lambda, but you will need to substitute your lambda role name in the CFT.

In this example I’ve placed the code inline. In practice, you’d likely want to zip your function and upload it to s3. You’ll need to create a deployment package with your lambda function. For a function with no external dependencies, you can zip up the python file and upload it to the bucket with the key specified, but if you have imports outside of the AWS SDK you’ll want to check out AWS Python Deployment for more information on how to package everything.

Note: Uploading your zip to a directory that includes a version number will force CloudFormation to detect a code update. It doesn’t check the code itself for updates, only the CFT, which changes with a path change.

Let’s move on to the API. It’s a bit more complicated than the lambda as there are multiple pieces to get running.

We’ll need to create our API, our resources, a method for that resource, an option request to enable CORs, and a permission so the API can trigger the lambda.

Let’s begin.

The API

LogEventAPI:
   Type: 'AWS::ApiGateway::RestApi'
   Properties:
     Name: 'log-event-api'
 

The API resource doesn’t need very much. Basically you’re giving it a name and a type. 

First create the API Resource

LogEventResource:
   Type: 'AWS::ApiGateway::Resource'
   Properties:
     RestApiId: !Ref LogEventAPI
     PathPart: logerror
     ParentId: !GetAtt
       - LogEventAPI
       - RootResourceId
 

Here we see !Ref. This will get the ApiId from our LogEventAPI Resource that we created above. The PathPart will be our endpoint and ParentId is the ResourceId for our LogEventAPI which is pulled using the !GetAtt command.

Create a POST to take in the event object

LogEventPOST:
  Type: 'AWS::ApiGateway::Method'
  Properties:
    AuthorizationType: NONE
    HttpMethod: POST
    Integration:
      Type: AWS

IntegrationHttpMethod: POST
      Uri: !Join
        - ''
        - - 'arn:aws:apigateway:'
          - !Ref 'AWS::Region'
          - >-
            :lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:
          - !Ref 'AWS::AccountId'
          - :function:log-event-example
          - /invocations
      IntegrationResponses:
        - StatusCode: 200
          ResponseTemplates:
            application/json: ''
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: >-
              'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
            method.response.header.Access-Control-Allow-Methods: '''POST,OPTIONS'''
            method.response.header.Access-Control-Allow-Origin: '''*'''
      PassthroughBehavior: WHEN_NO_MATCH
    ResourceId: !Ref LogEventResource
    RestApiId: !Ref LogEventAPI
    MethodResponses:
      - StatusCode: 200
        ResponseParameters:
          method.response.header.Access-Control-Allow-Headers: true
          method.response.header.Access-Control-Allow-Methods: true
          method.response.header.Access-Control-Allow-Origin: true
        ResponseModels:
          application/json: Empty

There is a lot going on here, but we essentially create a POST method and integrate our lambda with it.

  • Set the ResponseParameters with our CORs headers to enable CORs.
  • Link up the ‘LogEventPOST’ resource to our ‘LogEventResource’ API Resource (API Gateway refers to endpoints as resources, which is different than CFT resources) as well as our ‘LogEventAPI’ API resource via the ResourceId and RestApiId fields.
  • Add the headers to the MethodResponses, which will enable the browser to accept our response. Our ResponseModel is empty so the return value from the lambda will be returned by our API.

3. We need to create our OPTIONS to handle the browser preflight

LogEventOPTIONS:
  Type: 'AWS::ApiGateway::Method'
  Properties:
    ResourceId: !Ref LogEventResource
    RestApiId: !Ref LogEventAPI
    AuthorizationType: NONE
    HttpMethod: OPTIONS
    Integration:
      Type: MOCK
      IntegrationResponses:
        - ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: >-
              'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
            method.response.header.Access-Control-Allow-Methods: '''POST,OPTIONS'''
            method.response.header.Access-Control-Allow-Origin: '''*'''
          ResponseTemplates:
            application/json: ''
          StatusCode: '200'
      PassthroughBehavior: NEVER
      RequestTemplates:
        application/json: '{"statusCode": 200}'
    MethodResponses:
      - ResponseModels:
          application/json: Empty
        ResponseParameters:
          method.response.header.Access-Control-Allow-Headers: true
          method.response.header.Access-Control-Allow-Methods: true
          method.response.header.Access-Control-Allow-Origin: true
        StatusCode: '200'

We’re setting the CORs headers to let the browser know it can do a POST and OPTIONS.

Finally, we add our Permission to enable the API to call our lambda. 

LogEventPermission:
  DependsOn: LogEventLambda
  Type: 'AWS::Lambda::Permission'
  Properties:
    Action: 'lambda:invokeFunction'
    FunctionName: 'log-event-example'
    Principal: apigateway.amazonaws.com
    SourceArn: !Join
      - ''
      - - 'arn:aws:execute-api:'
        - !Ref 'AWS::Region'
        - ':'
        - !Ref 'AWS::AccountId'
        - ':'
        - !Ref LogEventAPI
        - /*

 Here, we’re creating a permission on the Lambda which is necessary to enable API gateway to call the lambda.

We need the DependsOn attribute to prevent the Permission from building before our Lambda. 

This concludes our basic CFT for our API, API resource/method, and lambda.

Deployment

 

You can deploy your CFT through the console by clicking Create Stack and uploading your CFT.

From the command line, you can trigger a deploy (or update) with:

aws cloudformation create-stack --region us-east-1 --stack-name log-event-api --template-body file://./api-lambda-cft.yaml --capabilities CAPABILITY_IAM ||
aws cloudformation update-stack  --region us-east-1 --stack-name log-event-api --template-body file://./log-event-api --capabilities CAPABILITY_IAM || echo "Either no update to perform or the command failed to execute."

The above command will try to create a new stack or update the existing stack if it already exists.

Deploying the API

Click Deploy API from the Actions dropdown.

Screen Shot 2018-06-29 at 1.32.09 PM

Or from the command line

until [ -n "${APIID}" ]; do
APIID=$(aws cloudformation describe-stacks --stack-name log-event-api --output json --query 'Stacks[].Outputs[0].OutputValue' --output text)
echo ${APIID}
done
aws apigateway create-deployment --rest-api-id ${APIID} --stage-name dev

aws apigateway create-deployment --rest-api-id ${APIID} --stage-name dev

Once deployed, we can test it out by hitting the url provided in API gateway. 

 

Conclusion

You should now have a deployed API that is integrated with your lambda built from a CFT. With a continuous deploy pipeline you can make changes to your CFT, deploy the updated CFT, and deploy the API to the stage desired. While this doesn’t prevent changes made through the console, it encourages code updates that will result in smoother deployments and provide a fallback point if breaking changes are introduced.

Click me

Recent Posts

Subscribe to the Blog