werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Il 27 maggio nella sala principale della conferenza DevOpsConf 2019, che si svolge nell'ambito del festival RIT++ 2019, durante la sezione "Continuous Delivery", è stata presentata una relazione intitolata "werf — il nostro strumento per CI/CD in Kubernetes". In essa si parla di problemi e sfide che ognuno incontra nel deploy in Kubernetes, così come delle sfumature che potrebbero non essere immediatamente evidenti. Esaminando possibili soluzioni, mostriamo come ciò sia realizzato nello strumento Open Source werf.

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

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Quindi, presentiamo il video della presentazione (~47 minuti, molto più informativo dell'articolo) e un riepilogo testuale. Iniziamo!

Consegna del codice in Kubernetes

La presentazione riguarderà più il CI/CD in Kubernetes, supponendo che il nostro software sia impacchettato in contenitori Docker (di questo ho parlato nella relazione del 2016), e K8s sarà utilizzato per il suo avvio in produzione (di questo — nel 2017).

Come si presenta la consegna in Kubernetes?

  • C'è un repository Git con il codice e le istruzioni per la sua compilazione. L'applicazione viene costruita in un'immagine Docker e pubblicata nel Docker Registry.
  • Nel stesso repository ci sono istruzioni su come distribuire e avviare l'applicazione. Durante la fase di distribuzione, queste istruzioni vengono inviate a Kubernetes, che riceve l'immagine necessaria dal registry e la avvia.
  • Inoltre, di solito ci sono dei test. Alcuni di questi possono essere eseguiti durante la pubblicazione dell'immagine. È anche possibile (seguendo le stesse istruzioni) distribuire una copia dell'applicazione (in un namespace K8s separato o in un cluster diverso) e eseguire i test lì.
  • Infine, è necessaria una CI system che riceve eventi da Git (o pressioni di pulsanti) e attiva tutte le fasi designate: build, publish, deploy, test.

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Qui ci sono alcune osservazioni importanti:

  1. Poiché abbiamo un'infrastruttura immutabile (immutable infrastructure), l'immagine dell'applicazione, che viene utilizzata in tutte le fasi (staging, production, ecc.) deve essere unica.. Ne ho parlato in dettaglio con esempi qui.
  2. Poiché seguiamo l'approccio infrastruttura come codice (IaC), il codice dell'applicazione, le istruzioni per la sua compilazione e avvio devono trovarsi nello stesso repository.. Per ulteriori dettagli, vedere la stessa presentazione..
  3. La catena di consegna (consegna) di solito la vediamo così: l'applicazione viene assemblata, testata e rilasciata (fase di rilascio) e tutto — la consegna è avvenuta. Ma in realtà l'utente riceve ciò che avete rilasciato, non solo quando l'avete consegnato in produzione, e quando ha potuto accedervi e questa produzione ha funzionato. Pertanto, ritengo che la catena di consegna finisca solo nella fase di esercizio (esecuzione), e per essere più precisi, anche nel momento in cui il codice viene rimosso dalla produzione (sostituito con uno nuovo).

Torniamo allo schema di consegna sopra menzionato in Kubernetes: non solo noi l'abbiamo inventato, ma anche letteralmente chiunque si sia occupato di questo problema. In sostanza, questo modello viene ora chiamato GitOps (puoi leggere di più sul termine e sulle idee che lo supportano qui). Diamo un'occhiata alle fasi dello schema.

Fase di assemblaggio

Sembrerebbe che nel 2019 ci sia poco da dire sull'assemblaggio delle immagini Docker, dato che tutti sanno scrivere Dockerfile e avviare docker build?.. Вот нюансы, на которые хотелось бы обратить внимание:

  1. Il peso dell'immagine è importante, quindi utilizza multi-stage, per mantenere nell'immagine solo ciò che è realmente necessario per il funzionamento dell'applicazione.
  2. Il numero di strati deve essere minimizzato, unendo le catene di comandi RUN-per significato.
  3. Tuttavia, questo aggiunge complessità al debug, poiché durante il fallimento della build bisogna trovare il comando giusto nella catena che ha causato il problema.
  4. La velocità di build è importante, perché vogliamo implementare rapidamente le modifiche e vedere i risultati. Ad esempio, non vogliamo ricompilare le dipendenze delle librerie del linguaggio ad ogni build dell'applicazione.
  5. Spesso è necessario molte immagini, il che può essere risolto con un insieme di Dockerfile (o fasi nominate in un unico file) e uno script Bash per la loro compilazione sequenziale.

Questa era solo la punta dell'iceberg con cui tutti si confrontano. Ma ci sono anche altri problemi, in particolare:

  1. Spesso durante la fase di build dobbiamo montare qualcosa (ad esempio, memorizzare nella cache il risultato di un comando come apt in una directory esterna).
  2. Vogliamo Ansible anziché scrivere in shell.
  3. Vogliamo compilare senza Docker (perché dovremmo avere una macchina virtuale aggiuntiva da configurare quando abbiamo già un cluster Kubernetes dove possiamo eseguire i container?).
  4. Build parallele, che può essere interpretato in diversi modi: diversi comandi da Dockerfile (se si utilizza il multi-stage), più commit di un singolo repository, più Dockerfile.
  5. Build distribuita: vogliamo assemblare qualcosa in pod che sono "efimeri", poiché la loro cache scompare, quindi deve essere archiviata da qualche parte separatamente.
  6. Infine, ho chiamato il culmine dei desideri automagia: sarebbe ideale entrare nel repository, digitare un comando e ricevere un'immagine pronta, costruita con la comprensione di come e cosa fare correttamente. Tuttavia, personalmente non sono sicuro che tutte le sfumature possano essere previste in questo modo.

Ecco alcuni progetti:

  • moby/buildkit — il costruttore della Docker Inc (già integrato nelle versioni attuali di Docker), che cerca di risolvere tutti questi problemi;
  • kaniko — un costruttore di Google che consente di costruire senza Docker;
  • Buildpacks.io — tentativo di CNCF di creare automagia e, in particolare, una soluzione interessante con rebase per i livelli;
  • e molte altre utilità come buildah, genuinetools/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. — la crescita parallela di costruttori alternativi, ognuno dei quali affronta una parte dei problemi, ne è la prova.

Costruzione in werf

Così siamo arrivati a werf (precedentemente nota come dapp) — un'utility Open Source dell'azienda «Flant», che sviluppiamo da molti anni. Tutto è iniziato circa 5 anni fa con script Bash per ottimizzare la costruzione dei Dockerfile, e negli ultimi 3 anni si è svolta una vera e propria sviluppo all'interno di un unico progetto con il proprio repository Git. (inizialmente in Ruby, poi riscritto in Go, e nel contempo rinominato). Quali problemi di costruzione sono stati risolti in werf?

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

I problemi evidenziati in blu sono già stati implementati, la costruzione parallela è stata effettuata all'interno di un unico host, mentre le questioni evidenziate in giallo prevediamo di completare entro la fine dell'estate.

Fase di pubblicazione nel registry (publish)

Abbiamo caricato docker push… — cosa potrebbe esserci di difficile nel caricare un'immagine nel registry? E qui sorge la domanda: «Quale tag assegnare all'immagine?» Essa si pone perché abbiamo Gitflow (o un'altra strategia di Git) e Kubernetes, e l'industria cerca di assicurare che quanto avviene in Kubernetes segua quanto fatto in Git. Dopotutto, Git è la nostra unica fonte di verità.

Qual è la difficoltà? Garantire la riproducibilità: dal commit in Git, che per sua natura è immutabile (immutabile), fino all'immagine Docker, che deve rimanere la stessa.

È inoltre importante per noi determinare l'origine, perché vogliamo capire da quale commit è stata compilata l'applicazione in esecuzione su Kubernetes (così possiamo fare diff e simili).

Strategie di tagging

La prima è un semplice git tag. Abbiamo un registry con un'immagine, taggata come 1.0. In Kubernetes ci sono stage e production, dove quest'immagine è stata caricata. In Git facciamo commit e a un certo punto mettiamo il tag 2.0. Lo raccogliamo secondo le istruzioni del repository e lo posizioniamo nel registry con il tag 2.0. Viene distribuito su stage e, se tutto va bene, poi su production.

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Il problema di questo approccio è che prima abbiamo messo il tag e solo dopo abbiamo testato e distribuito. Perché? Prima di tutto, non è logico: stiamo rilasciando una versione del software che non abbiamo nemmeno verificato (non possiamo fare diversamente, poiché per testare è necessario mettere un tag). In secondo luogo, questo percorso non si integra con Gitflow.

La seconda opzione è git commit + tag. Nella branch master c'è un tag 1.0; per lui nel registry — immagine distribuita in produzione. In aggiunta, nel cluster Kubernetes ci sono contorni di preview e staging. Proseguiamo quindi con Gitflow: nel ramo principale per lo sviluppo (develop) creiamo nuove funzionalità, il che porta alla creazione di un commit con l'identificativo #c1. Lo raccogliamo e lo pubblichiamo nel registry, utilizzando questo identificativo (#c1). Con lo stesso identificativo lo distribuiamo su preview. Facciamo lo stesso con i commit #c2 e #c3.

Quando ci rendiamo conto che la funzionalità è sufficiente, iniziamo a stabilizzarla. In Git creiamo un ramo release_1.1 (basato su #c3 di develop). Non sarà necessario ricompilare questo rilascio, poiché è stato fatto nel passaggio precedente. Pertanto possiamo semplicemente distribuirlo su staging. Correggiamo i bug in #c4 e allo stesso modo distribuiamo su staging. Parallelamente, continua lo sviluppo in develop, dove periodicamente vengono prelevate modifiche da release_1.1. A un certo punto otteniamo un commit compilato e distribuito su staging, di cui siamo soddisfatti (#c25).

Allora facciamo il merge (con fast-forward) del ramo di rilascio (release_1.1) in master. Mettiamo su questo commit un tag con la nuova versione (1.1). Ma questa immagine è già stata compilata nel registry, quindi, per non compilarla di nuovo, aggiungiamo semplicemente un secondo tag all'immagine esistente (ora nel registry ha i tag #c25 e 1.1). Dopo di che lo rilasciamo in produzione.

C'è uno svantaggio, perché su staging è stato estratto un'immagine (#c25), mentre in produzione — come se fosse un'altra (1.1), ma sappiamo che «fisicamente» è la stessa immagine dal registry.

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Il vero svantaggio è che non c'è supporto per i merge commit, bisogna fare fast-forward.

Possiamo andare oltre e fare un trucco... Consideriamo 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

Creeremo un file secondo questo principio, prendendo:

  • SHA256 dagli identificatori delle immagini utilizzate (ruby:2.3 e nginx:alpine), che sono le somme di controllo del loro contenuto;
  • tutte le comandi (RUN, CMD e simili);
  • SHA256 dei file che sono stati aggiunti.

... e prenderemo la somma di controllo (di nuovo SHA256) di tale file. Questa è la firma di tutto ciò che definisce il contenuto dell'immagine Docker.

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Torniamo allo schema e invece dei commit useremo tali firme, ossia etichettare le immagini con le firme.

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Ora, quando sarà necessario, ad esempio, 'mergiare' le modifiche dalla release nel master, possiamo effettuare un vero merge commit: avrà un identificatore diverso, ma la stessa firma. Con lo stesso identificatore faremo il rilascio anche in produzione.

Il problema è che ora non sarà possibile determinare quale commit è stato rilasciato in produzione: le stringhe di controllo funzionano solo in un'unica direzione. Questo problema viene risolto aggiungendo un ulteriore livello di metadati — ne parlerò più avanti.

Tagging in werf

In werf siamo andati anche oltre e ci prepariamo a realizzare una build distribuita con una cache che non è salvata su una sola macchina... Quindi, stiamo costruendo immagini Docker di due tipi, che chiamiamo stage e image.

Nel repository Git di werf sono memorizzate istruzioni specifiche per la build che descrivono le varie fasi della costruzione (beforeInstall, install, beforeSetup, setup). Creiamo un'immagine di fase con una firma definita come checksum dei primi passaggi. Aggiungiamo quindi il codice sorgente; per la nuova immagine di fase ne calcoliamo la checksum... Queste operazioni si ripetono per tutti i passaggi, generando un insieme di immagini di fase. Poi realizziamo l'immagine finale, contenente anche i metadati sulla sua origine. E questa immagine la tagghiamo in vari modi (dettagli più avanti).

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Supponiamo che dopo ciò si presenti un nuovo commit, in cui è stato modificato solo il codice dell'applicazione. Cosa succederà? Per le modifiche al codice verrà creato un patch e preparata una nuova immagine di fase. La sua firma sarà definita come checksum della vecchia immagine di fase e della nuova patch. Da quest'immagine verrà quindi generata una nuova immagine finale. Un comportamento analogo avverrà per le modifiche in altri passaggi.

In questo modo, le immagini di fase sono una cache che può essere conservata in modo distribuito, mentre le immagini finali create da esse vengono caricate nel Docker Registry.

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Pulizia del registry

Non si tratta di rimuovere i layer rimasti appesi dopo i tag rimossi, questa è una funzionalità standard di Docker Registry. La questione riguarda la situazione in cui si accumulano molti tag Docker e ci rendiamo conto che parte di essi non è più necessaria, ma occupano spazio (e/o stiamo pagando per esso).

Quali strategie di pulizia esistono?

  1. Si può semplicemente non fare nulla non pulire. A volte è davvero più semplice pagare un po' di più per lo spazio in eccesso piuttosto che districare un enorme groviglio di tag. Ma questo funziona solo fino a un certo punto.
  2. Ripristino completo. Se rimuoviamo tutte le immagini e ricostruiamo solo quelle attuali nel sistema CI, potrebbe sorgere un problema. Se nel production viene riavviato un container, verrà caricato una nuova immagine — una che non è stata testata da nessuno. Questo uccide l'idea di infrastruttura immutabile.
  3. Blue-green. Un registry ha iniziato a riempirsi — carichiamo le immagini in un altro. Stessa identica problematica del metodo precedente: quando si può pulire quel registry che ha iniziato a riempirsi?
  4. In base al tempo. Eliminare tutte le immagini più vecchie di 1 mese? Ma ci sarà sicuramente un servizio che non è stato aggiornato per un intero mese...
  5. Manuale determinare cosa può essere già rimosso.

Ci sono davvero due opzioni valide: non pulire oppure una combinazione di blue-green + manualmente. Nel secondo caso, si tratta di quanto segue: quando capisci che è ora di pulire il registry, crei un nuovo registro e aggiungi tutte le nuove immagini a esso per un mese, ad esempio. Dopo un mese, guardi quali pod in Kubernetes stanno ancora usando il vecchio registry e li trasferisci nel nuovo registry.

A cosa siamo arrivati a werf? Мы собираем:

  1. Git head: tutti i tag, tutti i rami, supponendo che tutto ciò che è taggato in Git ci serva anche nelle immagini (e se no, bisogna rimuoverlo in Git);
  2. tutti i pod che sono attualmente scaricati in Kubernetes;
  3. vecchi ReplicaSet (ciò che è stato scaricato di recente), e pianifichiamo di scansionare le release di Helm e selezionare le ultime immagini lì.

… e facciamo da questo insieme una whitelist — un elenco di immagini che non rimuoveremo. Tutto il resto viene pulito, dopo di che troviamo le immagini di stage orfane e le rimuoviamo anche.

Fase di deploy

Affidabilità dichiarativa

Il primo punto su cui vorrei soffermarmi nel deployment è l'implementazione di una configurazione aggiornata delle risorse, dichiarata in modo dichiarativo. Il documento YAML originale che descrive le risorse di Kubernetes è sempre molto diverso dal risultato che effettivamente funziona nel cluster. Questo perché Kubernetes aggiunge alla configurazione:

  1. identificatori;
  2. informazioni di servizio;
  3. numerosi valori predefiniti;
  4. una sezione con lo stato attuale;
  5. modifiche apportate durante il lavoro dell'admission webhook;
  6. il risultato del lavoro di vari controllori (e pianificatori).

Pertanto, quando appare una nuova configurazione delle risorse (new), non possiamo semplicemente prendere e sovrascrivere quella attuale, "viva", configurazione (live). Dobbiamo confrontarla new con la configurazione precedente applicata (last-applied) e applicare live la patch risultante.

Questo approccio è chiamato 2-way merge. Viene utilizzato, ad esempio, in Helm.

C'è anche un 3-way merge, che si differenzia per il fatto che:

  • confrontando last-applied e new, osserviamo cosa è stato rimosso;
  • confrontando new e live, osserviamo cosa è stato aggiunto o modificato;
  • applichiamo la patch cumulativa su live.

Deployiamo oltre 1000 applicazioni con Helm, quindi viviamo realmente con una fusione bidirezionale. Tuttavia, ci sono una serie di problemi che abbiamo risolto con le nostre patch, che aiutano Helm a funzionare correttamente.

Stato reale del rollout

Dopo che il nostro sistema CI ha generato una nuova configurazione per Kubernetes in seguito a un evento, la trasmette per l'applicazione (applica) al cluster — utilizzando Helm o kubectl apply. Successivamente si verifica già la fusione N-way descritta, alla quale l'API di Kubernetes risponde positivamente al sistema CI, che a sua volta comunica con l'utente.

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Tuttavia, c'è un grande problema: infatti l'applicazione riuscita non implica un rollout riuscito.Se Kubernetes ha capito quali modifiche applicare e le applica — non sappiamo ancora quale sarà il risultato. Ad esempio, l'aggiornamento e il riavvio dei pod nel frontend possono andare a buon fine, mentre nel backend possono fallire, e otteniamo versioni diverse delle immagini dell'applicazione in esecuzione.

Per fare tutto in modo corretto, in questo schema è necessario un ulteriore anello — un tracker speciale che riceverà informazioni sullo stato dall'API di Kubernetes e le trasmetterà per ulteriori analisi della situazione reale. Abbiamo creato una libreria Open Source in Go — kubedog (vedi il suo annuncio. qui), — che risolve questo problema ed è integrata in werf.

Il comportamento di questo tracker a livello di werf può essere configurato tramite annotazioni applicate a Deployments o StatefulSets. L'annotazione principale è fail-mode — riconosce i seguenti valori:

  • IgnoreAndContinueDeployProcess — ignoriamo i problemi di distribuzione di questo componente e continuiamo il deploy;
  • FailWholeDeployProcessImmediately — un errore in questo componente arresta immediatamente il processo di distribuzione;
  • HopeUntilEndOfDeployProcess — speriamo che questo componente funzioni entro la fine del deploy.

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

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Quando distribuiamo per la prima volta, il database (MongoDB) potrebbe non essere ancora pronto — i Deployment falliranno. Ma possiamo aspettare che si avvii e il deploy può comunque avvenire.

Ci sono anche altre due annotazioni per kubedog in werf:

  • failures-allowed-per-replica — il numero di fallimenti consentiti per ogni replica;
  • show-logs-until — regola il momento fino al quale werf mostra (in stdout) i log da tutti i pod distribuiti. Di default è PodIsReady — (per ignorare i messaggi che probabilmente non ci servono quando un pod inizia a ricevere traffico), tuttavia sono accettabili anche i valori ControllerIsReady e EndOfDeploy.

Cosa vogliamo ancora dal deploy?

Oltre ai due punti già descritti, vorremmo:

  • vedere i log — solo quelli necessari, non tutti indiscriminatamente;
  • monitorare il progresso, perché se un job resta "silenziosamente" bloccato per alcuni minuti, è importante capire cosa sta succedendo;
  • avere un rollback automatico nel caso in cui qualcosa vada storto (è quindi cruciale conoscere lo stato reale del deploy). Il rilascio deve essere atomico: o va a buon fine fino alla fine, o tutto torna allo stato precedente.

Risultati

Per noi, come azienda, per implementare tutte le sfumature descritte nelle diverse fasi di consegna (build, publish, deploy) è sufficiente un sistema CI e un'utilità werf.

In conclusione:

werf — il nostro strumento per CI/CD in Kubernetes (panoramica e video della presentazione)

Con werf abbiamo fatto buoni progressi nella risoluzione di numerosi problemi per ingegneri DevOps e saremo felici se una comunità più ampia provasse almeno questa utilità nella pratica. Ottenere buoni risultati insieme sarà più semplice.

Video e slide

Video della presentazione (~47 minuti):

Riproduci video

Presentazione della relazione:

P.S.

Altre relazioni su Kubernetes nel nostro blog:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster