As an application developer, I wanted to get more of an appreciation (and independence) of functions as a service (faas) (i.e. serverless). There is a product called OpenFaaS that is a framework for setting up your own serverless provider. This is akin to setting up your own AWS Lambda, Azure Functions, or Firebase Functions. The trouble is that I felt I had already wasted enough time fiddling with Kubernetes and other related technologies that frankly were too much for my needs.
But out of nowhere I ran into faasd
. faasd
is a light weight version of OpenFaaS services that allows me to throw up functions on a VM or even a RaspberryPi. Of course I dived right in, thinking this would be as simple as a download, install, run kind of operation. Oh how I was wrong. Even with its light weighty-ness, it requires a pretty vast knowledge of systems and networking to get going correctly. Additionally, this is a very immature product that doesn't have the friendly and polished UX that I was hoping for. None the less, its what we've got and I was determined to get it going.
System Overview
There are two primary systems that need to be setup. In the following article I intend to jump back and forth between these systems through the various phases of setup; OS Setup, Certificate Generation/Distribution, Service Setup, Service Usage. The two systems can be aarch64/arm64 or x86_64/amd64. I've chosen to use x86_64 as my development host where I will build my functions for deployment and an arm64 Raspberry Pi 4 to host my deployed functions.
TODO: Show picture.
OS Setup
The Development Host OS
The development host is where we'll develop our function code, build the OCI images that make up the function deployment, publish the images to a local docker registry, and deploy the functions from. For context, in all of my examples I'm using Ubuntu 20.04 on the development host. The development host can be a virtual machine or a real host. I am using a VirtualBox virtual machine with bridged network adapters for my setup.
Once you've got the VM and OS installed, you'll need to at a minimum, have internet, and install the following packages:
- Git
- Docker
- OpenSSL (at least version 1.1.1)
The network identity for our development host will hence be the IPv4 address 10.20.30.40.
The Function Host OS
My function host is a Raspberry Pi 4. It has 8GB of RAM and I'm running RaspiOS Lite 64bit on it. Its important that you don't use the standard RaspiOS because it has an armhf
user space architecture that is incompatible with the faasd
scripts and binaries that we'll be running.
Because of how new the arm64 repositories are for the Raspberry Pi are at the time of this writing, its a good idea to do the typical update/upgrade.
sudo apt-get -y update ; sudo apt-get -y upgrade
In addition to the typical getting things working, I recommend creating a new account, adding it to sudo, and removing the pi
account. The pi
account is just asking to get hacked if you ever expose your device to the internet. Some other recommendations (generally outside the scope of this article) include:
- Install iptables or ufw and lock down network access.
- Forbid all openssh root logins.
- Allow only openssh key logins. (i.e. no password logins).
After the initial operating system setup, ensure that there is no docker installed. This is because docker's containerd
implementation conflicts with the containerd
that faasd
will be using. I'm not just repeating some literature, I tried it and it didn't work. While there may be some containerd
plugins and configurations that'll get it going, I've yet to find the way to thread that needle.
Things you do want to install are:
- Git
- Curl
- An OpenSSH Server
The network identity for our function host will hence be the IPv4 address 10.20.30.41.
Certificate Generation
The Development Host Certificates
You'll want to generate some certificates that we'll be using everywhere. Generate these certificates on the development host with an OpenSSL that is newer than v1.1.1. The following is a script you can use to generate a certs
folder that'll contain all the files you need. Note: The IP specified in the script needs to match the actual IP of the development host that you intend faasd
to connect to. This IP is tied to the certificates that we intend to use and if there is a mismatch, you'll be constantly bombarded with an annoying mismatched certificate
error or no IP SANs
error.
The script source:
#!/bin/bash
: ${REGISTRY_ADDR:=10.20.30.40}
if [ ! -e certs ]; then
# Note: Rename or remove certs folder to re-run cert generation.
mkdir -p certs
openssl genrsa -out certs/ca.key 4096
openssl req -x509 -new -nodes -subj "/O=CA/CN=cert" -key certs/ca.key -sha256 -days 65535 -out certs/ca.crt
openssl genrsa -out certs/site.key 2048
openssl req -new -sha256 -key certs/site.key -out certs/site.csr \
-subj "/O=MyOrg/CN=site"
openssl req -in certs/site.csr -noout -text
openssl x509 -req -in certs/site.csr -out certs/site.crt \
-CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial \
-extfile <(printf "subjectAltName=IP:$REGISTRY_ADDR") \
-days 65535 -sha256
openssl x509 -in certs/site.crt -text -noout
fi
The ca.crt certificate is going to be distributed the most because its the certificate that is used to verify the legitimacy of the docker registry site certificate. The first place we'll install the certificate is in the docker client's configuration. Assuming we'll be hosting the docker registry on TCP port 5443:
sudo mkdir -p /etc/docker/certs.d/10.20.30.40:5443/
sudo cp certs/ca.crt /etc/docker/certs.d/10.20.30.40:5443/ca.crt
Additionally, for good measure, we'll want to install the ca.crt
into the system CA Certificate Bundle:
sudo cp certs/ca.crt /etc/ssl/certs/faas-system-root-ca.crt
sudo update-ca-certificates
# Note: You may need to restart any relevant services after this (or just reboot).
Later, we'll need to install the ca.crt
into a docker container, so don't lose track of your certs
folder's location.
The Function Host Certificates
Once the certificates are setup, you'll want to copy certs/ca.crt
file to the function host. I generally do this with an OpenSSH scp
command:
scp certs/ca.crt [email protected]:/home/user/faas-system-root-ca.crt
From the function host, you'll now want to install the certificate into the system's CA Certificate Bundle:
sudo cp /home/user/faas-system-root-ca.crt /etc/ssl/certs/
sudo update-ca-certificates
# This reboot may be optional here, but is required if there is an existing
# containerd service installed in the system when update-ca-certificates is run.
sudo reboot
Service Setup
The Development Host Tools
faas-cli
The first and most obvious thing to install on the host is the OpenFaaS faas-cli
. This is as simple as running the following:
curl -sLfS https://cli.openfaas.com | sudo sh
buildx
Next, we want to install the buildx
extension into the docker client. This is accomplished with:
wget https://github.com/docker/buildx/releases/download/v0.6.3/buildx-v0.6.3.linux-amd64
mkdir -p ~/.docker/cli-plugins
mv buildx-v0.6.3.linux-amd64 ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
You can checkout Docker Buildx documentation for more information.
buildkitd
After buildx
is installed, we need to manually inject our ca.crt
into its complimentary docker container moby/buildkit:buildx-stable-1
. First we need to pull and create the container:
docker pull moby/buildkit:buildx-stable-1
docker run -d moby/buildkit:buildx-stable-1
docker ps | grep buildkitd
Once the buildkitd container is up and running, we'll inject the process with our certificate by running:
CONTAINER=$(docker ps | grep moby/buildkit | cut -d ' ' -f1) && \
docker cp certs/ca.crt $CONTAINER:/etc/ssl/certs/faas-system-root-ca.crt && \
docker exec $CONTAINER update-ca-certificates && \
docker restart $CONTAINER
Note: This is a very ugly hack that can be made much more robust. But as long as the docker container stays up, it works for now.
Private Docker Registry
Finally, we need to setup our private docker registry where we'll host all of our function images. faasd
by default wants to use docker hub, so that can be a much easier route. I don't want to pay for docker hub (or expose my apps), so I host my own registry on my development host. When we setup the registry, we must ensure that we have TLS enabled or faasd
will refuse to do anything useful. This is where our site.crt
and site.key
files come in. They can be invoked with something like the following command:
docker run --rm \
--name registry \
-v $(pwd)/certs:/certs \
-e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/site.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/site.key \
-p 5443:443 \
registry:2
For more longer lived services, I recommend using -d --restart=always
instead of --rm
.
This setup can be tested by pulling hello-world
from docker hub and then pushing the same image into our newly hosted registry:
docker pull hello-world
docker tag hello-world 10.20.30.40:5443/hello-world
docker push 10.20.30.40:5443/hello-world
Once that is working, the docker bit is done. (But keep the registry running!)
The Function Host Tools
On the Raspberry Pi, most everything is automated by some scripts in the faasd
repository. Start by cloning the repo with (Note: I am currently using commit 57322c49476ec04aa176fb9bd76670ddec3c7c5c.):
git clone https://github.com/openfaas/faasd
Before we run the install script, there may be a bug in regards to the 1.5.4 version of containerd that the install script attempts to download and install. The file is actually named with version 1.3.5.
Change the line that says:
curl -sSL https://github.com/alexellis/containerd-arm/releases/download/v1.5.4/containerd-1.5.4-linux-arm64.tar.gz | $SUDO tar -xvz --strip-components=1 -C /usr/local/bin/
Replace the line with the following:
curl -sSL https://github.com/alexellis/containerd-arm/releases/download/v1.5.4/containerd-1.3.5-linux-arm64.tar.gz | $SUDO tar -xvz --strip-components=1 -C /usr/local/bin/
Once that's fixed, you can simply run the install command to install faasd, containerd, and some other relevant tools:
./hack/install.sh
Assuming that went well, the system should now be setup. I usually look for open ports with netstat -tna
to verify success. The ports you are looking for are 8080, 8081 (a bunch), and 9090.
You can verify the service's authentication by logging in with the following:
sudo cat /var/lib/faasd/secrets/basic-auth-password | faas-cli login --username admin --password-stdin --gateway http://10.20.30.41:8080
Create OpenFaaS Function
New Function On Development Host
To create a new nodejs based function, run a command similar to the following:
faas-cli new myfunction --lang node --gateway http://10.20.30.41:8080
That'll create a few files, including a handler.js
file. In that file, you can create dummy code with something like:
'use strict';
module.exports = async (event, context) => {
const result = {};
return result;
};
Once that is save, we have our function code. Now we need to update the function deployment configuration. This is found in a YAML file that should match the name of the function. Something like myfunction.yml
. In there we'll setup some host specific stuff:
version: 1.0
provider:
name: openfaas
gateway: http://10.20.30.41:8080
functions:
testfunc:
lang: node12
handler: ./myfunction
image: 10.20.30.40:5443/myfunction:latest
Note: The gateway is the function host and the image refers to the development host IP that is also references in our site.crt
(or the host of the docker registry). If any of these values change, the certificates need to be regenerated and distributed as appropriate. (A pain to be sure.)
Once all that is setup, we are ready to build and deploy. In a homogenous architecture setup, we could just do discrete build, push, and deploy steps; but since we're doing cross architectural builds, we have to use publish. The following will build the function for the platforms specified and push them into our docker registry (as specified in the YAML configuration file):
faas-cli publish --platforms linux/arm64 -f myfunction.yml
faas-cli deploy -f testfunc.yml
Note: If you're getting an error that is something like exec user process caused "exec format error"
, it's likely that you are attempting to run an x86
container on arm64
. In this case, double check your platforms values. (e.g. arm64 and amd64 look very similar.)
If everything went off without an error, you can login to the OpenFaaS service by going to http://10.20.30.40:8080 and then test the function in the browser.
References
- Raspberry Pi OS for arm64 finally released!
- Using private Docker registries
- Deploy a registry server
- faasd - lightweight Serverless for your Raspberry Pi
- Unmask a Masked Service in Systemd
- Github openfaas/faasd
- Getting "x509: certificate signed by unknown authority" by microk8s
- OpenFaaS - Create Functions
- Creating Docker multi-arch images for ARM64 from Windows
- Docker Buildx
- Github docker/buildx Releases
- What is Buildkit?
- Github moby/buildkit Dockerfile
- Github moby/buildkit
- A private CA buildkit workaround