werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

27 maggio nella sala principale della conferenza DevOpsConf 2019, tenutasi nell'ambito del festival RIT++2019, nell'ambito della sezione "Consegna continua" è stato fornito un rapporto "werf - il nostro strumento per CI/CD in Kubernetes". Si parla di quelli problemi e sfide che tutti devono affrontare durante la distribuzione su Kubernetes, così come sulle sfumature che potrebbero non essere immediatamente evidenti. Analizzando le possibili soluzioni, mostriamo come questa viene implementata in uno strumento Open Source werf.

Dalla presentazione, la nostra utility (precedentemente nota come dapp) ha raggiunto un traguardo storico 1000 stelle su GitHub - speriamo che la sua crescente comunità di utenti semplifichi la vita a molti ingegneri DevOps.

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Quindi, vi presentiamo video del resoconto (~47 minuti, molto più informativo dell'articolo) e il suo estratto principale in forma testuale. Andare!

Consegna del codice a Kubernetes

Non si parlerà più di werf, ma di CI/CD in Kubernetes, lasciando intendere che il nostro software è confezionato in contenitori Docker (ne ho parlato in Rapporto 2016)e i K8 verranno utilizzati per eseguirlo in produzione (maggiori informazioni su questo argomento in 2017 anno).

Come si presenta la consegna in Kubernetes?

  • C'è un repository Git con il codice e le istruzioni per costruirlo. L'applicazione è incorporata in un'immagine Docker e pubblicata nel registro Docker.
  • Lo stesso repository contiene anche istruzioni su come distribuire ed eseguire l'applicazione. In fase di distribuzione, queste istruzioni vengono inviate a Kubernetes, che riceve l'immagine desiderata dal registro e la avvia.
  • Inoltre, di solito ci sono dei test. Alcuni di questi possono essere eseguiti quando si pubblica un'immagine. Puoi anche (seguendo le stesse istruzioni) distribuire una copia dell'applicazione (in uno spazio dei nomi K8 separato o in un cluster separato) ed eseguire i test lì.
  • Infine, è necessario un sistema CI che riceva eventi da Git (o clic sui pulsanti) e chiami tutte le fasi designate: creazione, pubblicazione, distribuzione, test.

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Ci sono alcune note importanti qui:

  1. Perché abbiamo un’infrastruttura immutabile (infrastruttura immutabile), l'immagine dell'applicazione che viene utilizzata in tutte le fasi (staging, produzione, ecc.), deve essercene uno. Ne ho parlato in modo più dettagliato e con esempi. qui.
  2. Perché seguiamo l'approccio Infrastructure as Code (IaC), il codice dell'applicazione, le istruzioni per assemblarlo e avviarlo dovrebbero essere esattamente in un repository. Per ulteriori informazioni a riguardo, vedere lo stesso rapporto.
  3. Catena di consegna (consegna) di solito la vediamo così: l'applicazione è stata assemblata, testata, rilasciata (fase di rilascio) e questo è tutto: la consegna è avvenuta. Ma in realtà, l'utente ottiene ciò che hai lanciato, no poi quando l'hai consegnato alla produzione, e quando è stato in grado di andare lì e la produzione ha funzionato. Quindi credo che la catena di consegna finisca solo in fase operativa (correre), o più precisamente, anche nel momento in cui il codice è stato tolto dalla produzione (sostituendolo con uno nuovo).

Torniamo allo schema di consegna di cui sopra in Kubernetes: è stato inventato non solo da noi, ma letteralmente da tutti coloro che hanno affrontato questo problema. In effetti, questo modello ora si chiama GitOps (puoi leggere di più sul termine e sulle idee dietro di esso qui). Vediamo le fasi dello schema.

Costruisci il palco

Sembrerebbe che si possa parlare di creazione di immagini Docker nel 2019, quando tutti sapranno come scrivere Dockerfile ed eseguirli docker build?.. Ecco le sfumature a cui vorrei prestare attenzione:

  1. Peso dell'immagine conta, quindi usa multi-stageper lasciare nell'immagine solo l'applicazione realmente necessaria all'operazione.
  2. Numero di strati deve essere ridotto al minimo combinando catene di RUN-comandi in base al significato.
  3. Tuttavia, ciò aggiunge problemi debug, perché quando l'assembly si blocca, devi trovare il comando giusto dalla catena che ha causato il problema.
  4. Velocità di montaggio importante perché vogliamo implementare rapidamente le modifiche e vedere i risultati. Ad esempio, non vuoi ricostruire le dipendenze nelle librerie linguistiche ogni volta che crei un'applicazione.
  5. Spesso da un repository Git di cui hai bisogno molte immagini, che può essere risolto da una serie di Dockerfile (o fasi denominate in un file) e uno script Bash con il loro assemblaggio sequenziale.

Questa era solo la punta dell’iceberg che tutti devono affrontare. Ma ci sono altri problemi, in particolare:

  1. Spesso in fase di assemblaggio abbiamo bisogno di qualcosa montare (ad esempio, memorizza nella cache il risultato di un comando come apt in una directory di terze parti).
  2. Vogliamo ansible invece di scrivere in shell.
  3. Vogliamo costruire senza Docker (perché abbiamo bisogno di una macchina virtuale aggiuntiva in cui dobbiamo configurare tutto per questo, quando disponiamo già di un cluster Kubernetes in cui possiamo eseguire contenitori?).
  4. Assemblaggio parallelo, che può essere inteso in diversi modi: comandi diversi dal Dockerfile (se si utilizza il multistage), diversi commit dello stesso repository, diversi Dockerfile.
  5. Assemblaggio distribuito: Vogliamo raccogliere in pod cose che siano "effimere" perché la loro cache scompare, il che significa che deve essere archiviata da qualche parte separatamente.
  6. Alla fine ho nominato l'apice dei desideri automagia: L'ideale sarebbe andare nel repository, digitare qualche comando e ottenere un'immagine già pronta, assemblata con la comprensione di come e cosa fare correttamente. Tuttavia, personalmente non sono sicuro che tutte le sfumature possano essere previste in questo modo.

Ed ecco i progetti:

  • moby/buildkit — un builder di Docker Inc (già integrato nelle attuali versioni di Docker), che sta cercando di risolvere tutti questi problemi;
  • Kaniko — un builder di Google che ti consente di costruire senza Docker;
  • Buildpacks.io — Il tentativo di CNCF di realizzare magie automatiche e, in particolare, una soluzione interessante con rebase per layer;
  • e un sacco di altre utilità, come costruisci, strumenti genuini/img...

...e guarda quante stelle hanno su GitHub. Cioè, da un lato, docker build esiste e può fare qualcosa, ma in realtà la questione non è completamente risolta - prova di ciò è lo sviluppo parallelo di collettori alternativi, ognuno dei quali risolve una parte dei problemi.

Assemblaggio in werf

Quindi dobbiamo farlo werf (in precedenza conosciuto come dapp) — Un'utilità open source dell'azienda Flant, che produciamo da molti anni. Tutto è iniziato 5 anni fa con gli script Bash che ottimizzavano l'assemblaggio dei Dockerfile e negli ultimi 3 anni lo sviluppo completo è stato effettuato nell'ambito di un progetto con il proprio repository Git (prima in Ruby e poi copiato to Go, e allo stesso tempo rinominato). Quali problemi di assemblaggio vengono risolti in werf?

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

I problemi evidenziati in blu sono già stati implementati, la creazione parallela è stata eseguita all'interno dello stesso host e si prevede che i problemi evidenziati in giallo verranno completati entro la fine dell'estate.

Fase di pubblicazione nel registro (publish)

Abbiamo chiamato docker push... - cosa potrebbe esserci di difficile nel caricare un'immagine nel registro? E poi sorge la domanda: "Che tag devo mettere sull'immagine?" Sorge per la ragione che abbiamo GitFlow (o altra strategia Git) e Kubernetes, e l’industria sta cercando di garantire che ciò che accade in Kubernetes segua ciò che accade in Git. Dopotutto, Git è la nostra unica fonte di verità.

Cosa c'è di così difficile? Garantire la riproducibilità: da un commit in Git, che è di natura immutabile (immutabile), in un'immagine Docker, che dovrebbe essere mantenuta la stessa.

È importante anche per noi determinare l'origine, perché vogliamo capire da quale commit è stata creata l'applicazione in esecuzione in Kubernetes (poi possiamo fare diff e cose simili).

Strategie di etichettatura

Il primo è semplice tag git. Abbiamo un registro con un'immagine contrassegnata come 1.0. Kubernetes ha una fase e una produzione in cui viene caricata questa immagine. In Git effettuiamo commit e ad un certo punto tagghiamo 2.0. Lo raccogliamo secondo le istruzioni del repository e lo inseriamo nel registro con il tag 2.0. Lo portiamo sul palco e, se tutto va bene, poi in produzione.

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Il problema con questo approccio è che prima inseriamo il tag e solo dopo lo testiamo e lo implementiamo. Perché? Innanzitutto è semplicemente illogico: stiamo rilasciando una versione del software che non abbiamo ancora nemmeno testato (non possiamo fare altrimenti, perché per controllare dobbiamo mettere un tag). In secondo luogo, questo percorso non è compatibile con Gitflow.

La seconda opzione - git commit + tag. Il ramo principale ha un tag 1.0; per questo nel registro: un'immagine distribuita in produzione. Inoltre, il cluster Kubernetes dispone di contorni di anteprima e di staging. Successivamente seguiamo Gitflow: nel ramo principale per lo sviluppo (develop) creiamo nuove funzionalità, risultando in un commit con l'identificatore #c1. Lo raccogliamo e lo pubblichiamo nel registro utilizzando questo identificatore (#c1). Con lo stesso identificatore eseguiamo il rollout in anteprima. Facciamo lo stesso con i commit #c2 и #c3.

Quando ci rendiamo conto che ci sono abbastanza funzionalità, iniziamo a stabilizzare tutto. Crea un ramo in Git release_1.1 (sulla base #c3 di develop). Non è necessario ritirare questa liberatoria perché... questo è stato fatto nel passaggio precedente. Pertanto, possiamo semplicemente implementarlo nello staging. Correggiamo i bug #c4 e allo stesso modo passare alla messa in scena. Allo stesso tempo, è in corso lo sviluppo develop, da cui vengono periodicamente prese le modifiche release_1.1. Ad un certo punto, otteniamo un commit compilato e caricato nello staging, cosa di cui siamo soddisfatti (#c25).

Quindi uniamo (con l'avanzamento veloce) il ramo di rilascio (release_1.1) nel maestro. Inseriamo un tag con la nuova versione su questo commit (1.1). Ma questa immagine è già raccolta nel registro, quindi per non raccoglierla nuovamente aggiungiamo semplicemente un secondo tag all'immagine esistente (ora ha dei tag nel registro #c25 и 1.1). Successivamente, lo portiamo in produzione.

C'è uno svantaggio che viene caricata solo un'immagine nello staging (#c25), e nella produzione è un po' diverso (1.1), ma sappiamo che “fisicamente” si tratta della stessa immagine del registro.

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Il vero svantaggio è che non c'è supporto per i commit di unione, devi andare avanti velocemente.

Possiamo andare oltre e fare un trucchetto... Vediamo un esempio di un semplice Dockerfile:

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

Costruiamo un file da esso secondo il seguente principio:

  • SHA256 dagli identificatori delle immagini utilizzate (ruby:2.3 и nginx:alpine), che sono checksum del loro contenuto;
  • tutte le squadre (RUN, CMD e così via.);
  • SHA256 dai file aggiunti.

... e prendi il checksum (di nuovo SHA256) da tale file. Questo firma tutto ciò che definisce il contenuto dell'immagine Docker.

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Torniamo al diagramma e invece dei commit utilizzeremo tali firme, cioè. contrassegnare le immagini con firme.

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Ora, quando è necessario, ad esempio, unire le modifiche da una release alla master, possiamo fare un vero e proprio merge commit: avrà un identificatore diverso, ma la stessa firma. Con lo stesso identificatore distribuiremo l'immagine alla produzione.

Lo svantaggio è che ora non sarà possibile determinare quale tipo di commit è stato inviato alla produzione: i checksum funzionano solo in una direzione. Questo problema viene risolto da un livello aggiuntivo con metadati: ti dirò di più in seguito.

Etichettatura in werf

In werf siamo andati ancora oltre e ci stiamo preparando a fare una build distribuita con una cache che non è memorizzata su una macchina... Quindi, stiamo costruendo due tipi di immagini Docker, le chiamiamo palcoscenico и Immagine.

Il repository werf Git memorizza istruzioni specifiche per la build che descrivono le diverse fasi della build (primaInstalla, install, prima dell'installazione, flessibile.). Raccogliamo l'immagine della prima fase con una firma definita come checksum dei primi passaggi. Quindi aggiungiamo il codice sorgente, per la nuova immagine della scena calcoliamo il suo checksum... Queste operazioni vengono ripetute per tutte le fasi, a seguito delle quali otteniamo una serie di immagini della scena. Quindi creiamo l'immagine finale, che contiene anche i metadati sulla sua origine. E tagghiamo questa immagine in diversi modi (dettagli più avanti).

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Supponiamo che dopo questo venga visualizzato un nuovo commit in cui è stato modificato solo il codice dell'applicazione. Cosa accadrà? Per le modifiche al codice, verrà creata una patch e verrà preparata una nuova immagine dello stage. La sua firma verrà determinata come checksum della vecchia immagine dello stage e della nuova patch. Da questa immagine verrà creata una nuova immagine finale. Un comportamento simile si verificherà con i cambiamenti in altre fasi.

Pertanto, le immagini dello stage sono una cache che può essere archiviata in modo distribuito e le immagini già create da essa vengono caricate nel registro Docker.

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Pulizia del registro

Non stiamo parlando dell'eliminazione dei layer rimasti in sospeso dopo l'eliminazione dei tag: questa è una funzionalità standard del registro Docker stesso. Stiamo parlando di una situazione in cui si accumulano tanti tag Docker e capiamo che alcuni di essi non ci servono più, ma occupano spazio (e/o lo paghiamo).

Quali sono le strategie di pulizia?

  1. Non puoi fare proprio nulla non pulire. A volte è davvero più facile pagare un po' di spazio extra piuttosto che districare un enorme groviglio di etichette. Ma questo funziona solo fino a un certo punto.
  2. Ripristino completo. Se si eliminano tutte le immagini e si ricostruiscono solo quelle correnti nel sistema CI, potrebbe verificarsi un problema. Se il contenitore viene riavviato in produzione, verrà caricata una nuova immagine, che non è stata ancora testata da nessuno. Questo uccide l’idea di un’infrastruttura immutabile.
  3. Blu verde. Un registro ha iniziato a traboccare: carichiamo le immagini su un altro. Lo stesso problema del metodo precedente: a che punto puoi cancellare il registro che ha iniziato a traboccare?
  4. A tempo. Eliminare tutte le immagini più vecchie di 1 mese? Ma ci sarà sicuramente un servizio che non viene aggiornato da un mese...
  5. manualmente determinare cosa può già essere eliminato.

Ci sono due opzioni veramente praticabili: non pulire o una combinazione blu-verde + manualmente. In quest'ultimo caso, stiamo parlando di quanto segue: quando capisci che è ora di pulire il registro, ne crei uno nuovo e vi aggiungi tutte le nuove immagini nel corso, ad esempio, di un mese. E dopo un mese, controlla quali pod in Kubernetes utilizzano ancora il vecchio registro e trasferisci anche quelli nel nuovo registro.

A cosa siamo arrivati? werf? Raccogliamo:

  1. Git head: tutti i tag, tutti i rami - presupponendo che abbiamo bisogno di tutto ciò che è taggato in Git nelle immagini (e in caso contrario, dobbiamo eliminarlo in Git stesso);
  2. tutti i pod attualmente inviati a Kubernetes;
  3. vecchi ReplicaSet (ciò che è stato rilasciato di recente) e prevediamo anche di scansionare le versioni di Helm e selezionare lì le immagini più recenti.

... e crea una whitelist da questo set: un elenco di immagini che non elimineremo. Puliamo tutto il resto, dopodiché troviamo le immagini della fase orfana e cancelliamo anche quelle.

Fase di distribuzione

Dichiaratività affidabile

Il primo punto su cui vorrei attirare l'attenzione nella distribuzione è il lancio della configurazione aggiornata delle risorse, dichiarata in modo dichiarativo. Il documento YAML originale che descrive le risorse Kubernetes è sempre molto diverso dal risultato effettivamente in esecuzione nel cluster. Poiché Kubernetes aggiunge alla configurazione:

  1. identificatori;
  2. informazioni sul servizio;
  3. molti valori predefiniti;
  4. sezione con lo stato attuale;
  5. modifiche apportate nell'ambito del webhook di ammissione;
  6. il risultato del lavoro di vari controllori (e dello scheduler).

Pertanto, quando viene visualizzata una nuova configurazione di risorsa (nuovi), non possiamo semplicemente prendere e sovrascrivere con esso la configurazione corrente "live" (vivere). Per fare questo dovremo confrontare nuovi con l'ultima configurazione applicata (applicato per ultimo) e arrotolarlo vivere patch ricevuta.

Questo approccio si chiama Unione a 2 vie. Viene utilizzato, ad esempio, in Helm.

C'è anche Unione a 3 vie, che differisce in quanto:

  • confrontando applicato per ultimo и nuovi, guardiamo cosa è stato cancellato;
  • confrontando nuovi и vivere, guardiamo cosa è stato aggiunto o modificato;
  • a cui viene applicata la patch sommata vivere.

Distribuiamo oltre 1000 applicazioni con Helm, quindi in realtà viviamo con l'unione a 2 vie. Tuttavia, presenta una serie di problemi che abbiamo risolto con le nostre patch, che aiutano Helm a funzionare normalmente.

Stato reale di implementazione

Dopo che il nostro sistema CI ha generato una nuova configurazione per Kubernetes in base all'evento successivo, la trasmette per l'uso (fare domanda a) a un cluster: utilizzando Helm o kubectl apply. Successivamente avviene la già descritta fusione a N vie, alla quale l'API Kubernetes risponde con approvazione al sistema CI e quello al suo utente.

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

C’è però un grosso problema: dopo tutto un'applicazione riuscita non significa un'implementazione riuscita. Se Kubernetes capisce quali modifiche devono essere apportate e le applica, non sappiamo ancora quale sarà il risultato. Ad esempio, l'aggiornamento e il riavvio dei pod nel frontend potrebbero avere esito positivo, ma non nel backend e otterremo versioni diverse delle immagini dell'applicazione in esecuzione.

Per fare tutto correttamente, questo schema richiede un collegamento aggiuntivo: un tracker speciale che riceverà informazioni sullo stato dall'API Kubernetes e le trasmetterà per un'ulteriore analisi dello stato reale delle cose. Abbiamo creato una libreria Open Source in Go - cubedog (vedi il suo annuncio qui), che risolve questo problema ed è integrato in werf.

Il comportamento di questo tracker a livello werf è configurato utilizzando le annotazioni inserite su Deployments o StatefulSets. Annotazione principale - fail-mode - comprende i seguenti significati:

  • IgnoreAndContinueDeployProcess — ignoriamo i problemi legati all’implementazione di questa componente e continuiamo l’implementazione;
  • FailWholeDeployProcessImmediately — un errore in questo componente interrompe il processo di distribuzione;
  • HopeUntilEndOfDeployProcess — speriamo che questo componente funzioni entro la fine della distribuzione.

Ad esempio, questa combinazione di risorse e valori di annotazione fail-mode:

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Quando eseguiamo la distribuzione per la prima volta, il database (MongoDB) potrebbe non essere ancora pronto: le distribuzioni falliranno. Ma puoi aspettare il momento in cui inizia e la distribuzione avrà comunque luogo.

Ci sono altre due annotazioni per kubedog in werf:

  • failures-allowed-per-replica — il numero di cadute consentite per ciascuna replica;
  • show-logs-until — regola il momento fino al quale werf mostra (nello stdout) i log di tutti i pod lanciati. L'impostazione predefinita è PodIsReady (per ignorare i messaggi che probabilmente non vogliamo quando il traffico inizia ad arrivare al pod), ma sono validi anche i valori: ControllerIsReady и EndOfDeploy.

Cos'altro vogliamo dalla distribuzione?

Oltre ai due punti già descritti vorremmo:

  • видеть log - e solo quelli necessari, e non tutto di seguito;
  • traccia progresso, perché se il lavoro resta bloccato “in silenzio” per diversi minuti, è importante capire cosa sta succedendo lì;
  • иметь ripristino automatico nel caso in cui qualcosa sia andato storto (e quindi è fondamentale conoscere il reale stato della distribuzione). Il rollout deve essere atomico: o si arriva fino in fondo, oppure tutto ritorna allo stato precedente.

Risultati di

Per noi come azienda, per implementare tutte le sfumature descritte nelle diverse fasi di consegna (costruzione, pubblicazione, distribuzione), sono sufficienti un sistema CI e un'utilità werf.

Invece di una conclusione:

werf - il nostro strumento per CI/CD in Kubernetes (panoramica e rapporto video)

Con l'aiuto di werf, abbiamo fatto buoni progressi nella risoluzione di un gran numero di problemi per gli ingegneri DevOps e saremmo lieti se la comunità più ampia provasse almeno questa utilità in azione. Sarà più facile ottenere un buon risultato insieme.

Video e diapositive

Video dello spettacolo (~47 minuti):

Presentazione del rapporto:

PS

Altri resoconti su Kubernetes sul nostro blog:

Fonte: habr.com

Aggiungi un commento