Deploy AWS Lambda Functions and Amazon DynamoDB with AWS CDK on LocalStack

Introduction In my previous tech blog, I explained how to deploy AWS Lambda, API Gateway and DynamoDB using AWS SAM on LocalStack. In this blog, I’d like to introduce AWS CDK. AWS CDK (Cloud Development Kit) is a powerful tool that allows you to define cloud infrastructure using code in languages like Java, TypeScript, or Python. It then generates a CloudFormation template, which can be deployed to AWS or, in this case, to LocalStack that is used for cost savings, offline development, and faster testing. Prerequisites Before starting this tutorial, ensure you have the following installed: LocalStack (Download) CDK Local (cdklocal) (Download) AWS CLI Local (Dowload) Node.js and NPM (Download) Java 21 Python 3 Favorite IDE (Intellij IDEA or VS Code) By default, AWS CDK commands like cdk synth and cdk deploy use the AWS CLI. However, in this tutorial, we’ll use cdklocal, provided by LocalStack, to deploy our infrastructure locally. Creating a Java Project Using AWS CDK Step 1: Install AWS CDK CLI Ensure you have AWS CDK installed. If you haven't installed it yet, run: npm install -g aws-cdk Step 2: Create a New AWS CDK Project from scratch (Java) If you want to create a project from scratch, follow this step below or directly git clone the GitHub Repository. Run the following command to initialize a new Java AWS CDK project: mkdir cdk-lambda-dynamodb && cd cdk-lambda-dynamodb cdk init app --language=java This will generate a Java-based AWS CDK project with a basic folder structure. Step 3: Set Up the Project By default, the CDK generates a blank structure automatically, but here is my custom structure: cdk-lambda-dynamodb/ │-- infra/ │ ├── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ ├── be/axeldlv/infra (Java AWS CDK) │ │ │ │ │ ├── CDKLambdaDynamoDBStackApp.java │ │ │ │ │ ├── CDKLambdaDynamoDBStackStack.java │ ├-- pom.xml (Maven configuration) │ ├-- README.md │ ├-- cdk.json │-- lambda-getFlights/ (Python Application) │ ├-- getFlights.py │ ├-- requirements.txt │-- lambda-insertFlights/ (Java Application) │ ├── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ ├── be/axeldlv │ │ │ │ │ ├── InsertFlights.java Step 4: Define Your Infrastructure Stack If you are using the cdk init, remove java class previously create and rename it to CDKLambdaDynamoDBStackApp.java and CDKLambdaDynamoDBStackStack.java in src/main/java/be/axeldlv/infra to define your infrastructure. In this example, we need to set up two AWS Lambda, AWS Secrets Manager and Amazon DynamoDB resources. CDKLambdaDynamoDBStackApp.java package be.axeldlv.infra; import software.amazon.awscdk.App; import software.amazon.awscdk.Environment; import software.amazon.awscdk.StackProps; public class CDKLambdaDynamoDBStackApp { public static void main(final String[] args) { App app = new App(); new CDKLambdaDynamoDBStackStack(app, "CDKLambdaDynamoDBStack", StackProps.builder() .env(Environment.builder() .account(System.getenv("CDK_DEFAULT_ACCOUNT")) .region(System.getenv("CDK_DEFAULT_REGION")) .build()).build()); app.synth(); } } CDKLambdaDynamoDBStackStack.java package be.axeldlv.infra; import software.amazon.awscdk.*; import software.amazon.awscdk.services.dynamodb.Attribute; import software.amazon.awscdk.services.dynamodb.AttributeType; import software.amazon.awscdk.services.dynamodb.BillingMode; import software.amazon.awscdk.services.dynamodb.Table; import software.amazon.awscdk.services.lambda.*; import software.amazon.awscdk.services.lambda.Runtime; import software.amazon.awscdk.services.secretsmanager.Secret; import software.constructs.Construct; public class CDKLambdaDynamoDBStackStack extends Stack { public CdkFlightsFunctionsLambdaStack(final Construct scope, final String id) { this(scope, id, null); } public CdkFlightsFunctionsLambdaStack(final Construct scope, final String name, final StackProps props) { super(scope, name, props); Table flightTable = Table.Builder.create(this, "FlightTable") .tableName("FlightTable") .billingMode(BillingMode.PAY_PER_REQUEST) .sortKey(Attribute.builder().name("orderId").type(AttributeType.STRING).build()) .partitionKey(Attribute.builder().name("bookingId").type(AttributeType.STRING).build()).build(); Function insertFlightLambda = Function.Builder.create(this, "InsertFlightLambda") .functionName("InsertFlightLambda") .runtime(Runtime.JAVA_21) .handler("be.axeldlv.InsertFlight::handleRequest") .code(Code.fromAsset("../lambda-insertFlights/jar/sendtodynamodb-1.0.jar")) .memorySize(1024) .timeout(Duratio

Mar 28, 2025 - 09:41
 0
Deploy AWS Lambda Functions and Amazon DynamoDB with AWS CDK on LocalStack

Introduction

In my previous tech blog, I explained how to deploy AWS Lambda, API Gateway and DynamoDB using AWS SAM on LocalStack.

In this blog, I’d like to introduce AWS CDK. AWS CDK (Cloud Development Kit) is a powerful tool that allows you to define cloud infrastructure using code in languages like Java, TypeScript, or Python. It then generates a CloudFormation template, which can be deployed to AWS or, in this case, to LocalStack that is used for cost savings, offline development, and faster testing.

Prerequisites

Before starting this tutorial, ensure you have the following installed:

  • LocalStack (Download)
  • CDK Local (cdklocal) (Download)
  • AWS CLI Local (Dowload)
  • Node.js and NPM (Download)
  • Java 21
  • Python 3
  • Favorite IDE (Intellij IDEA or VS Code)

By default, AWS CDK commands like cdk synth and cdk deploy use the AWS CLI. However, in this tutorial, we’ll use cdklocal, provided by LocalStack, to deploy our infrastructure locally.

Creating a Java Project Using AWS CDK

Step 1: Install AWS CDK CLI

Ensure you have AWS CDK installed. If you haven't installed it yet, run:

npm install -g aws-cdk

Step 2: Create a New AWS CDK Project from scratch (Java)

If you want to create a project from scratch, follow this step below or directly git clone the GitHub Repository.

Run the following command to initialize a new Java AWS CDK project:

mkdir cdk-lambda-dynamodb && cd cdk-lambda-dynamodb
cdk init app --language=java

This will generate a Java-based AWS CDK project with a basic folder structure.

Step 3: Set Up the Project

By default, the CDK generates a blank structure automatically, but here is my custom structure:

cdk-lambda-dynamodb/
│-- infra/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   ├── be/axeldlv/infra  (Java AWS CDK)
│   │   │   │   │   ├── CDKLambdaDynamoDBStackApp.java
│   │   │   │   │   ├── CDKLambdaDynamoDBStackStack.java
│   ├-- pom.xml (Maven configuration)
│   ├-- README.md
│   ├-- cdk.json
│-- lambda-getFlights/ (Python Application)
│   ├-- getFlights.py
│   ├-- requirements.txt
│-- lambda-insertFlights/ (Java Application)
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   ├── be/axeldlv
│   │   │   │   │   ├── InsertFlights.java

Step 4: Define Your Infrastructure Stack

If you are using the cdk init, remove java class previously create and rename it to CDKLambdaDynamoDBStackApp.java and CDKLambdaDynamoDBStackStack.java in src/main/java/be/axeldlv/infra to define your infrastructure.

In this example, we need to set up two AWS Lambda, AWS Secrets Manager and Amazon DynamoDB resources.

CDKLambdaDynamoDBStackApp.java

package be.axeldlv.infra;

import software.amazon.awscdk.App;
import software.amazon.awscdk.Environment;
import software.amazon.awscdk.StackProps;

public class CDKLambdaDynamoDBStackApp {
    public static void main(final String[] args) {
        App app = new App();
        new CDKLambdaDynamoDBStackStack(app, "CDKLambdaDynamoDBStack", StackProps.builder()
                .env(Environment.builder()
                .account(System.getenv("CDK_DEFAULT_ACCOUNT"))
                .region(System.getenv("CDK_DEFAULT_REGION"))
                .build()).build());

        app.synth();
    }
}

CDKLambdaDynamoDBStackStack.java

package be.axeldlv.infra;

import software.amazon.awscdk.*;
import software.amazon.awscdk.services.dynamodb.Attribute;
import software.amazon.awscdk.services.dynamodb.AttributeType;
import software.amazon.awscdk.services.dynamodb.BillingMode;
import software.amazon.awscdk.services.dynamodb.Table;
import software.amazon.awscdk.services.lambda.*;
import software.amazon.awscdk.services.lambda.Runtime;
import software.amazon.awscdk.services.secretsmanager.Secret;
import software.constructs.Construct;

public class CDKLambdaDynamoDBStackStack extends Stack {
    public CdkFlightsFunctionsLambdaStack(final Construct scope, final String id) {
        this(scope, id, null);
    }
    public CdkFlightsFunctionsLambdaStack(final Construct scope, final String name, final StackProps props) {
        super(scope, name, props);

        Table flightTable = Table.Builder.create(this, "FlightTable")
                .tableName("FlightTable")
                .billingMode(BillingMode.PAY_PER_REQUEST)
                .sortKey(Attribute.builder().name("orderId").type(AttributeType.STRING).build())
                .partitionKey(Attribute.builder().name("bookingId").type(AttributeType.STRING).build()).build();

        Function insertFlightLambda = Function.Builder.create(this, "InsertFlightLambda")
                .functionName("InsertFlightLambda")       
                .runtime(Runtime.JAVA_21)
                .handler("be.axeldlv.InsertFlight::handleRequest")
                .code(Code.fromAsset("../lambda-insertFlights/jar/sendtodynamodb-1.0.jar"))
                .memorySize(1024)
                .timeout(Duration.seconds(60))
                .description("Lambda Function to insert data to DynamoDB")
                .tracing(Tracing.ACTIVE)
                .build();

        Function getFlightLambda = Function.Builder.create(this, "getFlightLambda")
                .functionName("getFlightLambda")   
                .runtime(Runtime.PYTHON_3_13)
                .handler("app.lambda_handler")
                .code(Code.fromAsset("../lambda-getFlights"))
                .memorySize(1024)
                .timeout(Duration.seconds(60))
                .description("Lambda function to retrieve data from DynamoDB")
                .tracing(Tracing.ACTIVE)
                .build();

        Secret addTableNameSecret = Secret.Builder.create(this, "addTableNameSecret")
                .secretName("table")
                .secretStringValue(SecretValue.unsafePlainText(flightTable.getTableName()))
                .build();

        addTableNameSecret.grantRead(insertFlightLambda);
        addTableNameSecret.grantRead(getFlightLambda);
        flightTable.grantReadWriteData(insertFlightLambda);       
        flightTable.grantReadData(getFlightLambda);
    }
}

Explanation of CdkFlightsFunctionsLambdaStack Code

This code defines an AWS CDK stack called CDKLambdaDynamoDBStack, which sets up a serverless architecture using AWS Lambda, Amazon DynamoDB, and AWS Secrets Manager. Below is a breakdown of the components defined in this stack:

DynamoDB Table (flightTable)

  • A DynamoDB table named FlightTable is created.
  • The table has two keys:
    • Partition Key: bookingId (type STRING) - booking#1
    • Sort Key: orderId (type STRING) - order#1
  • The table is set to Pay Per Request billing mode.

Lambda Function to Insert Flights (insertFlightLambda)

  • A Lambda function written in Java (Runtime.JAVA_21) is created to insert flight data into the DynamoDB table.
  • The Lambda function uses a locally stored JAR file (sendtodynamodb-1.0.jar).
  • The function is configured with:
    • Memory: 1GB
    • Timeout: 60 seconds
    • X-Ray Tracing: Active for monitoring > Don't forget to move the build in the jar folder in the project.

Lambda Function to Get Flights (getFlightLambda)

  • Another Lambda function, written in Python (Runtime.PYTHON_3_13), is created to retrieve flight data from DynamoDB.
  • The function’s code is located in the folder ../lambda-getFlights.
  • It is configured with:
    • Memory: 1GB
    • Timeout: 60 seconds
    • X-Ray Tracing: Active

Secrets Manager Secret (addTableNameSecret)

  • A secret is created in AWS Secrets Manager to store the name of the FlightTable.
  • The secret’s value is set to the table name (flightTable.getTableName()).
  • Both Lambda functions (insertFlightLambda and getFlightLambda) are granted read access to the secret.

Adding the table name in secret isn't really necessary, but it was done for the purpose of the tutorial.

Permissions

  • The Lambda functions are granted the necessary permissions to interact with the DynamoDB table and Secrets Manager secret:
    • The insertFlightLambda is granted ReadWriteData access to the flightTable and Read access to the addTableNameSecret.
    • The getFlightLambda is granted ReadData access to the flightTable and Read access to the secret as well.

Creating AWS Lambda Functions

We are creating two Lambda functions: one in Python for retrieving flight data and another in Java for inserting flight data. This will demonstrate how we can work with two different programming languages in AWS Lambda.

GetFlights Lambda Function - Python

getFlights.py

import boto3
import os
import json

region = "us-west-1"
localstackurl = "https://localhost.localstack.cloud:4566"

dynamoDBClient = boto3.client('dynamodb', 
             endpoint_url=localstackurl, 
             region_name=region)

dynamodb = boto3.resource('dynamodb', endpoint_url=localstackurl, region_name=region)

def lambda_handler(event, context):
    item_id = event.get('pathParameters', {}).get('bookingId')
    order_id = event.get('pathParameters', {}).get('orderId')

    if not item_id:
        return {
            'statusCode': 400,
            'body': json.dumps({'error': 'ID not provided in path parameters'})
        }

    table_name = "FlightTable"
    if not table_name:
        raise KeyError("TABLEName environment variable is not set")

    table = dynamodb.Table(table_name)

    # fetch todo from the database
    response = table.get_item(Key={'orderId': str(order_id), 'bookingId': str(item_id)}) 
    item = response.get('Item', {})   

    return {
        'statusCode': 200,
        'body': item
    }

InsertFlights Lambda Function - Java

This Java class implements an AWS Lambda function that processes API Gateway requests (not explain in this technical blog) and inserts data into a DynamoDB table. It retrieves the table name from AWS Secrets Manager and interacts with LocalStack for local testing.

InsertFlight.java

package be.axeldlv;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemResponse;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;

public class InsertFlight implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

    String region = "us-west-1";
    String localStackUrl = "https://localhost.localstack.cloud:4566";

    @Override
    public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent, Context context) {
        context.getLogger().log("Lambda function started...");

        SecretsManagerClient secretsManagerClient = createSecretsManagerClient();
        GetSecretValueRequest getSecretValueRequest = GetSecretValueRequest.builder()
                .secretId("table")
                .build();
        GetSecretValueResponse secretValue = secretsManagerClient.getSecretValue(getSecretValueRequest);
        DynamoDbClient dynamoDbClient = createDynamoDbClient();

        String tableName = secretValue.secretString();
        String requestBody = apiGatewayProxyRequestEvent.getBody();
        context.getLogger().log("requestBody " + requestBody);
        String bookingId = apiGatewayProxyRequestEvent.getPathParameters().get("bookingId");
        context.getLogger().log("bookingId " + bookingId);
        String orderId = apiGatewayProxyRequestEvent.getPathParameters().get("orderId");
        context.getLogger().log("orderId " + orderId);

        PutItemResponse response = insertItemToDynamoDb(dynamoDbClient, tableName, requestBody, bookingId, orderId);

        context.getLogger().log("Data saved successfully to DynamoDB: " + response);
        return createAPIResponse(requestBody);
    }

    private DynamoDbClient createDynamoDbClient() {
        return DynamoDbClient.builder()
                .endpointOverride(URI.create(localStackUrl))
                .credentialsProvider(DefaultCredentialsProvider.create())
                .region(Region.of(region))
                .build();
    }

    private PutItemResponse insertItemToDynamoDb(DynamoDbClient dynamoDbClient, String tableName, String requestBody, String bookingId, String orderId) {
        Map<String, AttributeValue> item = new HashMap<>();
        item.put("bookingId", AttributeValue.builder().s(bookingId).build());
        item.put("orderId", AttributeValue.builder().s(orderId).build());
        item.put("requestBody", AttributeValue.builder().s(requestBody).build());

        PutItemRequest putItemRequest = PutItemRequest.builder()
                .tableName(tableName)
                .item(item)
                .build();

        return dynamoDbClient.putItem(putItemRequest);
    }

    private APIGatewayProxyResponseEvent createAPIResponse(String body) {
        APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent();
        responseEvent.setBody(body);
        responseEvent.setStatusCode(201);
        return responseEvent;
    }

    private SecretsManagerClient createSecretsManagerClient() {
        return SecretsManagerClient.builder()
                .endpointOverride(URI.create(localStackUrl))
                .credentialsProvider(DefaultCredentialsProvider.create())
                .region(Region.of(region))
                .build();
    }
}

CDKLocal to deploy on LocalStack

Start LocalStack in background mode

localstack start -d

Generate the CloudFormation Template

To run commands, you need to go to the infra folder from your project.

Before deploying the Lambda function, you need to generate the CloudFormation template from the AWS CDK code. This step validates the stack definition and alerts you if there are any errors.

To synthesize the CloudFormation template, run:

cdklocal synth

This command will display the CloudFormation stack details in the console (example of output) :

CloudFormation console

It creates also a directory called cdk.out, which contains the generated template and related asset files.

Bootstrap the LocalStack Environment

Before deploying your project for the first time, you need to prepare the AWS environment. This process sets up a CloudFormation stack with essential resources, including an S3 bucket for storing deployment assets.

If you've already bootstrapped the environment, you can skip this step.

Run the following command:

cdklocal bootstrap

Example output:

⏳  Bootstrapping environment aws://000000000000/us-west-1...
CDKToolkit: creating CloudFormation changeset...
✅  Environment aws://000000000000/us-west-1 bootstrapped.

Step 3: Deploy the Lambda Function
Once the environment is ready, deploy the project by running:

cdklocal deploy

deploy CDK

During deployment, the AWS CDK will prompt for confirmation. Press "y" to proceed.

InsertFlightLambdaStack: deploying... [1/1]
InsertFlightLambdaStack: creating CloudFormation changeset...
 ✅  InsertFlightLambdaStack
✨  Deployment time: 70.5s
Stack ARN:
arn:aws:cloudformation:us-west-1:000000000000:stack/InsertFlightLambdaStack/90270911

Test the lambda function with AWS CLI

To add data using AWS CLI :

awslocal lambda invoke 
     --function-name InsertFlightLambda 
     --cli-binary-format raw-in-base64-out 
     --payload '{ "pathParameters": {"bookingId": "booking#1","orderId": "order#1"},"body": "{\"testBody\": \"test\"}" }'

To get data :

awslocal lambda invoke 
     --function-name getFlightLambda
     --cli-binary-format raw-in-base64-out 
     --payload '{ "pathParameters": { "bookingId": "booking#1", "orderId": "order#1" } }' 
     --output json output.txt

You can see the output in the output.txt file create in the root of infra folder.

Here is the result of the request :

{"statusCode": 200, "body": {"orderId": "order#1", "requestBody": "{\"testBody\": \"test\"}", "bookingId": "booking#1"}}

Cleaning Up Resources

First, you need to destroy all resources :

cdklocal destroy

And stop the LocalStack :

localstack stop

Conclusion

Deploying AWS Lambda functions, AWS Secrets Manager and DynamoDB using AWS CDK provides a powerful way to manage infrastructure as code. By leveraging LocalStack, developers can efficiently test and deploy their applications in a local environment before pushing them to AWS, reducing costs and improving development speed. This approach ensures a seamless and automated deployment process, making it easier to maintain and scale serverless applications.

By following this guide, you now have a solid foundation for setting up Java-based Lambda functions, setting a secret string in Secrets Manager, integrating DynamoDB, and deploying them using AWS CDK. Keep exploring AWS CDK to enhance your infrastructure automation and optimize your development workflow!