Serverless auf Kubernetes mit Knative

11.08.2021 Stefan Welsch
Cloud DevOps knative serverless k8s golang howto

Aktuell hosten wir unsere Serverless Applikationen direkt auf AWS Lambda. Da wir jedoch möglichst Provider unabhängig bleiben wollen schauen wir uns in diesem Techup Knative an. Knative ist eine Kubernetes basierte Platform um Serverless-Anwendungen direkt in Kubernetes bereitzustellen. Dadurch kann eine Serverless Applikation auch auf jeden anderen Provider portiert werden, solange Kubernetes installiert ist.

Knative ist ein Open-Source Projekt und wurde zum ersten Mal 2018 durch Google vorgestellt. Aber nicht nur Google arbeitet an dem Serverless-Framework, sondern auch zahlreiche andere grosse Firmen wie beispielsweise IBM oder RedHat. Die aktuellste Version zum Zeitpunkt dieses Blogbeitrags ist 0.24.

Knative kommt mit 2 Basiskomponenten “Serving” und “Eventing”. Serving ist dafür zuständig, um serverless Container in Kubernetes laufen zu lassen. Eventing bietet eine Schnittstelle um auf Ereignisse zu reagieren wie beispielsweise Github Hooks oder Message Queues.

In manchen Posts liest man noch von einer dritten Komponente “Build”. Diese wurde jedoch mit diesem Issue archiviert, da man zukünfitg auf Tekton Pipelines setzen will.

Knative Serving

Knative Serving nutzt Kubernetes und Istio um serverless Applikationen und Funktionen zu deployen. Dabei werden die folgenden Funktionen unterstützt:

  • Autoscaling inklusive “scale to zero”

  • Unterstützung der gängigsten Netzwerklayer wie Istio, Kourier, Ambassador, …

  • Snapshots von Code und Konfigurationen

Es gibt 4 Kubernetes CRDs (Custom Resource Definitions) um zu definieren, wie sich die serverless Applikationen im Cluster verhalten.

Service

Die Service Resource steuert den kompletten Lifecycle einer Applikation oder Funktion. Er erstellt ausserdem alle Objekte, welche zum Ausführen benötigt werden (Route, Konfiguration, Revisionen). Im Service wird auch geregelt welche Revision genutzt werden soll.

Route

Die Route-Resource verknüpft einen Netzwerk-Endpunkt mit einer oder mehreren Revisionen.

Configuration

Die Konfiguration Resource sorgt für den gewünschten State des Deployments. Es gibt eine strikte Trennung zwischen Code und Konfiguration. Bei jeder Änderung der Konfiguration wird eine neue Revision erstellt.

Revision

Die Revision Resource ist ein Snapshot des Codes und der Konfiguration, welche bei jeder Änderung erstellt wird. Eine Revision kann nach der Erstellung nicht mehr verändert werden. Revisionen können je nach Traffic autoskaliert werden.

Knative Eventing

Knative Eventing stellt Funktionen zur Verwaltung der Ereignisse zur Verfügung. Die Anwendungen werden in einem ereignisgesteuerten Modell ausgeführt.
Über Eventing lassen sich Produzenten (Producer) mit Konsumenten (Consumer) flexibel koppeln. Knative organisiert das Queueing der Ereignisse und liefert diese an die Container-basierten Services. Knative nutzt zum Senden und Empfangen von Events zwischen den Producer und Consumer HTTP Post Requests.

Knative Eventing definiert ein EventType-Objekt um Consumer einfach die Eventtypen, die sie konsumieren können, anzubieten. Diese EventTypes sind in der Event-Registry hinterlegt.

Wer meine Techup’s verfolgt der weiss, dass ich gerne die Framework’s in der Praxis sehe! 😄 Wollen wir uns nun also Knative mal im Cluster anschauen.

In der Praxis

Als erstes wollen wir Knative Serving in unserem Kubernetes Cluster installieren. Wir folgen hier den Schritten im Administration Guide mit allen Defaults (Kourier, Magic DNS).

Knative Serving Custom Resources und Knative Serving

# Install the required custom resources by running the command:
kubectl apply -f https://github.com/knative/serving/releases/download/v0.24.0/serving-crds.yaml

# Install the core components of Knative Serving by running the command:
kubectl apply -f https://github.com/knative/serving/releases/download/v0.24.0/serving-core.yaml

Netzwerklayer (Kourier)

# Install the Knative Kourier controller by running the command:
kubectl apply -f https://github.com/knative/net-kourier/releases/download/v0.24.0/kourier.yaml

# Configure Knative Serving to use Kourier by default by running the command:
kubectl patch configmap/config-network \
  --namespace knative-serving \
  --type merge \
  --patch '{"data":{"ingress.class":"kourier.ingress.networking.knative.dev"}}'

Magic DNS (sslip.io)

Knative bietet einen Kubernetes Job “default-domain” an, welcher Knative Serving so konfiguriert, dass sslip.io als default DNS Suffix genommen wird.

kubectl apply -f https://github.com/knative/serving/releases/download/v0.24.0/serving-default-domain.yaml

Nachdem wir Knative Serving installiert haben, installieren wir noch Knative Eventing.

Knative Eventing Custom Resources und Knative Eventing:

# Install the required custom resource definitions (CRDs):
kubectl apply -f https://github.com/knative/eventing/releases/download/v0.24.0/eventing-crds.yaml

# Install the core components of Eventing:
kubectl apply -f https://github.com/knative/eventing/releases/download/v0.24.0/eventing-core.yaml

Soweit so gut, Knative ist jetzt in unserem Cluster installiert.

Knative Serving Beispiel

Nun wollen wir unsere erste Serverless Applikation erstellen und mittels Knative deployen. Dazu schreiben wir uns einen ganz simplen Go-Server.

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func handler(w http.ResponseWriter, r *http.Request) {
	log.Print("b-nova: received a request")
	message := os.Getenv("MESSAGE")
	if message == "" {
		message = "World"
	}
	fmt.Fprintf(w, "Hello %s!\n", message)
}

func main() {
	log.Print("b-nova: starting server...")

	http.HandleFunc("/", handler)

	port := "8080"

	log.Printf("b-nova: listening on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

Anschliessend erstellen wir uns ein Dockerfile, um aus unserer Go-Applikation ein Image bauen zu können.

# Use the official Golang image to create a build artifact.
# This is based on Debian and sets the GOPATH to /go.
FROM golang:1.16 as builder

# Create and change to the app directory.
WORKDIR /app

# Retrieve application dependencies using go modules.
# Allows container builds to reuse downloaded dependencies.
COPY go.* ./
RUN go mod download

# Copy local code to the container image.
COPY . ./

# Build the binary.
# -mod=readonly ensures immutable go.mod and go.sum in container builds.
RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o server

# Use the official Alpine image for a lean production container.
# https://hub.docker.com/_/alpine
# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
FROM alpine:3
RUN apk add --no-cache ca-certificates

# Copy the binary to the production image from the builder stage.
COPY --from=builder /app/server /server

# Run the web service on container startup.
CMD ["/server"]

Anschliessend noch unser go.mod manifest erzeugen mit

go mod init

Daraus erstellen wir uns jetzt ein Image und pushen dies auf https://docker.io .

# Build the container on your local machine
docker build -t bnova/knative-hello-bnova .

# Push the container to docker registry
docker push bnova/knative-hello-bnova

Unser Image ist nun fertig um deployed zu werden. Wir erstellen und einen Knative Service. Dazu schreiben wir uns das folgende service.yaml File.

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: knative-hello-bnova
  namespace: knative-hello-bnova
spec:
  template:
    spec:
      containers:
        - image: docker.io/bnova/knative-hello-bnova
          env:
            - name: MESSAGE
              value: "from the whole b-nova team!"

Jetzt nur noch ein apply und schon sollte unsere erste Serverless Applikation zur Verfügung stehen.

kubectl apply --filename service.yaml

Nachdem unser Service erstellt wurde, wird Knative die folgenden Schritte für uns ausführen:

  • Eine neue Version unserer Applikation wird erstellt.

  • Es wird eine Route, Ingress, Service und ein LoadBalancer für unsere Applikation erstellt

  • Automatisches Up- und Downscaling unserer Pods.

Schauen wir uns dies im Detail an. Als Erstes wollen wir mal die URL für unseren Service herausfinden. Durch Magic DNS bekommen wir für jeden Service automatisch eine http://sslip.io URL. Durch folgenden Befehl sehen wir diese spezifische Url:

$ kubectl get ksvc knative-hello-bnova-service -n knative-hello-bnova  --output=custom-columns=NAME:.metadata.name,URL:.status.url
NAME                  URL
knative-hello-bnova   http://knative-hello-bnova.knative-hello-bnova.157.230.76.188.sslip.io

Wir sehen, dass aktuell keine Pods laufen. Starten wir also ein watch auf get Pods und schauen, was bei einem Request auf die Url passiert.

$ kubectl get pods -n knative-hello-bnova                                                                                                                                                                                                                                       2.6.3 ⎈ do-fra1-b-nova-openhub-cluster 15:14:57
No resources found in knative-hello-bnova namespace.
~/Development/go/src ❯ kubectl get pods -n knative-hello-bnova -w                                                                                                                                                                                                                 2.6.3 ⎈ do-fra1-b-nova-openhub-cluster 15:09:31
NAME                                               READY   STATUS    RESTARTS   AGE
knative-hello-bnova-00001-deployment-5d99758858-lklpt   0/2     Pending   0          0s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   0/2     Pending   0          0s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   0/2     ContainerCreating   0          0s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   1/2     Running             0          2s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   1/2     Running             0          3s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   2/2     Running             0          3s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   2/2     Terminating         0          64s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   1/2     Terminating         0          67s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   0/2     Terminating         0          97s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   0/2     Terminating         0          98s
knative-hello-bnova-00001-deployment-5d99758858-lklpt   0/2     Terminating         0          98s

Sobald ein Request gemacht wird, wird automatisch ein Pod gestartet und die Anfrage wird entgegengenommen. Sobald die Response gesendet wurde, fährt der Pod dann automatisch auch wieder nach einem gewissen Timeout (60s) herunter.

Fazit

Wir haben hier an einem einfachen Beispiel gesehen, wie man eine sehr einfache Serverless Applikation mittels Knative zur Verfügung stellen kann. Wir bei b-nova haben uns dazu entschlossen alle bestehenden Lambda Funktionen zu Knative zu migrieren! 🚀 Der Grund dafür ist relativ simpel. AWS Lambda braucht innerhalb der Applikation einen Handler um die Serverless Anfragen entgegenzunehmen.

Eine Go Lamdba Applikation sieht beispielsweise so aus:

func HandleRequest(ctx context.Context, event MyEvent) (string, error) {
	stage := event.Stage
	if stage == "" {
		stage = os.Getenv("stage")
	}

    // here is your logic
    
	return "ok", nil
}

func main() {
	lambda.Start(HandleRequest)
}

Die main()-Methode muss als Entrypoint die lambda.Start()-Funktion aufrufen. Wir müssen also unseren Applikationscode verändern, damit wir die Lambda-Funktion deployen können.

Bei Knative können wir unseren Applikationsode unverändert lassen. Es braucht lediglich eine zusätzliche, vom Quellcode unabhängige Konfiguration.

Nächste Schritte

Im heutigen TecUp haben wir uns lediglich Knative Serving angeschaut und dort auch nur die Basics. Es gibt in diesem Umfeld noch viele interessante Themen wie beispielsweise Traffic Splitting.

Für Knative Eventing haben wir momentan keinen Use Case, werden dies aber auf jeden Fall im Auge behalten und für ein weiteres TechUp vormerken.

Den vollständigen Quellcode findet ihr wie immer in unserem TechHub Git https://github.com/b-nova-techhub/knative-hello-bnova

Weiterführende Links:

https://knative.dev/docs/

https://stackoverflow.com/questions/58860118/how-does-knative-servings-activator-intercept-requests-to-scaled-down-revisions

Stefan Welsch - pioneer, stuntman, mentor. As the founder of b-nova, Stefan is always looking for new and promising fields of development. He is a pragmatist through and through and therefore prefers to write articles that are as close as possible to real-world scenarios.