A Guide to Starting a FastAPI + Poetry + Serverless Project

(My first DEV.to article)

Hello, in this article I will go over setting up a basic FastAPI app using Poetry, and deploying/packaging it using Serverless. I will also go over Serverless Lift, and use it to generate a DynamoDB instance.

If you are already aware of the technologies mentioned above, please skip to the next section.

Section 1 - Introduction to the Technologies.

FastAPI is a Python-based web framework based on ASGI (Starlette) that is used to make APIs, mostly. As the name suggests, it is much faster than Django and Flask, and comes with a few features (as compared to the star-studded Django) such as pydantic typing, and OpenAPI documentation. I have written an article on FastAPI over here.

Poetry is a package manager for Python. For people with background in Javascript, can think of it as a npm manager. Just like package.json (poetry.toml) and package-lock.json (poetry.lock), Poetry maintains dependency tree, virtual environments, and also comes with a CLI.

Using Poetry is not mandatory, I personally am new to it too. Poetry allows us to manage config dependencies and resolve dependency issues which normally occur in old/unmaintained third party libraries that results in conflicted dependencies. Not only that, it allows a better reproduction of the environment and publishing to Pypi.

To install Poetry CLI run curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - for osx, linux; and for windows run (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - in Powershell. More instructions here

Serverless is gaining a lot of popularity as we are transitioning to a microservices architecture. Serverless comes with a CLI and a dashboard that helps you monitor your serverless functions on a variety of different cloud providers. It provides a higher level layer to monitor and deploy these functions easily.

I have been using Serverless for a bunch of different side projects and it comes with various plugins to make deployment easier for cloud infrastructures.

To install Serverless CLI, download using curl -o- -L https://slss.io/install | bash or npm install -g serverless

Section 2 - Starting a FastAPI project with Poetry

After having installed Poetry, let us initialize a poetry project.

poetry new my-project # change project name to whatever you want

This creates a python package with a README, tests directory, and a couple of poetry files. Next we install fastapi using

poetry add fastapi uvicorn[standard]

These two are required in order to start a FastAPI project and run the ASGI Starlette server.

In my-project/my-project we create our app.py, a pretty basic one for the sake of this project. We write two endpoints for now as follows-

from fastapi import FastAPI
import os

stage = os.environ.get('STAGE', 'dev')


app = FastAPI()

@app.get("/")
def index():
    return {"Hello": "World"}


@app.get("/users/{user_id}")
def read_item(user_id: int):
    return {"user_id": user_id}

To run this project, normally we would run using

uvicorn my-project.app:app

Step 3 - Serverless Packaging and Deployment

Now we make a serverless.yml in the root folder my-project

service: My-Project
package:
  individually: true
provider:
  name: aws
  profile: ${opt:aws-profile, "default"}
  region: "us-west-2"
  stage: ${opt:stage, "dev"}
  runtime: python3.8

plugins:
  - serverless-offline
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: true
    usePoetry: true

functions:
  app:
    handler: my-project.app.handler #I will explain why we have handler here
    environment:
      STAGE: ${self:provider.stage}
    events:
      - http:
          method: get
          path: /
      - http:
          method: any
          path: /{proxy+}

This is pretty easy to understand, we use the standard python packaging plugin for serverless called serverless-python-requirements. It packages the python project that contains a requirements.txt or a poetry/pipenv package. We point our handler to the app.py.

Right now the app file does not export any handler, so you might be wondering why did I not use app.app instead. This is because we have to wrap our ASGI app to adapt AWS Lambda and API Gateway. For that we use Mangum.

First we install mangum using

poetry add mangum

Next we wrap our app into mangum using the following addition to app.py

from mangum import Mangum


handler = Mangum(app)

This makes our ASGI app (FastAPI) support API Gateway for HTTP, REST, and WebSockets.

Before proceeding to next step we just have to initialize our npm project and install serverless-python-requirements to it, since serverless is a node package.

In the project root run

npm init --yes
npm install serverless-python-requiremnts

Step 4 - Using Serverless Lift [🎁BONUS🎁]

serverless plugin install -n serverless-lift

and add it to our serverless.yml which should look like

plugins:
  - serverless-offline
  - serverless-python-requirements
  - serverless-lift

Next, we are gonna construct a DynamoDB table (single table) and use it to access user information in our database.

With the following code in serverless.yml we will have the following handled automatically:
⭐ Deploy, if not already exists, a DynamoDB table with generic PK and SK, and up to 20 configurable secondary composite indices called GSIs with generic PKs and SKs for each of them.
⭐ Stream setup with OLD_IMAGES and NEW_IMAGES
⭐ Other small setup configs such as cost set to pay per req and TTL enabled.
⭐ Automatically assigned necessary permissions to your lambda functions in this yaml file.
⭐ Variable name injection for table name and stream name

constructs:
    myTable:
        type: database/dynamodb-single-table

functions:
  app:
    handler: my-project.app.handler
    environment:
      STAGE: ${self:provider.stage}
      TABLE_NAME: ${construct:myTable.tableName}
    events:
      - http:
          method: get
          path: /
      - http:
          method: any
          path: /{proxy+}

Now in our app.py we can access this table name and don't have to worry about assigning the lambda an IAM role.

For the users endpoint we can do the following (make sure you populate your DB first obviously, but this is just for the sake of an example):

@app.get("/users/user_id")
def read_item(user_id: int):
    table_name = os.environ.get('TABLE_NAME', '')
    table = boto3.resource("dynamodb", region_name='us-west-2').Table(table_name)
    response = table.get_item(
        Key={
            'PK': user_id
        }
    )

    return {"user_obj": response['Item']}

To make this run just install boto3

poetry add boto3

Et voila!

Step 5 - Testing and Deploying

To test this locally we run

serverless offline --stage dev --noPrependStageInUrl

If you dont include the --noPrependStageInUrl flag, it will run your server at localhost:3000/dev/{proxy}+. If you to run it like that, make sure you include root_path='dev' parameter in app=FastAPI() to see the docs

We see that it runs locally, and also shows us the docs. To deploy this we use

serverless deploy --stage dev #or prod

And serverless will deploy it in our AWS profile, as long as you have the initial serverless config setup.

Known issue of serverless-python-requirements is that it will throw a Poetry not found error when you try to deploy or package the sls project. To fix that please go node_modules/serverless-python-requirements/lib/poetry.js and replace the res at line 17 with

const res = spawnSync(
    'poetry',
    [
      'export',
      '--without-hashes',
      '-f',
      'requirements.txt',
      '-o',
      'requirements.txt',
      '--with-credentials',
    ],
    {
      cwd: this.servicePath,
      shell: true // <- we added this 
    }
  );

This will prevent that issue. Kudos to this issue creator

"Error: poetry not found! Install it according to the poetry docs." on Windows 10 #609

Hi,

I'm on Windows10. Serverless environment:

Your Environment Information --------------------------- Operating System: win32 Node Version: 14.15.4 Framework Version: 2.42.0 Plugin Version: 5.1.2 SDK Version: 4.2.2 Components Version: 3.10.0

Version of plugin:

5.1.1

I'm using poetry and according to the documentation, this should work fine. From the doc:

If you include a pyproject.toml and have poetry installed instead of a requirements.txt this will use poetry export --without-hashes -f requirements.txt -o requirements.txt --with-credentials to generate them.

But I ran into this error when I tried to deploy:

PS > serverless deploy Serverless: Generating requirements.txt from pyproject.toml...

Error ---------------------------------------------------

Error: poetry not found! Install it according to the poetry docs. at ServerlessPythonRequirements.pyprojectTomlToRequirements (C:<path replaced>\node_modules\serverless-python-requirements\lib\poetry.js:34:13)

After some research I found this comment: https://stackoverflow.com/a/54515183/5759828 suggesting to use {shell:true}. As part of my testing I found that the output of spawnSync (line 17 in poetry.js) is:

error: Error: spawnSync poetry ENOENT

I then added shell:true to poetry.js like this:

const res = spawnSync(
    'poetry',
    [
      'export',
      '--without-hashes',
      '-f',
      'requirements.txt',
      '-o',
      'requirements.txt',
      '--with-credentials',
    ],
    {
      cwd: this.servicePath,
      shell: true  <--- added this
    }
  );

and now it works fine.

🎉🎉🎉🎉
If you read it till here, thank you for reading the article. Make sure you share and like this article. If you think there is a fault or if I missed something, please reach out.
The code for this is hosted on Github in case anyone is interested.

GitHub logo NimishVerma / ServerlessFastapiPoetry

Could not have thought of a better name, lol

References

27