This is the AWS CDK v2 Developer Guide. The older CDK v1 entered maintenance on June 1, 2022 and ended support on June 1, 2023.
Creating a serverless application using the AWS CDK
This example walks you through creating the resources for a simple widget dispensing service. (For the purpose of this example, a widget is just a name or identifier that can be added to, retrieved from, and deleted from a collection.) The example includes:
-
An AWS Lambda function.
-
An Amazon API Gateway API to call the Lambda function.
-
An Amazon S3 bucket that holds the widgets.
This tutorial contains the following steps.
-
Create an AWS CDK app
-
Create a Lambda function that gets a list of widgets with HTTP GET /
-
Create the service that calls the Lambda function
-
Add the service to the AWS CDK app
-
Test the app
-
Add Lambda functions to do the following:
-
Create a widget with POST /{name}
-
Get a widget by name with GET /{name}
-
Delete a widget by name with DELETE /{name}
-
-
Tear everything down when you're finished
Create an AWS CDK app
Create the app MyWidgetService in the current folder.
Note
The CDK names source files and classes based on the name of the project directory. If you don't use the name
MyWidgetService
as shown previously, it might be difficult to follow the rest of the steps.
Some of the files that the instructions tell you to modify wont' be there, because they will have different
names.
The important files in the blank project are as follows. (We will also be adding a couple of new files.)
Run the app and note that it synthesizes an empty stack.
cdk synth
You should see output beginning with YAML code like the following.
Resources: CDKMetadata: Type: AWS::CDK::Metadata Properties: ...
Create a Lambda function to list all widgets
The next step is to create a Lambda function to list all of the widgets in our Amazon S3 bucket. We will provide the Lambda function's code in JavaScript.
Create the resources
directory in the project's main directory.
mkdir resources
Create the following JavaScript file, widgets.js
, in the resources
directory.
import { S3Client, ListObjectsCommand } from "@aws-sdk/client-s3"; // The following code uses the AWS SDK for JavaScript (v3). // For more information, see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html. const s3Client = new S3Client({}); /** * @param {string} bucketName */ const listObjectNames = async (bucketName) => { const command = new ListObjectsCommand({ Bucket: bucketName }); const { Contents } = await s3Client.send(command); if (!Contents.length) { const err = new Error(`No objects found in ${bucketName}`); err.name = "EmptyBucketError"; throw err; } // Map the response to a list of strings representing the keys of the Amazon Simple Storage Service (Amazon S3) objects. // Filter out any objects that don't have keys. return Contents.map(({ Key }) => Key).filter((k) => !!k); }; /** * @typedef {{ httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', path: string }} LambdaEvent */ /** * * @param {LambdaEvent} lambdaEvent */ const routeRequest = (lambdaEvent) => { if (lambdaEvent.httpMethod === "GET" && lambdaEvent.path === "/") { return handleGetRequest(); } const error = new Error( `Unimplemented HTTP method: ${lambdaEvent.httpMethod}`, ); error.name = "UnimplementedHTTPMethodError"; throw error; }; const handleGetRequest = async () => { if (process.env.BUCKET === "undefined") { const err = new Error(`No bucket name provided.`); err.name = "MissingBucketName"; throw err; } const objects = await listObjectNames(process.env.BUCKET); return buildResponseBody(200, objects); }; /** * @typedef {{statusCode: number, body: string, headers: Record<string, string> }} LambdaResponse */ /** * * @param {number} status * @param {Record<string, string>} headers * @param {Record<string, unknown>} body * * @returns {LambdaResponse} */ const buildResponseBody = (status, body, headers = {}) => { return { statusCode: status, headers, body, }; }; /** * * @param {LambdaEvent} event */ export const handler = async (event) => { try { return await routeRequest(event); } catch (err) { console.error(err); if (err.name === "MissingBucketName") { return buildResponseBody(400, err.message); } if (err.name === "EmptyBucketError") { return buildResponseBody(204, []); } if (err.name === "UnimplementedHTTPMethodError") { return buildResponseBody(400, err.message); } return buildResponseBody(500, err.message || "Unknown server error"); } };
Save it and be sure the project still results in an empty stack. We haven't yet wired the Lambda function to the AWS CDK app, so the Lambda asset doesn't appear in the output.
cdk synth
Create a widget service
Create a new source file to define the widget service with the source code shown below.
Tip
We're using a lambda.Function
in to deploy this function because it supports a wide variety of programming
languages. For JavaScript and TypeScript specifically, you might consider a lambda-nodejs.NodejsFunction
. The latter uses esbuild to bundle up the script
and converts code written in TypeScript automatically.
Save the app and make sure it still synthesizes an empty stack.
cdk synth
Add the service to the app
To add the widget service to our AWS CDK app, we'll need to modify the source file that defines the stack to instantiate the service construct.
Be sure the app runs and synthesizes a stack (we won't show the stack here: it's over 250 lines).
cdk synth
Deploy and test the app
Before you can deploy your first AWS CDK app, you must bootstrap your AWS environment. Among other resources, this creates a staging bucket that the AWS CDK uses to deploy stacks containing assets. For details, see Bootstrapping your AWS environment. If you've already bootstrapped, you'll get a warning and nothing will change.
cdk bootstrap aws://
ACCOUNT-NUMBER
/REGION
Now we're ready to deploy the app as follows.
cdk deploy
If the deployment succeeds, save the URL for your server. This URL appears in one of the last lines in the window,
where GUID
is an alphanumeric GUID and REGION
is your AWS
Region.
https://
GUID
.execute-api-REGION
.amazonaws.com/prod/
Test your app by getting the list of widgets (currently empty) by navigating to this URL in a browser, or use the following command.
curl -X GET 'https://GUID.execute-api.REGION.amazonaws.com/prod'
You can also test the app by completing the following steps:
-
Open the AWS Management Console.
-
Navigate to the API Gateway service.
-
Find Widget Service in the list.
-
Select GET and Test to test the function.
Because we haven't stored any widgets yet, the output should be similar to the following.
{ "widgets": [] }
Add the individual widget functions
The next step is to create Lambda functions to create, show, and delete individual widgets.
Replace the code in widgets.js
(in resources
) with the following.
import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; // In the following code we are using AWS JS SDK v3 // See https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html const S3 = new S3Client({}); const bucketName = process.env.BUCKET; exports.main = async function(event, context) { try { const method = event.httpMethod; // Get name, if present const widgetName = event.path.startsWith('/') ? event.path.substring(1) : event.path; if (method === "GET") { // GET / to get the names of all widgets if (event.path === "/") { const data = await S3.send(new ListObjectsV2Command({ Bucket: bucketName })); const body = { widgets: data.Contents.map(function(e) { return e.Key }) }; return { statusCode: 200, headers: {}, body: JSON.stringify(body) }; } if (widgetName) { // GET /name to get info on widget name const data = await S3.send(new GetObjectCommand({ Bucket: bucketName, Key: widgetName})); const body = data.Body.toString('utf-8'); return { statusCode: 200, headers: {}, body: JSON.stringify(body) }; } } if (method === "POST") { // POST /name // Return error if we do not have a name if (!widgetName) { return { statusCode: 400, headers: {}, body: "Widget name missing" }; } // Create some dummy data to populate object const now = new Date(); const data = widgetName + " created: " + now; const base64data = Buffer.from(data, 'binary'); await S3.send(new PutObjectCommand({ Bucket: bucketName, Key: widgetName, Body: base64data, ContentType: 'application/json' })); return { statusCode: 200, headers: {}, body: data }; } if (method === "DELETE") { // DELETE /name // Return an error if we do not have a name if (!widgetName) { return { statusCode: 400, headers: {}, body: "Widget name missing" }; } await S3.send(new DeleteObjectCommand({ Bucket: bucketName, Key: widgetName })); return { statusCode: 200, headers: {}, body: "Successfully deleted widget " + widgetName }; } // We got something besides a GET, POST, or DELETE return { statusCode: 400, headers: {}, body: "We only accept GET, POST, and DELETE, not " + method }; } catch(error) { var body = error.stack || JSON.stringify(error, null, 2); return { statusCode: 400, headers: {}, body: body } } }
Wire up these functions to your API Gateway code at the end of the WidgetService
constructor.
Save and deploy the app.
cdk deploy
We can now store, show, or delete an individual widget. Use the following commands to list the widgets, create the widget example, list all of the widgets, show the contents of example (it should show today's date), delete example, and then show the list of widgets again.
curl -X GET 'https://
GUID
.execute-api.REGION
.amazonaws.com/prod' curl -X POST 'https://GUID
.execute-api.REGION
.amazonaws.com/prod/example' curl -X GET 'https://GUID
.execute-api.REGION
.amazonaws.com/prod' curl -X GET 'https://GUID
.execute-api.REGION
.amazonaws.com/prod/example' curl -X DELETE 'https://GUID
.execute-api.REGION
.amazonaws.com/prod/example' curl -X GET 'https://GUID
.execute-api.REGION
.amazonaws.com/prod'
You can also use the API Gateway console to test these functions. Set the name value to the name of a widget, such as example.
Clean up
To avoid unexpected AWS charges, destroy your AWS CDK stack after you're done with this exercise.
cdk destroy