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
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
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.
-
Create a new directory for the layer:
mkdir powertools-layer cd powertools-layer
-
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
-
Package the dependencies:
mkdir nodejs mv node_modules nodejs/ zip -r powertools-layer.zip nodejs
-
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
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
Note: X-Ray Tracing, you have to enable manually from lambda console for all the lambda functions
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();
}
};
2.2.2 lambda-powertool-list-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) => {
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();
}
};
2.2.3 lambda-powertool-update-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('## 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();
}
};
2.2.4 lambda-powertool-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();
}
};
Step 3: Integrating Lambda with API Gateway
Now that we have our Lambda functions ready, let's integrate them with API Gateway.
Create a new API in API Gateway.
Create four methods (POST, GET, PUT, DELETE) and link them to the respective Lambda functions.
Deploy the API.
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:
lambda-powertool-update-item:
lambda-powertool-list-item:
CloudWatch & X-Ray Tracing Console
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)