Si supponga di eseguire un servizio in un contenitore e che sia disponibile una nuova versione del servizio tramite l'immagine della finestra mobile. In tal caso, vorresti aggiornare il contenitore Docker.
L'aggiornamento di un container Docker non è un problema, ma l'aggiornamento del container Docker senza tempi di inattività è impegnativo.
Confuso? Lascia che ti mostri entrambi i modi uno per uno.
Metodo 1:aggiornamento del container docker all'immagine più recente (risulta in tempi di inattività)
Questo metodo consiste fondamentalmente in questi passaggi:
- Estrarre l'ultima immagine della finestra mobile
- Interrompi e rimuovi il contenitore che esegue la vecchia immagine Docker
- Crea un nuovo contenitore con l'immagine della finestra mobile appena estratta
Vuoi i comandi? Ecco a te.
Elenca le immagini della finestra mobile e ottieni l'immagine della finestra mobile con un aggiornamento. Ottieni le ultime modifiche a questa immagine usando il comando pull della finestra mobile:
docker pull image_name
Ora ottieni l'ID contenitore o il nome del contenitore che esegue l'immagine Docker precedente. Utilizzare il comando docker ps per questo scopo. Ferma questo contenitore:
docker stop container_ID
E rimuovi il contenitore:
docker rm container_id
Il passaggio successivo consiste nell'eseguire un nuovo contenitore con gli stessi parametri utilizzati per l'esecuzione del contenitore precedente. Credo che tu sappia quali sono quei parametri perché li avevi creati in primo luogo.
docker run --name=container_name [options] docker_image
Vedi il problema con questo approccio? Devi interrompere il contenitore in esecuzione e quindi crearne uno nuovo. Ciò comporterà un tempo di inattività per il servizio in esecuzione.
I tempi di inattività, anche se per un minuto, potrebbero avere un grande impatto se si tratta di un progetto mission-critical o di un servizio web ad alto traffico.
Vuoi conoscere un approccio più sicuro e migliore per questo problema? Leggi la prossima sezione.
Metodo 2:aggiornamento del contenitore docker in una configurazione del proxy inverso (con zero o tempi di fermo minimi)
Se stavi cercando una soluzione semplice, mi dispiace deluderti ma non sarà una soluzione perché qui dovrai distribuire i tuoi container in un'architettura proxy inverso con Docker Compose.
Se stai cercando di gestire i servizi critici utilizzando i contenitori docker, il metodo del proxy inverso ti aiuterà molto a lungo termine.
Consentitemi di elencare tre vantaggi principali della configurazione del proxy inverso:
- Puoi distribuire più servizi rivolti al pubblico sullo stesso server. Nessun blocco delle porte qui.
- Il server Let's Encrypt si occupa della distribuzione SSL per tutti i servizi, tutti i container.
- Puoi aggiornare i contenitori senza influire sui servizi in esecuzione (per la maggior parte dei servizi Web).
Se sei curioso di saperne di più, puoi consultare il glossario ufficiale di Nginx che mette in evidenza gli usi comuni di un proxy inverso e come si confronta con un sistema di bilanciamento del carico.
Abbiamo un ottimo tutorial approfondito sulla configurazione del proxy inverso Nginx per ospitare più di un'istanza di servizi Web in esecuzione in contenitori sullo stesso server. Quindi, non ho intenzione di discuterne di nuovo qui. Dovresti prima configurare i tuoi container usando questa architettura. Credimi, ne vale la pena.
In questo tutorial, ho progettato una metodologia passo passo che può essere molto utile nelle tue attività DevOps quotidiane. Questo requisito può non solo essere molto necessario quando aggiorni i tuoi container, ma anche quando desideri apportare una modifica molto necessaria a una qualsiasi delle tue app in esecuzione senza sacrificare il prezioso tempo di attività.
Da qui in poi, assumiamo che tu stia eseguendo le tue applicazioni web con una configurazione del proxy inverso che assicurerebbe che il reindirizzamento funzioni per il nuovo contenitore aggiornato come previsto dopo le modifiche alla configurazione che faremo.Mostrerò prima i passaggi di questo metodo, seguiti da un esempio di vita reale.
Passaggio 1:aggiorna il file di composizione della finestra mobile
Prima di tutto, devi modificare il file di composizione della finestra mobile esistente con il numero di versione dell'ultima immagine. Può essere visualizzato su Docker Hub, in particolare nella sezione "tag" dell'applicazione.
Spostarsi nella directory dell'applicazione e modificare il file di composizione mobile con un editor di testo della riga di comando. Ho usato Nano qui.
[email protected]:~/web-app$ nano docker-compose.yml
All'interno di
services:
, aggiornaimage: web-app:x.x.x
con il numero di versione più recente e salva il file.Potresti chiederti perché non utilizzare comunque l'ultimo tag invece di specificare manualmente il numero di versione? L'ho fatto apposta poiché ho notato che durante l'aggiornamento dei contenitori, può esserci un ritardo intermittente dell'ultimo tag nel raccogliere effettivamente l'ultima versione dell'applicazione dockerizzata. Quando usi direttamente il numero di versione, puoi sempre essere assolutamente certo.
Passaggio 2:ridimensionamento di un nuovo container
Quando utilizzi il comando seguente, viene creato un nuovo contenitore in base alle nuove modifiche apportate nel file di composizione della finestra mobile.
[email protected]:~/web-app$ docker-compose up -d --scale web-app=2 --no-recreate
Si noti che il contenitore precedente è ancora attivo e funzionante. Il
--scale
flag viene utilizzato per creare contenitori aggiuntivi come specificato. Qui,web-app
è stato impostato come nome del servizio per l'applicazione Web.Anche se specifichi il ridimensionamento fino a 2 contenitori,
--no-recreate
assicura che ne venga aggiunto solo uno poiché hai già il tuo vecchio contenitore in esecuzione.Per saperne di più su
--scale
e--no-recreate
flag, controlla la pagina della documentazione ufficiale per la composizione della finestra mobile.Fase 3:rimuovi il vecchio contenitore
Dopo il passaggio 2, attendi circa 15-20 secondi affinché le nuove modifiche abbiano effetto, quindi rimuovi il vecchio contenitore:
[email protected]:~/web-app$ docker rm -f old-web-app
In diverse app Web, le modifiche riflesse sono comportamentalmente diverse dopo aver eseguito il comando precedente (discusso come suggerimento bonus nella parte inferiore di questo tutorial).
Passaggio 4:ridimensiona alla configurazione del contenitore singolo come prima
Per il passaggio finale, riduci ancora una volta la configurazione del contenitore singolo:
[email protected]:~/web-app$ docker-compose up -d --scale web-app=1 --no-recreate
Ho testato questo metodo con istanze Ghost, WordPress, Rocket.Chat e Nextcloud. A parte il passaggio di Nextcloud alla modalità di manutenzione per alcuni secondi, la procedura funziona molto bene per gli altri tre.
Il discorso, tuttavia, è un'altra storia e può essere un'eccezione molto delicata in questo caso a causa del suo modello ibrido.
Il risultato finale è:più l'app Web utilizza la pratica docker standard durante la docker, più diventa conveniente gestire tutti i contenitori di app Web su base giornaliera.Esempio reale:aggiornamento di un'istanza Ghost live senza tempi di inattività
Come promesso, vi mostrerò un esempio di vita reale. Ti mostrerò come aggiornare Ghost in esecuzione nel contenitore Docker a una versione più recente senza tempi di inattività.
Ghost è un CMS e lo usiamo per Linux Handbook. L'esempio mostrato qui è quello che utilizziamo per aggiornare la nostra istanza Ghost che esegue questo sito Web.
Supponiamo di avere una configurazione esistente basata su una versione precedente situata in
/home/avimanyu/ghost
:version: '3.5' services: ghost: image: ghost:3.36 volumes: - ghost:/var/lib/ghost/content environment: - VIRTUAL_HOST=blog.domain.com - LETSENCRYPT_HOST=blog.domain.com - url=https://blog.domain.com - NODE_ENV=production restart: always networks: - net volumes: ghost: external: true networks: net: external: true
Nota che la configurazione di composizione della finestra mobile sopra si basa su una configurazione della finestra mobile Nginx preesistente descritta qui, in esecuzione su una rete denominata
net
. Anche il volume della finestra mobile è stato creato manualmente condocker volume create ghost-blog
.Quando lo controllo con
docker ps
:CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2df6c27c1fe3 ghost:3.36 "docker-entrypoint.s…" 9 days ago Up 7 days 2368/tcp ghost_ghost-blog_1 89a5a7fdcfa4 jrcs/letsencrypt-nginx-proxy-companion "/bin/bash /app/entr…" 9 days ago Up 7 days letsencrypt-helper 90b72e217516 jwilder/nginx-proxy "/app/docker-entrypo…" 9 days ago Up 7 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp reverse-proxy
Al momento in cui scrivo, questa è una versione precedente di Ghost. È ora di aggiornarlo all'ultima versione 3.37.1! Quindi, lo rivedo nella sezione dell'immagine come:
version: '3.5' services: ghost-blog: image: ghost:3.37.1 volumes: - ghost-blog:/var/lib/ghost/content environment: - VIRTUAL_HOST=blog.domain.com - LETSENCRYPT_HOST=blog.domain.com - url=https://blog.domain.com - NODE_ENV=production restart: always networks: - net volumes: ghost-blog: external: true networks: net: external: true
Ora per sfruttare al meglio il metodo di ridimensionamento:
[email protected]:~/ghost$ docker-compose up -d --scale ghost-blog=2 --no-recreate
Con il comando precedente, il contenitore più vecchio rimane inalterato ma uno nuovo si unisce con la stessa configurazione ma basato sull'ultima versione di Ghost:
[email protected]:~/ghost$ docker-compose up -d --scale ghost-blog=2 --no-recreate Pulling ghost (ghost:3.37.1)... 3.37.1: Pulling from library/ghost bb79b6b2107f: Already exists 99ce436c3449: Already exists f7bdc31da5f5: Already exists 7a1300b9ff59: Already exists a495c68fa838: Already exists 6e362a39ec35: Already exists b68b4f3c36f7: Already exists 41f8b02d4a71: Pull complete 3ecc736ea4e5: Pull complete Digest: sha256:595c759980cd22e99037811397012908d89efb799776db222a4be6d4d892917c Status: Downloaded newer image for ghost:3.37.1 Starting ghost_ghost-blog_1 ... done Creating ghost_ghost-blog_2 ... done
Se avessi usato l'approccio convenzionale con
docker-compose up -d
invece, non avrei potuto evitare che la ricreazione del container esistente fosse basata sull'ultima immagine di Ghost.La ricreazione comporta la rimozione del contenitore più vecchio e la creazione di uno nuovo al suo posto con le stesse impostazioni. Questo è quando si verificano tempi di inattività e il sito diventa inaccessibile.
Questo è il motivo per cui dovresti usare
--no-recreate
flag durante il ridimensionamento.Quindi ora ho due contenitori in esecuzione basati sulla stessa configurazione fantasma. Questa è la parte cruciale in cui evitiamo i tempi di fermo:
[email protected]:~/ghost$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f239f677de54 ghost:3.37.1 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 2368/tcp ghost_ghost-blog_2 2df6c27c1fe3 ghost:3.36 "docker-entrypoint.s…" 9 days ago Up 7 days 2368/tcp ghost_ghost-blog_1 89a5a7fdcfa4 jrcs/letsencrypt-nginx-proxy-companion "/bin/bash /app/entr…" 9 days ago Up 7 days letsencrypt-helper 90b72e217516 jwilder/nginx-proxy "/app/docker-entrypo…" 9 days ago Up 7 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp reverse-proxy
Nota che il nome del contenitore precedente è
ghost_ghost-blog_1
. Controlla il tuo nome di dominio e lo troverai ancora accessibile su blog.domain.com . Se aggiorni il pannello di amministrazione di Ghost, che si trova su blog.domain.com/ghost dopo il ridimensionamento, continuerebbe a provare a caricarsi fino a quando non rimuovi il vecchio contenitore:[email protected]:~/ghost$ docker rm -f ghost_ghost-blog_1
Ma per il blog Ghost stesso, non ci sono tempi di inattività! Quindi, in questo modo puoi garantire zero tempi di inattività durante l'aggiornamento dei blog Ghost.
Infine, ridimensiona la configurazione alle impostazioni originali:
[email protected]:~/ghost$ docker-compose up -d --scale ghost-blog=1 --no-recreate Starting ghost_ghost-blog_2 ... done
Come accennato in precedenza, dopo aver rimosso i vecchi contenitori, le modifiche si riflettono nelle rispettive app Web, ma ovviamente si comportano in modo diverso a causa dei diversi design delle app.
Ecco alcune osservazioni:
Su WordPress :Assicurati di aggiungere define( 'AUTOMATIC_UPDATER_DISABLED', true ); come riga di fondo sul file wp-config.php che si trova in /var/www/html e monta /var/www/html/wp-content invece di /var/www/html come volume. Controlla qui per i dettagli. Dopo il passaggio 3, il pannello di amministrazione di WordPress mostrerà che il tuo WordPress è aggiornato e ti chiederà di procedere e aggiornare il tuo database. L'aggiornamento avviene rapidamente senza alcun downtime sul sito WordPress e il gioco è fatto!
Su Rocket.Chat :Possono essere necessari circa 15-20 secondi per le
Admin>Info
pagina per mostrare che stai eseguendo l'ultima versione anche prima di eseguire il passaggio 3. Niente più tempi di inattività!Su Nextcloud :Dopo il passaggio 2, Nextcloud passerà alla modalità di manutenzione per alcuni secondi e quindi caricherà nuovamente i file. In
Administration > Overview > Security & setup warnings
, potresti ricevere un avviso del tipo "Il tuo server web non è impostato correttamente per risolvere "./well-known/carddav". Ciò è dovuto al fatto che il tuo vecchio contenitore è ancora in esecuzione. Una volta rimosso con il passaggio 3, questo avviso sarebbe non esiste più. Assicurati di concedergli un po' di tempo prima di accedere al tuo URL Nextcloud in quanto potrebbe mostrare un errore gateway 502 errato finché il tuo contenitore Nginx non vede il contenitore Nextcloud appena ridimensionato in base all'ultima versione.Suggerimenti bonus
Ecco alcuni suggerimenti e cose da tenere a mente mentre segui questo metodo.
Suggerimento 1
Per ridurre al minimo i tempi di inattività a zero tra le diverse applicazioni, assicurati di fornire tempo sufficiente ai contenitori appena ridimensionati e aggiornati in modo che i contenitori Nginx possano finalmente riconoscerli prima di rimuovere quelli vecchi nel passaggio 2.
Prima di passare al passaggio 2 sopra descritto, è consigliabile osservare il comportamento dell'applicazione sul browser (sia come aggiornamento della pagina dopo l'accesso che come nuovo accesso alla pagina su una finestra del browser privata e priva di cache) dopo aver eseguito il passaggio 1.
Poiché ogni applicazione è progettata in modo diverso, questa misura si rivelerebbe molto utile prima di far cadere i tuoi vecchi contenitori.
Suggerimento 2
Sebbene questo possa essere un metodo molto pieno di risorse per aggiornare i tuoi contenitori alle versioni più recenti delle app che eseguono, tieni presente che puoi utilizzare lo stesso metodo anche per apportare modifiche alla configurazione o modificare le impostazioni ambientali senza dover affrontare problemi di tempi di inattività.
Questo può essere fondamentale se devi risolvere un problema o eseguire una modifica che potresti ritenere necessaria in un contenitore attivo, ma non vuoi eliminarlo mentre lo fai. Dopo aver apportato le modifiche e aver verificato che il problema sia stato risolto, puoi eliminare facilmente il vecchio.
Abbiamo affrontato questo problema quando abbiamo scoperto che la rotazione dei log non era abilitata in uno dei nostri contenitori live. Abbiamo apportato le modifiche necessarie per abilitarlo e allo stesso tempo abbiamo evitato qualsiasi tempo di inattività mentre lo facevamo con questo metodo.
Suggerimento 3
Se hai assegnato un nome specifico al tuo container nel file YML usando
container_name
, il metodo non funzionerà come previsto perché prevede la creazione di un nuovo nome contenitore.Puoi evitare questo conflitto lasciando l'attività di denominazione del contenitore su Docker Compose (fatto automaticamente secondo la sua convenzione di denominazione). Docker Compose nomina i suoi contenitori come
directory-name_service-name_1
. Il numero alla fine aumenterebbe ogni volta che il contenitore viene aggiornato (fino a quando non usi docker-compose down per qualche motivo).Nel caso in cui stai già utilizzando un servizio utilizzando contenitori con nome personalizzato nel file di composizione della finestra mobile, per utilizzare questo metodo, è sufficiente commentare la riga (con un prefisso #) che include
container_name
all'interno della definizione del servizio.Dopo aver creato il nuovo contenitore per quanto sopra utilizzando il passaggio 1 come descritto in questo tutorial, per il passaggio 2, il nome del vecchio contenitore (che non è stato interrotto per evitare tempi di inattività) sarebbe come specificato in precedenza utilizzando
container_name
(può anche essere verificato con `docker ps
` prima di rimuovere il vecchio contenitore).Hai imparato qualcosa di nuovo?
So che questo articolo è un po' lungo, ma spero che aiuti la nostra community DevOps a gestire i problemi di downtime con i server di produzione che eseguono applicazioni Web dockerizzate. I tempi di fermo hanno un enorme impatto sia a livello individuale che commerciale ed è per questo che trattare questo argomento era un'assoluta necessità.
Partecipa alla discussione e condividi i tuoi pensieri, feedback o suggerimenti nella sezione commenti qui sotto.