There is an accompanying GitHub registry with a working demo
Serverless functions are a great way of building a scalable application. They're small, independent, fast-to-build and infinitely scalable. If you need to build an application with just a couple of endpoints, they will work great.
When it comes to building a big application or one that you want to expose as a public service, serverless functions can end up being just a bit cumbersome to develop. As most serverless frameworks focus on single functions, there can be a lot of repetition in getting a CRUD application set up.
What is a RESTful application
For more information, check out the Wikipedia article.
REST (Representational state transfer) is a way of representing data between machines. It has a several key features:
- no session state is maintained between calls
- the HTTP verb decides how the request is handled
- it returns the data model
What tech are we using
Whilst there's plenty of open-source tech out there that can help, this is my favoured way of achieving the end goal:
- OpenFaaS for the serverless functions
- Kong for the API gateway
- Kubernetes and Helm for deployment
- K3d and Skaffold for local development
- A custom NodeJS OpenFaaS template that uses MongoDB and Mongoose to manage the data models
Getting started
As I like to keep things simple, all you need to do to get the development cluster
up and running is to run make
. This will run a series of commands to check you
have the correct dependencies, create your k3d cluster and provision your cluster.
Once you've done that, you can access your cluster on localhost:9999.
Once you've got the cluster up and running, you can just use make serve
to reload
the Kubernetes objects.
Your first function
In the example repo, look at the
product
function as the first function
To create your first function, run the command FN_NAME=product make new
. This
will create a new OpenFaaS function in the /components
directory.
The important file is schema.js
which is a standard Mongoose schema.
In this example, we're just defining a modelName
of Product
- in a more complex
example, we can add in both synchronous and asynchronous validation, but a simple
name property will do for now.
module.exports = ({ model }, BaseSchema) => {
const modelName = 'Product';
const ProductSchema = new BaseSchema({
name: {
type: String,
required: true,
},
});
return model(modelName, ProductSchema);
};
Next, add this to the functions
Helm chart in /chart/functions/values.yaml
.
functions:
product:
image:
repository: ghcr.io/mrsimonemms/openfaas-rest-api/product
tag: latest
envvars:
LOGGER_LEVEL: info
MONGODB_URL: "mongodb://openfaas-mongodb.openfaas.svc.cluster.local:27017/openfaas"
Finally, add the image
to the artifactOverrides
section in the skaffold.yaml
file.
artifactOverrides:
functions:
product:
image: ghcr.io/mrsimonemms/openfaas-rest-api/product
If you visit the OpenFaaS dashboard, you will see the
product
function has been deployed to OpeenFaaS.
To get the login credentials, run
make openfaas
Making it into an endpoint
Now we have an OpenFaaS function running, we need to configure Kong so that it
acts as a gateway. OpenFaaS functions are exposed on /function/:name
, which is
what we're going to redirect to - in this template, the CRUD
endpoints live under /crud
.
First, in /charts/openfaas/values.yaml
, add a product-gateway
service:
services:
- name: product-gateway
annotations:
konghq.com/path: /function/product/crud
Finally, in the /charts/openfaas/template/ingress.yaml
, tell the Kong ingress
about the product-gateway
service:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: openfaas
annotations:
konghq.com/strip-path: "true"
spec:
ingressClassName: kong
rules:
- http:
paths:
- path: /api/v1/product
pathType: Prefix
backend:
service:
name: product-gateway
port:
name: http
Now you'll find this exposed on localhost:9999/api/v1/product.
Your second function and nested endpoints
In the example, this is the
product-size
function
So far, we've done a fairly simple example only. Next, we're going to up the complexity by adding a nested endpoint. A root endpoint is fairly straightforward because it just sends everything to the function and returns whatever that returns.
Conversely, a nested endpoint is locked to a product ID. Fortunately, Kong makes this fairly straightforward for us with its plugin system.
Firstly, repeat all the above steps to create a product-size
function. In the
schema.js
, add a productId
parameter - this will store the _id
from the
product
function.
This time, when creating the /charts/openfaas/template/ingress.yaml
, we're going
to add a named path parameter in the path
.
- path: /api/v1/product/(?<productId>[\w-]+)/size
pathType: Prefix
backend:
service:
name: product-size-gateway
port:
name: http
This searches for any alphanumeric character between /product/
and /size
in
the URL and assigns it the name productId
.
Next, we have to tell it what to do with it. In the /charts/openfaas/values.yaml
,
add this to the services
array:
- name: product-size-gateway
annotations:
konghq.com/path: /function/productsize/crud
konghq.com/plugins: product-request-transformer
Notice how this refers to a product-request-transformer
plugin. So, let's define
it in the same values.yaml
file.
plugins:
- name: product-request-transformer
plugin: request-transformer
config:
remove:
body:
- productId
add:
body:
- productId:$(uri_captures["productId"])
append:
querystring:
- filter:productId||$eq||$(uri_captures["productId"])
This does a few things. Firstly, it removes any productId
from the body and then
adds in the product ID in the URL. It also appends a query string filter=productId||$eq||<productId>
to the URL. This ensures that the product-size
function behaves like a nested
API endpoint.
Importantly, this won't return an HTTP 404 result if the product
doesn't exist.
That's beyond the scope of this demo, although you could achieve this by adding a
middleware.js
file to the function. This can be either a function or an array
of functions and it follows the same basic interface as an Express middleware function.
Conclusion
With this simple demo, you can see that it's very possible to create a fully RESTful API from a collection of serverless functions. It's important to remember that all these functions are entirely isolated from each other from a coding point of view (so you could even do something interesting like writing them in different languages). This makes them very powerful and almost infinitely scalable.
Credits
Photo by Volodymyr Hryshchenko