Cystore

FaaS - Function as a Service med OpenFaaS/MySQL/Ceph

Det nya heta är FaaS, Function as a Service. Grovt förenklat så är det är ett sätt att exekvera funktioner i molnet utan att ha en egen webbserver. Man betalar därför endast för CPU-tid som används istället för att betala för resurser som inte används. Om man jämför med en egen server så belastas den troligen inte till max utan man vill ha lite marginal. Den marginalen betalar man för. Men det gäller inte för FaaS eftersom man betalar endast för den tid man exekverar något. Som kanske märks så krävs det lite annorlunda tänk när man utvecklar mot FaaS. Allt är stateless och man kan nästan se varje funktion som en egen liten micro-service som hanterar en specifik sak.

Test av FaaS

För att götta ner sig lite i ämnet så har jag satt upp en guide för att testa FaaS med hjälp av Docker Swarm, OpenFaaS, MySQL, Nginx samt Ceph. Tanken är att skapa en websida som huserar på Ceph (objekt-lagring, vilket vi på Cygate snart lanserar, stay tuned!). Ceph har stöd för både S3 (Amazon) samt Swift (OpenStack) protokollen. I den är guiden används S3. MySQL används som en lokal-databas men skulle kunna vara en DBaaS (Database as a Service). Nginx används endast i guiden för att hantera CORS (Cross-Origin Resource Sharing)  så att min FaaS går att nå från en annan domän än vad mitt objekt ligger lagrat (websidan). Docker Swarm används för att sätta upp OpenFaaS lokalt på min burk, via Docker. Endast en worker-nod används i det här fallet.

OpenFaaS fungerar ungefär som AWS Lambda men är ett open-source projekt. Man kan deploya olika sorters funktioner i OpenFaaS, bland annat i språken Ruby, Golang, Java, NodeJS etc. I guiden testas Ruby funktioner, men skulle egentligen kunna skapa funktioner i olika språk och anropa dem utan någon skillnad.

Guiden i den här bloggen skall absolut inte ses som någon best-practice utan endast som en laboration av hur FaaS fungerar.

En engelsk version av blogposten samt all källkod finns på github.

Steg 1

Docker behöver vara installerat och vi sätter upp ett Swarm kluster lokalt genom att helt enkelt skriva:

docker swarm init

Eftersom vi endast ska köra en swarm-nod behöver vi inte joina någon mer nod.

Steg 2

Klona OpenFaaS git repo för att sätta upp OpenFaaS services på vårt Swarm kluster.

git clone https://github.com/openfaas/faas

cd faas

./ deploy_stack.sh

Stacken sätts upp och kan ses med “docker ps”. Allt sker automatiskt.

Steg 3

Vi vill spara data I en MySQL databas, så vi förbereder den genom att skapa en databas vid namn ”faas” samt en tabell där vi lägger vår data. Vi kallar den users.

mysql -e ”create database faas”

mysql -D faas -e ”create table users (id INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), name varchar(255), date DATETIME)”

Steg 4

I vårt fall vill vi köra MySQL I Ruby och default används ruby:2.4-alpine-3.6 image från Docker Hub. Eftersom MySQL kräver att mysql-dev samt build-base finns installerat för att kunna bygga mysql2 gemet så behöver vi skapa en ny Dockerfile med samma namn lokalt.

Dockerfile ser ut som följer:

FROM ruby:2.4-alpine3.6 

# Alternatively use ADD https:// (which will not be cached by Docker builder)

RUN apk –no-cache add curl \

    && echo ”Pulling watchdog binary from Github.” \

    && curl -sSL https://github.com/openfaas/faas/releases/download/0.6.9/fwatchdog > /usr/bin/fwatchdog \

    && chmod +x /usr/bin/fwatchdog \

    && apk del curl –no-cache \

    && apk update \

    && apk add build-base \

    && apk add mysql-dev

Vi bygger sedan ny container på följande sätt (i samma katalog som Dockerfile finns):

docker build -t ruby:2.4-alpine3.6 .

Steg 5

Nu till det roliga, dags att skapa funktioner! Men för att kunna göra det behöver vi OpenFaaS CLI, vilket är det kommando som kommer användas för hantering av våra funktioner. Det installeras enkelt via följande kommando:

curl -sL https://cli.openfaas.com | sudo sh

 

Tanken med vår demo är att skapa 3 stycken funktioner. En som lägger till en användare i databasen, en som tar bort en användare med givet namn samt en funktion som listar alla användare i databasen.

 

Vi börjar med att initiera en ny funktion för språket ruby:

faas-cli new demo –lang ruby

 

Nu skapas det en katalog vid namn demo samt en demo.yml. I katalogen ”demo” finns nu en boilerplate (handler.rb) för ruby funktionen vi vill skriva, samt en Gemfile.

 

Eftersom vi ska använda oss av mysql2 gem så lägger vi in det i Gemfile:

source ’https://rubygems.org’

gem ’mysql2’

 

demo.yml innehåller konfiguration för vilka funktioner vi vill tillhandahålla samt konfiguration av miljövariabler etc. De miljövariabler vi vill sätta är konfiguration för vår MySQL-databas, det vill säga IP/Hostname, användare, lösenord samt databas. Vi skapar då en fil som vi kan döpa till env.yml med följande innehåll (ändra till korrekta värden som gäller för er miljö):

environment:

  MYSQL_USER: root

  MYSQL_PASS: hemligt

  MYSQL_DB: faas

  MYSQL_HOST: 192.168.1.157

Environmentfilen kommer sedan pekas ut i demo.yml så varje funktion får ta del av den och då kunna nå databasen baserat på miljövariabler som sätts i dess exekveringsmiljö.

Steg 6

Dags att skapa konfiguration för våra funktioner! Det är som tidigare nämnt i demo.yml vi konfigurerar det.

Innehållet i vår demo.yml ska se ut som följer:

provider:

  name: faas

  gateway: http://localhost:8080

 

functions:

  new_user:

      lang: ruby

      handler: ./new_user

      image: demo1

      environment_file:

          – env.yml

  delete_user:

      lang: ruby

      handler: ./delete_user

      image: demo2

      environment_file:

          – env.yml

  list_users:

      lang: ruby

      handler: ./list_users

      image: demo3

      environment_file:

          – env.yml

 

Först konfigurerar vi vart vår provider är, vilket default är localhost:8080 eftersom vi kör det på en lokal Docker Swarm. Sedan specificerar vi 3 funktioner, nämligen new_user, delete_user samt list_users. Varje funktion sätts till ruby (vilket det är i vårt fall, men skulle kunna vara olika). Vi specificerar vad vår image skall heta (som är vår exekveringsmiljö) samt vart våra miljövariabler finns med vår MySQL konfiguration.

Handler pekar ut var vår funktion finns att hämta, vilka vi skapar i Steg 7.

Steg 7

Dags att skriva lite kod! Vi kopierar katalogen som genererades tidigare till 3 nya kataloger, en för varje funktion, med samma namn som funktionerna.

cp –r demo new_user

cp –r demo delete_user

cp –r demo list_users

rm –rf demo

 

Till sist kan vi ta bort demo katalogen. Varje funktion skrivs i ruby och de ser ut som följer.

Här skapar vi en ”user” med namn som kommer i input från ett POST-anrop mot funktionen. Notera att vi inte har någon som helst säkerhet här, det är endast ett demo!

new_user/handler.rb:

require ’mysql2’

class Handler

    def run(req)

        db = Mysql2::Client.new(:host => ENV[”MYSQL_HOST”],

                                 :username => ENV[”MYSQL_USER”],

                                 :password => ENV[”MYSQL_PASS”],

                                 :database => ENV[”MYSQL_DB”],

                                 :reconnect => true)

        q = db.prepare(”insert into users set name = ?, date = now()”)

        q.execute(req)

        db.close

        return ”Saved user: #{req} to DB: #{ENV[”MYSQL_HOST”]}”

    end

end

Sedan skapar vi vår delete funktion som ska ta bort en ”user” från vår user-tabell. Funktionen tar bort alla användare med namnet som skickas in via POST request.

delete_user/handler.rb:

require ’mysql2’

class Handler

    def run(req)

        db = Mysql2::Client.new(:host => ENV[”MYSQL_HOST”],

                                 :username => ENV[”MYSQL_USER”],

                                 :password => ENV[”MYSQL_PASS”],

                                 :database => ENV[”MYSQL_DB”],

                                 :reconnect => true)

        q = db.prepare(”delete from users where name = ?”)

        q.execute(req)

        db.close

        return ”Delete users: #{req} from DB: #{ENV[”MYSQL_HOST”]}”

    end

end

 

Till sist ska vi lista våra ”users” från user-tabellen. Det gör vi med vår funktion list_users.

list_users/handler.rb:

require ’mysql2’

class Handler

    def run(req)

        db = Mysql2::Client.new(:host => ENV[”MYSQL_HOST”],

                                 :username => ENV[”MYSQL_USER”],

                                 :password => ENV[”MYSQL_PASS”],

                                 :database => ENV[”MYSQL_DB”],

                                 :reconnect => true)

        q = db.prepare(”select * from users”)

        res = q.execute()

        users = []

        res.each(:as => :array) do |u|

            users << [u[”name”], u[”date”]]

        end

        return users

    end

end

Notera att i varje funktion används de miljövariabler som vi specifierade i env.yml filen.

Steg 8

Dags att deploya våra funktioner på OpenFaaS!

Men först måste vi bygga vår miljö:

faas-cli build -f demo.yml

När det gjorts så kan vi deploya våra funktioner med följande commando:

faas-cli deploy -f demo.yml

Om man re-deployar och den senaste deployment felar (t.ex. pga fel i koden) så verkar OpenFaaS automatiskt rulla tillbaka till den senaste fungerande deploymenten som gjorts, om det gjorts någon tidigare vill säga.

Steg 9

Nu kan vi testa våra funktioner med ett passande verktyg som curl.

Coolt! Men nu vill vi ju ha en websida till det hela.

 

Steg 10

Låt oss skapa en väldig enkel websida i form av en HTML-fil med lite JavaScript som anropar våra funktioner. Man ska helt enkelt kunna lägga till, ta bort och lista användare på vår enkla sida.

Vi skapar index.html med följande innehåll:

<html>

    <head>

        <title>FaaS Demo</title>

        <script src=”https://code.jquery.com/jquery-3.2.1.min.js”></script>

    </head>

    <body>

    <script>

    $(document).ready(function() {

        ListUsers();

    });

    function AddUser() {

        $.ajax({

               type: ”POST”,

               url: ”http://nergal.se:3000/function/new_user”,

               data: $(”#username”).val(),

               success: function(data) {

                   ListUsers();

               },

        });

    }

    function DeleteUser(username) {

        $.ajax({

               type: ”POST”,

               url: ”http://nergal.se:3000/function/delete_user”,

               data: username,

               success: function(data) {

                   ListUsers();

               }

        });

    }

    function ListUsers() {

        $.ajax({

               type: ”POST”,

               url: ”http://nergal.se:3000/function/list_users”,

               data: ””,

               success: function(data) {

                   var users = ””;

                   data = data.split(”\n”);

                   for(var i = 0; i < data.length – 1; i+=2) {

                       users += ”<tr><td>”+data[i]+”</td><td>”+data[i+1]+”</td><td><a href=’javascript:DeleteUser(\””+data[i]+”\”);’>Delete</a></td></tr>”;

                   }

                   $(”#users”).html(users);

               }

        });

    }

    </script>

 

    <input id=”username” type=”text” placeholder=”user to add…”></input>

    <button onclick=”javascript:AddUser();”>Add User</button>

 

    <h3>Users:</h3>

    <table id=”users”>

 

    </table>

    </body>

</html>

 

Som man kan se i koden ovan anropar jag nergal.se:3000 som jag satt upp en på en Nginx webserver med proxy-forward till OpenFaaS eftersom jag vill komma runt CORS vilket OpenFaaS inte har inbyggt stöd för. Det här är dock inget att rekommendera men som jag påpekat flera gånger, det är en demo.

Konfigurationen jag använt för Nginx proxy-forward är följande:

server {

    listen       8081;

    server_name localhost;

 

    access_log /usr/local/var/log/nginx/access-faas.log main;

    error_log /usr/local/var/log/nginx/error-faas.log info;

 

    location / {

        if ($request_method = ’OPTIONS’) {

            add_header ’Access-Control-Allow-Origin’ ’*’;

            add_header ’Access-Control-Allow-Methods’ ’GET, POST, OPTIONS’;

            add_header ’Access-Control-Allow-Headers’ ’DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range’;

            add_header ’Access-Control-Max-Age’ 1728000;

            add_header ’Content-Type’ ’text/plain; charset=utf-8’;

            add_header ’Content-Length’ 0;

            return 204;

        }

        if ($request_method = ’POST’) {

            add_header ’Access-Control-Allow-Origin’ ’*’;

            add_header ’Access-Control-Allow-Methods’ ’GET, POST, OPTIONS’;

            add_header ’Access-Control-Allow-Headers’ ’DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range’;

            add_header ’Access-Control-Expose-Headers’ ’DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range’;

            proxy_pass http://localhost:8080;

        }

        if ($request_method = ’GET’) {

            add_header ’Access-Control-Allow-Origin’ ’*’;

            add_header ’Access-Control-Allow-Methods’ ’GET, POST, OPTIONS’;

            add_header ’Access-Control-Allow-Headers’ ’DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range’;

            add_header ’Access-Control-Expose-Headers’ ’DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range’;

        }

 

    }

}

När vi fått funktionerna att lira nätverksmässigt så kan vi ladda upp vår index.html på vårt Ceph-konto. I det här fallet använder jag mig av s3cmd programmet som hjälper mig att skapa buckets och ladda upp filer via S3 till min Ceph-plattform.

 

Skapa en bucket vid namn ”faas”:

s3cmd mb s3://faas

 

Ladda upp index.html till den nya bucketen:

s3cmd put index.html s3://faas/

 

Och till sist sätt filen till public så att man kan nå den via webben:

s3cmd setacl –acl-public s3://faas/index.html

 

Nu kan jag anropa min fil i min bucket via min webläsare:

http://ceph.magstar.cygate.io:7480/cexhbeao:faas/index.html

Nedan är resultatet:

Jag kan lägga till, ta bort och lista users direkt på sidan och allt sker via mina FaaS funktioner.

Slutsats

Ok, vad hände precis och vad är det för coolt med det här?

Det som är coolt är att sidan som visas är endast en fil på en objektlagrings-plattform som där man förhoppningsvis endast betalar för sin lagring (i vissa fall per request etc). Man kan på sidan hantera en databas och utföra funktioner utan en bakomliggande webapp.

Och det tuffaste! Jag behöver ingen webbserver för att göra det!

Om man bortser från en del kringliggande setup som den här guiden krävde så skulle jag egentligen bara behöva skapa ett objekt-lagrings konto hos en leverantör, ett konto för att komma åt en FaaS och sedan skapa min websida. Jag skulle endast behöva betala för min index.html på 4Kb samt den CPU-tid mina funktioner skulle ta att exekvera för mina sidbesökare.

Att skapa en websida på det här sättet är troligen inte något som passar för de flesta men om man tänker ett steg längre så kan man använda FaaS för att exekvera funktioner som kräver en del CPU men kanske endast anropas ibland. Exempelvis konvertera filmer eller skapa thumbnails av bilder vid uppladdning på en websida för att slippa belasta den vanliga webservern med krävande jobb. Fantasin sätter gränserna!

Bra länkar

https://github.com/openfaas/faas

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

https://github.com/Lallassu/openfaas-demo