
L'articolo esamina la problematica della pulizia delle immagini che si accumulano nei registri dei contenitori (Docker Registry e suoi analoghi) nel contesto degli attuali pipeline CI/CD per le applicazioni cloud native distribuite su Kubernetes. Vengono forniti i principali criteri di attualità delle immagini e le difficoltà derivanti nell'automazione della pulizia, nel risparmio di spazio e nella soddisfazione delle esigenze dei team. Infine, attraverso un esempio concreto di un progetto Open Source, spiegheremo come queste difficoltà possano essere superate.
Introduzione
Il numero di immagini nel registro dei contenitori può crescere rapidamente, occupando più spazio di archiviazione e, di conseguenza, aumentando significativamente il suo costo. Per controllare, limitare o mantenere una crescita accettabile dello spazio occupato nel registry, è consuetudine:
- utilizzare un numero fisso di tag per le immagini;
- in qualche modo pulire le immagini.
La prima limitazione è talvolta accettabile per piccoli team. Se agli sviluppatori bastano tag fissi (latest, main, test, boris e simili), il registro non si ingrandirà e per un lungo periodo non sarà necessario nemmeno pensare alla pulizia. Infatti, tutte le immagini obsolete vengono sovrascritte e non rimane lavoro da fare per la pulizia (tutto viene gestito dal garbage collector standard).
Tuttavia, questo approccio limita notevolmente lo sviluppo e raramente si adatta ai progetti CI/CD moderni. Un elemento imprescindibile dello sviluppo è diventato automazione, che consente di testare, distribuire e fornire nuove funzionalità agli utenti in modo molto più rapido. Ad esempio, in tutti i nostri progetti, ad ogni commit viene automaticamente creato un pipeline CI. In esso viene assemblata un'immagine, testata, distribuita in diversi ambienti Kubernetes per debugging e ulteriori verifiche, e se tutto va bene, le modifiche arrivano all'utente finale. E non è più una questione di rocket science, ma una routine per molti — probabilmente anche per te, dato che leggi questo articolo.
Poiché la risoluzione dei bug e lo sviluppo di nuove funzionalità avvengono in parallelo, e i rilasci possono avvenire più volte al giorno, è ovvio che il processo di sviluppo è accompagnato da un considerevole numero di commit, e quindi — un gran numero di immagini nel registry. Di conseguenza, sorge l'importante questione di come organizzare una pulizia efficace del registry, ossia la rimozione delle immagini obsolete.
Ma come si può definire se un'immagine è attuale?
Criteri di rilevanza dell'immagine
Nella stragrande maggioranza dei casi, i criteri principali saranno i seguenti:
1. Il primo (il più ovvio e critico di tutti) è rappresentato dalle immagini che attualmente vengono utilizzate in Kubernetes. La rimozione di queste immagini potrebbe comportare costi significativi dovuti ai tempi di inattività della produzione (ad esempio, le immagini potrebbero essere necessarie durante la replica) o vanificare gli sforzi del team che si occupa del debug su uno dei contorni. (Per questo motivo abbiamo persino creato un , per monitorare l'assenza di tali immagini in qualsiasi cluster Kubernetes.)
2. Il secondo (meno ovvio, ma comunque molto importante e ancora relativo all'operatività) sono le immagini che sono necessarie per il downgrade in caso di gravi problemi nella versione attuale. Ad esempio, nel caso di Helm, si tratta delle immagini utilizzate nelle versioni salvate del rilascio. (A proposito, di default in Helm c'è un limite di 256 revisioni, ma è improbabile che qualcuno abbia realmente bisogno di mantenere tanto grande numero di versioni?..) Infatti, noi, per esempio, conserviamo versioni in modo da poterle utilizzare successivamente, cioè "tornare" su di esse in caso di necessità.
3. Il terzo è il bisogno degli sviluppatori: tutte le immagini correlate ai loro lavori attuali. Ad esempio, se stiamo considerando un PR, ha senso mantenere l'immagine corrispondente all'ultimo commit e, diciamo, al commit precedente: in questo modo lo sviluppatore può tornare rapidamente a qualsiasi attività e lavorare con le ultime modifiche.
4. Il quarto sono le immagini che corrispondono alle versioni della nostra applicazione, cioè sono il prodotto finale: v1.0.0, 20.04.01, sierra, ecc.
NB: I criteri indicati qui sono stati formulati sulla base dell'esperienza di interazione con decine di team di sviluppo di diverse aziende. Tuttavia, naturalmente, a seconda delle peculiarità nei processi di sviluppo e dell'infrastruttura utilizzata (ad esempio, se non viene utilizzato Kubernetes), questi criteri possono variare.
Corrispondenza dei criteri e soluzioni esistenti
I servizi popolari con container registry offrono generalmente le proprie politiche di pulizia delle immagini: in esse è possibile definire le condizioni in base alle quali un tag viene rimosso dal registry. Tuttavia, le possibilità di queste condizioni sono limitate a parametri come nomi, data di creazione e numero di tag*.
* Dipende dalle implementazioni specifiche del container registry. Abbiamo considerato le possibilità delle seguenti soluzioni: Azure CR, Docker Hub, ECR, GCR, GitHub Packages, GitLab Container Registry, Harbor Registry, JFrog Artifactory, Quay.io — a partire da settembre 2020.
Un tale insieme di parametri è più che sufficiente per soddisfare il quarto criterio — cioè selezionare le immagini corrispondenti alle versioni. Tuttavia, per tutti gli altri criteri è necessario scegliere una qualche soluzione di compromesso (una politica più rigida o, al contrario, più permissiva) — a seconda delle aspettative e delle possibilità finanziarie.
Ad esempio, il terzo criterio — legato alle esigenze degli sviluppatori — può essere affrontato organizzando i processi all'interno dei team: denominazione specifica delle immagini, gestione di speciali allow lists e accordi interni. Ma alla fine deve comunque essere automatizzato. E se le funzionalità delle soluzioni pronte non sono sufficienti, è necessario fare qualcosa di proprio.
La situazione è analoga per i primi due criteri: non possono essere soddisfatti senza ottenere dati da un sistema esterno — quello in cui avviene il deployment delle applicazioni (nel nostro caso, Kubernetes).
Illustrazione del workflow in Git
Supponiamo che tu stia lavorando più o meno con questo schema in Git:

Nella diagramma, le icone con la testa indicano le immagini dei contenitori attualmente distribuite in Kubernetes per alcuni utenti (utenti finali, tester, manager, ecc.) o utilizzate dagli sviluppatori per il debug e scopi simili.
Cosa succederà se le politiche di pulizia consentono di mantenere (non eliminare) le immagini solo in base ai nomi dei tag specificati?

È ovvio che tale scenario non farà piacere a nessuno.
Cosa cambierà se le politiche permettono di non eliminare le immagini in base a un intervallo temporale specificato / numero di ultimi commit?

Il risultato è notevolmente migliorato, ma è ancora lontano dall'ideale. Infatti, abbiamo ancora sviluppatori che necessitano di immagini nel registro (o addirittura distribuite in K8s) per il debugging dei bug...
Ricapitolando la situazione attuale sul mercato: le funzionalità disponibili nei registri dei contenitori non offrono la flessibilità necessaria durante la pulizia, e la principale ragione è che non c'è possibilità di interagire con il mondo esterno. Si tratta quindi di team che richiedono tale flessibilità, costretti a gestire autonomamente la rimozione delle immagini "esternamente", utilizzando l'API di Docker Registry (o l'API nativa della relativa implementazione).
Tuttavia, stavamo cercando una soluzione universale che automatizzasse la pulizia delle immagini per diversi team, che utilizzano registri diversi…
Il nostro percorso verso una pulizia universale delle immagini
Perché c'è questa necessità? Il fatto è che non siamo un gruppo di sviluppatori isolato, ma un team che supporta molteplici gruppi di questo tipo, aiutando a risolvere complessivamente le questioni CI/CD. E il principale strumento tecnico per questo è un'utilità Open Source . La sua particolarità è che non esegue una sola funzione, ma accompagna i processi di consegna continua in tutte le fasi: dalla compilazione al deployment.
La pubblicazione nel registro* delle immagini (subito dopo la loro costruzione) è una funzione ovvia di tale utilità. E poiché le immagini vengono memorizzate lì, se il tuo archivio non è illimitato, è necessario occuparsi anche della loro successiva pulizia. Di come abbiamo raggiunto il successo in questo, soddisfacendo tutti i criteri richiesti, si parlerà più avanti.
* Anche se i registri stessi possono essere diversi (Docker Registry, GitLab Container Registry, Harbor, ecc.), i loro utenti si trovano di fronte agli stessi problemi. La soluzione universale nel nostro caso non dipende dall'implementazione del registro, poiché viene eseguita al di fuori dei registri stessi e offre un comportamento uniforme per tutti.
Anche se utilizziamo werf come esempio di implementazione, speriamo che gli approcci utilizzati possano essere utili anche ad altri team che si trovano ad affrontare difficoltà simili.
Quindi, ci siamo occupati esternamente dell'implementazione di un meccanismo per la pulizia delle immagini — invece delle funzionalità già integrate nei registri per i contenitori. Il primo passo è stato l'uso dell'API di Docker Registry per creare le stesse politiche primitive riguardo al numero di tag e al loro tempo di creazione (menzionati sopra). A esse è stata aggiunta una lista di autorizzazione basata sulle immagini utilizzate nell'infrastruttura distribuita, cioè Kubernetes. Per quest'ultimo è stato sufficiente scorrere tutte le risorse distribuite tramite l'API di Kubernetes e ottenere un elenco di valori. image.
Una soluzione così triviale ha risolto il problema più critico (criterio n. 1), ma è stata solo l'inizio del nostro viaggio per migliorare il meccanismo di pulizia. Il passo successivo — decisamente più interessante — è stato trovare una soluzione per collegare le immagini pubblicate con la storia di Git.
Schemi di tagging
Per iniziare, abbiamo scelto un approccio in cui l'immagine finale deve conservare le informazioni necessarie per la pulizia e abbiamo costruito il processo sugli schemi di tagging. Quando si pubblica un'immagine, l'utente seleziona una specifica opzione di tagging (git-branch, git-commit o git-tag) e utilizza il valore corrispondente. Nei sistemi CI, l'impostazione di questi valori avveniva automaticamente sulla base delle variabili d'ambiente. In sostanza l'immagine finale era collegata a un particolare primitivo Git, conservando i dati necessari per la pulizia nelle etichette.
Con questo approccio, è stata realizzata una serie di politiche che consentivano di utilizzare Git come unica fonte di verità:
- Quando un ramo/tag veniva eliminato in Git, le immagini correlate venivano automaticamente rimosse dal registry.
- Il numero di immagini associate ai tag e ai commit di Git poteva essere regolato dal numero di tag utilizzati nello schema selezionato e dal tempo di creazione del commit correlato.
In generale, l'implementazione risultante soddisfaceva le nostre esigenze, ma ben presto ci aspettavano nuove sfide. Il problema è che, mentre utilizzavamo schemi di tagging sui primitivi Git, ci siamo imbattuti in vari svantaggi. (Poiché la loro descrizione va al di là dell'argomento di questo articolo, chiunque fosse interessato può consultare i dettagli .) Pertanto, decidendo di passare a un approccio di tagging più efficace (tagging basato sul contenuto), abbiamo dovuto rivedere anche l'implementazione della pulizia delle immagini.
Nuovo algoritmo
Perché? Con il tagging basato sul contenuto, ogni tag può soddisfare molteplici commit in Git. Nella pulizia delle immagini non è più possibile partire solo dal commit su cui è stato aggiunto il nuovo tag al registro.
Per il nuovo algoritmo di pulizia, è stato deciso di abbandonare gli schemi di tagging e costruire un processo su meta-immagini, ognuna delle quali conserva un collegamento tra:
- il commit in cui è stata effettuata la pubblicazione (senza che abbia importanza se l'immagine nel registro sia stata aggiunta, modificata o rimasta invariata);
- e il nostro identificatore interno corrispondente all'immagine assemblata.
In altre parole, è stata garantita la connessione tra i tag pubblicati e i commit in Git.
La configurazione finale e l'algoritmo generale
Agli utenti, durante la configurazione della pulizia, sono state rese disponibili politiche tramite le quali viene effettuato il campionamento delle immagini attuali. Ogni politica è definita da:
- un insieme di references, ovvero tag Git o branch Git, utilizzati durante la scansione;
- e un limite di immagini ricercabili per ogni reference dell'insieme.
A titolo esemplificativo, ecco come è diventata la configurazione delle politiche predefinite:
cleanup:
keepPolicies:
- references:
tag: \/.*\/
limit:
last: 10
- references:
branch: \/.*\/
limit:
last: 10
in: 168h
operator: And
imagesPerReference:
last: 2
in: 168h
operator: And
- references:
branch: \/^(main|staging|production)\$\/
imagesPerReference:
last: 10
Questa configurazione contiene tre politiche che seguono le seguenti regole:
- Conservare l'immagine per gli 10 ultimi tag Git (in base alla data di creazione del tag).
- Conservare al massimo 2 immagini pubblicate nell'ultima settimana per al massimo 10 branch con attività nell'ultima settimana.
- Conservare 10 immagini per i branch
main,stagingeproduction.
L'algoritmo finale si riassume nei seguenti passi:
- Ottenere i manifesti dal container registry.
- Escludere le immagini utilizzate in Kubernetes, poiché già le abbiamo selezionate preventivamente interpellando l'API di K8s.
- Scansionare la storia di Git e escludere le immagini secondo le politiche designate.
- Eliminare le immagini rimanenti.
Tornando alla nostra illustrazione, ecco cosa accade con werf:

Tuttavia, anche se non utilizzate werf, un approccio simile alla pulizia avanzata delle immagini — in una qualsiasi delle sue implementazioni (in conformità all'approccio preferito per la taggatura delle immagini) — può essere applicato anche in altri sistemi/utilità. È sufficiente tenere a mente i problemi che sorgono e individuare quelle opportunità nel vostro stack che consentono di integrare la loro soluzione nel modo più snodato possibile. Speriamo che il percorso che abbiamo seguito aiuti anche a considerare il vostro caso specifico con nuovi dettagli e riflessioni.
Conclusione
- Prima o poi, la maggior parte dei team si trova ad affrontare il problema del sovraccarico del registry.
- Nella ricerca di soluzioni, è fondamentale innanzitutto definire i criteri di rilevanza dell'immagine.
- Gli strumenti offerti dai popolari servizi di container registry consentono di organizzare una pulizia molto semplice, che non considera il «mondo esterno»: le immagini utilizzate in Kubernetes e le peculiarità dei flussi di lavoro del team.
- Un algoritmo flessibile ed efficace deve avere una comprensione dei processi CI/CD, operando non solo con i dati delle immagini Docker.
P.S.
Leggi anche nel nostro blog:
- «»;
- «»;
- «»;
- «».
Fonte: habr.com
