Chiariamo subito tutti i dubbi : è sbagliato avere più di un container in un singolo Pod. Ci sono diverse ragioni dietro a questa affermazione e ne menzionerò solo alcune. In ogni caso, il modo in cui Kubernetes è stato progettato ci porta al fatto che avere solo un container per Pod ci offre molta più flessibilità.
Due container: una cattiva idea
Abbiamo un’applicazione, composta da vari microservizi. La distribuiamo utilizzando i Deployments. In uno di questi Deployments, scegliamo di avere 2 dei nostri microservizi, diciamo il Microservizio A e il Microservizio B. Le prime versioni di entrambi i microservizi sono etichettate come v1.0. Il tempo passa e sviluppiamo una seconda versione del Microservizio A, etichettata come v2.0. La distribuiamo, e il Deployment ora ha v2.0 per il Microservizio A e v1.0 per il Microservizio B. Nel frattempo, il team del Microservizio B ha lavorato per apportare alcuni miglioramenti minori, e possiamo distribuire il Microservizio B v1.1. Aggiorniamo il nostro Deployment e ora abbiamo una situazione con il Microservizio A v2.0 e il Microservizio B v1.1. Aggiorniamo il nostro Deployment e ora abbiamo la situazione mostrata di seguito.
Mentre usiamo l’applicazione, alcuni dei nostri utenti hanno segnalato alcuni bug gravi. Analizziamo il problema e notiamo che dobbiamo fare rollback del Microservizio A alla v2.0. Andiamo al nostro Deployment e… Oh, fermi tutti ! Non possiamo fare rollback senza perdere i miglioramenti del Microservizio B. Dobbiamo quindi distribuire una nuova versione del nostro Deployment, contenente MicroservizioA:v1.0 e MicroservizioB:v1.1, invece di eseguire un rollback. Questo decisamente non è il modo in cui i Deployments dovrebbero essere utilizzati.
Solo alcuni esempi in più:
Scalabilità: immagina ci sia un Horizontal Pod Autoscaler in azione. Sappiamo che l’autoscaler aggiungerà nuove repliche quando il consumo di CPU di un container raggiunge una certa soglia. Se ci sono più container nello stesso Pod, l’autoscaler creerà nuovi Pod che però contengono anche quei container che non hanno bisogno di scalare creando un eccessivo consumo di risorse;
Scheduling: basta considerare la Pod Affinity. La regola va elaborata a livello di Pod, quindi influisce su ogni container che si trova all’interno. Oppure considera che lo Scheduler cercherà di ottimizzare il consumo di risorse: avere più container, che consumano più risorse, sarà il risultato di uno scheduling poco ottimizzato.
È quindi sempre sbagliato avere più di un solo container all’interno di un Pod? Beh, ci sono alcuni casi in cui è piuttosto conveniente utilizzare alcuni container companion, che aiuteranno l’application container a fare il suo lavoro. Ciò che è importante è avere un solo application container.
Quando è giusto: i pattern Sidecar, Adapter e Ambassador
Avendo capito che dovremmo avere solo un application container in un singolo Pod, è naturale immaginare quando è giusto avere più di un container: quando gli altri container servono il container principale. In che modo? Beh, il modo in cui sono funzionali all’ application container ci dice quale design pattern stiamo usando, e ne abbiamo 3: Sidecar, Adapter e Ambassador. Da un punto di vista dei container non c’è differenza: abbiamo solo un container companion che esegue operazioni invece di farle fare all’application container.
Questo è molto conveniente:
L’application container non dovrà fare cose che non sono legate alle problematiche che risolve, garantendo una buona separazione delle responsabilità; abbiamo una modularità che ci permette di cambiare facilmente il container companion senza dover cambiare quello dell’applicazione. I container sono accoppiati in modo lasso e molto manutenibili; avere un insieme di container companion riduce il tempo di sviluppo: possiamo riutilizzare molti companion quando dobbiamo affrontare gli stessi problemi.
Pattern Sidecar
Pensa a tutti quei casi in cui è necessario aggiungere funzionalità essenziali, come il logging, il monitoraggio, la memorizzazione nella cache. Potremmo migliorare la nostra applicazione, aggiungendo codice per implementare queste funzionalità, oppure possiamo semplicemente scrivere il nostro codice in un container companion. L’oggetto companion aggiungerà logging, monitoraggio, memorizzazione nella cache e funzionalità simili, agendo al posto della nostra applicazione, raccogliendo log e metriche, o memorizzando nella cache dati usati frequentemente.
Pattern Adapter
Similmente al pattern di Design Adapter dei GoF, il pattern Adapter permette di utilizzare un container companion per agire come un oggetto che traduce le comunicazioni tra l’ application container e il mondo esterno. Possiamo usarlo quando, ad esempio, dobbiamo comunicare con API esterne che utilizzano strutture dati diverse. Immagina di avere molte di queste API esterne, e un adapter può tradurre tutte le strutture dati in quella utilizzata dalla nostra applicazione. Lo stesso vale se usiamo un broker di messaggi per scambiare messaggi: l’adapter tradurrà i messaggi in modo che l’applicazione possa elaborarli usando solo una struttura. Ogni volta che hai bisogno di colmare lacune di comunicazione con sistemi esterni, un adapter è la scelta perfetta.
Pattern Ambassador
Un container ambassador agisce come intermediario, gestendo le richieste esterne e inoltrandole all’ application container . Possiamo usarlo, ad esempio, per far rispettare policy di autenticazione e autorizzazione prima di passare le richieste all’applicazione. Ancora, possiamo fare da proxy alla connessione tra l’applicazione e un database remoto, fornendo un canale sicuro senza costringere l’application container a occuparsi della sicurezza, semplificandone l’implementazione. Il pattern ambassador promuove un maggiore controllo e agilità. Ti permette di implementare misure di sicurezza, ottimizzare il flusso di traffico e gestire le istanze dell’applicazione senza modificare l’application container .
Articolo consigliato: cosa sono i design pattern orientati ai microservizi?
Impostazione del Pod: InitContainer
Ci sono casi in cui il companion di cui abbiamo bisogno dovrebbe solo preparare il Pod, inizializzandolo per l’applicazione. Ad esempio, potremmo dover clonare un repository git nel Volume montato localmente, perché l’applicazione utilizzerà i file scaricati. Potremmo dover generare dinamicamente un file di configurazione. O potremmo dover registrare l’intero Pod con un server remoto dall’API downward. In questi casi, Kubernetes ci offre un’opzione potente: gli InitContainers.
Gli InitContainers sono container che vengono eseguiti fino al completamento durante l’inizializzazione del Pod. Possiamo definirne tanti, e ognuno di loro deve essere eseguito fino al completamento prima che l’application container parta. Kubelet li eseguirà sequenzialmente, uno dopo l’altro, nell’ordine in cui li definiamo. Sono ovviamente diversi da un sidecar o un ambassador, poiché non saranno un companion. Imposteranno solo il Pod per l’applicazione e si fermeranno.
“Possiamo vedere un esempio della definizione di un InitContainer qui sotto. Si noti che SERVICE_INITIALIZATION e DB_INITIALIZATION sono dei segnaposto per un comando generico che eseguirà alcune inizializzazioni.”
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: myapp-container
image: myapp-image
initContainers:
- name: init-myservice
image: busybox:1.28
command: ['sh', '-C', 'SERVICE_INITIALIZATION']
- name: init-myservice
image: busybox:1.28
command: ['sh', '-C', 'DB_INITIALIZATION']
Code language: CSS (css)
Conclusione
I pattern Sidecar, Adapter e Ambassador, insieme agli InitContainers, sono approcci molto utili per tutti quei lavori che, altrimenti, richiederebbero cambiamenti all’ application container. Forniscono supporto al container principale e devono essere distribuiti come container secondari. Quale pattern useremo dipende dal tipo di lavoro che il container companion farà. Inoltre, in questo modo abbiamo container altamente riutilizzabili per diversi casi d’uso.
Ricorda solo di non mettere più di un container di applicazione nello stesso Pod: avere un companion è sempre utile, ma non vogliamo mettere due piedi nella stessa scarpa.