DEV Community

Cover image for Enhancing AWS Lambda with AWS Lambda Powertools: A Complete Guide to CRUD Operations in DynamoDB
Sunil Yaduvanshi
Sunil Yaduvanshi

Posted on

Enhancing AWS Lambda with AWS Lambda Powertools: A Complete Guide to CRUD Operations in DynamoDB

Introduction

AWS Lambda is a powerful service that lets you run code without provisioning or managing servers. However, when building serverless applications, you might encounter challenges in maintaining observability, logging, and tracing. This is where AWS Lambda Powertools come into play. Powertools provide a suite of utilities that enhance your Lambda functions with robust logging, tracing, and metrics, making your serverless applications more resilient and easier to debug.

In this blog post, we'll walk through a practical use case where we perform CRUD (Create, Read, Update, Delete) operations on a DynamoDB table using different Lambda functions. We'll integrate these functions with API Gateway and use AWS Lambda Powertools for enhanced logging, tracing, and metrics.

Prerequisites

Before we start, make sure you have the following set up:

  • An AWS account
  • Basic knowledge of AWS Lambda, API Gateway, and DynamoDB
  • AWS CLI installed and configured
  • Node.js installed

Step 1: Setting Up the DynamoDB Table

First, let's create a DynamoDB table named UsersTable with userId as the partition key.

aws dynamodb create-table \
    --table-name UsersTable \
    --attribute-definitions AttributeName=userId,AttributeType=S \
    --key-schema AttributeName=userId,KeyType=HASH \
    --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5
Enter fullscreen mode Exit fullscreen mode

DynamoDb Table

Step 2: Creating the Lambda Functions

We'll create four Lambda functions for each CRUD operation: lambda-powertool-create-item, lambda-powertool-list-item, lambda-powertool-update-item, and lambda-powertool-delete-item. Each function will be integrated with API Gateway methods (POST, GET, PUT, DELETE) to handle the respective operations.

Create An IAM Role With Required Permissions

IAM Permissions

2.1 Create a Lambda Layer for AWS Lambda Powertools

To avoid duplicating dependencies in each Lambda function, we'll create a Lambda layer for AWS Lambda Powertools.

  1. Create a new directory for the layer:

    mkdir powertools-layer
    cd powertools-layer
    
  2. Initialize a Node.js project and install the AWS Lambda Powertools:

    npm init -y
    npm install @aws-lambda-powertools/logger @aws-lambda-powertools/tracer @aws-lambda-powertools/metrics
    
  3. Package the dependencies:

    mkdir nodejs
    mv node_modules nodejs/
    zip -r powertools-layer.zip nodejs
    
  4. Deploy the Lambda layer to AWS:

    aws lambda publish-layer-version --layer-name powertools-layer --zip-file fileb://powertools-layer.zip --compatible-runtimes nodejs14.x nodejs16.x nodejs18.x
    
  5. Note the Layer ARN provided after publishing.

2.2 Create Lambda Functions

Now, let's create the Lambda functions. Here's a breakdown of each function:

2.2.1 lambda-powertool-create-item

Create Lambda Function

Lambda IDE

Note: X-Ray Tracing, you have to enable manually from lambda console for all the lambda functions

x-ray tracing

const AWS = require('aws-sdk');
const { Logger } = require('@aws-lambda-powertools/logger');
const { Tracer } = require('@aws-lambda-powertools/tracer');
const { Metrics, MetricUnits } = require('@aws-lambda-powertools/metrics');

const logger = new Logger();
const tracer = new Tracer();
const metrics = new Metrics();
const docClient = new AWS.DynamoDB.DocumentClient();

const countUnit = MetricUnits ? MetricUnits.Count : 'Count';
exports.handler = async (event) => {
    const segment = tracer.getSegment();
    const subsegment = segment.addNewSubsegment('## createItem');

    try {
        logger.info('Received event for creating item', { event });

        console.log('EventId', event.userId)


        if (!event.userId) {
            throw new Error('Validation Error: userId is required');
        }

        console.log(event)
        const params = {
            TableName: 'UsersTable',
            Item: event
        };

        let result = await docClient.put(params).promise();



        // // Add a metric for a successful item creation
        try {
            metrics.addMetric('CreatedItems', countUnit, 1);
        } catch (metricError) {
            logger.error('Error adding metric', { metricError });
            throw metricError;
        }

        const response = {
            statusCode: 201,
            body: JSON.stringify({ message: 'Item created successfully' }),
        };

        subsegment.addMetadata('item', event);
        subsegment.addMetadata('dynamodbResult', result);

        return response;

    } catch (error) {
        logger.error('Error creating item', { error });
        metrics.addMetric('CreateItemErrors', countUnit, 1);
        return {
            statusCode: 500,
            body: JSON.stringify({ message: error.message }),
        };
    } finally {
        subsegment.close();
    }
};
Enter fullscreen mode Exit fullscreen mode

2.2.2 lambda-powertool-list-item

list item lambda

const AWS = require('aws-sdk');
const { Logger } = require('@aws-lambda-powertools/logger');
const { Tracer } = require('@aws-lambda-powertools/tracer');
const { Metrics, MetricUnits } = require('@aws-lambda-powertools/metrics');

const logger = new Logger();
const tracer = new Tracer();
const metrics = new Metrics();
const docClient = new AWS.DynamoDB.DocumentClient();
const countUnit = MetricUnits ? MetricUnits.Count : 'Count';

exports.handler = async (event) => {
    console.log(event)
    const segment = tracer.getSegment();
    const subsegment = segment.addNewSubsegment('## readItem');

    try {
        logger.info('Received event for reading item', { event });

        if (!event.queryStringParameters.userId) {
            throw new Error('Validation Error: userId is required');
        }
        var userId = Number(event.queryStringParameters.userId)

        const params = {
            TableName: 'UsersTable',
            Key: { userId: userId },
        };

        const result = await docClient.get(params).promise();

        if (!result.Item) {
            throw new Error('Item not found');
        }

        // Add a metric for a successful item read
        try {
            metrics.addMetric('ReadItems', countUnit, 1);
        } catch (metricError) {
            logger.error('Error adding metric', { metricError });
            throw metricError;
        }

        const response = {
            statusCode: 200,
            body: JSON.stringify(result.Item),
        };

        subsegment.addMetadata('item', result.Item);
        return response;

    } catch (error) {
        logger.error('Error reading item', { error });
        metrics.addMetric('ReadItemErrors', countUnit, 1);
        return {
            statusCode: 500,
            body: JSON.stringify({ message: error.message }),
        };
    } finally {
        subsegment.close();
    }
};
Enter fullscreen mode Exit fullscreen mode

2.2.3 lambda-powertool-update-item

update Item lambda

const AWS = require('aws-sdk');
const { Logger } = require('@aws-lambda-powertools/logger');
const { Tracer } = require('@aws-lambda-powertools/tracer');
const { Metrics, MetricUnits } = require('@aws-lambda-powertools/metrics');

const logger = new Logger();
const tracer = new Tracer();
const metrics = new Metrics();
const docClient = new AWS.DynamoDB.DocumentClient();
const countUnit = MetricUnits ? MetricUnits.Count : 'Count';

exports.handler = async (event) => {
    const segment = tracer.getSegment();
    const subsegment = segment.addNewSubsegment('## updateItem');

    try {
        logger.info('Received event for updating item', { event });

        if (!event.userId) {
            throw new Error('Validation Error: userId is required');
        }

        const params = {
            TableName: 'UsersTable',
            Key: { userId: event.userId },
            UpdateExpression: 'set #userName = :userName, #userEmail = :userEmail',
            ExpressionAttributeNames: {
                '#userName': 'userName',
                '#userEmail': 'userEmail',
            },
            ExpressionAttributeValues: {
                ':userName': event.userName,
                ':userEmail': event.userEmail,
            },
            ReturnValues: 'UPDATED_NEW',
        };

        const result = await docClient.update(params).promise();

        // Add a metric for a successful item update
        try {
            metrics.addMetric('UpdatedItems', countUnit, 1);
        } catch (metricError) {
            logger.error('Error adding metric', { metricError });
            throw metricError;
        }

        const response = {
            statusCode: 200,
            body: JSON.stringify({ message: 'Item updated successfully', updatedAttributes: result.Attributes }),
        };

        subsegment.addMetadata('updatedItem', result.Attributes);
        return response;

    } catch (error) {
        logger.error('Error updating item', { error });
        metrics.addMetric('UpdateItemErrors', countUnit, 1);
        return {
            statusCode: 500,
            body: JSON.stringify({ message: error.message }),
        };
    } finally {
        subsegment.close();
    }
};
Enter fullscreen mode Exit fullscreen mode

2.2.4 lambda-powertool-delete-item

delete item

const AWS = require('aws-sdk');
const { Logger } = require('@aws-lambda-powertools/logger');
const { Tracer } = require('@aws-lambda-powertools/tracer');
const { Metrics, MetricUnits } = require('@aws-lambda-powertools/metrics');

const logger = new Logger();
const tracer = new Tracer();
const metrics = new Metrics();
const docClient = new AWS.DynamoDB.DocumentClient();
const countUnit = MetricUnits ? MetricUnits.Count : 'Count';

exports.handler = async (event) => {
    const segment = tracer.getSegment();
    const subsegment = segment.addNewSubsegment('## deleteItem');

    try {
        logger.info('Received event for deleting item', { event });

        if (!event.userId) {
            throw new Error('Validation Error: userId is required');
        }

        const params = {
            TableName: 'UsersTable',
            Key: { userId: event.userId },
        };

        const result = await docClient.delete(params).promise();

        // Add a metric for a successful item deletion
        try {
            metrics.addMetric('DeletedItems', countUnit, 1);
        } catch (metricError) {
            logger.error('Error adding metric', { metricError });
            throw metricError;
        }

        const response = {
            statusCode: 200,
            body: JSON.stringify({ message: 'Item deleted successfully' }),
        };

        subsegment.addMetadata('deletedItem', event.userId);
        return response;

    } catch (error) {
        logger.error('Error deleting item', { error });
        metrics.addMetric('DeleteItemErrors', countUnit, 1);
        return {
            statusCode: 500,
            body: JSON.stringify({ message: error.message }),
        };
    } finally {
        subsegment.close();
    }
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Integrating Lambda with API Gateway

Now that we have our Lambda functions ready, let's integrate them with API Gateway.

  1. Create a new API in API Gateway.

  2. Create four methods (POST, GET, PUT, DELETE) and link them to the respective Lambda functions.

  3. Deploy the API.

API Gateway Console

Step 4: Testing the CRUD Operations

Now you can test the CRUD operations by sending HTTP requests to the API Gateway endpoints. Each operation should log data to CloudWatch, generate traces in AWS X-Ray, and create metrics that you can monitor.

EndToEnd Testing

lambda-powertool-create-item:

Post API

POST Logs

lambda-powertool-update-item:

Request Body

PUT API

update logs

lambda-powertool-list-item:

GET API

list Logs
lambda-powertool-delete-item

delete item

delete logs

CloudWatch & X-Ray Tracing Console

Metrices

Trace Console

Trace Map

Conclusion

In this blog post, we demonstrated how to enhance AWS Lambda functions with AWS Lambda Powertools. We created a set of Lambda functions to perform CRUD operations on a DynamoDB table, integrated them with API Gateway, and utilized Powertools to add structured logging, distributed tracing, and custom metrics. This setup not only improves the observability of your serverless application but also makes it easier to diagnose issues and monitor performance.

By creating a Lambda layer for Powertools, we ensured that our functions remain lightweight and reusable. This approach is particularly beneficial in larger projects where multiple Lambda functions share the same dependencies.
Feel free to expand upon this use case by adding more complex operations or integrating additional AWS services. Happy coding!

Top comments (0)