În ultima perioadă cuvântul Kubernetes se aude mai des în lumea IT decât cuvântul antibiotic în farmacii. Amândouă sunt unelte utile, care se pot aplica după o analiză aprofundată de situație.. Asupra fiecăreia dintre ele planează riscul de aplicare inutilă sau necorespunzătoare care cauzează complicații nedorite.
Wikipedia definește Kubernetes (în mod obișnuit prescurtat ca k8s) ca fiind "un sistem de gestionare a containerelor open source pentru automatizarea implementării, scalării și distribuirii aplicațiilor. Acesta a fost inițial proiectat de Google și este acum un proiect întreținut de Cloud Native Computing Foundation. Funcționează cu o gamă largă de unelte pentru containerizare, inclusiv cu Docker."
În termeni practici, Kubernetes ne permite să ne gândim la infrastructura noastră ca la o colecție de resurse de computație pe care putem rula aplicații containerizate conform regulilor și necesităților definite de către ingineri, fără a depinde de locul și particularitățile execuţiei. Nu întâmplător, termenul grecesc kubernetes înseamnă "guvernator", " cârmaci " sau "căpitan". Acesta ascultă cerințele declarate de către călători (ingineri) și se asigură că navele (serviciile containerizate) ajung la destinația (starea) dorită, printr-un traseu fezabil și optim.
K8s este compus din două sisteme mari: Master și Nodes.
Multitudinea componentelor care alcătuiesc sistemul Master se mai numesc și Control Plane, fiindcă scopul lor principal este de a controla starea clusterului. Interacțiunile externe cu un cluster de k8s se declanșează prin apelarea metodelor oferite de kube-apiserver. Obiectele durabile sunt salvate într-un cluster de etcd, care este un key-value store distribuit, redundant și highly available, care implementează protocolul Raft. Componenta kube-scheduler este responsabilă să ofere un nod din cluster care este ales să ruleze un serviciu containerizat, respectând restricțiile definite de către inginer. Aceste restricții pot fi legate de necesitățile resurselor de computație a unei aplicații: CPU sau Memorie, dar și reguli mai sofisticate de afinitate: un cache și un web api să fie co-locate, sau anti afinitate: o instanță de PostgreSQL primary și replicile ei să nu fie executate pe același nod, pentru a nu le pierde simultan în caz de defecțiune.
Fiecare obiect gestionat de k8s are o specificație, în care este definită starea dorită a obiectului, numit spec. Pe de altă parte, este monitorizată periodic starea fiecărui obiect, starea observată reflectându-se în proprietatea numită status. Componenta kube-controller-manager este responsabilă să reconcilieze starea observată și să acționeze asupra obiectului într-un mod prin care să ajungă în starea dorită.
Decizia autorilor de a oferi o semantică declarativă oferă un avantaj major, atât în definirea manifestelor de obiecte, cât și în procesele de investigare a eventualelor probleme. Putem să fim siguri că k8s aplică consistent strategiile implementate în controllerele de obiecte, fără a fi nevoie ca operatorul să solicite schimbări în mod imperativ. Putem observa acest trend al folosirii DSL-urilor (Domain Specific Language) declarative și la alte unelte pentru gestionarea infrastructurii (ex. HashiCorp Terraform), dar și în paradigme de programare actuale, cum ar fi programarea funcțională și reactivă.
Ultima componentă și singura opțională a Control Plane-ului, cloud-controller-managerul, este specifică mediului de rulare, fiind implementată de către companiile de IaaS și cloud. Scopul ei este de a ușura integrarea dintre concepte din k8s și servicii de cloud, cum ar fi un load balancer, definit independent de implementare și în mod abstract într-un obiect de tip Service în k8s. Implementarea se realizează prin asocierea unui load balancer specific platformei de cloud (de ex. AWS ELB/ALB/NLB).
Celălalt sistem principal dintr-o instalație de k8s este colecția node-urilor, numit și Data Plane. Ele rulează două servicii: kubelet, care este un agent responsabil de comunicarea cu control plane-ul, și kube-proxy care facilitează comunicarea între serviciile instalate pe cluster, indiferent unde se află acestea.
Obiectul principal care definește o unitate executabilă pe un cluster de k8s este numit pod (adică păstăi), care este o abstracție asupra unor sau a mai multor containere(de ex. Docker). Această abstracție permite rularea mai multor componente care alcătuiesc un serviciu. Este anti-pattern folosirea podului pentru a rula stackuri întregi de aplicații, ca : Spring Boot App + PostgreSQL, Memcached rulat într-un singur pod. Cazurile de containere multiple în același pod sunt în mare majoritate situații în care podul este alcătuit dintr-un container serviciu principal, care implementează o capabilitate de business și unul sau mai multe containere auxiliare, cum ar fi un sidecar pentru mTLS. Mai există și noțiunea de init-container, care va fi pornit înainte de containerele standard și care permite inițializarea și configurarea mediului de execuție.
Chiar dacă podul este entitatea principală a ecosistemei k8s, aproape niciodată nu le creăm în mod direct. Există posibilitatea să creăm poduri în mod explicit. Dar în acest caz nici un mecanism al clusterului nu va veghea asupra existenței lui: dacă se oprește pentru o eroare, sau se pierde node-ul pe care este executat, acesta nu va mai fi repornit sau relocat. Există concepte și resurse de nivel mai înalt, cum ar fi Deployment, DaemonSet sau StatefulSet cu care putem rula poduri și prin care predăm Control Plane-ului, mai exact controllerelor specifice(ex. DeploymentController, DaemonSetController), responsabilitatea de a le controla ciclul de viață.
Un obiect de tip Deployment, odată definit, va avea atașat un ReplicaSet care la rândul lui definește un număr fix de poduri de același tip și servește ca mecanism direct de scalare orizontală. În afară de asigurarea unui număr de poduri rulate simultan prin ReplicaSet-ul atașat, un Deployment ne ajută și la orchestrarea schimbării acestora: la un potențial upgrade înlocuirea podurilor se va întâmpla după strategia stabilită în Deployment.
Majoritatea podurilor sunt efemere și nu au nevoie ca starea lor să fie durabilă între execuții. În cazuri în care avem nevoie de poduri cu stare durabilă, putem opta pentru a le crea prin StatefulSet-uri, care vor asocia un volum persistent la fiecare pod. Un exemplu bun ar fi rularea a 3 node-uri de HashiCorp Consul în mod HA, folosind un cvorum de 3 instanțe, unde fiecare pod are atașat un sistem de fișiere bine definit care se păstrează și la o eventuală schimbare a imaginii. Acest lucru este ușor de sesizat și dacă ne uităm la podurile care sunt create de către un StatefulSet, putem observa un număr de indice în numele lor(ex. consul-1, consul-2, consul-3).
O altă necesitate în privința rulării de poduri apare în momentul în care vrem să rulăm un anumit pod, pe fiecare k8s node din cluster și să ne asigurăm ca numărul podurilor va fi ajustat automat, indiferent dacă mărimea clusterului crește sau se reduce. Această strategie de rulare o putem defini prin folosirea unui DaemonSet. Câteva exemple de poduri care, de regulă, se definesc prin DaemonSet sunt : un agent de forwardarea logurilor sau un agent -atenție! nu server- de Consul.
Este ușor de constatat că aspectele execuției unor aplicații sunt elegant controlabile, tot ceea ce trebuie să facem este să definim starea dorită, iar controllerul specific tipului de obiect va executa acțiunile necesare pentru a ajunge în această stare.
Merită menționat că, dacă necesitățile noastre nu sunt acoperite de către mecanismele standard ale k8s-ului, atunci putem să implementăm propria noastră componentă de orchestrare. Această posibilitate este destul de nouă, fiind implementată de către cei de la RedHat (pentru varianta lor de k8s, numit OpenShift). Astfel de componente se numesc operatori (operators).
În compania unde lucrez, utilizăm practici moderne de machine learning, mai exact învățăm modele vaste și le actualizăm periodic. De fiecare dată când se salvează un model actualizat și proaspăt învățat, avem nevoie să se execute un rolling-update pe componenta care depinde de modelul respectiv pentru a lua decizii. Astfel putem actualiza datele pe baza cărora facem decizii, în mod elegant și frecvent. Această problemă a fost relativ ușor de rezolvat implementând operatorul nostru, cu logică customizată de orchestrare. Există un framework prin care putem scrie ușor un operator numit operator-sdk, care necesită implementarea logicii de orchestrare în limbajul de programare Go și atașându-se la punctele de extensii oferite de către k8s.
În companiile mari cu infrastructuri stufoase și complexe, necesitatea de a avea o abstracție asupra resurselor de computație este una evidentă. Nu poate exista Google, Facebook sau Amazon fără că echipele de dezvoltare să fie înzestrate cu un limbaj de nivel înalt în care își pot exprima expectanțele în legătură cu aspectele de operare a serviciilor gestionate. În astfel de contexte, nici nu este dezbătută legitimitatea unor soluții pentru scheduling distribuit. Nu este o coincidență că Google este autorul original a k8s-ului, ci există o cauzalitate: Google și-a creat o soluție prin care a fost capabil să gestioneze milioanele de servere pe care le deține.
Din ce în ce mai multe companii cu inventar mai modest prezintă interes pentru a se migra pe k8s sau în a-l încerca. Chiar și startupuri mici care au de gestionat o platformă SaaS pot beneficia de un cluster de k8s. Pană la urmă farmecul și avantajul unei cluster constă în simplitatea de a scala în plan orizontal. Folosite corespunzător, uneltele oferite de k8s pot fi arme esențiale în mână inginerilor, ușurând viața atât în timp de dezvoltare cât și în timpul deploymentului și a operațiilor zilnice. Trendul și paradigma de a dezvolta produse ca microservicii aduce simplitate și posibilitatea de a identifica entitățile unui context limitat și specific de domeniu. Complexitatea arhitecturală și operațională crește proporțional cu granularitatea și numărul serviciilor, interacțiunea dintre componente, iar definirea și urmărirea planului de operare devin complexe în mod exponențial.
Revizitând analogia din deschiderea articolului, în care am afirmat trendul incontestabil în ceea ce privește adopția k8s, conștienți de riscul de a fi aplicat în situații în care nu este o nevoie întemeiată, dar în cunoștință de cauză, cred că putem afirma că acest kubernetes nu este salvatorul neamului de programatori. Cu toate acestea, putem cădea de acord că oferă soluții bine gândite pentru probleme des întâlnite în viața unui proiect de software, nemaivorbind de multitudinea uneltelor și soluțiilor auxiliare pe care le primim gata integrate.
de Mircea Vădan
de Ovidiu Mățan
de Diana Țelman