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