Deploy FastAPI Serverless-ly
Tags: python, serverless, redis
Published: 2020-11-14 11:13:51

In this post, we look at how to deploy a FastAPI application on a serverless platform. The platform that we deploy to is [vercel](https://vercel.com). Vercel provides a generous free tier to host our application. The application we build for this post is a GitHub view counter, which counts the number of page visits.

### What is serverless?

Serverless is not server-less. It is a kind of platform where the developer worries less about the deployment. Serverless allows you to spend all the time on the development of your application. When it comes to deployment, push, and the serverless platform takes care of the rest. 

##### Benefits of serverless
- **No server management** - The application runs on the server, but the developer need not worry about the server setup
- **Pay for what you use** - Serverless platform charges for what you use, like CPU, function calls, etc. Some even calculate the price down to the millisecond. 
- **Automatic Scaling** - They scale up and down according to demand
- **Quick deployment** - Push the repository/workspace to deploy. Serverless often deploys in quick time, and this makes it easy to ship updates.  
- **Low latency** - The code can run closer to the user.

##### Downsides
- **Testing is a pain** - It isn't easy to replicate the serverless environment. 
- **Short-running process** - The free tier of vercel only provides 10 second per function call(a request-response cycle). This is convenient for some tasks, this is convenient, but you cannot run any background tasks that run long. 
- **Cold start** - If functions are not called for some time, the serverless code goes to sleep to save memory and CPU usage. When a request is made, the code wakes up(`cold start`). This creates performance issues because of the slow response. If the serverless code is consistently running, there won't be any performance issues.


### GitHub view counter

A GitHub view counter is a simple application that counts the number of times a page is viewed. VC became popular after GitHub introduced profile READMEs. 

The counter counts when a request is sent to an endpoint. It responds by sending an SVG that contains the count. Images/SVG are the only way to fetch external sources into a markdown flavored README file. 

![](https://img.shields.io/badge/You%20are%20reading-deploy%20fastapi%20serverless_ly-green)


Since these assets are fetched over insecure http call, [GitHub proxies all `<img>` calls to a server running on heroku](https://github.blog/2010-11-13-sidejack-prevention-phase-3-ssl-proxied-assets/), now all the requests come from one source, the proxy server. This makes it very difficult to count the unique users, so we count the number of times the resource was fetched.


### Building our application

##### Installing requirements

```bash
pip install fastapi 
```

We don't need a server here, as serverless will take care of it.


##### Getting a custom SVG(badge)

We need to get an SVG to send back to the user. You should see a custom badge form to navigate to [https://shields.io](https://shields.io) and scroll down. Type in the labels as `Profile Views` and some random value for the label `26`. 

![badge_form.PNG](https://cdn.hashnode.com/res/hashnode/image/upload/v1605349725303/_xNPrfTLn.png)


Now click generate. This should generate a badge in a new tab. Now right click and `view page source` to get the HTML template for the badge. Create a lambda function to take in a variable `count` and replace all instances of 26 with `{count}`(use f-strings). 


```python
SVG_STRING = (
    lambda count: f"""
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="104" height="20" role="img" aria-label="Profile Views: {count}">
    <title>Profile Views: 
        {count}
    </title>
    <linearGradient id="s" x2="0" y2="100%">
        <stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/>
    </linearGradient>
    <clipPath id="r">
        <rect width="104" height="20" rx="3" fill="#fff"/>
    </clipPath>
    <g clip-path="url(#r)">
        <rect width="81" height="20" fill="#555"/>
        <rect x="81" width="23" height="20" fill="#97ca00"/>
        <rect width="104" height="20" fill="url(#s)"/>
    </g>
    <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
        <text aria-hidden="true" x="415" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="710">
            Profile Views
        </text>
        <text x="415" y="140" transform="scale(.1)" fill="#fff" textLength="710">
            Profile Views
        </text>
        <text aria-hidden="true" x="915" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">
            {count}
        </text>
        <text x="915" y="140" transform="scale(.1)" fill="#fff" textLength="130">
            {count}
        </text>
    </g>
</svg>
"""
)
```


I used a simple dict for the database. But when the serverless created multiple instances of the functions, it created multiple copies of the dict with different values—this lead to returning different values on each request. Using an external database works best. We'll use `Redis`. Head over to [https://redislabs.com/](https://redislabs.com/) and create a free account. Login and choose `Redis Enterprise Cloud`. Create a new subscription on any cloud service(AWS, GCP, Azure) by choosing the free plan(30MB). Once the subscription is created, copy the endpoint and password and store them in a secure location. 

Install Redis for python.

```bash
pip install redis
```


Create a Redis connection in `app.py`

```python
db = Redis(
    host=getenv("REDIS_ENDPOINT"),
    port=getenv("REDIS_PORT"),
    password=getenv("REDIS_PASSWORD"),
)
```


Now we define the counter endpoint as,

```python
@app.get("/api/")
def user_count(username: str = "amalshaji", title: str = "Profile"):
    title = title.capitalize()
    count = db.get(username)
    if count is not None:
        db.incr(username, amount=1)
        count = count.decode()
        return Response(
            content=SVG_STRING(title, count + 1), media_type="image/svg+xml"
        )
    db.set(username, 1)
    return Response(content=SVG_STRING(title, 1), media_type="image/svg+xml")
```

The route `/api` takes two parameters, `username` and `title`. The username defaults to `amalshaji`, and the title defaults to `Profile`. You can use a random id as username and title as `Project` to track a particular project's view count. 


### Deploying to vercel

Create an account on [vercel](https://vercel.com). 


After creating the account, install vercel-cli and login
```bash
npm i -g vercel
vercel login
```


Now create a `vercel.json` in the project directory to setup vercel configuration.

```json
// vercel.json

{
    "version": 2,
    "builds": [
        {
            "src": "app.py",
            "use": "@now/python"
        }
    ],
    "routes": [
        {
            "src": "(.*)",
            "dest": "app.py"
        }
    ]
}
```

Vercel lets you create a serverless environment using the `vercel dev` command. In my case, it was always failing, even though the code was deployed successfully. 

Before running the application, we need to set up environment variables. Navigate to the project dashboard(in my case https://vercel.com/amalshaji/pvc). Click on the `Settings` tab and choose `Environment Variables`. Set the three environment variables, `REDIS_ENDPOINT`, `REDIS_PORT`, and `REDIS_PASSWORD`.

> Redis port is not provided separately. The endpoint is in the format `URL: PORT`.

Vercel automatically encrypts all environment variables.

![envs.PNG](https://cdn.hashnode.com/res/hashnode/image/upload/v1605349832231/6DivXEyCR.png)

Once this is done, deploy the preview app by running `vercel`. Once the deployment is finished, it'll provide a preview link. 

![preview.PNG](https://cdn.hashnode.com/res/hashnode/image/upload/v1605349851237/G2RmvVBMu.png)

Navigate to the preview link and test the application. Once satisfied, push the preview deployment to production using `vercel --prod`

![prod.PNG](https://cdn.hashnode.com/res/hashnode/image/upload/v1605349867011/XT7mh36Ms.png)

That's it. The profile view counter is successfully running, in my case, at `https://pvc.vercel.app`. 

### Demo

| Code | Response |
|- |- |
| `![](https://pvc.vercel.app/api/?username=amalshaji)` | ![](https://pvc.vercel.app/api/) |
| `![](https://pvc.vercel.app/api/?username=xsdf434&title=project)` | ![](https://pvc.vercel.app/api/?username=xsdf434&title=project) |

> This counter can be faked. There is no point in faking the numbers by spamming. A billion views mean the counter hit 1 billion times.

Instructions for self-hosting will be available on the GitHub repo. You can tweak to fit your use case.

### Links

GitHub: [amalshaji/pvc](https://github.com/amalshaji/pvc)

Read about vercel limits on free tier [here](https://vercel.com/docs/platform/limits)

If you have setup Redis or any other database, you can directly deploy by clicking the button below,

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Famalshaji%2Fpvc%2Ftree%2Fmaster)