Simple, Serverless CRUD with the CDK
TLDR: Hands-on tutorial for building serverless CRUD applications with AWS CDK. Learn to set up infrastructure as code, create Lambda functions, and implement authenticated API endpoints with practical examples.
This post provides some basic information on the AWS CDK, along with code examples (updated for CDK v2, the aws-cdk-lib package) to get you started.
How do I set up the AWS CDK?
The first step is to get the CDK installed. This requires:
- Installing Node.js (the CDK CLI runs on Node even when your app is written in Python)
- Installing Python 3.9 or later
- Setting up an AWS account
- Installing the CDK Toolkit (
npm install -g aws-cdk) - Reviewing working with the CDK in Python
How do I use the CDK CLI?
The two main commands for the CDK cli are cdk synth and cdk deploy. synth will output a template. deploy will generate the template and deploy it.
What does the base CDK template look like?
The AWS docs do a good job of explaining how to get off the ground. The focus here is a backend for simple CRUD operations. Here is a minimal CDK v2 stack (in Python) that stands up a DynamoDB table, a Lambda function, and a REST API in front of it:
from pathlib import Path
from aws_cdk import App, Stack
from aws_cdk import aws_dynamodb as dynamodb
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_apigateway as apigw
from constructs import Construct
class CrudStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs):
super().__init__(scope, id, **kwargs)
table = dynamodb.Table(
self, "Table",
partition_key=dynamodb.Attribute(
name="pk", type=dynamodb.AttributeType.STRING),
sort_key=dynamodb.Attribute(
name="sk", type=dynamodb.AttributeType.STRING),
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
)
handler = lambda_.Function(
self, "CrudFn",
runtime=lambda_.Runtime.PYTHON_3_13,
handler="handler.handler",
code=lambda_.Code.from_asset(str(Path(__file__).parent / "lambda")),
environment={"TABLE": table.table_name},
)
# One call wires up the IAM policy and role permissions.
table.grant_read_write_data(handler)
# A REST API that proxies every route to the function.
apigw.LambdaRestApi(self, "CrudApi", handler=handler)
app = App()
CrudStack(app, "CrudStack")
app.synth()
Note the CDK v2 imports: App and Stack now come straight from aws_cdk (in v1 they lived in aws_cdk.core), and the construct base class comes from the separate constructs package. Add a Cognito or Lambda authorizer to the API when you need authenticated endpoints.
Why should I use the CDK?
There are a few reasons why the CDK is awesome!
You get to work in a language you’re familiar with
YAML/JSON can be challenging to work with. There are tools like cfn-lint, but it can only go so far in linting a YAML file.
Using a language of your choice makes life easier for you and you get extensive type checking, linting, and validation. synth and deploy ensure that you have a valid template.
Declarative with use of programming language features
The CDK is highly declarative. Related to higher level constructs, rather than having to explicitly define a policy and attach it to a role, you can do something like:
table.grant_read_write_data(function)
The CDK defines all sorts of interfaces to make this possible. All that’s required is the argument passed to grant_read_write_data adheres to an interface that exposes an IAM principal.
You can also make use of programming language features such as if/else and looping. If you know all of your Lambda functions will use the Python 3.8 runtime, have the same handler function definition, and make use of a table you could create:
def create_python_function(function_name, function_path):
parent_path = Path(__file__).parent.absolute()
return lambda_.Function(self, function_name,
code=lambda_.Code.from_asset(str(Path(parent_path, function_path))),
runtime=lambda_.Runtime.PYTHON_3_13,
handler='handler.handler',
environment={'TABLE': table.table_name})
Bye bye boilerplate!
Higher Level Constructs
The CDK represents CloudFormation resources as constructs. Raw CloudFormation is quite verbose. For the resources that make use of higher level constructs, things are quieted down. For example in the CDK a table may look like:
table = dynamodb.Table(self, 'Table',
partition_key=dynamodb.Attribute(name='pk', type=dynamodb.AttributeType.STRING),
sort_key=dynamodb.Attribute(name='sk', type=dynamodb.AttributeType.STRING))
table.add_global_secondary_index(
partition_key=dynamodb.Attribute(name='sk', type=dynamodb.AttributeType.STRING),
sort_key=dynamodb.Attribute(name='gsi1sk', type=dynamodb.AttributeType.STRING),
index_name='gsi1')
and a corresponding YAML file may look like:
myDynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
-
AttributeName: "Album"
AttributeType: "S"
-
AttributeName: "Artist"
AttributeType: "S"
-
AttributeName: "Sales"
AttributeType: "N"
-
AttributeName: "NumberOfSongs"
AttributeType: "N"
KeySchema:
-
AttributeName: "Album"
KeyType: "HASH"
-
AttributeName: "Artist"
KeyType: "RANGE"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
TableName: "myTableName"
GlobalSecondaryIndexes:
-
IndexName: "myGSI"
KeySchema:
-
AttributeName: "Sales"
KeyType: "HASH"
-
AttributeName: "Artist"
KeyType: "RANGE"
Projection:
NonKeyAttributes:
- "Album"
- "NumberOfSongs"
ProjectionType: "INCLUDE"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
Where to go next
This same pattern, infrastructure as code deploying Lambda functions behind an API, runs the Cybersource MLE/JWT Lab on this site: its backend is a couple of Lambdas provisioned with the CDK. Because the whole stack is code, it also drops cleanly into a CI/CD pipeline.