11 February 2020

Een operator om te automatiseren – Hoe pak je dat aan?

Vincent van Dam
Automation Containerization

Je kent het wel, voordat je namespace, of OpenShift project, helemaal klaar is voor gebruik heb je een kleine waslijst van acties die je met de hand moet uitvoeren. Misschien is er een bepaalde deployment die je in elke namespace standaard wil draaien, misschien zijn er service accounts die je nodig hebt, of moet er een specifieke token toegevoegd worden. Het zijn in ieder geval handelingen die je niet met de hand zou willen uitvoeren. Hoe kun je dit nu eenvoudig automatiseren? Lees in dit blog hoe je een operator voor je kan laten werken

Een krachtige doorbaak

Ergens in 2018 maakte CoreOS het Operator Framework publiekelijk bekend. Een mijlpaal in de nog niet zo lange Kubernetes geschiedenis. Met dit framework werden de deuren geopend om Kubernetes uit te breiden met applicatie specifieke toepassingen. Gaandeweg kwamen, en komen, er steeds meer operators die het opereren van applicaties makkelijker maken door hun beheer complexiteit in dit framework te implementeren. Een mooi voorbeeld is bijvoorbeeld de Strimzi operator. Deze operator vat het beheren van Kafka clusters samen tot slechts enkele nieuw geïntroduceerde Kubernetes resources definitions, en hierdoor kan je een cluster aanmaken met slechts ‘1 simpele yaml’.

Maar, wat doet zo’n operator nou eigenlijk?

Een operator doet precies hetzelfde als Kubernetes zelf. Het observeert wijzigingen in het platform en op basis van deze wijzigingen neemt hij acties. Zo’n actie is bijvoorbeeld het deployen van nieuwe services, of ze juist weghalen. Om de Strimzi operator er dan nog een keer bij te pakken; deze operator is zo slim dat hij een upgrade van Kafka voor je kan vereenvoudigen. Als gebruiker verander je simpelweg de versie, en de operator ontzorgt jou van alle details die deze upgrade met zich meebrengt. De operator heeft de complexiteit van het upgraden geautomatiseerd, en in de achtergrond werden er nieuwe deployments gedaan, en ook weer weggehaald.

Vaak komt een operator gebundeld met een ‘custom resource definition’, in het kort CRD. Een CRD maakt het mogelijk om een eigen definitie op te stellen voor wat de operator moet beheren. Strimzi heeft bijvoorbeeld een ‘kafka’ CRD geïntroduceerd, en in deze definitie staat wat Strimzi moet weten om voor jou een cluster op te zetten. Simpel gezegd; de CRD is eigenlijk een nieuwe ‘yaml’ die gericht is op wat de operator wil doen. Een CRD is iets wat in het cluster moet worden geïnstalleerd, en is daarna ook voor het hele cluster beschikbaar. Dit betekent dan ook dat er cluster admin rechten nodig zijn om deze toe te voegen.

Hoe maak je dan zo’n operator?

Het observeren van veranderingen in het cluster, en daarop acteren? Dat klinkt als de oplossing voor het probleem wat ik schetste aan het begin van deze blog. Met een operator moet het mogelijk zijn om te kijken of er nieuwe namespaces gecreëerd worden, dat is namelijk een verandering in het cluster. We hebben geen CRD nodig. Eigenlijk moeten we alleen kunnen herkennen dat er een namespace wordt gecreëerd, om vervolgens wat extra acties uit te voeren.

Er zijn meerdere mogelijkheden om operators te maken. “Vroeger” was dit eigenlijk alleen mogelijk in go met de operator-sdk. De ontwikkelingen hebben echter niet stil gestaan, niet in het go landschap, maar zeker ook daarbuiten niet. Tegenwoordig is het ook mogelijk om operators te schrijven in andere talen en tools. We hebben in één van onze Meetups een keer uitgelegd hoe je een Ansible operator kan maken (zie ook onze slides). Daar hebben we laten zien hoe je de restanten van OpenShift builds, images en deployments netjes kan opruimen met een operator. En dit dus gewoon met Ansible.

Verschillende smaakjes

En er zijn nog meer frameworks beschikbaar om operators te maken. Zalando heeft bijvoorbeeld python framework gemaakt genaamd Kopf. Ook is er een shell operator waar je met shell scripts aan de gang kan gaan. Het is maar net welke smaak het beste past bij jouw probleem. Tegenwoordig kan je alle kanten op en de drempel om zelf een operator te maken is een stuk lager geworden.

De uitdaging

Weer even terug naar onze uitdaging. Nu we wat meer kennis van de materie hebben, zou het toch niet zo moeilijk moeten zijn om zelf een operator te maken die het één en ander standaard installeert als er een nieuwe namespace gemaakt is? In principe willen we alleen maar wat deployments of resources aanmaken, wat we normaal met de hand in een shell zouden intypen. De shell operator die ik eerder noemde zou dan perfect moeten matchen.

Als we dan toch bezig zijn, laten we er dan even iets langer over nadenken. Ik ben een fan van Kustomize (tevens een onderwerp tijdens een van onze Meetup geweest ;-), zie hier de slides). Het helpt mij om efficiënt resources te deployen. Ideaal zou zijn als ik gewoon een deployment doe van een kustomize configuratie.

Een annotatie op de namespace, die aangeeft wat voor type omgeving het is, en een operator die vervolgens een kustomize run doet? Dit zou het wel heel makkelijk maken. In dat geval worden ook de resources, die ik automatisch wil aanmaken, met kustomize beheerd.

Let’s go: de praktijk

Aan de slag! Het idee voor een environment-init operator is geboren. Ben je ongeduldig? In deze github repository zie je het eindresultaat, de tekst hieronder zoomt in op de kern van de implementatie van deze operator.

We beginnen eerst met ervoor te zorgen dat de tools die we nodig hebben beschikbaar zijn in de operator. In ons geval voegen we kustomize toe aan het docker image. We hebben een multi-stage docker image gebruikt hier, en in de eerste stap ‘download’ hebben we kustomize gedownload. In het uiteindelijke image kopiëren we de kustomize binary naar de bin folder. Op deze manier zorgen we ervoor dat alleen de software die we daadwerkelijk willen toevoegen ook in het image komen, en er bijvoorbeeld geen software op komt te staan die misbruikt zou kunnen worden.

FROM docker.io/flant/shell-operator:latest

COPY –from=download /bin/kustomize /bin/kustomize
ADD /environment /environment
ADD /hooks/*.sh /hooks

Verder voegen we een environment folder toe, waar de scripts in staan die gestart moeten worden wanneer er een namespace added event komt. In de hooks folder staan de scripts die door de operator gestart worden, en bepalen of er iets moet gaan gebeuren. In ons geval staat daar één script; namespace-hook.sh.

Het namespace-hook script wordt door de operator op twee manieren aangeroepen. De eerste keer, bij het initialiseren van deze shell operator, roept hij het script aan met –config. De shell operator verwacht dan een json terug waarin gespecificeerd wordt welke resources er geobserveerd moeten worden, en specifiek welke events. In ons geval willen we reageren op “added” events van “namespaces”. Met andere woorden, als er een nieuwe namespace gemaakt wordt.

if [[ $1 == “–config” ]] ; then
cat <<EOF
{
“configVersion”:”v1″,
“kubernetes”: [
{
“apiVersion”: “v1”,
“kind”: “Namespace”,
“watchEvent”: [“Added”]
}
]
}
EOF
exit 0
fi

Als er vervolgens een event komt, zoals deze hook was geconfigureerd, wordt het script nogmaals aangeroepen. Dit keer zonder het config argument. Er wordt een environment variable gezet “BINDING_CONTEXT_PATH”, waarin het path staat naar een json-file met de details van het event. In onze operator zijn we geïnteresseerd in namespaces die gecreëerd worden met een annotatie “envinit.joyrex2001.com/type”. Ook willen we weten om welke namespace het gaat. We gebruiken jq om de annotatie en name uit de json te plukken. Met een sanitize functie beschermen we ons script tegen code injection.

type=$(jq -r ‘.[0].object.metadata.annotations[“envinit.joyrex2001.com/type”]’ $BINDING_CONTEXT_PATH) type=$(sanitize ${type})

name=$(jq -r ‘.[0].object.metadata.name’ $BINDING_CONTEXT_PATH)
name=$(sanitize ${name})

We willen onze operator een kustomize script laten starten op basis van ‘type’. We doen dit door te kijken of er in de ‘environment’ folder een folder bestaat met de naam die in type staat. Als dit zo is, dan starten we daar een nieuw script met de naam ‘run.sh’. Op deze manier kunnen we verschillende scripts maken voor verschillende omgevingen, denk aan ‘dev’, ‘test’, of misschien zelfs iets als ‘wordpress’.

NAMESPACE=$1
[ -z “${NAMESPACE}” ] && log “Missing namespace (arg 1), bailing out…” && exit 1
kustomize build kustomize | kubectl apply -f – -n ${NAMESPACE}

Moeilijk te volgen? In een sequence diagram kunnen we dit proces als volgt samenvatten; 

De installatie

De operator is nu klaar wat betreft het coderen. Om de operator te installeren moeten we ervoor zorgen dat deze operator de juiste rechten heeft. De operator moet in ieder geval in staat zijn om namespace events te kunnen ontvangen. Maar omdat we in de operator ook daadwerkelijk deployments willen kunnen doen, geven we het service account waaronder de operator draait ‘edit’ rechten op het cluster.

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: envinit-operator-edit
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: edit
subjects:
– kind: ServiceAccount
name: envinit-operator-sa

Als je precies wil weten welke resources je wel of niet kunt installeren met deze operator, kan er natuurlijk ook een rol gedefinieerd worden die minder rechten heeft.