BoxBoat Blog

Service updates, customer stories, and tips and tricks for effective DevOps

x ?

Get Hands-On Experience with BoxBoat's Cloud Native Academy

Kubeless FastAPI Runtime

by John Hooks | Tuesday, Sep 28, 2021 | Kubernetes Serverless

featured.png

Recently, one of our customers required a serverless offering for Kubernetes. After looking through multiple solutions, we settled on Kubeless. It gave us a good mix of compliance by allowing us to select only certain runtimes, but also the flexibility to create our own runtimes.

Overview

By default, Kubeless uses Bottle with Python as its web framework. This is great when simplicity is needed. You can just pass in a request and function context getting whatever you need from that request item. Here's a simple example:

def handle(event, context):
    return event["data"]

You can also access the request directly at event["extensions"]["request"]. So if you need a specific header call event["extensions"]["request"].headers.get("X-My-Header"). Along with the body in [event.data](http://event.data) you get these other properties as well:

event:
  data:
    mykey: "my value"
  event-id:
  event-time:
  event-namespace:
  extensions:
    request:
    response:

While this is initially great for simplicity, our client already Settled on FastAPI, and they wanted the same simplicity you can get out of the box with Bottle. FastAPI gives you OpenAPI documentation automatically, integrations with Pydantic for API contract enforcement, and much more. All of these advantages would be non-existent with the default Bottle framework. So we took the time to write a FastAPI based runtime for Kubeless. Now we get documentation and easy API enforcement with Pydantic. The downside to using FastAPI for a runtime is the code is a bit longer. Most of the code is for the documentation that is generated through FastAPI's automated OpenAPI generator.

Developer Flow

FastAPI uses function decorators to define the method for the handler, we need a way to determine what the handler is expecting. So in our runtime, you just begin your function with the method type and an underscore (e.g. get_, post_, put_, delete_). The BaseModels are also based on the method type (e.g. GetResponse, PostResponse, GetRequest, PutRequest, etc). A large reason for having a request/response model per method is because with FastAPI GET requests are not allowed to receive a request body.

We also have a dataclass for our parameters. This is a dataclass and not a BaseModel because of a limitation with FastAPI where BaseModels cannot be used for parameter definitions. To declare your params, just define a dataclass named Params with the names of the params and their types (Query, Header, Cookie, Path).

Kubeless also has a Custom Resource Definition (CRD) for their functions. You can define the function above as a Kubernetes Kind vs using the kubeless cli tool to deploy the function in the cluster. This allows you to easily version the functions in Git and deploy them with a pipeline.

Simple Get Example

from fastapi import HTTPException, Query, Header
from pydantic import BaseModel
from dataclasses import dataclass

FUNCTION_NAME="mytest"
FUNCTION_VERSION="1.0.0"
FUNCTION_SUMMARY="A basic function"
FUNCTION_RESPONSE_DESC="Some get response information"

class GetResponse(BaseModel):
    details: dict

@dataclassclass Params:
    request_id: str = Header(None)
    my_param: str = Query(None)

def get_handler(event, context):
    try:
        data ={
            "request_id": event.headers["request-id"],
            "param": event.query_params["my_param"]
        }
    except Exception:
        raise HTTPException(status_code=400, detail="request id and my_param must be set")

    try:
        res = GetResponse(details=data)
    except Exception:
        raise HTTPException(status_code=500, detail="something went wrong")
    return res

Generated Documentation from Example

documentation

Security

The default image Kubeless uses for their runtimes is a custom, minimal Debian image. After scanning, it had quite a few vulnerabilities. It was also around 500MB in size. This would never fly with our client.

default vulnerabilities

What we ended up doing was using Python's Alpine image. This substantially shrunk our runtime container size to ~73MB and removed every single vulnerability.

alpine vulnerabilities

Compliance

As I mentioned, our client desires strong compliance. Kubeless fits this bill by only using images defined in a configmap. We can limit the existing public runtimes to disable undesired languages and versions of languages. Kubeless uses an init container to inject the application into the runtime image with the handler. This was desired over how other solutions like OpenFaaS work since those serverless runtimes are built into an image and then deployed which makes container image compliance more difficult.

More Information

For more information you can view the project on GitHub here: https://github.com/boxboat/kubeless-fastapi

This includes more examples, and instructions on configuring Kubeless to include this image as a runtime option.