Serving Python on AWS Lambda with Zappa
This post introduces the Zappa project – a tool for deploying Python services to run on AWS Lambda, without needing a server running full-time. If you write Python applications and have access to an AWS environment, Zappa offers a distinctive deployment model that can be both cost-effective and scalable. It’s not right for every project, but when it fits, it can be an elegant solution worth knowing about.
What is Zappa?
Zappa is a fascinating library for running Python code, including web apps, on AWS Lambda.
So, instead of a web server running 24/7, your services get spun up on demand in response to HTTP requests as they come in, and then destroyed (or at least, allowed to sleep) again.
This can be a good fit in a couple of situations:
-
Low-traffic services. Because you pay only for the time when your service is actively executing code, sites with low traffic can be very inexpensive (say, less that $1/month and potentially free) to run in this way.
-
High-traffic services. Responding to web requests using serverless Lambda functions effectively gives you infinite parallel web servers.
Zappa does some really clever things behind the scenes to package up your Python code so that it runs smoothly on AWS Lambda (and it does need to be AWS – no other cloud services are supported so far). It builds a zip file containing all the dependencies needed by your Python code, automatically fetching Lambda-compatible wheels.
For deployment, Zappa builds the package Zip, uploads it to AWS S3 blob storage, loads it into a Lambda function, and deletes the S3 blob again.
Okay, but this only works for simple things, right?
Well, no. Zappa supports WSGI web servers, including full-blown Django applications.
For the project I decided to try Zappa with, I chose to use another library I was interested in learning about: nanodjango. This provides a simple, lightweight way to start a Django project using a single file, with the ability to convert it to a full-blown Django project automatically later.
Setting up Zappa in practice
This isn’t intended to be a full how-to. But I did want to provide some specific notes from my implementation about several specific areas: package zip sizes, AWS permissions, and keeping the service warm.
Package zip sizes
You can run zappa package [env]
to create the package’s zip file locally.
Then you can look inside it to see what’s included. Then you can specify
exclude
and exclude_glob
entries in your zappa_settings.json
file to
trim the size down (and to ensure no sensitive information is included).
I used these settings:
"exclude": [
".gitignore",
".pytest_cache",
"README.md",
"__pycache__",
"_pytest",
"black",
"justfile",
"mypy_extensions.py",
"poetry",
"poetry_plugin_export",
"poetry.lock",
"pyproject.toml",
"pytest",
"zappa_settings.json",
"zappa_settings.py"
],
"exclude_glob": [
"*.dist-info",
"big-day-diaries-prod-template*.json",
"django/test/*",
"docs/*",
"test_*.py"
]
which took my package zip file size down from about 65 MiB to about 53 MiB
– a reasonable reduction. I expect there’s more that could be pruned,
particularly unused things inside the django/
directory.
Note that this is, shall we say, chunky for a Lambda function. Zappa issues a warning that going above 50 MiB probably won’t work, but it does (it runs fine, but AWS won’t allow you to edit it interactively).
Minimal AWS permissions set
In general, spinning up a working nanodjango site on AWS Lambda using Zappa was straightforward. But by far the most annoying part of the process was figuring out the minimal set of AWS permissions that were necessary to enable the deployment steps to work cleanly.
It’s good practice not to over-provision the permissions: just use the minimal subset that will work. Unfortunately, there’s no guidance in Zappa’s documentation about what’s required, so I fell back into trial and error:
- try to deploy
- get an error message
- add the missing permission in AWS
- tear down and try again.
I applied the following two AWS-managed policies:
-
AWSLambda_FullAccess
-
AWSCodeDeployRoleForLambda
In addition, I created a custom policy to allow access to the specific resources used in this deployment: the relevant Lambda execution role, S3 bucket, API Gateway routes, and CloudFormation stack. It also included limited permissions for IAM role creation and for interacting with associated services like CloudFront, Route 53, and CloudWatch Events.
I’m not sharing the full policy document here, partly because it’s specific to my setup and partly because I don’t want to mislead anyone into copying a configuration that might not be appropriate for their environment.
So, be prepared for some annoyance with this part of the process.
Keeping warm
AWS Lambda caches the Lambda functions for a few minutes so that they are available straight away when called multiple times in succession. After that, they are unloaded, and reloading them can take a significant amount of time (say 30 seconds for these large zip packages). For some workloads, that scale of delay would be fine, but for responding to Web requests it is too long.
Zappa includes a keep_warm
setting, which will ping the deployed service
every few minutes to keep it warm and in the cache. This way, the service is
always available and can respond immediately to incoming requests. These ping
calls are quick and cheap, so their cost is negligible.
Example service
To test all this out, I created a simple service for generating a custom PDF countdown calendar for a big day: a birthday, anniversary, wedding, etc.
After a few rounds of redesign, the single-page user interface looks like this:
Django is overkill for this: it’s essentially just a form for entering the title and date, validation of the form, generation of a PDF (I used ReportLab for this) and serving up the PDF. There’s no database to speak of in this case, and I was also able to directly embed the small images, which kept things nice and simple.
Here is an example extract of the PDFs that this service can generate:
Notice that it marks public holidays and draws quite a pleasing red swirl around the big day itself. The actual generated PDF is for the full year leading up to the big day.
Conclusion
Frameworks
- Remember Zappa in case you need a way to
run Python on AWS Lambda.
- Also take a look at Chalice, which is another library for deploying Python to AWS Lambda. I haven’t tried it myself. It was written by Amazon themselves.
- Another notable framework is Serverless. Again, I haven’t tried this.
- Consider using nanodjango for simple Django sites that might need to grow over time.
Example project
- Try out the free Big Day Countdown Calendar if you have your own big day coming up.