AWS Developer Tools Blog

AWS Chalice adds support for the AWS CDK

In a previous post, we showed how you can use the AWS Cloud Development Kit (AWS CDK) and AWS Chalice to develop both infrastructure and application logic as code. To help combine the two frameworks together, a cdk-chalice construct library was used that handled the low-level communication details. In collaboration with the original developer of the cdk-chalice construct, this functionality has been merged into the Chalice framework and is now a built-in feature as of Chalice version 1.22.0. In this post, we’ll look at how this new integration works.

Overview

For this example, we’ll rebuild the previous sample app using the new CDK integration with Chalice. Before diving into the details let’s review the frameworks we’ll be using.

The AWS CDK is an open source software development framework to define your cloud application resources using familiar programming languages. It lets developers manage their resources in the same way that they manage their application code, enabling you to provision your resources in a safe, repeatable manner through AWS CloudFormation. AWS Chalice is a framework for writing serverless applications in Python, providing familiar decorator-based APIs used in other frameworks such as Flask and FastAPI. It allows you to focus on writing application code instead of the low level details of how to connect all your resources together. By combining the high level constructs of the CDK with the programming APIs of Chalice, you can deploy your serverless applications as a single cohesive stack.

Installation and configuration

First we’ll install the CDK.

$ npm install -g aws-cdk

You should now have a cdk executable you can run.

$ cdk --version
1.85.0 (build 5f44668)

See Getting started with the AWS CDK for more details on installing the CDK.

Next we’ll create a virtual environment and install Chalice. We’re using Python 3.7 in this example.

$ python3 -m venv venv
$ . venv/bin/activate
$ python3 -m pip install chalice
$ chalice --version
chalice 1.22.0, python 3.7.8, darwin 19.6.0

To use the CDK with Chalice, we need to install the CDK packages used by Chalice. Run the following command to do this.

$ python3 -m pip install chalice[cdk]

To create our sample application we’ll use the new-project command. In this latest version of Chalice, 1.22.0, we’ve added several project templates to help you quickly get started. We’ll use the CDK sample template to create our project structure for us. Run the new-project command without any arguments. We’ll provide cdkdemo for our project name and select the [CDK] REST API with a DynamoDB table 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 a DynamoDB table
   REST API
   S3 Event Handler
   Lambda Functions only
   Legacy REST API Template
   [CDK] REST API with a DynamoDB table

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

Our application is deployed with the CDK. First, we’ll cd into the infrastructure directory. If this is our first time deploying a CDK application in our environment (a combination of AWS account and region), we’ll need to bootstrap the CDK. You can do this with the bootstrap command, and you only need to to this once per environment.

$ cd infrastructure
$ cdk bootstrap
Creating deployment package.
 ⏳  Bootstrapping environment aws://12345/us-west-2...
CDKToolkit: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (3/3)



 ✅  Environment aws://12345/us-west-2 bootstrapped.
 

Now we’re ready to deploy our application. Run the cdk deploy command from 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.

Testing

To test our application, we make HTTP requests to the cdkdemo.EndpointURL endpoint. We’ll use the HTTPie package to make 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 runtime/.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,
    aws_iam as iam,
    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, id, **kwargs):
        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 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 our 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

You can continue to experiment by modifying the runtime/app.py and the infrastructure/stacks/chaliceapp.py file. To deploy new changes, rerun the cdk deploy command from the infrastructure directory. For more information you can check out the API documentation for Chalice as well as the Chalice and CDK tutorial.

We encourage you to try out this new feature and let us know what you think!