werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

27 mai în sala principală a conferinței DevOpsConf 2019, desfășurată în cadrul festivalului RIT++ 2019, ca parte a secțiunii „Livrare continuă”, a primit un raport „werf - instrumentul nostru pentru CI/CD în Kubernetes”. Vorbește despre acestea problemele și provocările cu care se confruntă toată lumea la implementarea în Kubernetes, precum și despre nuanțe care pot să nu fie imediat vizibile. Analizând posibilele soluții, arătăm cum aceasta este implementată într-un instrument Open Source werf.

De la prezentare, utilitatea noastră (cunoscută anterior ca dapp) a atins o piatră de hotar istorică 1000 de stele pe GitHub — sperăm că comunitatea sa în creștere de utilizatori va face viața mai ușoară multor ingineri DevOps.

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Deci, vă prezentăm video cu raportul (~47 de minute, mult mai informativ decât articolul) și extrasul principal din acesta sub formă de text. Merge!

Livrarea codului către Kubernetes

Discuția nu va mai fi despre werf, ci despre CI/CD în Kubernetes, ceea ce înseamnă că software-ul nostru este ambalat în containere Docker (Am vorbit despre asta în raport 2016), iar K8-urile vor fi folosite pentru a-l rula în producție (mai multe despre asta în Anul 2017).

Cum arată livrarea în Kubernetes?

  • Există un depozit Git cu codul și instrucțiunile pentru al construi. Aplicația este încorporată într-o imagine Docker și publicată în Registrul Docker.
  • Același depozit conține, de asemenea, instrucțiuni despre cum să implementați și să rulați aplicația. În etapa de implementare, aceste instrucțiuni sunt trimise către Kubernetes, care primește imaginea dorită din registru și o lansează.
  • În plus, există de obicei teste. Unele dintre acestea se pot face la publicarea unei imagini. De asemenea, puteți (urmând aceleași instrucțiuni) să implementați o copie a aplicației (într-un spațiu de nume K8s separat sau un cluster separat) și să rulați teste acolo.
  • În cele din urmă, aveți nevoie de un sistem CI care primește evenimente de la Git (sau clicuri pe buton) și apelează toate etapele desemnate: construirea, publicarea, implementarea, testarea.

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Există câteva note importante aici:

  1. Pentru că avem o infrastructură imuabilă (infrastructură imuabilă), imaginea aplicației care este utilizată în toate etapele (montare, producție etc.), trebuie să fie unul. Am vorbit despre asta mai detaliat și cu exemple. aici.
  2. Pentru că urmăm abordarea infrastructurii ca cod (IaC), codul aplicației, instrucțiunile de asamblare și lansare ar trebui să fie exact într-un singur depozit. Pentru mai multe informații despre aceasta, consultați acelasi raport.
  3. Lanț de livrare (livrare) de obicei o vedem așa: aplicația a fost asamblată, testată, lansată (etapa de lansare) și asta este tot - livrarea a avut loc. Dar, în realitate, utilizatorul primește ceea ce ați lansat, nu apoi când l-ai livrat la producție și când a putut să meargă acolo și această producție a funcționat. Așa că cred că lanțul de livrare se termină doar în stadiul de exploatare (alerga), sau mai precis, chiar și în momentul în care codul a fost scos din producție (înlocuindu-l cu unul nou).

Să revenim la schema de livrare de mai sus în Kubernetes: a fost inventată nu numai de noi, ci și de toți cei care s-au ocupat de această problemă. De fapt, acest model se numește acum GitOps (puteți citi mai multe despre termen și ideile din spatele lui aici). Să ne uităm la etapele schemei.

Etapa de construcție

S-ar părea că puteți vorbi despre construirea de imagini Docker în 2019, când toată lumea știe să scrie Dockerfiles și să ruleze docker build?.. Iată nuanțele cărora aș dori să le acord atenție:

  1. Greutatea imaginii contează, așa că folosește mai multe etapesa lase in imagine doar aplicatia care este cu adevarat necesara operatiunii.
  2. Numărul de straturi trebuie reduse la minimum prin combinarea lanțurilor de RUN-comenzi după sens.
  3. Cu toate acestea, acest lucru adaugă probleme depanare, pentru că atunci când ansamblul se prăbușește, trebuie să găsiți comanda potrivită din lanțul care a cauzat problema.
  4. Viteza de asamblare important pentru că dorim să implementăm rapid modificările și să vedem rezultatele. De exemplu, nu doriți să reconstruiți dependențe în bibliotecile de limbi de fiecare dată când construiți o aplicație.
  5. Adesea, dintr-un singur depozit Git de care aveți nevoie multe imagini, care poate fi rezolvată printr-un set de fișiere Dockerfile (sau etape numite într-un singur fișier) și un script Bash cu asamblarea lor secvențială.

Acesta a fost doar vârful aisbergului cu care se confruntă toată lumea. Dar există și alte probleme, în special:

  1. Adesea, la etapa de asamblare avem nevoie de ceva montură (de exemplu, memorați în cache rezultatul unei comenzi precum apt într-un director terță parte).
  2. Noi vrem ansiblu în loc să scrie în coajă.
  3. Noi vrem construi fără Docker (de ce avem nevoie de o mașină virtuală suplimentară în care trebuie să configuram totul pentru asta, când avem deja un cluster Kubernetes în care putem rula containere?).
  4. Asamblare paralelă, care poate fi înțeles în moduri diferite: comenzi diferite din Dockerfile (dacă se utilizează mai multe etape), mai multe comiteri ale aceluiași depozit, mai multe Dockerfile.
  5. Asamblare distribuită: Vrem să adunăm lucruri în păstăi care sunt „efemere” pentru că cache-ul lor dispare, ceea ce înseamnă că trebuie stocat undeva separat.
  6. În cele din urmă, am numit vârful dorințelor automagie: Ar fi ideal să mergeți la depozit, să tastați o comandă și să obțineți o imagine gata făcută, asamblată cu o înțelegere a cum și ce să faceți corect. Cu toate acestea, personal nu sunt sigur că toate nuanțele pot fi prevăzute în acest fel.

Și iată proiectele:

  • moby/kit de construcție — un constructor de la Docker Inc (deja integrat în versiunile actuale de Docker), care încearcă să rezolve toate aceste probleme;
  • kaniko — un constructor de la Google care vă permite să construiți fără Docker;
  • Buildpacks.io — încercarea CNCF de a face magie automată și, în special, o soluție interesantă cu rebase pentru straturi;
  • și o grămadă de alte utilități, cum ar fi buildah, genuinetools/img...

...și uite câte stele au pe GitHub. Adică, pe de o parte, docker build există și poate face ceva, dar în realitate problema nu este complet rezolvată - dovada in acest sens este dezvoltarea paralela a colectoarelor alternative, fiecare dintre acestea rezolvand o parte a problemelor.

Asamblare în werf

Așa că am ajuns werf (fostă faimos ca dapp) — Un utilitar open source de la compania Flant, pe care îl facem de mulți ani. Totul a început în urmă cu 5 ani cu scripturi Bash care au optimizat asamblarea Dockerfiles, iar în ultimii 3 ani s-a realizat o dezvoltare completă în cadrul unui proiect cu propriul depozit Git. (mai întâi în Ruby, apoi rescris to Go și, în același timp, redenumit). Ce probleme de asamblare sunt rezolvate în werf?

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Problemele umbrite în albastru au fost deja implementate, construcția paralelă a fost realizată în cadrul aceleiași gazde, iar problemele evidențiate în galben sunt planificate să fie finalizate până la sfârșitul verii.

Stadiul publicării în registru (publicare)

Am format docker push... - ce ar putea fi dificil la încărcarea unei imagini în registru? Și atunci apare întrebarea: „Ce etichetă ar trebui să pun pe imagine?” Apare din motivul pe care îl avem Gitflow (sau altă strategie Git) și Kubernetes, iar industria încearcă să se asigure că ceea ce se întâmplă în Kubernetes urmează ceea ce se întâmplă în Git. La urma urmei, Git este singura noastră sursă de adevăr.

Ce este atât de greu în asta? Asigurați reproductibilitatea: dintr-un commit în Git, care este imuabil în natură (imuabil), la o imagine Docker, care ar trebui să rămână aceeași.

Este important și pentru noi determina originea, pentru că vrem să înțelegem din ce commit a fost construită aplicația care rulează în Kubernetes (atunci putem face diferențe și lucruri similare).

Strategii de etichetare

Primul este simplu git zi. Avem un registru cu o imagine etichetată ca 1.0. Kubernetes are scenă și producție, unde este încărcată această imagine. În Git facem comite și la un moment dat etichetăm 2.0. Îl colectăm conform instrucțiunilor din depozit și îl plasăm în registru cu eticheta 2.0. Îl lansăm în scenă și, dacă totul este bine, apoi în producție.

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Problema cu această abordare este că mai întâi am setat eticheta și abia apoi am testat-o ​​și l-am lansat. De ce? În primul rând, este pur și simplu ilogic: lansăm o versiune de software pe care nici măcar nu am testat-o ​​încă (nu putem face altfel, pentru că pentru a verifica, trebuie să punem o etichetă). În al doilea rând, această cale nu este compatibilă cu Gitflow.

A doua opțiune - git commit + tag. Ramura principală are o etichetă 1.0; pentru el în registru - o imagine implementată în producție. În plus, clusterul Kubernetes are contururi de previzualizare și punere în scenă. În continuare urmăm Gitflow: în ramura principală pentru dezvoltare (develop) realizăm funcții noi, rezultând un commit cu identificatorul #c1. Îl colectăm și îl publicăm în registru folosind acest identificator (#c1). Cu același identificator, lansăm pentru previzualizare. Facem același lucru cu commit-urile #c2 и #c3.

Când ne-am dat seama că există suficiente funcții, începem să stabilizăm totul. Creați o ramură în Git release_1.1 (pe bază #c3 de develop). Nu este nevoie să colectați această versiune, deoarece... acest lucru a fost făcut în pasul anterior. Prin urmare, putem pur și simplu să-l lansăm în scenă. Remediem erorile în #c4 și, în mod similar, se desfășoară în scenă. În același timp, dezvoltarea este în curs de desfășurare în develop, de unde modificările sunt preluate periodic release_1.1. La un moment dat, primim un commit compilat și încărcat în staging, de care suntem mulțumiți (#c25).

Apoi îmbinăm (cu derulare rapidă înainte) ramura de lansare (release_1.1) în master. Am pus o etichetă cu noua versiune pe acest commit (1.1). Dar această imagine este deja colectată în registru, așa că pentru a nu o colecta din nou, pur și simplu adăugăm o a doua etichetă la imaginea existentă (acum are etichete în registru #c25 и 1.1). După aceea, îl lansăm în producție.

Există un dezavantaj că doar o singură imagine este încărcată în scenă (#c25), iar în producție este oarecum diferit (1.1), dar știm că „fizic” acestea sunt aceeași imagine din registru.

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Adevăratul dezavantaj este că nu există suport pentru merge commit-uri, trebuie să faci înainte rapid.

Putem merge mai departe și facem un truc... Să ne uităm la un exemplu de Dockerfile simplu:

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

Să construim un fișier din acesta conform următorului principiu:

  • SHA256 din identificatorii imaginilor utilizate (ruby:2.3 и nginx:alpine), care sunt sume de control ale conținutului lor;
  • toate echipele (RUN, CMD și așa mai departe.);
  • SHA256 din fișierele care au fost adăugate.

... și luați suma de control (din nou SHA256) dintr-un astfel de fișier. Acest semnătură tot ceea ce definește conținutul imaginii Docker.

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Să revenim la diagramă și în loc de comite vom folosi astfel de semnături, adică etichetați imaginile cu semnături.

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Acum, când este necesar, de exemplu, să îmbinăm modificările de la o versiune la master, putem face un real merge commit: va avea un alt identificator, dar aceeași semnătură. Cu același identificator vom lansa imaginea în producție.

Dezavantajul este că acum nu va fi posibil să se determine ce fel de commit a fost împins în producție - sumele de control funcționează doar într-o singură direcție. Această problemă este rezolvată printr-un strat suplimentar cu metadate - vă voi spune mai multe mai târziu.

Etichetarea în werf

În werf am mers și mai departe și ne pregătim să facem o construcție distribuită cu un cache care nu este stocat pe o singură mașină... Deci, construim două tipuri de imagini Docker, le numim etapă и imagine.

Depozitul werf Git stochează instrucțiuni specifice versiunii care descriu diferitele etape ale construcției (înainte de instalare, instala, înainte de configurare, configurarea). Colectăm imaginea primei etape cu o semnătură definită ca suma de control a primilor pași. Apoi adăugăm codul sursă, pentru noua imagine de etapă îi calculăm suma de control... Aceste operații se repetă pentru toate etapele, în urma cărora obținem un set de imagini de etapă. Apoi facem imaginea finală, care conține și metadate despre originea ei. Și etichetăm această imagine în moduri diferite (detalii mai târziu).

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Să presupunem că după aceasta apare un nou commit în care doar codul aplicației a fost modificat. Ce se va intampla? Pentru modificările codului, va fi creat un patch și va fi pregătită o nouă imagine de etapă. Semnătura sa va fi determinată ca sumă de control a imaginii vechi de scenă și a noului patch. Din această imagine se va forma o nouă imagine finală. Comportament similar va avea loc cu schimbări în alte etape.

Astfel, imaginile de scenă sunt un cache care poate fi stocat în mod distribuit, iar imaginile deja create din acesta sunt încărcate în Registrul Docker.

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Curățarea registrului

Nu vorbim despre ștergerea straturilor care au rămas suspendate după etichetele șterse - aceasta este o caracteristică standard a Registrului Docker în sine. Vorbim despre o situație în care se acumulează foarte multe etichete Docker și înțelegem că nu mai avem nevoie de unele dintre ele, dar ocupă spațiu (și/sau plătim pentru asta).

Care sunt strategiile de curățare?

  1. Pur și simplu nu poți face nimic nu curata. Uneori este cu adevărat mai ușor să plătești puțin pentru spațiu suplimentar decât să deslușești o încurcătură imensă de etichete. Dar acest lucru funcționează doar până la un anumit punct.
  2. Resetare completă. Dacă ștergeți toate imaginile și reconstruiți numai pe cele actuale în sistemul CI, poate apărea o problemă. Dacă containerul este repornit în producție, va fi încărcată o nouă imagine pentru el - una care nu a fost încă testată de nimeni. Acest lucru distruge ideea de infrastructură imuabilă.
  3. Albastru verde. Un registru a început să depășească - încărcăm imagini în altul. Aceeași problemă ca în metoda anterioară: în ce moment puteți șterge registry care a început să se debordeze?
  4. Cu timpul. Ștergeți toate imaginile mai vechi de o lună? Dar cu siguranță va exista un serviciu care nu a fost actualizat de o lună...
  5. manual stabiliți ce poate fi deja șters.

Există două opțiuni cu adevărat viabile: nu curățați sau o combinație de albastru-verde + manual. În acest ultim caz, vorbim despre următoarele: când înțelegeți că este timpul să curățați registry, creați unul nou și adăugați toate imaginile noi la acesta pe parcursul, de exemplu, o lună. Și după o lună, vezi ce pod-uri din Kubernetes folosesc încă vechiul registru și transferă-le și pe noul registru.

La ce am ajuns werf? Colectăm:

  1. Cap Git: toate etichetele, toate ramurile - presupunând că avem nevoie de tot ceea ce este etichetat în Git în imagini (și dacă nu, atunci trebuie să-l ștergem în Git);
  2. toate podurile care sunt în prezent pompate în Kubernetes;
  3. vechi ReplicaSets (ceea ce a fost lansat recent) și, de asemenea, intenționăm să scanăm versiunile Helm și să selectăm cele mai recente imagini acolo.

... și faceți o listă albă din acest set - o listă de imagini pe care nu le vom șterge. Curățăm orice altceva, după care găsim imagini de scenă orfane și le ștergem și ele.

Etapa de implementare

Declarativitate de încredere

Primul punct asupra căruia aș dori să atrag atenția în implementare este lansarea configurației actualizate a resurselor, declarată declarativ. Documentul YAML original care descrie resursele Kubernetes este întotdeauna foarte diferit de rezultatul care rulează efectiv în cluster. Deoarece Kubernetes adaugă la configurație:

  1. identificatori;
  2. informații despre servicii;
  3. multe valori implicite;
  4. secțiunea cu starea curentă;
  5. modificările făcute ca parte a webhook-ului de admitere;
  6. rezultatul muncii diverșilor controlori (și programatorului).

Prin urmare, atunci când apare o nouă configurație de resurse (nou), nu putem doar să luăm și să suprascriem configurația actuală, „live” cu ea (trăi). Pentru a face acest lucru va trebui să comparăm nou cu ultima configurație aplicată (ultima aplicată) și rulați pe trăi patch primit.

Această abordare se numește îmbinare în două sensuri. Este folosit, de exemplu, în Helm.

Există, de asemenea îmbinare în două sensuri, care diferă prin aceea că:

  • comparând ultima aplicată и nou, ne uităm la ce a fost șters;
  • comparând nou и trăi, ne uităm la ceea ce a fost adăugat sau schimbat;
  • se aplică plasturele însumat trăi.

Implementăm peste 1000 de aplicații cu Helm, așa că trăim de fapt cu îmbinarea bidirecțională. Cu toate acestea, are o serie de probleme pe care le-am rezolvat cu patch-urile noastre, care îl ajută pe Helm să funcționeze normal.

Starea reală a lansării

După ce sistemul nostru CI generează o nouă configurație pentru Kubernetes pe baza următorului eveniment, o transmite pentru utilizare (aplica) la un cluster - folosind Helm sau kubectl apply. În continuare, are loc îmbinarea N-way deja descrisă, la care API-ul Kubernetes răspunde cu aprobare sistemului CI și utilizatorului acestuia.

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Cu toate acestea, există o problemă uriașă: până la urmă aplicarea reușită nu înseamnă lansare reușită. Dacă Kubernetes înțelege ce modificări trebuie aplicate și le aplică, încă nu știm care va fi rezultatul. De exemplu, actualizarea și repornirea podurilor în front-end poate avea succes, dar nu și în backend și vom obține diferite versiuni ale imaginilor aplicației care rulează.

Pentru a face totul corect, această schemă necesită o legătură suplimentară - un tracker special care va primi informații de stare de la API-ul Kubernetes și le va transmite pentru o analiză ulterioară a stării reale a lucrurilor. Am creat o bibliotecă Open Source în Go - cubedog (vezi anunțul acestuia aici), care rezolvă această problemă și este încorporat în werf.

Comportamentul acestui instrument de urmărire la nivel de werf este configurat folosind adnotări care sunt plasate pe Deployments sau StatefulSets. Adnotare principală - fail-mode - înțelege următoarele semnificații:

  • IgnoreAndContinueDeployProcess — ignorăm problemele de implementare a acestei componente și continuăm implementarea;
  • FailWholeDeployProcessImmediately — o eroare în această componentă oprește procesul de implementare;
  • HopeUntilEndOfDeployProcess — sperăm că această componentă va funcționa până la sfârșitul implementării.

De exemplu, această combinație de resurse și valori de adnotare fail-mode:

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Când implementăm pentru prima dată, este posibil ca baza de date (MongoDB) să nu fie încă gata - implementările vor eșua. Dar puteți aștepta momentul să înceapă, iar desfășurarea va avea loc în continuare.

Mai sunt două adnotări pentru kubedog în werf:

  • failures-allowed-per-replica — numărul de căderi permise pentru fiecare replică;
  • show-logs-until — reglează momentul până la care werf arată (în stdout) jurnalele din toate podurile rulate. Valoarea implicită este PodIsReady (pentru a ignora mesajele pe care probabil nu le dorim atunci când traficul începe să vină la pod), dar valorile sunt și ele valabile: ControllerIsReady и EndOfDeploy.

Ce altceva ne dorim de la implementare?

Pe lângă cele două puncte deja descrise, ne-am dori:

  • să văd busteni - și numai cele necesare, și nu totul la rând;
  • urmări progres, pentru că dacă treaba se blochează „în tăcere” câteva minute, este important să înțelegem ce se întâmplă acolo;
  • avea rollback automat în cazul în care ceva nu a mers prost (și, prin urmare, este esențial să cunoaștem starea reală a implementării). Lansarea trebuie să fie atomică: fie trece până la sfârșit, fie totul revine la starea anterioară.

Rezultatele

Pentru noi, ca companie, pentru a implementa toate nuanțele descrise în diferite etape de livrare (construire, publicare, implementare), un sistem CI și un utilitar sunt suficiente werf.

În loc de o concluzie:

werf - instrumentul nostru pentru CI / CD în Kubernetes (prezentare generală și raport video)

Cu ajutorul werf, am făcut progrese bune în rezolvarea unui număr mare de probleme pentru inginerii DevOps și am fi bucuroși dacă comunitatea mai largă ar încerca măcar acest utilitar în acțiune. Va fi mai ușor să obțineți un rezultat bun împreună.

Videoclipuri și diapozitive

Videoclip de la spectacol (~47 de minute):

Prezentarea raportului:

PS

Alte rapoarte despre Kubernetes pe blogul nostru:

Sursa: www.habr.com

Adauga un comentariu