Lambda with DynamoDB and API Gateway

Introduction

Using the CDK to define the following AWS resources.

  • DynamoDB table
  • API Gateway
  • Lambda Function
  • Lambda Role

Steps taken from Lambda API Gateway.

Assets

apigw-dynamodb.js

The following example code receives a API Gateway event input and processes the messages that it contains.

console.log('Loading function');

var AWS = require('aws-sdk');
var dynamo = new AWS.DynamoDB.DocumentClient();

/**
 * Provide an event that contains the following keys:
 *
 *   - operation: one of the operations in the switch statement below
 *   - tableName: required for operations that interact with DynamoDB
 *   - payload: a parameter to pass to the operation being performed
 */
exports.handler = function(event, context, callback) {
    //console.log('Received event:', JSON.stringify(event, null, 2));

    var operation = event.operation;

    if (event.tableName) {
        event.payload.TableName = event.tableName;
    }

    switch (operation) {
        case 'create':
            dynamo.put(event.payload, callback);
            break;
        case 'read':
            dynamo.get(event.payload, callback);
            break;
        case 'update':
            dynamo.update(event.payload, callback);
            break;
        case 'delete':
            dynamo.delete(event.payload, callback);
            break;
        case 'list':
            dynamo.scan(event.payload, callback);
            break;
        case 'echo':
            callback(null, "Success");
            break;
        case 'ping':
            callback(null, "pong");
            break;
        default:
            callback('Unknown operation: ${operation}');
    }
};

App

#!/usr/bin/env python3

from aws_cdk import core

from lambda_api_gw_dynamodb.LambdaApiGwStack import LambdaApiGwStack
from lambda_api_gw_dynamodb.LambdaIAMStack import LambdaIAMStack
from lambda_api_gw_dynamodb.LambdaFunctionStack import LambdaFunctionStack
from lambda_api_gw_dynamodb.LambdaDynamoDBStack import LambdaDynamoDBStack

props = {
    'namespace' : 'Lambda',
    'roleName': "LambdaApigatewayRole",
    'lambdaName':'lambda-microservice',
    'lambdaFile':'./assets/lambda/apigw-dynamodb.js',
    'lambdaRuntime' : 'nodejs12.x',
    'apiGwName' : 'DynamoDBOperations',
    'tableName' : 'lambda-apigateway'
}

app = core.App()

lambda_iam_stack = LambdaIAMStack(app,f"{props['namespace']}-IAM",props)
labada_function_stack =  LambdaFunctionStack(app,f"{props['namespace']}-Function",lambda_iam_stack.outputs)
labada_function_stack.add_dependency(lambda_iam_stack)

lambda_apigw_stack = LambdaApiGwStack(app, f"{props['namespace']}-ApiGateway",labada_function_stack.output_props)
lambda_apigw_stack.add_dependency(labada_function_stack)

lambda_db_stack = LambdaDynamoDBStack(app, f"{props['namespace']}-DB",props)

app.synth()

DynamoDB table

from aws_cdk import (
    core,
    aws_dynamodb as dynamodb
)

import os,sys

class LambdaDynamoDBStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str,props,**kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here
        
        # Create a DynamoDB table, with a primary key called id of type string
        
        dynamodb.Table(
            self,
            "DynamoDBTable",
            table_name=props['tableName'],
            partition_key=dynamodb.Attribute(name="id",type=dynamodb.AttributeType.STRING)
        )

Lambda function

from aws_cdk import (
    core,
    aws_lambda as _lambda,
    aws_iam as iam
)

import os,sys

class LambdaFunctionStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, props,**kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here
        
        # Create the Lambda function

        lambda_code = ""
        runtime = ""

        with(open(os.path.abspath(props['lambdaFile']))) as file:
            lambda_code += file.read()

        file.close()


        if(props['lambdaRuntime'] == 'nodejs12.x'):
            runtime = _lambda.Runtime.NODEJS_12_X

        lambda_func = _lambda.Function(
            self,
            "LambaFunction",
            code=_lambda.Code.inline(code=lambda_code),
            handler="index.handler",
            runtime=runtime,
            function_name=props['lambdaName'],
            role=iam.Role.from_role_arn(self,"RoleLookup",role_arn=props['role_arn'])
        )

        self.output_props = props.copy()
        self.output_props['lambda_func_arn'] = lambda_func.function_arn

    
    @property
    def outputs(self):
        return self.output_props

Lambda integrated API Gateway

from aws_cdk import (
    core,
    aws_apigateway as apigw,
    aws_lambda as _lambda,
    aws_iam as iam

)
class LambdaApiGwStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, props, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here

        # Define the rest api
        
        rest_api = apigw.LambdaRestApi(
            self,
            "LambdaRestAPI",
            handler=_lambda.Function.from_function_arn(self,"LambdaFunc",props['lambda_func_arn']),
            proxy=False,
            endpoint_configuration=apigw.EndpointConfiguration(types=[apigw.EndpointType.EDGE]),
            api_key_source_type=apigw.ApiKeySourceType.HEADER,
            rest_api_name=props['apiGwName'],
        )
    
        integration_response = apigw.IntegrationResponse(
            status_code="200",
            
        )

        
        # define a LambdaIntegration
        integration = apigw.LambdaIntegration(
            handler=_lambda.Function.from_function_arn(self,"LambaFunctionArn",function_arn=props['lambda_func_arn']),
                proxy=False,
                allow_test_invoke=True,
                passthrough_behavior=apigw.PassthroughBehavior.WHEN_NO_MATCH,
                cache_namespace=rest_api.root.resource_id,
                cache_key_parameters=[],
                integration_responses=[
                    integration_response
                ]
        )

        method_response = apigw.MethodResponse(
            status_code="200",
            response_models={"application/json":apigw.Model.from_model_name(self,"EmptyModel","Empty")}
        )

        rest_api.root.add_method(
            "POST",
            integration,
            
            method_responses=[ 
                method_response
            ]
            
        )

        output_props = props.copy()
        output_props['apigw_id'] = rest_api.rest_api_id

        
        # call nested stack to create api gw permission

        LambdaApiGwPermissionsStack(self,"LambdaApiGwPermissionsStack",output_props)

class LambdaApiGwPermissionsStack(core.NestedStack):

    def __init__(self, scope: core.Construct, id: str, props, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        rest_api = apigw.LambdaRestApi.from_rest_api_id(self,"FromRestApiId",props['apigw_id'])

        # apply extra permissions to the lambda function, so that the api gateway can invoke the lambda function

        lambda_func = _lambda.Function.from_function_arn(self,"LambdaFunc",function_arn=props['lambda_func_arn'])
        
        _lambda.CfnPermission(
            self,
            "LambdaPermission",
            action="lambda:InvokeFunction",
            source_arn=rest_api.arn_for_execute_api().replace('*','').replace('/','').strip() + "/*/POST/DynamoDBManager",
            principal="apigateway.amazonaws.com",
            function_name=lambda_func.function_name
        )

Lambda role with custom policy

This role allows the Lambda function to write data to the DynamoDB table and update logs.

from aws_cdk import (
    
    core,
    aws_iam as iam,
    aws_lambda as _lambda

)
class LambdaIAMStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, props, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here

        # Create the Lambda role

        lambda_role = iam.Role(
            self,
            "LambdaRole",
            role_name=props['roleName'],
            assumed_by=iam.ServicePrincipal("lambda",),
        )

        # Define a custom policy to allow  access to actions for DynamoDB and CloudWatch
        policy = iam.Policy(
            self,
            "DynamoDBCloudWatchPolicy",
            policy_name="DynamoDBCloudWatchPolicy",
            statements=[
                iam.PolicyStatement(resources=["*"],
                                    sid="Stmt1428341300017",
                                    effect=iam.Effect.ALLOW,
                                    actions=[
                                        "dynamodb:DeleteItem",
                                        "dynamodb:GetItem",
                                        "dynamodb:PutItem",
                                        "dynamodb:Query",
                                        "dynamodb:Scan",
                                        "dynamodb:UpdateItem"
                                    ]
                ),
                iam.PolicyStatement(
                    resources=["*"],
                    sid="",
                    actions=[
                        "logs:CreateLogGroup",
                        "logs:CreateLogStream",
                        "logs:PutLogEvents"
                    ],
                    effect=iam.Effect.ALLOW
                )
            ]
        )

        lambda_role.attach_inline_policy(policy=policy)

        self.output_props = props.copy()
        self.output_props['role_arn'] = lambda_role.role_arn
       
    @property
    def outputs(self):
        return self.output_props

Testing

Once deployed.

Use the API Gateway to test the following operations.

Request create to create an item

Creates a row in the Dynamodb table.

Payload.

{
    "operation": "create",
    "tableName": "lambda-apigateway",
    "payload": {
        "Item": {
            "id": "1234ABCD",
            "number": 5
        }
    }
}

Then use curl to post the payload.

$ curl -X POST -d "{\"operation\":\"create\",\"tableName\":\"lambda-apigateway\",\"payload\":{\"Item\":{\"id\":\"1\",\"name\":\"Bob\"}}}" https://$API.execute-api.$REGION.amazonaws.com/prod/DynamoDBManager

Request echo to retrieve items

Gets row from the DynamoDB table.

Payload.


{
  "operation": "echo",
  "payload": {
    "somekey1": "somevalue1",
    "somekey2": "somevalue2"
  }
}

Then use curl to post the payload.

$ curl -X POST -d "{\"operation\":\"echo\",\"payload\":{\"somekey1\":\"somevalue1\",\"somekey2\":\"somevalue2\"}}" https://$API.execute-api.$REGION.amazonaws.com/prod/DynamoDBManager
Last updated on 21 Aug 2020
Published on 21 Aug 2020