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/)

pip install virtualenv
virtualenv venv
source venv/bin/activate

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:

Your Zappa configuration can support multiple production stages, like 'dev', 'staging', and 'production'.
What do you want to call this environment (default 'dev'):

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

Your Zappa deployments will need to be uploaded to a private S3 bucket.
If you don't have a bucket yet, we'll create one for you too.
What do you want to call your bucket? (default 'zappa-xxxxxxxxxx'):

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

It looks like this is a Flask application.
What's the modular path to your app's function?
This will likely be something like 'your_module.app'.
We discovered: jinjaTemplates.app_template.app, v2.app.app
Where is your app's function? (default 'jinjaTemplates.app_template.app'):

Put in the entrance function.

You can optionally deploy to all available regions in order to provide fast global service.
If you are using Zappa for the first time, you probably don't want to do this!
Would you like to deploy this application globally? (default 'n') [y/n/(p)rimary]: n

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:

{
    "dev": {
        "app_function": "v2.run.app", // entrance function
        "profile_name": null, // boto3 profile
        "project_name": "FleetOpsAPI", // a name
        "runtime": "python3.6",  // Refer to AWS for list. Zappa only supports Python 2.7 and 3.6 for now.
        "s3_bucket": "zappa-xxxxxx",  // code temp bucket
        "memory_size": 256,  // Memory. You will pay for per second memory use so choose wisely!
        "environment_variables": {  // Everything that used to live in export
            "ENV": "dev",
           ..........
        },
        "vpc_config": {
            "SubnetIds": ["subnet-xxxxxxxx"],  // Put down your subnet IDs. We use all 3 zones within the same AZ and I recommend you do the same.
            "SecurityGroupIds": ["sg-xxxxxx"]  // Security group for access of other AWS service.
        },
    }
}

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:

Downloading and installing dependencies..
 - pymongo==3.7.2: Using locally cached manylinux wheel
 - pycrypto==2.6.1: Using precompiled lambda package
 - protobuf==3.6.1: Using locally cached manylinux wheel
 - msgpack==0.6.0: Using locally cached manylinux wheel
 - markupsafe==1.1.0: Using locally cached manylinux wheel
 - greenlet==0.4.15: Using locally cached manylinux wheel
 - gevent==1.3.7: Using locally cached manylinux wheel
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading xxxxxx-dev-1546239781.zip (22.9MiB)..
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 24.0M/24.0M [00:00<00:00, 77.6MB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading xxxxxxx.json (1.6KiB)..
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.63K/1.63K [00:00<00:00, 84.7KB/s]
Deploying API Gateway..
Scheduling..
Unscheduled xxxxxx-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled xxxxx-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev

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 *