AWS Chalice + Terraform: A Serverless Codebase That Makes Sense
AWS Chalice is yet another Python serverless framework, like Zappa and the Serverless Framework (what a confusing name).
What makes Chalice special is the fact that it has Terraform Support, meaning that it is able to translate all of its infrastructure to Terraform code, ready to be applied to AWS. This provides all the benefits of the Serverless Framework, like configure your Lambda triggers and set up API Gateway, without fragmenting your infrastructure in CloudFormation and Terraform.
Having just one Infrastructure-as-Code tool in your project provides simplicity, more control over your application, and being able to reference serverless values directly in your Terraform without having to use a middleware data storage, like SSM.
Chalice also handles event subscriptions and HTTP routing elegantly using Python decorators defined in the code itself, instead of unnecessarily verbose YAML files.
How-to
Let’s start by creating a sample Chalice project:
1 | # create a virtualenv |
This creates a simple REST API with a hello world endpoint.
Now the fun part: let’s package and export this simple application to Terraform:
1 | chalice package --pkg-format terraform . |
This does two main things:
- Package your Python code and requirements into a zip file, located at
deployment.zip
- Generate the Terraform code with all the infra required to deploy your app, located at
chalice.tf.json
You will have to run this command every time you change your code, so make sure you add it to your CI/CD pipeline.
Now, let’s test it:
1 | terraform init && terraform plan |
We see a few notable resources here:aws_api_gateway_deployment.rest_api
and aws_api_gateway_rest_api.rest_api
:
API Gateway resources. This is one of the helpful parts of Chalice.
aws_lambda_function.api_handler
:
The lambda function itself, with its code in a zip file.
aws_iam_role.default-role
and aws_iam_role_policy.default-role
:
The default IAM role, with a generated policy that allows accessing the resources created. This is helpful for a quick POC, but in a production environment you might want to customize the IAM policy yourself. More on this later.
Let’s apply this code:
1 | terraform apply |
If we hit the endpoint url with curl
:
1 | curl https://ht0npswgm8.execute-api.us-east-1.amazonaws.com/api |
If you want to read more about setting up a REST API using Chalice, you can follow https://aws.github.io/chalice/tutorials/basicrestapi.html
Add your own Terraform code
This is a minimal example of AWS Chalice, but we can do better. Let’s create an SQS queue and an SNS topic so we can test those triggers as well.
Create a Terraform file with the following code:
1 | resource "aws_sqs_queue" "this" { |
Now open app.py
and add these two functions:
1 |
|
Package the new Chalice code:
1 | chalice package --pkg-format terraform . |
Apply the new terraform code:
1 | terraform apply |
With this, we’ve just created two new lambda functions, with SNS and SQS triggers.
You can read more about Lambda Event Sources supported by Chalice here: https://aws.github.io/chalice/topics/events.html
Now publish messages to your newly created topic and queue:
1 | aws sns publish --topic-arn arn:aws:sns:us-east-1:123456789123:chalice-tf-topic --message "Hello from SNS!" |
Check the CloudWatch logs of your functions using Chalice itself:
1 | chalice logs --name handle_sns_message |
At the time of writing, this doesn’t seem to work.
See https://github.com/aws/chalice/issues/1665
We can use the AWS CLI to get our CloudWatch logs:
1 | LOG_GROUP_NAME="/aws/lambda/chalice-tf-dev-handle_sns_message" |
1 | LOG_GROUP_NAME="/aws/lambda/chalice-tf-dev-handle_sqs_message" |
After we are all done testing, we can clean after ourselves by running:
1 | terraform destroy |
Referencing Terraform values inside Chalice
Chalice can be configured using its own config file, located at .chalice/config.json
. See https://aws.github.io/chalice/topics/configfile.html for more information about the available settings.
At the time of writing this is not properly documented, but it is possible to reference Terraform values on the Chalice config file, like this:
1 | { |
As you can see, we are using Terraform syntax, since these keys as passed as literals to chalice.tf.json
during packaging.
In this example, we are setting a Security group and Subnet to all of our functions. These IDs are retrieved using Terraform, without the need of a middleware data storage, like SSM.
More info here: https://github.com/aws/chalice/issues/1533
You could also set here the iam_role_arn
of a pre-existing IAM role, instead of letting Chalice generate one for you. This is a good approach for production environments.
See https://aws.github.io/chalice/topics/configfile.html#iam-roles-and-policies for a practical example.
Fixing the duplicated provider error
If you try to add your own AWS Terraform provider, you will run into the following error:
1 | terraform init |
If we peek at the auto-generated Terraform code at chalice.tf.json
, looking for the provider key, we see a simple AWS provider definition with just a version constraint:
1 | { |
To fix this, we have two options:
- Add an alias like the error suggests. This is annoying to deal with, since we will have to add an alias to all of our Terraform resources
- Remove the provider defined by Chalice. This gives us flexibility, since we are able to define our provider as we see fit. Here’s a
jq
script that removes the provider:
1 | cat <<< $(jq 'del(.provider.aws)' chalice.tf.json) > chalice.tf.json |
What’s next?
These are the basics you need to know to get started with Chalice and how to integrate it with Terraform. This post is the first part of a series about Chalice and how to run a solid and maintainable app. Check part 2, dedicated to local development and how to speed up the process of adding new features and bug squashing:
AWS Chalice + Terraform Part 2: Local development with LocalStack