Recently I have been spending some time configuring and learning about Fission, which is a framework for running serverless functions on Kubernetes. Since it seemed like most of the blog posts I could find that demonstrated something beyond just “Hello, World!” were mostly written in Python, I thought I would write a small Typescript based application with a MongoDB backend and demonstrate how I deployed it.
Prerequisites
-
A Kubernetes cluster with Fission deployed as well as ingress setup
-
Knowledge of how to deploy an application via Helm chart
-
Some familiarity with the overall architecture of Fission
Setup MongoDB in Kubernetes
Part of my goal here was to deply an application that interacts with a MongoDB instance in Kubernetes since I had not found any examples of that in the Fission blogs (which their blogs are great, read through them if you would like some cool other project ideas for Fission).
I deployed Mongo using the Bitnami MongoDB Helm chart. I deployed both of these into the same namespace as well by using a small wrapper chart that depends on both fission-all
and mongodb
. An example of my Chart.yaml
file is below:
# Chart.yaml apiVersion: v2
name: fission
description: fission Wrapper Chart
type: application
version: 0.1.0
dependencies:
- name: fission-all
version: 1.18.0
repository: https://fission.github.io/fission-charts
- name: mongodb
version: 13.6.7
repository: https://charts.bitnami.com/bitnami
Below is the values.yaml
file that I have been using in my wrapper chart. I’ve found that these settings seem to be the most reliable for deploying Fission (in my environment anyway), however it did take a little trial and error to arrive at these settings for fission-all
so your milage may vary with these settings depending on how your Kubernetes cluster is setup.
# values.yaml fission-all:
routerServiceType: ClusterIP
enableIstio: true
analytics: false
additionalFissionNamespaces:
- fission
preUpgradeChecks:
enabled: false
mongodb:
auth:
enabled: false
I have disabled auth on mongodb as well since this is just a simple example, but in a production deployment of this I would certainly have this turned on and be using authentication.
Once deployed, an instance of MongoDB should be deployed in the cluster:
[aaron@fedora fission]$ kubectl get deployment -n fission | grep mongo fission-mongodb 1/1 1 1 15h
Lastly, lets setup a secret that will hold the URI for our MongoDB instance that our application will use. First I will encode the MongoDB URI with base64:
echo -n "mongodb://fission-mongodb:27017/fission_todo" | base64 -w 0
Now create a new Kubernetes secret and add the base64 encoded Mongo URI as the data. Save this in /templates/secret.yaml
:
# templates/secret.yaml apiVersion: v1
kind: Secret
metadata:
name: mongodb-uri
data:
MONGO_URI: bW9uZ29kYjovL2Zpc3Npb24tbW9uZ29kYjoyNzAxNy9maXNzaW9uX3RvZG8=
type: opaque
Once applied, the secret should be present in the Kubernetes cluster:
[aaron@fedora fission]$ kubectl get secret -n fission | grep mongo mongodb-uri opaque 1 37m
The Typescript Todo Application
I built a small Todo Application (very original, I know) that is just a simple CRUD app. The code for that application can be found in this repo on my Github. It did require me to design this application in kind of an unusual manner from what I am used to, but in the end I felt like it is pretty easy to follow.
My Fission function is defined in the app.ts
file below:
// app.ts import { Request, Response } from "express";
import { TodoService } from "./todo.service";
import mongoose from "mongoose";
import { ITodoDocument } from "./todo";
import * as fs from "fs";
// read the mongodb secret
const MONGO_URI = fs.readFileSync("/secrets/fission/mongodb-uri/MONGO_URI", "utf-8");
mongoose.connect(MONGO_URI);
interface ApiReponse {
status: number;
body?: ITodoDocument | ITodoDocument[] | { deletedCount: number }
}
module.exports.todo = async function(context: {request: Request, response: Response }): Promise<ApiReponse> {
const method = context.request.method;
let data;
switch (method) {
case 'GET': {
const id = context.request.headers['x-fission-params-id'] as string;
if (id) {
data = await TodoService.getOne(id);
break;
}
// list all
data = await TodoService.getAll();
break;
}
case 'POST': {
const id = context.request.headers['x-fission-params-id'] as string;
if (id) {
data = await TodoService.update(id, context.request.body);
break;
}
data = await TodoService.create(context.request.body);
break;
}
case 'DELETE': {
const id = context.request.headers['x-fission-params-id'] as string;
data = await TodoService.delete(id);
break;
}
default: {
return {
status: 405
}
}
}
return {
status: 200,
body: data
};
}
I have created a single asyncronous function todo
that takes a single argument context
that is passed to it from the server that is created by the environment. This context
object contains an express request
and a response
.
I was able to determine these types by looking at the server.js file for the Fission node environment. This is the runtime server for Fission, and it is creating an express server and then routing traffic for the endpoints that are created to the function. So the context
argument will always contain a Request
and a Response
object as long as we are not using a custom server, which I have not yet experimented with.
Notice that before the function definition, I am setting up the MongoDB connection using a secret named mongodb-uri
secret I created earlier. Fission will store the secrets that are defined when creating the function on the pod in /secrets/<namespace>/<secret-name>/<key>
, which is where I am reading the contents of the file from. In a later step in the function deployment, I will tell Fission which secrets I want mounted to the pod and it will be stored at this path.
In this function I have a switch
based on the HTTP method used. Notice that path variables will be split up appropriately and stored in the x-fission-params-<variable>
header. In this case, my variable is named id
and that is determined by my route, which we will get into in a later section.
I have made this a single function, mainly for ease of deployment but it very easily could be split into multiple functions. The way to do that would be to just add another module.exports.FUNCTION_NAME = <code>
.
Building the Source Code
I will admit, I may have embellished a little bit by saying that I deployed a “Typescript” application to Fission. I suppose that could be possible had I built a builder that would build the Typescript code automatically and run the built version, but for the time being I have been building the code on my local machine, compressing the built version of my code, and deploying that to my Fission instance.
First, set the build command in the package.json
to also copy ONLY the package.json
into the output directory of the build application. Fission only wants the package.json
, do not include the lockfile.
scripts: { "build": "npx tsc && cp ./package.json ./dist"
}
Build the source code from inside of the directory containing the Typescript project:
npm run build
Compress the /dist
directory:
zip -jr nodejs.zip dist/
Deployment to Fission with Specs
When I first started working with Fission, I deployed and managed everything using the fissio-cli commands. While their CLI is a wonderful tool, it became quite tedious to have to make every change for the routes, functions, and packages one at a time every time I wanted to change even the smallest bit of code in my application. I thought that there had to be a better way than doing this via commands, and Fission does provide a much easier solution to deploying functions called “specs”.
Specs are basically Kubernetes manifests that use the Fission CRDs and can easily be generated by adding --spec
to the end of any of the commands above that are creating environments, packages, functions, or routes. This will output the action that would be taken by Fission into a yaml file that can be modified and re-applied for easy deployment.
Issue the following commands to generate the Fission specs for the application, they will be generated in the spec/
directory of the current working directory. These can be changed in the files as needed too.
fission env create --name nodeenv --image fission/node-env-16:latest --builder fission/node-builder-16:latest -n fission --spec
fission pkg create --name todo-pkg --env nodeenv --sourcearchive nodejs.zip -n fission --spec
fission fn create --name todo-list --pkg todo-pkg --entrypoint "app.todo" --secret mongodb-uri -n fission --spec
fission route create --url /todo --function todo-list --createingress -n fission --method POST --spec --name create-route
fission route create --url /todo/{id} --function todo-list --createingress -n fission --method POST --spec --name update-route
fission route create --url /todos --function todo-list --createingress -n fission --spec --name list-route
fission route create --url /todos/{id} --function todo-list --createingress -n fission --spec --name delete-route --method DELETE
fission route create --url /todos/{id} --function todo-list --createingress -n fission --spec --name get-route
An example of what a spec looks like is below, this spec is for deploying the compressed version of the Todo application to a package object:
include: - nodejs.zip
kind: ArchiveUploadSpec
name: nodejs-zip-AFQG
---
apiVersion: fission.io/v1
kind: Package
metadata:
creationTimestamp: null
name: todo-pkg
namespace: fission
spec:
deployment:
checksum: {}
environment:
name: nodeenv
namespace: fission
source:
checksum: {}
type: url
url: archive://nodejs-zip-AFQG
status:
buildstatus: pending
lastUpdateTimestamp: "2023-02-11T20:30:20Z"
All of the specs for the Todo app can be found in the spec
directory in the repository for this project.
Once all of the specs are created, apply them:
fission spec apply
This will apply changes to Fission using the instructions from the spec manifests. This workflow is where I felt Fission really became powerful since deployment is super fast and it allows a developer to re-deploy changes to existing environments by just re-applying their spec manifest files. I highly suggest reading the Spec Docs from Fission for more information about how this works on the backend.
Test the Application
Now that the the package is deployed, functions are created, routes are created, and we have a repeatable workflow, it is time to test the application.
I will start by sending a cURL request to the endpoint that is hosting my routes to create a new Todo:
curl -H "Content-Type: application/json" https://fission.YOUR_DOMAIN.com/todos
[]
Create a new Todo by invoking the create-todo
route and assign it the name “learn k8s” (because we are never done learning, right?):
curl -H "Content-Type: application/json" -X POST -d '{"name":"learn k8s"}' https://fission.YOUR_DOMAIN.com/todo
{"name":"learn k8s","_id":"63f63f57cd5183450cb185fc","__v":0}
Verify that we have created this Todo by listing all of the Todos:
curl -H "Content-Type: application/json" https://fission.YOUR_DOMAIN.com/todos
[{"_id":"63f63f57cd5183450cb185fc","name":"learn k8s","__v":0}]
Try updating the name of the todo object to something else:
curl --location --request POST 'https://fission.YOUR_DOMAIN.com/todos/63f63f57cd5183450cb185fc' \ -H 'Content-Type: application/json' \
-d '{
"name": "updated todo"
}'
{"name":"updated todo","_id":"63f63f57cd5183450cb185fc","__v":0}
Lastly, we should be able to delete the todo:
curl -H "Content-Type: application/json" -X DELETE https://fission.YOUR_DOMAIN.com/todo/63f63f57cd5183450cb185fc
{"acknowledged": true,"deletedCount": 1}
Conclusion
I have really enjoyed working with Fission and learning more about serverless deployment with Kubernetes. I wouldn’t say this experience came without some occasional bumps though, there were some areas where it took me a little bit to figure out how to get things working correctly, particularly with getting the deployment for the fission-all
helm chart right since I ran into issues with the pre upgrade checks failing that were quite annoying. However, Fission as a whole is a really interesting platform and has been quite fun to play around with.
Overall I would certainly recommend someone use Fission if they are thinking about trying it to deploy a small function or small application. I hope you found this post helpful, feel free to check out my LinkedIn or Github!
Github Repo for the Todo application with specs.