werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

27. svibnja u glavnoj dvorani konferencije DevOpsConf 2019, koja se održava u okviru festivala RIT++ 2019, u okviru sekcije „Neprestana isporuka“, održano je predavanje „werf — naš alat za CI/CD u Kubernetesu“. U njemu se govori o problemima i izazovima s kojima se svi susreću prilikom implementacije u Kubernetes, kao i o nijansama koje možda nisu odmah vidljive. Razmatrajući moguće puteve rješenja, prikazujemo kako je to ostvareno u Open Source alatu werf.

Od trenutka prikazivanja, naš alat (ranije poznat kao dapp) prešao je povijesnu granicu od 1000 zvijezda na GitHubu — nadamo se da će rastuća zajednica korisnika olakšati život mnogim DevOps inženjerima.

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Dakle, predstavljamo video s predavanja (~47 minuta, puno informativnije od članka) i glavnu sažetak iz njega u tekstualnom obliku. Počnimo!

Isporuka koda u Kubernetes

Tema predavanja će biti više ne o werfu, već o CI/CD u Kubernetesu, podrazumijevajući da je naš softver upakovan u Docker kontejnere (o tome sam govorio u predavanju iz 2016. godine), a K8s će se koristiti za pokretanje u produkciji (o tome — u 2017. godini).

Kako izgleda isporuka u Kubernetes?

  • Postoji Git repozitorij s kodom i uputama za izradu. Aplikacija se gradi u Docker slici i objavljuje u Docker Registry.
  • U istom repozitoriju postoje i upute o tome kako implementirati i pokrenuti aplikaciju. U fazi implementacije, ove upute se šalju u Kubernetes, koji preuzima potrebnu sliku iz registry i pokreće je.
  • Osim toga, obično postoje i testovi. Neki od njih se mogu izvršiti prilikom objavljivanja slike. Također, može se (po istim uputama) rasporediti kopija aplikacije (u zasebnom prostoru imena K8s ili zasebnom klasteru) i izvršavati testove tamo.
  • Na kraju, potrebni su sustavi CI koji primaju događaje iz Gita (ili pritiske gumba) i pozivaju sve navedene faze: izrada, objavljivanje, implementacija, testiranje.

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Evo nekoliko važnih napomena:

  1. Budući da imamo nepromjenjivu infrastrukturu (immutable infrastructure), slika aplikacije koja se koristi u svim fazama (staging, produkcija itd.) mora biti jedna. Više o tome i s primjerima sam govorio ovdje.
  2. Budući da pratimo pristup infrastruktura kao kod (IaC), kod aplikacije, upute za njegovu izradu i pokretanje trebaju biti upravo u jednom repozitoriju. Više o tome — pogledajte u tome istom predavanju.
  3. Lanac isporuke (delivery) obično vidimo ovako: aplikacija je izgrađena, testirana, objavljena (faza release) i to je to — isporuka je izvršena. No u stvarnosti korisnik dobiva ono što ste objavili, ne kada ste to isporučili u proizvodnju, a kada je mogao pristupiti toj proizvodnji i kada je ta proizvodnja radila. Stoga smatram da lanac isporuke završava samo na fazi eksploatacije (run), a ako želimo biti precizniji, čak i u trenutku kada je kod uklonjen iz proizvodnje (zamjenom novim).

Vratimo se na gore navedenu shemu isporuke u Kubernetesu: izmislili su je ne samo mi, već doslovno svatko tko se bavio tim problemom. U osnovi, ovaj obrazac sada nazivamo GitOps (više o pojmu i idejama koje stoje iza njega možete pročitati ovdje). Pogledajmo faze sheme.

Faza izgradnje (build)

Činilo bi se da se 2019. godine može reći mnogo o izradi Docker slika, kada svi znaju pisati Dockerfile-ove i pokretati docker build?.. Вот нюансы, на которые хотелось бы обратить внимание:

  1. Veličina slike ima značaj, stoga koristite multi-stage, kako biste u slici ostavili samo ono što je zaista potrebno za rad aplikacije.
  2. Broj slojeva treba minimizirati spajanjem nizova od POCNI-komandi po značenju.
  3. Međutim, to dodaje probleme debuggiranju, budući da prilikom pada izgradnje trebate pronaći onu pravu komandu iz lanca koja je uzrokovala problem.
  4. Brzina izgradnje je važna, jer želimo brzo implementirati promjene i gledati rezultate. Na primjer, ne želimo ponovo graditi ovisnosti u bibliotekama jezika pri svakom buildanju aplikacije.
  5. Često se iz jednog Git repozitorija zahtijeva više slika, što se može riješiti setom od Dockerfile-ova (ili imenovanim fazama u jednom datoteci) i Bash skriptom za njihovo sekvencijalno izgradnju.

Ovo je samo vrh sante leda s kojim se svi suočavaju. No postoje i drugi problemi, između ostalog:

  1. Često tokom izgradnje trebamo nešto montirati (na primjer, cacheirati rezultat komande poput apt-a u vanjskom direktoriju).
  2. Želimo Ansible umjesto pisanja na shelle.
  3. Želimo graditi bez Dockera (zašto nam treba dodatna virtualna mašina u kojoj treba sve za to postaviti, kada već postoji Kubernetes klaster u kojem se mogu pokretati kontejneri?).
  4. Paralelno graditi, što se može različito shvatiti: razne komande iz Dockerfile-a (ako se koristi multi-stage), nekoliko commit-a jednog repozitorija, nekoliko Dockerfile-ova.
  5. Distribuirana izgradnja: želimo prikupljati nešto u podovima koji su "efemerni", jer im keš nestaje, a to znači da ga treba pohraniti negdje drugo.
  6. Na kraju, vrh želja sam nazvao automagijom: bilo bi idealno ući u repozitorij, upisati neku naredbu i dobiti gotovu sliku, sastavljenu s razumijevanjem kako i što pravilno učiniti. Ipak, osobno nisam siguran da se svi detalji mogu tako predvidjeti.

I evo projekata:

  • moby/buildkit — graditelj tvrtke Docker Inc (već integriran u aktualne verzije Dockera), koja pokušava riješiti sve te probleme;
  • kaniko — graditelj iz Googlea, koji omogućuje sastavljanje bez Dockera;
  • Buildpacks.io — pokušaj CNCF-a da napravi automagiju i, između ostalog, zanimljivo rješenje s rebaseom za slojeve;
  • i još mnoštvo drugih alata, kao što su buildah, genuinetools/img…

… i pogledajte koliko imaju zvjezdica na GitHubu. Dakle, s jedne strane, docker build ima i može nešto učiniti, ali u stvarnosti pitanje nije do kraja riješeno — dokaz tome je paralelni razvoj alternativnih graditelja, od kojih svaki rješava neki dio problema.

Gradnja u werfu

Tako smo došli do werf (ranije poznatoj kao dapp) — Open Source alat tvrtke "Flant", koji radimo već dugi niz godina. Sve je počelo prije 5 godina sa Bash skriptama koje optimiziraju sastavljanje Dockerfile-a, a posljednje 3 godine odvija se puna razvojna aktivnost u okviru jednog projekta s vlastitim Git repozitorijem (isprva na Ruby-ju, a potom prepisali na Go, a usput i preimenovali). Koje su probleme sastavljanja riješili u werfu?

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Problemi obojeni plavom su već realizirani, paralelno sastavljanje je napravljeno unutar jednog hosta, a pitanja označena žutom planiramo dovršiti do kraja ljeta.

Faza objavljivanja u registru (publish)

Prikupili smo docker push… — što bi moglo biti teško u tome da se slika učita u registar? I ovdje se javlja pitanje: "Koju oznaku staviti slici?" Ono nastaje zbog toga što imamo Gitflow (ili drugu Git strategiju) i Kubernetes, a industrija teži tome da se ono što se događa u Kubernetesu uskladi s onim što se radi u Gitu. Jer Git je naš jedini izvor istine.

Što je u tome teško? Jamčiti reproducibilnost: od commit-a u Gitu, koji po svojoj prirodi ostaje nepromijenjen (immutable), do Docker slike koja se mora čuvati istom.

Nama je također važno utvrditi podrijetlo, jer želimo razumjeti iz kojeg je commit-a aplikacija građena koja se pokreće u Kubernetesu (tada možemo raditi razlike i slične stvari).

Strategije označavanja

Prva — jednostavna git tag. Imamo registry s slikom, označenom kao 1.0. U Kubernetesu imamo stage i production, gdje je ta slika preuzeta. U Git-u radimo commit-e i u nekom trenutku postavljamo oznaku 2.0. Građujemo je prema uputama iz repozitorija i stavljamo u registry s oznakom 2.0. Implementiramo na stage, a ako je sve uredu, onda na production.

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Problem ovog pristupa je što prvo postavljamo oznaku, a tek onda testiramo i objavljujemo. Zašto? Prvo, to je jednostavno nelogično: izdajemo verziju softvera koji još nismo provjerili (ne možemo drugačije, jer da bismo provjerili, trebamo postaviti oznaku). Drugo, takav put se ne slaže s Gitflow.

Druga opcija — git commit + tag. U master grani postoji oznaka 1.0; za nju u registry je slika, implementirana na production. Osim toga, u Kubernetes klasteru postoje konture preview i staging. Dalje slijedimo Gitflow: u glavnoj grani za razvoj (develop) radimo nove značajke, uslijed čega nastaje commit s identifikatorom #c1. Građujemo ga i objavljujemo u registry, koristeći taj identifikator (#c1). S istim identifikatorom implementiramo na preview. Slično činimo s commit-ima #c2 i #c3.

Kada shvatimo da je značajki dovoljno, počinjemo sve stabilizirati. U Git-u kreiramo granu release_1.1 (na osnovi #c3 iz develop). Građenje ovog izdanja neće biti potrebno, jer je to već učinjeno u prethodnoj fazi. Stoga ga možemo jednostavno implementirati na staging. Ispravljamo greške u #c4 i slično objavljujemo na staging. Paralelno se u isto vrijeme odvija razvoj u develop, kamo povremeno preuzimamo promjene iz release_1.1. U nekom trenutku dobivamo dignuti i objavljen commit na staging-u, s kojim smo zadovoljni (#c25).

Tada radimo merge (s fast-forward) izdane grane (release_1.1) u master. Stavljamo oznaku na ovaj commit s novom verzijom (1.1). No, ova slika je već sakupljena u registry, tako da, kako ne bismo opet sakupljali, jednostavno dodajemo drugu oznaku na postojeću sliku (sada ima oznake #c25 i 1.1). Nakon toga implementiramo ga na production.

Postoji nedostatak, što je na staging-u implementirana jedna slika (#c25), a na production-u — kao da je druga (1.1), ali znamo da je 'fizički' to ista slika iz registry.

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Pravi minus je taj što nema podrške za merge commit-ove, moramo raditi fast-forward.

Možemo ići dalje i napraviti trik... Razmotrimo primjer jednostavnog Dockerfile-a:

FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb

FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public

Izgradimo datoteku prema načelu da ćemo uzeti:

  • SHA256 od identifikatora korištenih slika (ruby:2.3 i nginx:alpine), koji predstavljaju kontrolne sume njihovog sadržaja;
  • sve naredbe (POCNI, CMD itd.);
  • SHA256 od datoteka koje su dodane.

... i uzet ćemo kontrolnu sumu (ponovno SHA256) od takve datoteke. To je potpis svega što definira sadržaj Docker slike.

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Vratimo se shemi i umjesto commit-a koristit ćemo takve potpise, tj. označavati slike potpisima.

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Sada, kada zatreba, na primjer, 'merge' promjene iz izdanja u master, možemo napraviti pravi merge commit: imat će drugačiji identifikator, ali istu potpis. S istim identifikatorom izvest ćemo sliku i na produkciju.

Nedostatak je u tome što sada ne možemo odrediti koji je commit izvučen na produkciju - kontrolne sume rade samo u jednom smjeru. Ovaj problem rješava dodatni sloj s metapodacima - više ću detaljno objasniti kasnije.

Označavanje u werfu

U werfu smo otišli još dalje i pripremamo raspodijeljenu izgradnju s cache-om koji se ne pohranjuje na jednoj mašini... Dakle, sastavljamo Docker slike dvaju tipova, nazivamo ih ) - jedinica organizacije pipeline-a, sadrži 1+ zadatak, i image.

U Git repozitoriju werf pohranjene su specifične upute za izgradnju, koje opisuju različite faze izgradnje (beforeInstall, install, beforeSetup, setup). Prvu stage sliku sastavljamo s potpisom, koji je definiran kao kontrolna suma prvih koraka. Zatim dodajemo izvorni kod, za novu stage sliku računamo njenu kontrolnu sumu... Ove operacije se ponavljaju za sve faze, kao rezultat čega dobivamo set od stage slika. Zatim radimo konačnu image sliku, koja također sadrži metapodatke o njenom porijeklu. I tu sliku označavamo na različite načine (više detalja kasnije).

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Neka nakon toga nastane nova promjena, u kojoj je izmijenjen samo kod aplikacije. Što će se dogoditi? Za promjene koda bit će stvoren zakrpa, pripremljen nova stage slika. Njegova potpis bit će određena kao kontrolna suma stare stage slike i nove zakrpe. Iz te slike bit će formirana nova finalna slika.

Dakle, stage slike — keš koji se može pohranjivati distribuirano, a slike koje se iz njega stvaraju se učitavaju u Docker Registry.

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Čišćenje registra

Riječ nije o brisanju slojeva koji su ostali visi nakon obrisanih oznaka — to je standardna mogućnost samog Docker Registry. Riječ je o situaciji kada se nakuplja mnoštvo Docker oznaka i shvaćamo da nam neki od njih više nisu potrebni, a zauzimaju prostor (i/ili za njega plaćamo).

Koje postoje strategije čišćenja?

  1. Možete jednostavno ništa ne čistiti. Ponekad je zaista lakše malo platiti za dodatni prostor nego odmotati ogroman klupko oznaka. No, to funkcionira samo do određenog trenutka.
  2. Potpuni reset. Ako izbrišete sve slike i ponovo sastavite samo aktualne u CI sustavu, može se pojaviti problem. Ako se na produkciji ponovno pokrene kontejner, za njega će se učitati nova slika — tako da još nikada nije testirana. To ubija ideju nepromjenjive infrastrukture.
  3. Plava-zelena. Jedan registry je počeo prepunjavati — učitavamo slike u drugi. Ista problema kao u prethodnom načinu: kada možemo očistiti taj registry koji je počeo prepunjavati?
  4. Po vremenu. Izbrisati sve slike starije od 1 mjeseca? No, sigurno će se naći usluga koja nije ažurirana cijeli mjesec...
  5. Ručni utvrđivati što se već može brisati.

Stvarno održivi varijante su dvije: ne čistiti ili kombinacija plave-zelene + ručno. U posljednjem slučaju, riječ je o sljedećem: kada shvatite da je vrijeme za čišćenje registra, kreirate novi i dodajete sve nove slike u njega tijekom, na primjer, mjeseca. A nakon mjesec dana provjeravate koje podove u Kubernetesu još uvijek koriste stari registry, i premještate ih također u novi registry.

K čemu smo došli u werf? Мы собираем:

  1. Git head: sve oznake, sve grane — pretpostavljajući da je sve što je označeno u Gitu potrebno i u slikama (a ako nije, treba obrisati u samom Gitu);
  2. svi podovi koji su trenutno preuzeti u Kubernetesu;
  3. stari ReplicaSet-ovi (oni koji su nedavno preuzeti), kao i planiramo skenirati Helm-izdanja i odabrati najnovije slike tamo.

… i od ovog skupa kreiramo whitelist — popis slika koje nećemo brisati. Sve ostalo čistimo, nakon čega pronalazimo siroče stage-slike i brišemo ih također.

Faza implementacije (deploy)

Pouzdana deklarativnost

Prva stvar na koju bih skrenuo pažnju u implementaciji je izdavanje ažurirane konfiguracije resursa, koja je deklarativno objavljena. Izvorni YAML-dokument s opisom Kubernetes resursa često se značajno razlikuje od rezultata koji stvarno radi u klasteru. To se događa jer Kubernetes dodaje u konfiguraciju:

  1. identifikatore;
  2. administrativne informacije;
  3. mnogo zadatih vrijednosti;
  4. odjeljak s trenutnim statusom;
  5. promjene učinjene tijekom rada admission webhook-a;
  6. rezultat rada različitih kontrolera (i planera).

Stoga, kada se pojavi nova konfiguracija resursa (new), ne možemo jednostavno zamijeniti trenutnu, „živu“ konfiguraciju (live). Zato ćemo morati usporediti new s prethodno primijenjenom konfiguracijom (last-applied) i primijeniti live dobiveni patch.

Ovaj pristup se naziva 2-way merge. Koristi se, primjerice, u Helm-u.

Postoji i 3-way merge, koji se razlikuje po tome što:

  • uspoređujući last-applied i new, gledamo što je uklonjeno;
  • uspoređujući new i live, gledamo što je dodano ili promijenjeno;
  • zbrojeni patch primjenjujemo na live.

Implementiramo 1000+ aplikacija s Helm-om, tako da zapravo živimo s 2-way merge. Međutim, on ima niz problema koje smo riješili vlastitim patch-evima, koji pomažu Helmu da normalno funkcioniše.

Stvarni status implementacije

Nakon što je zbog događaja naša CI-sustav generirao novu konfiguraciju za Kubernetes, ona ju šalje na primjenu (apply) u klaster — pomoću Helm-a ili kubectl apply. Potom se odvija već opisani N-way merge, na što Kubernetes API odobrava CI sustavu, a on korisniku.

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Međutim, postoji veliki problem: uspješna primjena ne znači uspješnu implementaciju. Ako je Kubernetes shvatio koje promjene treba primijeniti, primjenjuje ih — još ne znamo kakav će biti rezultat. Na primjer, ažuriranje i ponovo pokretanje pod-ova na frontend-u može proći uspješno, dok na backend-u — ne, i dobit ćemo različite verzije pokrenutih slika aplikacije.. Если Kubernetes понял, что за изменения надо применить, применяет его — мы ещё не знаем, что получится в результате. Например, обновление и рестарт pod’ов во frontend’е может пройти успешно, а в backend’е — нет, и мы получим разные версии запущенных образов приложения.

Da bismo sve radili ispravno, u ovoj shemi nameće se dodatna jedinica — poseban traker koji će primati informacije o statusu putem Kubernetes API-a i prenositi ih za dalju analizu stvarnog stanja. Stvorili smo Open Source biblioteku na Go — kubedog (vidi njen najavu ovdje), — koja rešava ovaj problem i integrisana je u werf.

Ponašanje ovog trakera na nivou werf podešava se pomoću anotacija koje se postavljaju na Deploymente ili StatefulSets. Glavna anotacija — fail-mode — razume sledeće vrednosti:

  • IgnoreAndContinueDeployProcess — ignorišemo probleme u vezi sa implementacijom ovog dela i nastavljamo proces implementacije;
  • FailWholeDeployProcessImmediately — greška u ovom delu zaustavlja proces implementacije;
  • HopeUntilEndOfDeployProcess — nadamo se da će ovaj deo raditi do kraja implementacije.

Na primer, kombinacija resursa i vrednosti anotacije fail-mode:

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Kada deployujemo po prvi put, baza podataka (MongoDB) možda još nije spremna — Deployments će pasti. Međutim, možemo sačekati da se pokrene, i deploy će proći.

Postoje još dve anotacije za kubedog u werf:

  • failures-allowed-per-replica — broj dozvoljenih padova za svaku replikaciju;
  • show-logs-until — reguliše trenutak do kojeg werf prikazuje (u stdout) logove iz svih implementiranih podova. Podrazumevano je to PodIsReady (kako bismo ignorisali poruke koje nam verovatno nisu potrebne kada pod počne primati saobraćaj), ali su takođe prihvatljive vrednosti ControllerIsReady i EndOfDeploy.

Šta još želimo od implementacije?

Pored već opisana dva aspekta, želeli bismo:

  • videti logove — ali samo potrebne, a ne sve nasumično;
  • pratiti napredak, jer ako se job "tiho" zadrži nekoliko minuta, važno je razumeti šta se događa;
  • imati automatizovani rollback u slučaju da nešto pođe po zlu (stoga je ključno znati stvarni status implementacije). Implementacija bi trebala biti atomska: ili prolazi do kraja, ili se sve vraća na prethodno stanje.

Zaključci

Nama kao kompaniji za realizaciju svih ovih nijansi na različitim fazama isporuke (build, publish, deploy) dovoljna je CI sistem i alat werf.

Umesto zaključka:

werf — naš alat za CI/CD u Kubernetesu (pregled i video prezentacija)

Uz pomoć werf-a postigli smo značajan napredak u rešavanju mnogih problema DevOps inženjera i biće nam drago ako šira zajednica isproba ovaj alat u praksi. Postizanje dobrog rezultata zajedno biće lakše.

Video i slajdovi

Video sa nastupa (~47 minuta):

Pogledajte video

Prezentacija izlaganja:

P.S.

Druge objave o Kubernetesu na našem blogu:

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster