Skip to main content

K3s Kubernetes

OpenFaaS First Function

OpenFaaS First Function

In this example, we are going to deploy Python3 functions to our OpenFaaS.

Before we start, make sure all is working, and you followed the guides before on how to set up K3s on Raspberry Pi 4, create TLS private registry and install faas-cli/OpenFaaS

Read the official documentation

Always, the best and first thing to do is to read the official OpenFaaS documentation.

Let’s start

💡
Keep in mind, we are going this on control01 which is my control node for my Kubernetes cluster and where we set up Docker.

Create a new directory where you are going to work. I called mine openfaas_scripts. We need to download templates into this directory. Faas-cli is reading a skeleton of the function from this directory when creating a new function.

cd
mkdir openfaas_scripts
cd openfaas_scripts
faas-cli template pull

Now you should have new folder called template.

Template store

We are going to use Python3 because that’s what I'm the most familiar with, but the beauty of OpenFaaS is that you are not limited to one language. You can list supported templates like this:

root@control01:~/openfaas# faas-cli template store list

NAME                     SOURCE             DESCRIPTION
csharp                   openfaas           Classic C# template
dockerfile               openfaas           Classic Dockerfile template
go                       openfaas           Classic Golang template
java11                   openfaas           Java 11 template
java11-vert-x            openfaas           Java 11 Vert.x template
node17                   openfaas           HTTP-based Node 17 template
node16                   openfaas           HTTP-based Node 16 template
node14                   openfaas           HTTP-based Node 14 template
node12                   openfaas           HTTP-based Node 12 template
node                     openfaas           Classic NodeJS 8 template
php7                     openfaas           Classic PHP 7 template
python                   openfaas           Classic Python 2.7 template
python3                  openfaas           Classic Python 3.6 template
python3-dlrs             intel              Deep Learning Reference Stack v0.4 for ML workloads
ruby                     openfaas           Classic Ruby 2.5 template
ruby-http                openfaas           Ruby 2.4 HTTP template
python27-flask           openfaas           Python 2.7 Flask template
python3-flask            openfaas           Python 3.7 Flask template
python3-flask-debian     openfaas           Python 3.7 Flask template based on Debian
python3-http             openfaas           Python 3.7 with Flask and HTTP
python3-http-debian      openfaas           Python 3.7 with Flask and HTTP based on Debian
golang-http              openfaas           Golang HTTP template
golang-middleware        openfaas           Golang Middleware template
python3-debian           openfaas           Python 3 Debian template
powershell-template      openfaas-incubator Powershell Core Ubuntu:16.04 template
powershell-http-template openfaas-incubator Powershell Core HTTP Ubuntu:16.04 template
rust                     booyaa             Rust template
crystal                  tpei               Crystal template
csharp-httprequest       distantcam         C# HTTP template
csharp-kestrel           burtonr            C# Kestrel HTTP template
vertx-native             pmlopes            Eclipse Vert.x native image template
swift                    affix              Swift 4.2 Template
lua53                    affix              Lua 5.3 Template
vala                     affix              Vala Template
vala-http                affix              Non-Forking Vala Template
quarkus-native           pmlopes            Quarkus.io native image template
perl-alpine              tmiklas            Perl language template based on Alpine image
crystal-http             koffeinfrei        Crystal HTTP template
rust-http                openfaas-incubator Rust HTTP template
bash-streaming           openfaas-incubator Bash Streaming template
cobol                    devries            COBOL Template

Creating new function

Creating new functions in OpenFaaS is super easy.

Run:

faas-cli new --lang python3 mailme

--lang specifies the template name from above, and the next parameter is the name of our function, in my case it’s mailme.

Now you should have new folder called mailme, and in it:

root@control01:~/openfaas_scripts# ls -all mailme
total 12
drwx------ 2 root root 4096 May 26 12:54 .
drwxr-xr-x 7 root root 4096 Jun  3 10:01 ..
-rw-r--r-- 1 root root    0 May 26 12:40 __init__.py
-rw-r--r-- 1 root root 3215 May 26 12:54 handler.py
-rw-r--r-- 1 root root    0 May 26 12:40 requirements.txt

And also mailme.yml outside it.

This is super simple:

  • handler.py - This is where your function will live.
  • requirements.txt - If you require any additional modules to be present that are not in Python3 by default, here you can add the names, and they will get automagically installed (it works as any requirements.txt in standalone python).

What will our function do?

I wanted something simple that would also include some core stuff that OpenFaaS supports, especially secrets and passing variables to script from outside. Our function will send mail. Simple as that 🙂.

It’s good to define some input parameters ahead of starting so that we do not change stuff in the middle of coding. Our function will accept 3 parameters in YAML format.

  • api-key - a secret api-key that will live in Kubernetes secrets, and will be presented to your function as a readable file (so it’s not hard coded into code), and can be shared with other functions if you want. Read more about Kubernetes secretskubernetes.io
  • msg - What message to send.
  • to - To what email to send the message.

Input parameters in YAML are:

{
"api-key": "gr35p4inyyr4e9",
"msg": "test msg",
"to": "my@gmail.com"
}

Secrets

Secrets are the preferred way to pass sensitive information to your function. They need to be created ahead of the deployment of the function. We are going to put our api-key and password for the smtp server we are going to use to send mail.

kubectl create secret generic api-key --from-literal api-key="gr35p4inyyr4e9" --namespace openfaas-fn
kubectl create secret generic email-pass --from-literal email-pass="smtp_email_password_goes_here" --namespace openfaas-fn

In case you want to list your secret names use:

root@control01:~/openfaas_scripts# kubectl get secret -n openfaas-fn
NAME                  TYPE                                  DATA   AGE
default-token-5dcwg   kubernetes.io/service-account-token   3      12d
api-key               Opaque                                1      12d
email-pass            Opaque                                1      12d

To delete secret:

kubectl delete secret -n openfaas-fn <secret_name>

To read the secret:

# You might need to install jq
root@control01:~/openfaas_scripts# kubectl get secret api-key -n openfaas-fn -o json | jq '.data '
{
  "api-key": "Z3IzNXA0aW55eXI0ZTk="
}
echo "Z3IzNXA0aW55eXI0ZTk=" | base64 --decode
gr35p4inyyr4e9

Secrets in OpenFaaS

To get to the secret inside your OpenFaaS function you first need to create it, then define it in your function YAML file.

My whole mailme.yml looks like this (I will get to the other parameters later on):

version: 1.0
provider:
  name: openfaas
  gateway: http://openfaas.cube.local:8080
functions:
  mailme:
    lang: python3
    handler: ./mailme
    image: registry.cube.local:5000/mailme:latest

    environment:
      smtp_server: smtp.websupport.sk
      smtp_login: admin@rpi4cluster.com
      sender: admin@rpi4cluster.com

    secrets:
      - api-key
      - email-pas

You can see there is a section called secrets. Here, we specify the names of secrets we created.

But how the fuck did I get to them inside my function? I'm glad you asked. They will be presented as a file. It’s up to you to read and use it.

They will always be in format:

'/var/openfaas/secrets/' + secret_name

That’s secrets, but what about not so secret stuff I don't want to bake directly into code and be able to change without recompiling everything? Well, environmental variables are what you want.

Environment variables in OpenFaaS

These are variables defined outside your code. Something you might like to change without going through the process of rebuilding your whole function. There are for non-sensitive data, perhaps configuration.

In this case, you can see above in my mailme.yml I defined three such variables:

  • smtp_server - Server we are going to use to send my mail through.
  • smtp_login - Username for that server (remember we have the password in Kubernetes secrets).
  • sender - Who’s the sender of this message?

Super simple right?

To get to these in your code, you need to query environmental variables from the OS. This depends very much on language you're going to use, for example in Python 3 we can d

#query environment variables
os.getenv(var_name)

That should be it for secrets and environment variables. Let’s get to the main function.

OpenFaaS Function

Go to ../mailme/handler.py. The handler.py is your function, and it’s pre-populated with:

def handle(req):
    """handle a request to the function
    Args:
        req (str): request body
    """

    return req

This function handle(input) is called when your function is invoked by OpenFaaS. We are going to extend it with my mailing functionality.

💡
Read the comments in code to get what's going on.
# import libraries we going to use
# no shebang is needed at the start
# all libs I'm importing are native to python so I did not put anything in requirements.txt
import os
import json
import smtplib
from smtplib import SMTPException
from smtplib import SMTPDataError
from email.mime.text import MIMEText
from email.utils import formataddr

# Lets define some usefull functions

def get_secret(secret_name):
    '''
    - Returns secret value if exists, if not return False
    - Secret needs to be create before the function is build
    - Secret needs to be defined in functions yaml file
    '''
    try:
        with open('/var/openfaas/secrets/' + secret_name) as secret:
            secret_key = secret.readline().rstrip()
            return secret_key
    except FileNotFoundError:
        return False

def get_variable(var_name):
    '''
    - Returns environment variable value if exists, if not return False
    - Variable needs to be defined in functions yaml file
    '''
    return os.getenv(var_name, False)


def api_key_check(provided_key):
    '''
    - Check if provided api key is valid
    '''
    if get_secret('api-key') == provided_key:
        return True
    else:
        return False


def key_present(json, key):
    '''
    - Return true if Key exist in json
    '''
    try:
        _x = json[key]
    except KeyError:
        return False
    return True

# Main function
def handle(req):
    """handle a request to the function
    Args:
        req (str): request body
    """

    # If there is value passed to function
    if req:
        # check if its json formated by trying to load it
        try:
            json_req = json.loads(req)
        except ValueError as e:
            return "Bad Request", 400
    else:
        return "Bad Request", 400

    # Before anything check if api key from secret match api key provided
    # Might me good to implement this so there is no random spamming of function
    if key_present(json_req, 'api-key'):
        key = json_req["api-key"]
        if api_key_check(key) is False:
            return "Unauthorized", 401
    else:
        return "Unauthorized", 401

    # Cool if we are here api key was authorized
    # Let check if in posted body are keys that we need ( msg, to)
    if key_present(json_req, 'msg'):
        msg_text = json_req["msg"]
    else:
        return "Bad Request", 400

    if key_present(json_req, 'to'):
        to = json_req["to"]
    else:
        return "Bad Request", 400

    # So we have values for message, to whom to send it, lets get sender
    sender = get_variable('sender')

    # Lets try to build message body and send it out.
    try:
        msg = MIMEText(msg_text)
        msg['From'] = formataddr(('Author', get_variable('sender')))
        msg['To'] = formataddr(('Recipient', to))
        msg['Subject'] = 'OpenFaas Mailer'

        mail_server = smtplib.SMTP_SSL(get_variable('smtp_server'))
        mail_server.login(get_variable('smtp_login'), get_secret('email-pass'))
        mail_server.sendmail(sender, to, msg.as_string())
        mail_server.quit
        return "request accepted", 202
    except SMTPException:
        return "Failed to send email", 500
    except SMTPDataError:
        return "Failed to send email", 500

As you can see, the function is quite simple, and contains no sensitive data.

Build and Push OpenFaaS function

Using buildx with private registry

OpenFaaS is using buildx container do build its container to correct architecture. And to have it working with private registry is a bit tricky.

This is the trick I use to tell buildx to use private registry.

Fist, we need to initialize buildx. This is done by trying to build our function in OpenFaaS. Go where the mailme.yml is and run:

faas-cli publish -f mailme.yml --platforms linux/arm64

This will most likely fail, but it will create the buildx container for us in docker.

root@control01:~/openfaas_scripts# docker ps
CONTAINER ID   IMAGE                           COMMAND       CREATED       STATUS       PORTS     NAMES
c4384581eb01   moby/buildkit:buildx-stable-1   "buildkitd"   11 days ago   Up 11 days             buildx_buildkit_multiar

I use this script that I stored on my control01 to update the container with variables we need. I call it fix_buildkit.sh :

#!/bin/bash
#Set variables
HOST_ENTRY="192.168.0.202 registry registry.cube.local"
CERT="/root/docker-registry/registry.crt"

#get builder ID from docker
BUILDER=$(sudo docker ps | grep buildkitd | cut -f1 -d' ')

#Add certificates to docker container
sudo docker cp "$CERT" "$BUILDER":/usr/local/share/ca-certificates/ 2>/dev/null
sudo docker exec "$BUILDER" update-ca-certificates 2>/dev/null
sudo docker restart "$BUILDER" 2>/dev/null

sleep 5s

#get builder ID from docker
BUILDER=$(sudo docker ps | grep buildkitd | cut -f1 -d' ')
#use builder ID to inspect contaner and  grep host location
HOST=$(sudo docker inspect $BUILDER | grep HostsPath | cut -f4 -d'"')
#Add $HOST_ENTRY to $HOST at the end of file
echo "$HOST_ENTRY" >> "$HOST"

Run this script, and you should be good to go.

chmod +x fix_buildkit.sh
root@control01:~/openfaas_scripts# ./fix_buildkit.sh

What it does it update container hostiles, so it knows where to find private registry, and also adds our certificate to it. This way it won't complain about HTTPS of private registry.

💡
This needs to be run every time node reboots or the buildx container is restarted.

Build the function again

faas-cli publish -f mailme.yml --platforms linux/arm64

This should build the function docker image and push it to your repository. Ending with something like this:

#25 pushing layers
#25 pushing layers 4.7s done
#25 pushing manifest for registry.cube.local:5000/mailme:latest
#25 pushing manifest for registry.cube.local:5000/mailme:latest 0.1s done
#25 DONE 18.1s
Image: registry.cube.local:5000/mailme:latest built.
[0] < Building mailme done in 57.37s.
[0] Worker done.

Total build time: 57.37s

Fantastic! The last steps are ahead. Deploy this sucker to OpenFaaS on your Kubernetes server!

faas-cli deploy -f mailme.yml

An example of a successful deployment:

root@control01:~/openfaas_scripts# faas-cli deploy -f mailme.yml
Deploying: mailme.
WARNING! Communication is not secure, please consider using HTTPS. Letsencrypt.org offers free SSL/TLS certificates.

Deployed. 202 Accepted.
URL: http://openfaas.cube.local:8080/function/mailme.openfaas-fn

Check it in Kubernetes:

root@control01:~/openfaas_scripts# kubectl get pod -n openfaas-fn
NAME                              READY   STATUS    RESTARTS   AGE
mailme-bcd59f6d5-rcmkf            1/1     Running   0          12d

The function is deployed here, the mailme-bcd59f6d5-rcmkf is our function and status should be Running. If there is some error there, run the following command to get some feeling as to why it’s not working.

sudo kubectl describe pod mailme-bcd59f6d5-rcmkf  -n openfaas-fn

Invoking OpenFaaS function

You have a couple of options.

faas-cli

echo '{ "api-key": "gr35p4inyyr4e9", "msg": "test msg", "to": "vladoportos@gmail.com" }' | faas-cli invoke mailme

And mail was delivered, you just have to trust me on this 🙂.

curl

curl openfaas.cube.local:8080/function/mailme -d '{ "api-key": "gr35p4inyyr4e9", "msg": "test msg", "to": "vladoportos@gmail.com" }'

Web UI

Open the web UI of OpenFaaS, refer to the section about installing OpenFaaS to know how I got it. Install OpenFaaS

The three toggles on top * Text * JSON * Download Are what output are you expecting from your function? Not input, so don't switch to JSON if your function is not returning JSON.

All of them should return:

('request accepted', 202)

Did you like it, or was it helpful? Get yourself a drink and maybe get me one as well 🙂.