Deploying with the AWS CDK¶
In this tutorial, we’re going to create a REST API with an Amazon DynamoDB table as our data store. We’ll be using the AWS Cloud Development Kit (CDK) to deploy our application, and we’ll show how to use the integration between Chalice and the CDK in order to build and deploy our application.
By combining Chalice and the CDK together, you can use Chalice to write your application code using its familiar, decorator-based APIs, and use the CDK and the full breadth of its construct libraries to create the service infrastructure and resources needed for your application. We’ll also see how we can use the Chalice construct to manipulate our Chalice application using the CDK APIs as well as take resources from CDK constructs and map them into our Chalice application.
Installation and Configuration¶
This tutorial requires that both Chalice and the AWS CDK is installed. The CDK is written in Typescript and requires node and npm to be installed. See the Getting started with the AWS CDK for more details on install the CDK.
First, we’ll install the CDK.
$ npm install -g aws-cdk
You should now have a cdk
executable you can run.
$ cdk --version
1.83.0 (build 827c5f4)
Next we’ll create a Python virtual environment and install Chalice. Be sure to use Python 3.6 or greater.
$ python3 -m venv demo
$ . demo/bin/activate
$ python3 -m pip install chalice
$ chalice --version
chalice 1.22.0, python 3.7.8, darwin 19.6.0
CDK integration with Chalice is available as an optional package installation. To install the necessary dependencies run the following command:
$ python3 -m pip install "chalice[cdkv2]"
Note: Please use CDK version 2, support for CDK version 1 ended on June 1, 2023. See Working with the AWS CDK in Python Doc for more information.
You’re now ready to create your first Chalice and CDK application.
Project Creation¶
To create a new project we’ll use the chalice new-project
command with no
arguments. Enter a name for your project and select
[CDK] REST API with DynamoDB backend
for the project type.
$ chalice new-project
___ _ _ _ _ ___ ___ ___
/ __|| || | /_\ | | |_ _|/ __|| __|
| (__ | __ | / _ \ | |__ | || (__ | _|
\___||_||_|/_/ \_\|____||___|\___||___|
The python serverless microframework for AWS allows
you to quickly create and deploy applications using
Amazon API Gateway and AWS Lambda.
Please enter the project name
[?] Enter the project name: cdkdemo
[?] Select your project type: [CDK] REST API with DynamoDB backend
REST API
S3 Event Handler
Lambda Functions only
Legacy REST API Template
[CDK] REST API with DynamoDB backend
Your project has been generated in ./cdkdemo
Next, we’ll cd
into the cdkdemo
directory and see what Chalice has
generated.
$ cd cdkdemo
$ tree
.
├── README.rst
├── infrastructure # CDK Application
│ ├── app.py
│ ├── cdk.json
│ ├── requirements.txt
│ └── stacks
│ ├── __init__.py
│ └── chaliceapp.py
├── requirements.txt
└── runtime # Chalice Application
├── app.py
└── requirements.txt
There’s two top level directories, infrastructure
and runtime
, which
correspond to the CDK application and the Chalice application. The
infrastructure
directory is where we can add additional AWS resources
needed by our application, and the runtime
directory is where we write
our application code for our Lambda functions. We’ll look at these in more
detail, but first we’ll deploy our application.
In order to build and deploy our application, we need to install the dependencies used by our application. We can do this by installing the requirements file in the top level directory of our project.
$ python3 -m pip install -r requirements.txt
If this is your first time using the CDK, you’ll need to bootstrap your
account, which will deploy an AWS CloudFormation stack that contains
resources needed to store our application. You can do this by running the
cdk bootstrap
command from the infrastructure
directory.
$ cd infrastructure
$ cdk bootstrap
Packaging Chalice app for cdkdemo
Creating deployment package.
The stack cdkdemo already includes a CDKMetadata resource
⏳ Bootstrapping environment aws://12345/us-west-2...
CDKToolkit: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (3/3)
✅ Environment aws://12345/us-west-2 bootstrapped.
We can now deploy our application using the cdk deploy
command. Make sure
you’re still in the infrastructure
directory.
$ cdk deploy
Packaging Chalice app for cdkdemo
Creating deployment package.
Reusing existing deployment package.
The stack cdkdemo already includes a CDKMetadata resource
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
...
Do you wish to deploy these changes (y/n)? y
cdkdemo: deploying...
[0%] start: Publishing abcd:current
[100%] success: Published abcd:current
cdkdemo: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (10/10)
✅ cdkdemo
Outputs:
cdkdemo.APIHandlerArn = arn:aws:lambda:us-west-2:12345:function:cdkdemo-APIHandler-C8OLGQT9YIDO
cdkdemo.APIHandlerName = cdkdemo-APIHandler-C8OLGQT9YIDO
cdkdemo.AppTableName = cdkdemo-AppTable815C50BC-1OPGOPFYODZOJ
cdkdemo.EndpointURL = https://abcd.execute-api.us-west-2.amazonaws.com/api/
cdkdemo.RestAPIId = abcd
Stack ARN:
arn:aws:cloudformation:us-west-2:12345:stack/cdkdemo/574c4850-1d23-11eb-8cae-0aea264da24f
We’ve now deployed a Chalice application powered by the CDK. We can now test our REST API.
Note
If you’ve Chalice before, you may be familiar with the chalice deploy
command. When we use the AWS CDK to deploy our application we no longer
use chalice deploy
and instead we run cdk deploy
from the
infrastructure/
directory. You should not use chalice deploy
to deploy your application when using Chalice’s CDK integration.
Testing¶
To test our application, we make HTTP requests to our EndpointUrl
, which is
shown as the value for cdkdemo.EndpointUrl
in the output section above.
We’re using httpie to make our HTTP requests from the
command line.
$ python3 -m pip install httpie
$ http POST https://abcd.execute-api.us-west-2.amazonaws.com/api/users/ username=jamesls name=James
HTTP/1.1 200 OK
...
{}
$ http https://abcd.execute-api.us-west-2.amazonaws.com/api/users/jamesls
HTTP/1.1 200 OK
Content-Type: application/json
...
{
"name": "James",
"username": "jamesls"
}
Now that we have our sample application up and running, let’s walk through the project code so we can better understand what’s happening.
Code Walkthrough¶
The runtime/
directory contains code where you define your Lambda event
handlers (e.g. @app.route()
, @app.on_s3_event()
, etc.). When you
create a Chalice application without the CDK, this is normally the root
directory for your application. You should also see your Chalice config file
in .chalice/config.json
. The infrastructure/
directory contains the
definitions for the AWS resources used by your application. This is the
directory structure that would be generated if you were only using the
CDK and not Chalice. This is why the combined Chalice/CDK application template
has a new top level directory with separate sub directories for the CDK app
and the Chalice app.
To better understand how the two applications communicate with each other, we’ll examine how the DynamoDB table was added to the application.
First, let’s look at the code for our REST API in runtime/app.py
.
import os
import boto3
from chalice import Chalice
app = Chalice(app_name='cdkdemo')
dynamodb = boto3.resource('dynamodb')
dynamodb_table = dynamodb.Table(os.environ.get('APP_TABLE_NAME', ''))
@app.route('/users', methods=['POST'])
def create_user():
...
@app.route('/users/{username}', methods=['GET'])
def get_user(username):
...
The name of the DynamoDB table is passed through an environment variable,
APP_TABLE_NAME
. We then create a dynamodb.Table
resource given this
name. This environment variable is generated and mapped in the CDK stack that
Chalice generated for us. This is located in
../infrastructure/stacks/chaliceapp.py
.
Let’s look at the contents of the ../infrastructure/stacks/chaliceapp.py
file now.
import os
from aws_cdk import (
aws_dynamodb as dynamodb,
core as cdk
)
from chalice.cdk import Chalice
RUNTIME_SOURCE_DIR = os.path.join(
os.path.dirname(os.path.dirname(__file__)), os.pardir, 'runtime')
class ChaliceApp(cdk.Stack):
def __init__(self, scope: cdk.Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
self.dynamodb_table = self._create_ddb_table()
self.chalice = Chalice(
self, 'ChaliceApp', source_dir=RUNTIME_SOURCE_DIR,
stage_config={
'environment_variables': {
'APP_TABLE_NAME': self.dynamodb_table.table_name
}
}
)
self.dynamodb_table.grant_read_write_data(
self.chalice.get_role('DefaultRole')
)
def _create_ddb_table(self):
dynamodb_table = dynamodb.Table(
self, 'AppTable',
partition_key=dynamodb.Attribute(
name='PK', type=dynamodb.AttributeType.STRING),
sort_key=dynamodb.Attribute(
name='SK', type=dynamodb.AttributeType.STRING
),
removal_policy=cdk.RemovalPolicy.DESTROY)
cdk.CfnOutput(self, 'AppTableName',
value=dynamodb_table.table_name)
return dynamodb_table
Our CDK stack is using the Chalice construct from the chalice.cdk
package. This provides us two benefits. First, we can generate CDK resources
and pass them into our Chalice application by mapping environment variables.
Second, we can take resources generated in our Chalice application and
reference them with the CDK API. For example, we’re generating a DynamoDB
table in the self._create_ddb_table()
method, and then mapping it into our
Chalice application by providing a stage_config
override. This dictionary
is merged with the existing Chalice configuration located in
./runtime/.chalice/config.json. If we want to pass additional values into our
Chalice application we can update the environment_variables dictionary in our
stage_config.
We’re also able to retrieve references to our resources in our Chalice
application and reference them in our CDK stack. For example, once we’ve
created our DynamoDB table we also need to grant the IAM role associated with
your Lambda function access to this table. We do this by using the
grant_read_write_data
method on our table resource, and we provide a
reference to the default role that Chalice creates for us by using the
self.chalice.get_role()
method.
Next Steps¶
Feel free to experiment with this sample app. Add new resources to your
application by updating the infrastructure/stacks/chaliceapp.py
file, map
CDK resources into your Chalice app through environment variables, and
redeploy your application by running cdk deploy
from the
infrastructure/
directory.