FaaS on Kubernetes: From AWS Lambda & API Gateway To Knative & Kong API Gateway
Tags: docker, serverless, kubernetes, javascript
Published: 2022-02-13 04:09:07

---
title: FaaS on Kubernetes: From AWS Lambda & API Gateway To Knative & Kong API Gateway
published: true
description: Learn how to build REST APIs powered by serverless functions, Knative, Kong, and kazi
tags: docker, serverless, kubernetes, javascript
canonical_url: https://pmbanugo.me/faas-on-kubernetes-from-aws-lambda-api-gateway-to-knative-kong-api-gateway
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1umkharurky7ky4p4wie.png
---

Serverless functions are modular pieces of code that respond to a variety of events. It's a fast and efficient way to run single-purpose services/functions. Although you can run "[fat](https://mobile.twitter.com/brianleroux/status/1471265073008054273)-[functions](https://twitter.com/jeremy_daly/status/1192090251046670337)" within them, I prefer the single-responsibility functions, that can be grouped in one endpoint using an API Gateway. Developers benefit from this paradigm by focusing on code and shipping a set of functions that are triggered in response to certain events. No server management is required and you can benefit from automated scaling, elastic load balancing, and the “pay-as-you-go” computing model.

> Deploying a single function usually completes in less than a minute. This is a huge improvement in development and delivery speed, compared to most other workflows/patterns.

Kubernetes, on the other hand, provides a set of primitives to run resilient distributed applications using modern container technology. Using Kubernetes requires some infrastructure management overhead and it may seem like a conflict putting serverless and Kubernetes in the same box.

Hear me out. I come at this with a different perspective that may not be evident at the moment.

Serverless is based on the following tenets:

- no server management
- pay-for-use services
- automatic scaling
- built-in fault tolerance

You get auto-scaling and fault tolerance in Kubernetes, and using [Knative](knative.dev) makes this even simpler. While you take on some level of infrastructure management, you are not tied to any particular vendor's serverless runtime, nor constrained to the size of the application artefact.

## Serverless Function With Knative

There's so much to be said about Knative than I can cover in two sentences. Go to [knative.dev](knative.dev) to learn more. This post aims to show you how you can run serverless functions with Knative. The aim is to show people who are familiar with AWS Lambda and API Gateway, how to build and deploy functions, then expose them via a single API.

This will be based on a Knative installation with Kong Ingress as the networking layer. Go to this [URL](https://docs.konghq.com/kubernetes-ingress-controller/2.1.x/guides/using-kong-with-knative/) for steps on how to install and use Kong with Knative.

## Pre-requisite

I'm going to walk you through building a simple URL shortening service in Node.js. You will need Knative and Kong set up on your Kubernetes cluster, and the following tools if you want to code along.

1. Pack [CLI](https://buildpacks.io/docs/tools/pack/)
2. Docker or a similar tool, e.g Podman
3. Node.js (version 16.10 or above) and npm

## Project Set-Up

We're going to create a monorepo with two functions, one to generate a shortened URL and another to process a shortened URL and redirect the user. Using a monorepo makes it easy to manage a group of functions that you want to expose via a single API endpoint.

The diagram above depicts how the request would flow from the user to the Kong Ingress controller. The Ingress controller will route the traffic to the right service based on the HTTP method.

![Request Flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1umkharurky7ky4p4wie.png)

We're going to use [Nx](nx.dev) to manage the monorepos. Run the command `npm install -g nx` to install nx CLI globally. Now create the monorepo workspace by running the command below:

```bash
npx create-nx-workspace@latest tinyurl --preset=core --nx-cloud=false --packageManager=npm
```

A workspace named **tinyurl** is created with the following file structure:

```
packages/
nx.json
workspace.json
tsconfig.base.json
package.json
```

We're going to make a few changes to the files. First, delete the **workspace.json** file and **packages/** directory. Open **package.json** and update the `workspaces` key to the value below:

```json
  "workspaces": [
    "functions/**"
  ]
```

These changes causes Nx to treat the workspace as a regular npm workspace, and you can invoke the scripts in each project's package.json using Nx.

## The generate-tinyurl function

We're going to use [kazi](https://github.com/kazi-faas/kazi) to generate and deploy the functions. Kazi is a CLI that helps you to build and deploy serverless functions to Knative. You can create functions and deploy them using the CLI. It's still a pretty new tool with a few handy commands to create, deploy and retrieve functions deployed on Knative.

The function runtime is based on a fast, and lightweight HTTP library called [micro](https://github.com/vercel/micro). To use kazi, you first have to install it via npm. Run the command `npm i -g @kazi-faas/cli` to install it.

The first function we're going to create will be triggered by a POST request. It'll get the URL to shorten from the request body, generate a unique code for it, save the data to a DB, then return the shortened URL in the response.

Open your terminal and browse to your workspace directory. Then run the command `kazi create functions/generate-tinyurl --registry=YOUR_REGISTRY_NAMESPACE --workspace-install` to scaffold the project. Replace `YOUR_REGISTRY_NAMESPACE` with your container registry endpoint. For example, docker.io/jenny. This command will create a new Node project with the following file structure:

```
config.json
index.js
package.json
README.md
```

The **config.json** stores the configuration for building source code and deploying it to Knative. At the moment it has just two values, `name` and `registry`. _name_ is used by kazi as the image and Knative Service name. The _registry_ value is the container registry to publish the image.

The **index.js** file contains the function to handle incoming request. Open index.js and add the following require statements:

```javascript
const { json, send } = require("micro");
const { isWebUri } = require("valid-url");
const { nanoid } = require("nanoid");

const { db, q } = require("./db");
```

The `db` module is used to interact with a Fauna database. We will get to that in a moment. For now, open your terminal and navigate to your workspace directory. Install the required packages using the command below.

```bash
npm i valid-url nanoid faunadb -w generate-tinyurl
```

Go back to **index.js** and update the function with the code below.

```javascript
module.exports = async (req, res) => {
  const { url } = await json(req);

  if (!isWebUri(url)) {
    send(res, 401, "Invalid URL");
  } else {
    const code = nanoid(10);

    await db.query(
      q.Create(q.Collection("tinyurls"), {
        data: { url, code },
      })
    );

    return { shortUrl: `${process.env.BASE_URL}/${code}`, originalUrl: url };
  }
};
```

The code above simply retrieves the URL from the request body, save the data to the database, and return a response to the user.

The `json()` function is used to parse the request body and retrieve the `url`. Afterwards, the code checks if the URL is valid and returns 401 if it's invalid. If the URL is valid, a unique string is generated. This string is used as the identifier for the URL.

The `code` and `url` is saved to the database, and a response containing the shortened URL is returned as a response.

### Connect To The Database

Next, add a new file **/generate-tinyurl/db.js** and paste the code below in it.

```javascript
const faunadb = require("faunadb");
exports.q = faunadb.query;

exports.db = new faunadb.Client({
  secret: process.env.FAUNADB_SECRET,
  domain: process.env.FAUNADB_ENDPOINT,
  port: 443,
  scheme: "https",
});
```

This code connects to FaunaDB using the `faunadb` JS client. The _secret_ and _domain_ values are retrieved from environment variables. You can use an existing database or follow these steps to create a new Fauna database

1. Go to your Fauna [dashboard](https://dashboard.fauna.com/) and create a new database.
2. Create a Collection named **tinyurls**.
3. Click **SECURITY** in the left-side navigation menu and create a new key for your database. _Be sure to save the key’s secret in a safe place, as it is only displayed once._
4. Go to the tinyurls collection and create an index named `urls_by_code` with the terms set to `code`. This will allow you to query the DB using an index that checks the `code` property in the document.

### Add Environment Variables

Create a new **.env** file in the **generate-tinyurl** directory. Here you will add the necessary environment variables. The values in this file are automatically loaded when you're running locally (see the `dev` script in package.json), and are saved in your cluster (using ConfigMap objects) when you deploy.

Add the following key-value pair to the .env file.

```
FAUNADB_SECRET=YOUR_SECRET_KEY
FAUNADB_ENDPOINT=db.fauna.com
BASE_URL=YOUR_API_DOMAIN
```

Replace `YOUR_SECRET_KEY` with the secret generated from the previous section. The `FAUNADB_ENDPOINT` endpoint should be changed to reflect the region where the database was created. That is, `db.us.fauna.com` for the US region or `db.eu.fauna.com` for the EU region.

The `BASE_URL` is the domain from which the service is accessible. This is the domain you'll use when you configure an Ingress resource for your service. You can fill it out if you already have an idea, or update it after you've created the Ingress. For example, I'm using a local Kubernetes cluster and have set mine to `BASE_URL=tinyurl.localhost`.

## The resolve-tinyurl function

It's time to create the second function that will resolve the shortened URL and direct the user to the original URL. Create the project using the command `kazi create functions/resolve-tinyurl --registry=docker.io/pmbanugo --workspace-install`. Replace `YOUR_REGISTRY_NAMESPACE` with your container registry endpoint.

Install the `faunadb` package using the command `npm i faunadb -w resolve-tinyurl`.

Copy the **db.js** and **.env** files from the other project to this one. You could have the db.js module in a separate project, that both function projects can use. But for the sake of this post, I'll duplicate the code.

Open **functions/resolve-tinyurl/index.js** and update it with the code below.

```javascript
const { send } = require("micro");
const { db, q } = require("./db");

module.exports = async (req, res) => {
  const code = req.url.substring(1);

  try {
    const {
      data: { url },
    } = await db.query(q.Get(q.Match(q.Index("urls_by_code"), code)));

    res.setHeader("Location", url);
    send(res, 301);
  } catch {
    send(res, 404, "No URL Found");
  }
};
```

The code above extracts the unique code from the URL and uses that to query the database. If there's no result, we return a 404 status. Otherwise, the _Location_ header is set and a 301 redirect status is returned.

## Deploy The Functions

Now that the functions are ready, the next thing to do is deploy them. There's a deploy script in each function's package.json, which executes the `kazi deploy` command. Before you run this script, you will update the **nx.json** file so that the result of this script is cached by Nx. That way, running the _deploy_ script multiple times without any file changes will be faster.

Go to the workspace root directory and open the **nx.json** file. Add `deploy` to the `cacheableOperations` array values.

```
"cacheableOperations": ["build", "lint", "test", "e2e", "deploy"]
```

Next, open the root package.json and add the script below:

```json
  "scripts": {
    "deploy": "nx run-many --target=deploy --all"
  },
```

This command will execute the _deploy_ command for every project. Now run `npm run deploy` in the workspace root to execute this script. This will execute both scripts in parallel just like you can see in the screenshot below.

![Nx deploy in parallel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sh95tajajxie4jg2jwy9.png)

When it is done, you should get a `Successfully ran target deploy for 2 projects` in the terminal. You can very that it was deployed by running the command `kazi list`. It will return a list of functions deployed using the CLI.

![kazi list command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qpat8m0q6xt0obo1gm0l.png)

In the screenshot above, you can see that the **generate-tinyurl** function is available at **http://generate-tinyurl.default.localhost**, and the **resolve-tinyurl** function at **http://resolve-tinyurl.default.localhost** (I'm running a local Kubernetes cluster ;) ).

> It's possible to create these functions as services that are accessible only within the cluster, and then expose them via an API Gateway.

## One Endpoint To Rule Them All

While you can access these functions with their respective URL, the goal here is to have a single endpoint where a specific path or HTTP method will trigger a function. To achieve this, we're going to create an Ingress resource that will route GET requests to `resolve-tinyurl` and POST requests to `generate-tinyurl`.

First, create a new file **kong-plugin.yaml** and paste the YAML below in it.

```yaml
# Create a Kong request transformer plugin to rewrite the original host header
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: generate-tinyurl-host-rewrite
  # The plugin must be created in the same namespace as the ingress.
  namespace: kong
plugin: request-transformer
config:
  add:
    headers:
      - "Host: generate-tinyurl.default.svc.cluster.local"
  replace:
    headers:
      - "Host: generate-tinyurl.default.svc.cluster.local"
---
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: resolve-tinyurl-host-rewrite
  # The plugin must be created in the same namespace as the ingress.
  namespace: kong
plugin: request-transformer
config:
  add:
    headers:
      - "Host: resolve-tinyurl.default.svc.cluster.local"
  replace:
    headers:
      - "Host: resolve-tinyurl.default.svc.cluster.local"
```

The YAML above defines two Kong plugins that will rewrite the Host header for incoming requests. This is how the kong proxy knows which Knative service to proxy to.

Finally, create a new file **ingress.yaml** and paste the YAML below in it.

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tinyurl-get
  # The ingress must be created in the same namespace as the kong-proxy service.
  namespace: kong
  annotations:
    kubernetes.io/ingress.class: kong
    konghq.com/methods: GET
    konghq.com/plugins: resolve-tinyurl-host-rewrite
spec:
  rules:
    - host: tinyurl.localhost
      http:
        paths:
          - pathType: ImplementationSpecific
            backend:
              service:
                name: kong-proxy
                port:
                  number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tinyurl-post
  namespace: kong
  annotations:
    kubernetes.io/ingress.class: kong
    konghq.com/methods: POST
    konghq.com/plugins: generate-tinyurl-host-rewrite
spec:
  rules:
    - host: tinyurl.localhost
      http:
        paths:
          - pathType: ImplementationSpecific
            backend:
              service:
                name: kong-proxy
                port:
                  number: 80
```

Here you defined two ingresses pointing to the same host, but using different plugins and methods. Replace `tinyurl.localhost` with `tinyurl` plus your Knative domain (e.g tinyurl.dummy.com).

Now open the terminal and run `kubectl apply -f kong-plugin.yaml -f ingress.yaml` to apply these resources.

Now reach for your HTTP client and send a POST request. For example, the following command will send a POST request to the service at tinyurl.localhost:

```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"url": "https://pmbanugo.me"}' \
http://tinyurl.localhost
```

The response will be something similar to the following.

```json
{
  "shortUrl": "tinyurl.localhost/ppqFoY0rh6",
  "originalUrl": "https://pmbanugo.me"
}
```

Open the shortUrl in the browser and you should be redirected to https://pmbanugo.me.

Now you have a REST API where specific calls get routed to different functions that are scaled independently! How awesome can that be 🔥.

## What Next?

In this post, I showed you how to build and deploy a REST API powered by serverless functions running on Kubernetes. Most of this was made possible using Knative, Kong API Gateway, and kazi CLI. You maintained the monorepo using Nx, which is quite a handy tool for developing with monorepo. I briefly talked about some of these tools but you can read more about them using the following links:

- Knative [docs](knative.dev)
- [Kong](https://docs.konghq.com/kubernetes-ingress-controller/)
- Nx [docs](nx.dev)
- [kazi](https://github.com/kazi-faas/kazi)

kazi features are still minimal at the moment but there will be more features added in the near future, with more in-depth documentation. I will be sharing more of it here as new features get added. You can follow me on [Twitter](https://twitter.com/p_mbanugo) or subscribe to my [newsletter](https://www.getrevue.co/profile/pmbanugo) if you don't want to miss these updates 😉.

You can find the complete source code for this example on [GitHub](https://github.com/kazi-faas/examples/tree/main/tinyurl)