Deploy your First Serverless App Using AWS CDK
ADVERTISEMENT
Table of Contents
- Introduction
- Serverless App Data Model
- Set up basic AWS CDK Infrastructure
- Setting Up AWS API Gateway
- How do you use API gateway and Lambda?
- Adding a DynamoDB Table for Persistent Storage
- Writing AWS Lambda Function Code to Handle Requests
- Testing with Postman
- Summary
- Next steps
- References
Introduction
Serverless architecture has become increasingly popular in recent years due to its ability to reduce operational costs and improve scalability. AWS Lambda, API Gateway, and DynamoDB are three powerful tools provided by Amazon Web Services (AWS) that can be used to create a serverless todo list application. In this article, we will be deploying this application using the AWS Cloud Development Kit (CDK).
Using the AWS CDK, we can easily deploy and manage our serverless todo list application. CDK is a software development framework that provides high-level abstractions for AWS services. This allows developers to use familiar programming languages and tools to define their cloud infrastructure.
This article will focus on creating a simple todo list application, but the approach we discuss can be generalized to nearly any application which implements an API and/or persistent storage. This framework is highly flexible and can be used as a subcomponent of various architectures.
Serverless App Data Model
Since we are creating a todo list application, we will create a data model which represents individual data points (tasks) in our todo list. This task data model represents the repeatable structure for each todo list task that we will store in our DynamoDB database. Our tasks will have two fields: an identifier (task_id
) and a title for the task’s content (task_title
).
{
"id" : String (primary key),
"task_title" : String
}
Each item that we store in DynamoDB contains a “primary key,” or a designated index which we can query for when we want to find an item in the database. In this case, our primary key is id
, as we can look for items based on this UUID (universally unique identifier). For more information on DynamoDB or setting up your first table, see our article on How to Create a DynamoDB Table on AWS.
For this application, we will assume that the user application/API call supplies the UUID, but with some minor modifications it is possible to do this server-side through the Lambda function.
Set up basic AWS CDK Infrastructure
To get started, we will start by setting up the skeleton for our infrastructure using CDK. First, let’s create a new CDK application.
$ mkdir todolist-app
$ cd todolist-app
$ cdk init app --language typescript
The todolist-app-stack.ts
file is the main entry point for the AWS CDK application. When you run the cdk init
command, the following file structure will be created:
todolist-app
- README.md
- bin
-- todolist-app.ts
- lib
-- todolist-app-stack.ts
- package.json
- tsconfig.json
- webpack.config.js
The todolist-app-stack.ts
file will be located in the lib
directory and will contain the following initial code:
import * as cdk from 'aws-cdk-lib';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as route53 from 'aws-cdk-lib/aws-route53';
export class TodolistAppStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
}
}
The TodolistAppStack
class extends the Stack
class provided by the aws-cdk-lib
package. This class will be used to define the AWS resources that make up your todo list application. The Constructor
function is used to initialize the stack and will be where you will define the resources that make up your application. The scope
and id
parameters are required by the Stack
class, while the props
parameter is optional and can be used to specify additional properties for the stack.
Setting Up AWS API Gateway
The first step for creating our todo list application is to build an API. We can build this API using the REST API framework provided through API Gateway. We will use the RestApi
class provided by the aws-cdk-lib/aws-apigateway
package. Here is an example of how you can define the API and its resources in the TodolistAppStack
class.
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as route53 from 'aws-cdk-lib/aws-route53';
export class TodolistAppStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Define the API Gateway construct
const api = new apigateway.RestApi(this, 'TodoListAPI', {
restApiName: 'TodoListAPI',
});
// Define the "get-tasks" resource
const getTasks = api.root.addResource('get-tasks');
const getTasksMethod = getTasks.addMethod('GET');
// Define the "save-task" resource
const saveTask = api.root.addResource('save-task');
const saveTaskMethod = saveTask.addMethod('POST');
// Define the "delete-task" resource
const deleteTask = api.root.addResource('delete-task');
const deleteTaskMethod = deleteTask.addMethod('DELETE');
}
}
In this code, we first create an instance of the RestApi
class and give it a unique ID and a name.
Next, we define the three resources for our API: "get-tasks", "save-task", and "delete-task". For each resource, we create an instance of the addResource
method and give it a unique ID. We then specify the HTTP method for each resource using the addMethod
method. In this case, we are using the GET
, POST
, and DELETE
methods for the "get-tasks", "save-task", and "delete-task" resources, respectively.
Note that this code is just an example and does not include all the necessary code to fully define the API and its resources. You will need to add additional code to define the integration with the Lambda functions and DynamoDB tables that will be used to implement the API's functionality.
How do you use API gateway and Lambda?
To add a single AWS Lambda construct to handle all three API resources, you can use the Function
class provided by the aws-cdk-lib/aws-lambda
package. Here is an example of how you can define the Lambda function and its integrations with the API in the TodolistAppStack
class:
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as route53 from 'aws-cdk-lib/aws-route53';
export class TodolistAppStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Define the API Gateway construct
const api = new apigateway.RestApi(this, 'TodoListAPI', {
restApiName: 'TodoListAPI',
});
// Define the "get-tasks" resource
const getTasks = api.root.addResource('get-tasks');
const getTasksMethod = getTasks.addMethod('GET');
// Define the "save-task" resource
const saveTask = api.root.addResource('save-task');
const saveTaskMethod = saveTask.addMethod('POST');
// Define the "delete-task" resource
const deleteTask = api.root.addResource('delete-task');
const deleteTaskMethod = deleteTask.addMethod('DELETE');
// Define the Lambda function
const todoListLambda = new lambda.Function(this, 'TodoListLambda', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromAsset('lambda/todolist'),
handler: 'index.handler',
});
// Add Lambda integrations for the API methods
getTasksMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
saveTaskMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
deleteTaskMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
}
}
In this code, we first create an instance of the Function
class and give it a unique ID. We then specify the runtime, code, and handler for the function. In this case, we are using Node.js 12.x as the runtime, and we are loading the code for the function from the lambda/todolist
directory.
Next, we add Lambda integrations for the API methods using the addIntegration
method. In this case, we are using the same Lambda function for all three methods by passing the todoListLambda
Adding a DynamoDB Table for Persistent Storage
To add a DynamoDB table for persistent data storage, you can use the Table
class provided by the aws-cdk-lib/aws-dynamodb
package. Here is an example of how you can define the table and grant read/write permissions to the Lambda functions in the TodolistAppStack
class:
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as route53 from 'aws-cdk-lib/aws-route53';
export class TodolistAppStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Define the API Gateway construct
const api = new apigateway.RestApi(this, 'TodoListAPI', {
restApiName: 'TodoListAPI',
});
// Define the "get-tasks" resource
const getTasks = api.root.addResource('get-tasks');
const getTasksMethod = getTasks.addMethod('GET');
// Define the "save-task" resource
const saveTask = api.root.addResource('save-task');
const saveTaskMethod = saveTask.addMethod('POST');
// Define the "delete-task" resource
const deleteTask = api.root.addResource('delete-task');
const deleteTaskMethod = deleteTask.addMethod('DELETE');
// Define the Lambda function
const todoListLambda = new lambda.Function(this, 'TodoListLambda', {
runtime: lambda.Runtime.NODEJS_12_X,
code: lambda.Code.fromAsset('lambda/todolist'),
handler: 'index.handler',
});
// Add Lambda integrations for the API methods
getTasksMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
saveTaskMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
deleteTaskMethod.addIntegration(new apigateway.LambdaIntegration(todoListLambda));
const dynamoDb = new dynamodb.DynamoDB(this, 'TodoListTable', {
// Additional DynamoDB table configuration options
});
const todoListTable = new dynamodb.Table(this, 'TodoList', {
// Table name and other properties
tableName: 'TodoList',
partitionKey: {
name: 'id',
type: dynamodb.AttributeType.STRING
}
// Other configuration options
});
todoListTable.grantReadWriteData(todoListLambda);
}
}
Writing AWS Lambda Function Code to Handle Requests
Now, we must write Lambda function code in order to actually process API requests.
Start by creating a lambda directory and a file to store function code.
$ cd todolist-app
$ mkdir lambda
$ cd lambda
$ mkdir todolist
$ cd todolist
$ touch index.js
Now, open the file and start adding code. We will begin by adding a skeleton which uses a switch statement to call helper functions based on the value of the resource
key in the event request:
exports.handler = async (event) => {
console.log(`Received event: ${JSON.stringify(event)}`);
// Define helper functions for each resource
const resourceHandlers = {
'get-tasks': getTasks,
'save-task': saveTask,
'delete-task': deleteTask
};
// Call the appropriate helper function based on the resource key in the request
const resource = event.requestContext.resourcePath;
const handler = resourceHandlers[resource];
return handler ? handler(event) : handleError(new Error(`Unsupported resource: ${resource}`));
};
// Helper function for the "get-tasks" resource
const getTasks = async (event) => {
// Get the data from the DynamoDB table
const data = await getDataFromTable();
// Return the data in the response
return {
statusCode: 200,
body: JSON.stringify(data)
};
};
// Helper function for the "save-task" resource
const saveTask = async (event) => {
// Parse the JSON data in the request body
const taskData = JSON.parse(event.body);
// Save the data to the DynamoDB table
await saveDataToTable(taskData);
// Return an empty response
return {
statusCode: 200,
body: ''
};
};
// Helper function for the "delete-task" resource
const deleteTask = async (event) => {
// Parse the JSON data in the request body
const taskData = JSON.parse(event.body);
// Delete the data from the DynamoDB table
await deleteDataFromTable(taskData);
// Return an empty response
return {
statusCode: 200,
body: ''
};
};
// Helper function to handle errors
const handleError = (err) => {
// Log the error
console.error(err);
// Return an error response
return {
statusCode: 500,
body: JSON.stringify({
error: err.message
})
};
};
We will also need to implement the helper functions, such as getDataFromTable
, saveDataToTable
, and deleteDataFromTable
, to perform the desired actions on the DynamoDB table.
Next, we must implement our helper functions which are being called by the switch statement. We must import a DynamoDB DocumentClient, and then implement it as follows:
// Import the AWS SDK and the DynamoDB client
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
...
// Helper function to get all items from the DynamoDB table
const getDataFromTable = async () => {
// Define the parameters for the query
const params = {
TableName: 'TodoList'
};
// Use the scan method to retrieve all items from the table
const result = await dynamoDb.scan(params).promise();
// Return the items
return result.Items;
};
...
In this code, the DynamoDB
client is imported and instantiated, and then the scan
method is used to retrieve all items from the TodoList
table. The items are then returned from the function.
You can then use this helper function in the main Lambda handler function to return the data in the response to the get-tasks
resource request.
We can do the same with save-task
and delete-task
. To implement the helper functions for the save-task
and delete-task
cases, you can use the following code:
// Import the AWS SDK and the DynamoDB client
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
// Helper function to save an item to the DynamoDB table
const saveDataToTable = async (taskData) => {
// Define the parameters for the put method
const params = {
TableName: 'TodoList',
Item: taskData
};
// Use the put method to save the item to the table
await dynamoDb.put(params).promise();
};
// Helper function to delete an item from the DynamoDB table
const deleteDataFromTable = async (taskData) => {
// Define the parameters for the delete method
const params = {
TableName: 'TodoList',
Key: {
id: taskData.id
}
};
// Use the delete method to delete the item from the table
await dynamoDb.delete(params).promise();
};
Congratulations on building your first serverless stack! You have defined an API Gateway with three resources, and implemented a Lambda function to handle the requests for those resources. You have also added a DynamoDB table to store data persistently, and granted your Lambda function read/write permissions to the table.
Now that all the necessary resources have been created, you can test out your API to see if it works as expected. You can use a tool like Postman to send requests to your API and verify that the Lambda function handles the requests correctly and performs the desired actions on the DynamoDB table.
Testing with Postman
To test out your API using Postman, you can follow these steps:
- Install and open the Postman application on your computer.
- In the Postman application, create a new collection for your API by clicking the "New" button in the top left corner, and then selecting "Collection" from the dropdown menu. Give your collection a name and description, and then click the "Create" button.
- In the new collection, create a new request for each of the three resources in your API Gateway. To do this, click the "New" button in the top left corner, and then select "Request" from the dropdown menu. Give your request a name and select the collection where you want to save it, and then click the "Create" button.
- For each request, set the HTTP method and the URL of your API Gateway endpoint. For example, if your API Gateway endpoint is
https://abc123.execute-api.us-east-1.amazonaws.com
, you can set the URL for theget-tasks
request ashttps://abc123.execute-api.us-east-1.amazonaws.com/get-tasks
. - For the
save-task
anddelete-task
requests, set the request body to a JSON object that contains theid
andtask_title
keys, with the appropriate values for your test case. For example, if you want to save a task with the id123
and the task titleMy task
, you can set the request body as follows:
{
"id": "123",
"task_title": "My task"
}
- For each request, click the "Send" button to send the request to your API Gateway endpoint.
- After sending the request, check the response from your API to verify that it is as expected. For the
get-tasks
request, you should receive a response with an array of tasks in the body. For thesave-task
anddelete-task
requests, you should receive an empty response with a status code of 200.
Here are three sample requests that you can use to test your API using Postman:
- Get all tasks
- Method:
GET
- URL:
https://abc123.execute-api.us-east-1.amazonaws.com/get-tasks
- Request body:
N/A
- Expected response:
{
"statusCode": 200,
"body": [
{
"id": "1",
"task_title": "Task 1"
},
{
"id": "2",
"task_title": "Task 2"
},
// Other tasks
]
}
- Save a new task
- Method:
POST
- URL:
https://abc123.execute-api.us-east-1.amazonaws.com/save-task
- Request body:
{
"id": "123",
"task_title": "My task"
}
- Expected response:
{
"statusCode": 200,
"body": ""
}
- Delete a task
- Method:
DELETE
- URL:
https://abc123.execute-api.us-east-1.amazonaws.com/delete-task
- Request body:
{
"id": "123"
}
Expected response:
{
"statusCode": 200,
"body": ""
}
Summary
In conclusion, you have successfully built a serverless stack using AWS CDK with Lambda, API Gateway, and DynamoDB. You have defined an API Gateway with three resources, and implemented a Lambda function to handle the requests for those resources. You have also added a DynamoDB table to store data persistently, and granted your Lambda function read/write permissions to the table, creating a cohesive app.
Congratulations on completing this tutorial and building your first serverless stack! This is a great achievement, and it is a strong foundation for building more complex and powerful serverless applications in the future.
Next steps
If you're interested in learning more about the basics of coding and software development, check out our Coding Essentials Guidebook for Developers, where we cover the essential languages, concepts, and tools that you'll need to become a professional developer.
Thanks and happy coding! We hope you enjoyed this article. If you have any questions or comments, feel free to reach out to jacob@initialcommit.io.
References
- CDK Reference Documentation - https://docs.aws.amazon.com/cdk/api/v2/](https://docs.aws.amazon.com/cdk/api/v2/
- Sending requests with Postman - https://learning.postman.com/docs/getting-started/sending-the-first-request/
Final Notes
Recommended product: Coding Essentials Guidebook for Developers