Flask from Docker to Lambda with Zappa: the more-or-less complete guide

TLDR

Step-by-step guide of how FleetOps migrate the Docker-based Flask API to AWS Lambda.

History

At FleetOps.ai we use Docker extensively when building APIs. Our API is built on Flask, with microservices supporting async features.

Since we are moving microservices to AWS Lambda … What if the main API could also run on Lambda?

AWS Lambda & Serverless

Serverless is probably the hottest word in the DevOps world in 2018. Does not sound very interesting?

Compared to SaaS(Google App Engine, Heroku, Openshift V2, Sina App Engine, etc.): serverless does not have severe vendor lock-in problem. Most of the time you do not need to edit ANYTHING to migrate to serverless. You CAN choose to write the code in a SaaS way: and if you don’t fancy that a DIY approach is still available. In this case I did not make any change to the original codebase!

Compared to Docker: although Docker is more flexible and you have access to a full Linux OS within the VM, it’s still hard to manage when scaling. Kubernetes is good: but the burden for DevOps is dramatic. At FleetOps we do not want to put so much energy into DevOps: not to say hobby project.

Compared to Web Hosting: serverless supports more languages(Java, Node, etc.) which are not possible to get in the Hosting world.

Problem/limits with AWS Lambda

To name a few:

  • Does not support ALL the languages like Docker, and definitely not ALL the versions of Python. AWS is working on super lightweight OS image so maybe we can see something different?
  • Have to bring your binary/library should you want to use any special software, and they have to be statically linked, while with Docker you can do anything you want. Well, does not sound very bad, but:
  • The size limit of code: if you love 3rd party library it may be very hard to put everything into one zipball. Well technically you can grab them on the fly upon function invoked from S3, BUT:
  • Cold start problem: you have absolutely no control the life cycle of those function. God bless you if your function needs 10s to start.
  • Hard max runtime: 900s is the limit. Maybe you can get it raised but YMMV.
  • Stateless: Like container committing suicide after every invoke.
  • No access to special hardware, like GPU.
  • No debugger: do some print()s instead.
  • Confusing networking: I will try to sort out this issue in this article.

So if your task is:

  • not require any special technology, and uses the most common stack
  • stateless, or is able to recover state from other services(which should be the standard for every API – at least in FleetOps we ensure that every API call shall be stateless)
  • one task does not run forever and does not consume lots of memory
  • not really benefiting from JIT or similar caching
  • not super huge
  • not using fancy hardware
  • having an uneven workload

Then you could benefit from AWS Lambda.

The Guide

1. Get ready

We use Python 3.6 for the API for now.

Get a requirement.txt ready. Not there yet? pip freeze > requirements.txt.

On your dev machine, make a virtual environment: (ref: https://docs.python-guide.org/dev/virtualenvs/)

Install Zappa(https://github.com/Miserlou/Zappa ): pip install zappa

Get your AWS CLI ready: pip install boto3 and refer to steps in https://pypi.org/project/boto3/ . Make sure that account has full access to S3, Lambda, SQS, API Gateway, and the whole network stack.

2. Some observations and calculations:

  • Where is your main function? Make a note of that.
  • How much memory do you need? If you cannot provide a definite number yet, let it here.
  • What is your target VPC & security group? Note their IDs.
  • What 3rd party binary do you need? Compile them with statically linked library – you cannot easily call apt-get on the remote machine!
  • Do you need any environment variables? There are different ways of setting them, and I am using the easiest approach – putting them in the config JSON.

3. Get the Internet right!

Further reading: https://gist.github.com/reggi/dc5f2620b7b4f515e68e46255ac042a7

Quote from @reggi ‘s article:

So it might be really unintuitive at first but lambda functions have three states.
1. No VPC, where it can talk openly to the web, but can’t talk to any of your AWS services.
2. VPC, the default setting where the lambda function can talk to your AWS services but can’t talk to the web.
3. VPC with NAT, The best of both worlds, AWS services and web.

Use 1. if you do not need this function to access any AWS service, or you only need the function to access them via the Internet. Use 2. if you are building a private API. And for FleetOps, we are going down path 3.

Note that not all the AWS services are accessible by VPC: e.g., S3 and RDS are accessible by VPC, while SQS and DynamoDB would require Internet access, even you are calling from within Lambda.

My recommended step is:

  1. Create Internet Gateway.

  1. Create 4 subnets.

  1. Create NAT Gateway.

  1. Create Route table.

Take note of the 3 private-faced subnet ids.

We will use Zappa to configure the networking. Note if you want to deploy the function to multiple AZ, you may need to do the steps multiple times, once at each AZ.

4. Wrap it up

Get back to your virtual env, and active it.

Do a zappa init. You will be asked the following questions:

Use whatever name: and you can carry on the stage’s configuration for further stages.

By default, Zappa will only use this bucket when uploading/updating the function.

Put in the entrance function.

Depends on your use case.

Now you may want to edit the zappa_settings.json: all the arguments are at https://github.com/Miserlou/Zappa#advanced-settings but this is the basic one that get our API running:

There are TONS of settings Zappa provides but I am not using all them: You can use a selective set of feature to make sure you do not have vendor lock-in. For example, Lambda can handle URL routing by itself but I am not using it to avoid any kind of lock-in. By doing so you can easily take the code and put them back on the container if you wish.

Zappa does provide some exciting feature:

  • Setting AWS Environment variables: If you prefer to put the secret key in another place
  • Auto packing huge project: if your project is >50M, Zappa will handle that.
  • Keep warm: Use CloudWatch to make sure there is one function running.

Save zappa_settings.json.

5. PROFIT!

Do a pip install -r requirement.txt to install all the packages.

Now do a zappa deploy.

You would see:

And you now have a serverless API ready to serve!

6. Clean up

You want to do the following tasks to save $$$, boost performance and secure the setup:

  • View some CloudWatch log and set the memory to a reasonable value afterwards.
  • Adjust warmer period.
  • Adjust API Gateway caching.
  • Setup cronjobs if you have them: either with Zappa or with CloudWatch.
  • Change the scope of IAM user for Zappa: the default one is super powerful.
  • Adjust X-Ray if you need it.

Conclusion

Migrating Flask API to serverless could be painless. I did not adjust one single line of code: and there is no vendor lock-in as every step can be reproduced by Dockerfile.

Good luck with your journey with serverless!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.