Hvordan skrive en Kubernetes Operator

Skrevet av: Bjorn Tungesvik, Vignesh Sivarajah

Kubernetes og containerbaserte plattformer er i vinden for tiden. Ut av boksen håndterer Kubernetes tilstandsløse applikasjoner uten nevneverdige tilpasninger i standard oppsett.

Men, hva med når du trenger å integrere legacy systemer inn i Kubernetes plattformen? Eller, når du har ressurser utenfor plattformen som skal være en del av applikasjonslivssyklusen? Har du tenkt på hvordan en integrerer organisasjonens sikkerhetsmønstre inn i plattformen?

Kubernetes Operatorer kan hjelpe deg med å løse mange av disse utfordringene.

Det er stadig flere bedrifter som velger å migrere fra monolitt-arkitektur til mikrotjenestearkitektur. En slik migrering innebærer containerisering av applikasjoner både on-premise og i skyen. Mikrotjenestearkitektur løser mange problemer, som blant annet “loose coupling” av applikasjoner, men byr samtidig på et sett med nye problemer.

Mikrotjenestearkitektur består av veldig mange små bevegelige deler som konstant er i endring på både godt og vondt. Dette kompliserer drift og forvaltning av tjenestene, da man raskt sitter med hundrevis av applikasjoner som igjen benytter tilsvarende antall infrastrukturtjenester. Det kan raskt bli en håpløs oppgave å holde styr på alt. Ved å fokusere på automatisering av drift og forvalting av mikrotjenester kan vi løse nevnte utfordringer.

Kubernetes kommer med en løsning på mange av disse problemene, men det er sjeldent et standard oppsett er tilstrekkelig å dekke alle behovene til en organisasjon. Ved innføring av ny teknologi vil organisasjoner ofte havne i en situasjon der det i tillegg er nødvendig å støtte gammel teknologi. Det kan være flere grunner til hvorfor systemer må integreres inn til den nye plattformen, deriblant avhengigheter til underliggende infrastruktur og ikke minst sikkerhetsmønstre knyttet til organisasjonen. I slike tilfeller kan det være nødvendig å automatisere integrasjonen mellom tjenester og plattform, slik at de enklere kan benyttes i den nye plattformen.

Manuelle bestillinger var standarden for monolittiske tjenester. Routing, nettverk, oppsett av databaser og diverse andre oppgaver ble utført etter manuelle bestillinger. Dette er greit når det er snakk om få tjenester, men uholdbart når man snakker om hundrevis eller tusenvis av mikrotjenester.

Kubernetes vil i de fleste tilfeller tilby tilstrekkelig funksjonalitet for “greenfield” prosjekter eller for organisasjoner med lite teknisk gjeld. Men i de få tilfellene det ikke er tilstrekkelig, kommer fleksibiliteten og utvidbarheten til Kubernetes til gode.

Fleksibiliteten til Kubernetes har over tid vist seg å være meget nyttig, spesielt for større organisasjoner der situasjonen gjerne er annerledes med mye teknisk gjeld og kultur som skal med i flytteprosessen. Komponenter som før var tett integrert med hverandre, er nå designet til å være mer fleksibelt og utvidbart. Dette gjøres via et godt dokumentert API, med klart definerte kontrakter og rikelig med funksjonalitet. Blant disse har man Custom Resource Defintions(CRD) som lar deg utvide APIet med egne obekter. Slike objekter får støtte for standard CRUD operasjoner i Kubernetes APIet, samtidig som vi kan skrive controllere og operators for implementere forretningslogikk rundt objektene.

I dette innlegget skal vi gi en kort introduksjon til hvordan vi kan utvide Kubernetes plattformen med en operator. Dette vil forhåpentligvis inspirere og vise hvordan en kan utvide plattformen med funksjonalitet som ikke dekkes av standard løsning.

Kubernetes tilbyr både deklarativ og imperativ tilnærming for beskrivelse av ressurser og ønsket tilstand. Deklarativ tilnærming betyr å beskrive ønsket tilstand via en konfigurasjonsfil enten i json eller yaml, mens imperativ tilnærming betyr en stegvis tilnærming hvor en spesfiserer hvert steg. Et eksempel kan være å beskrive ønsket tilstand via kubectl kommandoer.

Prosessen med å rulle ut en applikasjon i Kubernetes krever for eksempel:

  • Beskrivelse av hva man ønsker å deploye(Deployment, Pod, ConfigMap)
  • Konfigurasjon(cpu, minne, labels)
  • Antall instanser

Controllere orkestrerer nevnt prosess slik at en bestemt ressurs oppnår ønsket tilstand. Dette skjer gjennom “reconcilitiation”.

Reconciliation er en prosess som lytter på hendelser til en ressurs og foretar endringer i henhold til ønsket tilstand.

Det finnes tre typer endringer; CREATED, UPDATED, og DELETED. Ved å aktivt lytte og agere til hendelsene kan vi oppnå ønsket tilstand.

https://www.openshift.com/blog/kubernetes-operators-best-practices

En operator er en eller flere controllere med ansvar for en spesifikk type workload og/eller ressurs. Det vanlige er å utvikle en CRD, som er en beskrivelse av ønsket tilstand, i kombinasjon med Controller(e). I en CRD kan vi definere nye ressurser og dermed utvide Kubernetes med skreddersydd logikk. En CRD vil inneholde konfigurasjonen som Controlleren fanger opp når den agerer på hendelser. Controllere er fleksible og kan utvikles til å inneholde egendefinert logikk slik at man selv bestemmer hva som gjøres med hendelser.

Vi skal i dette innlegget utvikle en enkel operator som provisjonerer s3 buckets i AWS med tilhørende IAM ressurser for tilgangskontroll. Kort fortalt skal det opprettes en CRD som beskriver konfigurasjon som videre blir brukt for å opprette, oppdatere eller slette en S3 bucket i AWS.

Vi utvikler operatoren vår i go, men det kan nevnes at det finnes uoffisielle alternativer til operator-sdk for andre språk. Vi lar det være opp til leseren å utforske disse.

https://github.com/systek/s3-operator

Operator-sdk er et bibliotek som gjør det enklere å skrive en operator. Biblioteket benytter seg av kodegenerering og tilbyr en rekke abstraksjoner slik at vi kan fokusere på forretningslogikk fremfor kubernetes detaljer. Det kommer til å bli klart etterhvert som vi begynner å skrive operatoren vår.

Vi genererer prosjektet ved å benytte klientverktøyet som medfølger operator-sdk.

operator-sdk init --domain systek.no --repo github.com/systek/s3-operator

Neste steg er å utvide kubernetes med vår egen Custom Resource Definition. Her beskriver vi til Kubernetes hvordan det nye S3 objektet ser ut og hvordan det skal valideres.

operator-sdk create api --version v1 --kind S3

Kommandoen over genererer all nødvendig boilerplate kode rundt interaksjonen med det nye objektet. Dette inkluderer selve definisjonen og nødvendig klientkode. Definisjonen opprettes under api/v1og tilhørende controller genereres under controllersmappen.

Innholdet i S3Spec, S3Status er tilpasset for å dekke de funksjonelle behovene til S3 operatoren.

Kubernetes skiller mellom ønsket state for et objekt → Spec og nåværende tilstand for objektet →Status. I S3Spec feltet legger vi til en komplett beskrivelse og nødvendig konfigurasjon av ønsket state. S3Status beskriver nåværende tilstand i systemet og er håndtert av automatiske prosesser → controllere. Det er statusfeltet som styrer når det er behov for å utføre logikk i reconciler loopen. Statusfeltet blir modulert som en subresource for å unngå dirty writes.

Videre bruker vi Operator-sdk igjen for kodegenerering.

make manifests

Kommandoen genererer en CRD med OpenAPI valideringsregler og tilhørende manifestfiler som er nødvendig for å deploye en operator. OpenAPI schema validerer objektet ved opprettelse og oppdatering av objektet. En komplett liste over valideringsregler kan du se her. Under kan du se filer generert av make manifests.

❯ tree -l
.
|-- certmanager
| |-- certificate.yaml
| |-- kustomization.yaml
| `-- kustomizeconfig.yaml
|-- crd
| |-- bases
| | `-- systek.no_s3s.yaml
| |-- kustomization.yaml
| |-- kustomizeconfig.yaml
| `-- patches
| |-- cainjection_in_s3s.yaml
| `-- webhook_in_s3s.yaml
|-- default
| |-- kustomization.yaml
| |-- manager_auth_proxy_patch.yaml
| `-- manager_config_patch.yaml
|-- manager
| |-- controller_manager_config.yaml
| |-- kustomization.yaml
| `-- manager.yaml
|-- prometheus
| |-- kustomization.yaml
| `-- monitor.yaml
|-- rbac
| |-- auth_proxy_client_clusterrole.yaml
| |-- auth_proxy_role.yaml
| |-- auth_proxy_role_binding.yaml
| |-- auth_proxy_service.yaml
| |-- kustomization.yaml
| |-- leader_election_role.yaml
| |-- leader_election_role_binding.yaml
| |-- role.yaml
| |-- role_binding.yaml
| |-- s3_editor_role.yaml
| `-- s3_viewer_role.yaml
|-- samples
| |-- _v1_s3.yaml
| `-- kustomization.yaml
`-- scorecard
|-- bases
| `-- config.yaml
|-- kustomization.yaml
`-- patches
|-- basic.config.yaml
`-- olm.config.yaml

make manifestsmå kjøres på nytt når det er endringer i CRD eller Kubebuilder annotasjoner.

I vårt eksempel oppretter vi to controllere; primær-controller → ansvar for opprettelse av S3 buckets og IAM ressurser, sekundær-controller → håndterer sletting av IAM ressurser. IAM ressursene bindes opp mot et kubernetes secret objekt for å lagre sensitiv informasjon og ikke minst nødvendig metadata for å ivareta bindingen til IAM ressursene.

s3-operator

Controllere, som tidligere nevnt orkestrerer prosessen slik at en bestemt ressurs oppnår ønsket tilstand. Dette skjer i reconciler loopen.

Hovedsakelig består reconcileren av tre deler

  • Opprettelse
  • Modifikasjon
  • Sletting

Alt av logikk foregår inne i Reconciler funksjonen. Vi vil beskrive i detaljer hvordan de tre delene blir implementert, supplert med kode.

Vi starter med å introdusere s3 objektet vi deployer til Kubernetes. Et forholdsvis enkelt objekt som har et felt; bucketName. S3 objektet deployes med følgende kommando.

kubectl apply -f s3.yaml
s3.yaml

Eventet fanges opp av reconciler funksjonen og utfører et eksternt kall mot AWS for å opprette ressursene.

Reconcile loopen sørger for synkronisasjon av ressurser for å oppnå ønsket tilstand. En reconcile loop stopper ikke før alle betingelser i forretningslogikken er oppfylt, som kan styres av flere ulike returverdier.

  • return reconcile.Result{}, err— En error blir logget og eventen blir lagt tilbake til køen. Siden denne blir sendt tilbake til reconcile loopen er det viktig at en er sikker på at feilen kan fikses gjennom rekjøring av reconcile.
  • return reconcile.Result{Requeue: true}, nil — Legger eventen tilbake i køen uten error.
  • return reconcile.Result{}, nil — Avslutter reconcile loopen
  • return reconcile.Result{RequeueAfter: time.Second, Requeue: true}, nil— Reconcile igjen etter x tid.

Som tidligere nevnt skiller Kubernetes mellom ønsket state for et objekt, Spec, og nåværende tilstand for objektet, Status. I Spec feltet legger vi ved en komplett beskrivelse ønsket state, og konfigurasjon som er nødvendig for å nå ønsket state. Statusfeltet beskriver nåværende tilstand i systemet og er håndtert av automatiske prosesser; controllere. Det er statusfeltet som styrer når det er behov for å utføre logikk i reconcile loopen.

For å oppdatere/modifisere S3 objektet sjekker vi om det er en forskjell mellom nåværende Status og Spec.

I vårt tilfelle er det en endring når navn på bucket har endret seg; da sletter vi s3 ressursen sammen med tilhørende secret.

Sletting av en Custom Resource Definition(CRD) kan være komplisert. En kan måtte tas hensyn til rekkefølge på sletting av ressurser som ligger utenfor Kubernetes. Det kan også være avhengigheter på objekter og ressurser internt som må løses før det kan slettes. Kubernetes byr på et sett med innstillinger som hjelper med å takle nevnte utfordringer:

  • Finalizer
  • Owner references
  • Cascading delete

Kommandoen for å slette et S3 objekt er som følger:

kubectl delete S3 s3-bucket-v1

Når vi sletter et objekt som krever ekstra opprydding av ressurser, legger vi på en Finalizer. Finalizer forhindrer sletting av objektet før finalizeren er fjernet.

Når et objekt med finalizer blir slettet, setter Kubernetes enDeletionTimestamppå objektet for å markere det som “klar for sletting” . Det er designet for å varsle controllere om at objektet krever opprydding før det kan slettes. Når controlleren oppdager DeletionTimestampfeltet, kjøres det logikk for å fjerne alle eksterne ressurser og tilslutt fjernes finalizeren.

I vår operator bruker vi finalizers for å kunne rydde bort provisjonerte AWS ressurser. Owner references brukes for å instruere hvordan og hvilke objekter som skal slettes. S3 objektet bruker cascading delete på tilknyttede kubernetes secret.

Kort oppsummert om sletting av S3 objekt i kronologisk rekkefølge:

  • kubectl delete S3 s3-bucket-v1trigger en event i Kubernetes. Det er satt på en owner reference fra S3 → Secrets, som forhindrer sletting av S3 før tilhørende secret er slettet. Dette medfører til en reconcile på secrets controlleren(sekundær controlleren)
  • Inne i reconciler funksjonen til secrets controlleren slettes AWS policy, AWS accesskey, AWS user.
  • Finalizeren i secrets controllerenfjernes slik at secrets objektet kan slettes.
  • S3 Bucket blir slettet fra AWS. Kallet blir gjort fra primær-controlleren/s3-controller.
  • Finalizer i s3 controlleren fjernes slik at S3 objektet frigjøres.

Deploye og teste Operatoren

Kommandoen under genererer, formaterer og validerer koden, i tillegg til å bygge binærfilen.

make all

docker-build tagger og bygger imaget med navnetlocalhost:5000/s3-operator

make docker-build

docker-push laster opp imaget til lokalt registry.

make docker-push

Deploy ruller ut alle Kubernetes manifester til clusteret under namespacet s3-operator-system. For at S3 operatoren skal kunne fungere trenger den tilgang til AWS. https://github.com/systek/s3-operator/blob/master/set_config.sh → legger inn AWS access keyog secret keyinn i Deployment, forutsatt at de ligger som miljøvariabler i ditt utviklingsmiljø.

make deploy

Statusen til Operatoren som ble deployet kan ses ved å kjøre kommandoen under.

❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
s3-operator-controller-manager-764b9c4bc-nkzr9 2/2 Running 0
69s

Feilsøking kan gjøres ved å se på loggen til controlleren

❯ kubectl logs s3-operator-controller-manager-764b9c4bc-nkzr9 manager

Konklusjon

I dette innlegget har vi sett hvordan vi kan lage en enkel operator for å provisjonere S3 buckets. Denne operatoren er ikke spesielt nyttig i seg selv, men vi håper dere er inspirert og ser mulighetsrommet som kombinasjonen av CRDer og operators gir. Avsluttningsvis kan det være vært å nevne noen få tips og triks når en jobber med operators:

  • Reconcile loopen bør være idempotent.
  • Unngå design hvor en controller har ansvar for reconcile av flere typer objekter. I tilfeller der det er behov for å styre flere objekter er det en fordel å splitte opp i flere operatorer og/eller controllere.
  • Det anbefales ikke å la et objekt bli styrt av flere operatorer.
  • Bruk predicates for å redusere kommunikasjonen med kubernetes APIet
  • Om en lager ressurser i hierarkier bør en bruke sette “owner reference” på tilknyttede objekter. Dette vil gjøre det enklere å håndtere livssyklusen til objektene.
  • Status bør håndteres som en subresource. Når status modelleres som en subresource vil ikke en oppdatering resultere i en ny generasjon av manifestet. Sammen med predicates vil det hindre at reconcile prosessen trigges for statusoppdateringer.
  • Bruk finalizers når objektet krever en form for opprydding.
  • Tenk nøye igjennom hvordan feil skal håndteres i reconcile loppen. Når en controller returnerer en feil vil denne bli logget, og eventen lagt tilbake i køen. Dette kan i verste fall resultere i en evig loop.
  • Skriv tester. Operator-sdk kommer med EnvTest for intergrasjonstester og FakeClient for unit tester.

Systek er et IT-konsulentselskap med hovedsete i Oslo. Denne bloggen er et sted hvor våre konsulenter ytrer seg om hva vi brenner for innen teknologi og metode i IT-prosjekter.