Blue-Green Deployment semplificato

In questo articolo useremo bash, ssh, docker e nginx per organizzare un deployment senza interruzioni dell'applicazione web. Blue-green deployment è una tecnica che consente di aggiornare immediatamente un'applicazione senza rifiutare alcuna richiesta. È una delle strategie di zero downtime deployment e si adatta meglio ad applicazioni con un'istanza singola, ma con la possibilità di caricarne una seconda, pronta per l'uso, accanto alla prima.

Supponiamo che tu abbia un'applicazione web con cui molti clienti stanno interagendo attivamente e che non possa assolutamente essere fermata anche solo per qualche secondo. E tu hai davvero bisogno di rilasciare un aggiornamento della libreria, una correzione di un bug o una nuova funzionalità straordinaria. Di solito, in una situazione normale, dovresti fermare l'applicazione, sostituirla e poi riavviarla. Con Docker, puoi prima sostituire e poi riavviare, ma ci sarà comunque un periodo in cui le richieste all'applicazione non verranno elaborate, poiché di solito l'applicazione richiede del tempo per il caricamento iniziale. E se si avvia, ma risulta non funzionante? Ecco un compito: vediamo come affrontarlo con il minimo sforzo e con il massimo di eleganza.

DISCLAIMER: La maggior parte dell'articolo è presentata in un formato sperimentale — sotto forma di registrazione di una sessione della console. Spero che non sia troppo difficile da comprendere e che questo codice si auto-documenti a sufficienza. Per creare un'atmosfera, immaginate che non siano solo dei frammenti di codice, ma la carta di un teletipo "metallico".

Blue-Green Deployment semplificato

Tecniche interessanti, che sono difficili da trovare semplicemente leggendo il codice, sono descritte all'inizio di ogni sezione. Se ci fosse qualcosa che non è chiaro — cercate su Google e controllate in explainshell (fortunatamente, è di nuovo funzionante grazie alla sblocco di Telegram). Ciò che non è ricercabile — chiedete nei commenti. Sarò felice di integrare la sezione "Tecniche interessanti".

Iniziamo.

$ mkdir blue-green-deployment && cd $_

Servizio

Creeremo un servizio di prova e lo inseriremo in un contenitore.

Tecniche interessanti

  • cat < file-name (Here Document + I/O Redirection) — è un modo per creare un file multilinea con un solo comando. Tutto ciò che bash leggerà da /dev/stdin dopo questa riga e fino alla riga EOF verrà scritto in file-name.
  • wget -qO- URL (explainshell) — per stampare il documento ottenuto tramite HTTP in /dev/stdout (analogamente a curl URL).

Stampa

Sto appositamente rompendo il snippet per includere l'evidenziazione per Python. Alla fine ci sarà un altro pezzo simile. Considerate che in questi punti la carta è stata tagliata per il trasferimento al reparto evidenziazione (dove il codice è stato colorato a mano con evidenziatori), e poi questi pezzi sono stati reinseriti.

$ cat < uptimer.py
from http.server import BaseHTTPRequestHandler, HTTPServer
from time import monotonic

app_version = 1
app_name = f'Uptimer v{app_version}.0'
loading_seconds = 15 - app_version * 5

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            try:
                t = monotonic() - server_start
                if t &lt; loading_seconds:
                    self.send_error(503)
                else:
                    self.send_response(200)
                    self.send_header(&#039;Content-Type&#039;, &#039;text/html&#039;)
                    self.end_headers()
                    response = f&#039;<h2>{app_name} è in esecuzione da {t:3.1f} secondi.</h2>n'
                    self.wfile.write(response.encode('utf-8'))
            except Exception:
                self.send_error(500)
        else:
            self.send_error(404)

httpd = HTTPServer(('', 8080), Handler)
server_start = monotonic()
print(f'{app_name} (si carica in {loading_seconds} sec.) avviato.')
httpd.serve_forever()
EOF

$ cat << EOF > Dockerfile
FROM python:alpine
EXPOSE 8080
COPY uptimer.py app.py
CMD [ "python", "-u", "./app.py" ]
EOF

$ docker build --tag uptimer .
Invio del contesto di build al demone Docker  39.42kB
Passo 1/4 : FROM python:alpine
 ---&gt; 8ecf5a48c789
Passo 2/4 : EXPOSE 8080
 ---&gt; Utilizzando la cache
 ---&gt; cf92d174c9d3
Passo 3/4 : COPY uptimer.py app.py
 ---&gt; a7fbb33d6b7e
Passo 4/4 : CMD [ "python", "-u", "./app.py" ]
 ---&gt; Esecuzione in 1906b4bd9fdf
Rimozione del container intermedio 1906b4bd9fdf
 ---&gt; c1655b996fe8
Build completato con successo c1655b996fe8
Tag completato con successo uptimer:latest

$ docker run --rm --detach --name uptimer --publish 8080:8080 uptimer
8f88c944b8bf78974a5727070a94c76aa0b9bb2b3ecf6324b784e782614b2fbf

$ docker ps
ID CONTENITORE        IMMAGINE               COMANDO                CREATO             STATO              PORTE                    NOME
8f88c944b8bf        uptimer             "python -u ./app.py"   3 secondi fa       In esecuzione da 5 secondi        0.0.0.0:8080-&gt;8080/tcp   uptimer

$ docker logs uptimer
Uptimer v1.0 (carica in 10 sec.) avviato.

$ wget -qSO- http://localhost:8080
  HTTP/1.0 503 Servizio non disponibile
  Server: BaseHTTP/0.6 Python/3.8.3
  Data: Sab, 22 Ago 2020 19:52:40 GMT
  Connessione: chiusa
  Content-Type: text/html;charset=utf-8
  Content-Length: 484

$ wget -qSO- http://localhost:8080
  HTTP/1.0 200 OK
  Server: BaseHTTP/0.6 Python/3.8.3
  Data: Sab, 22 Ago 2020 19:52:45 GMT
  Content-Type: text/html
<h2>Uptimer v1.0 è in esecuzione da 15.4 secondi.</h2>

$ docker rm --force uptimer
uptimer

Reverse proxy

Per permettere alla nostra applicazione di avere la possibilità di cambiare senza essere notata, è necessario che ci sia un'altra entità davanti a essa, che nasconda il suo cambio. Questo può essere un server web nginx in in modalità reverse proxy. Il reverse proxy si colloca tra il cliente e l'applicazione. Riceve richieste dai clienti e le inoltra all'applicazione, mentre le risposte dell'applicazione sono inviate ai clienti.

L'applicazione e il reverse proxy possono essere collegati all'interno di Docker tramite docker network. In questo modo, il contenitore con l'applicazione potrebbe anche non esporre porte nel sistema host, permettendo di isolare l'applicazione dalle minacce esterne.

Se il reverse proxy deve trovarsi su un host diverso, sarà necessario rinunciare alla docker network e collegare l'applicazione al reverse proxy tramite la rete dell'host, esponendo la porta applicativi parametro --publish, come al primo avvio e come per il reverse proxy.

Il reverse proxy verrà avviato sulla porta 80, poiché è proprio questa l'entità che deve ascoltare l'esterno. Se la porta 80 è occupata sul tuo host di test, cambia il parametro --publish 80:80 con --publish ANY_FREE_PORT:80.

Tecniche interessanti

Stampa

$ docker network create web-gateway
5dba128fb3b255b02ac012ded1906b7b4970b728fb7db3dbbeccc9a77a5dd7bd

$ docker run --detach --rm --name uptimer --network web-gateway uptimer
a1105f1b583dead9415e99864718cc807cc1db1c763870f40ea38bc026e2d67f

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer:8080
<h2>Uptimer v1.0 è in esecuzione da 11,5 secondi.</h2>

$ docker run --detach --publish 80:80 --network web-gateway --name reverse-proxy nginx:alpine
80695a822c19051260c66bf60605dcb4ea66802c754037704968bc42527bf120

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
80695a822c19        nginx:alpine        "/docker-entrypoint.…"   27 secondi fa       In esecuzione da 25 secondi       0.0.0.0:80-&gt;80/tcp   reverse-proxy
a1105f1b583d        uptimer             "python -u ./app.py"     Circa un minuto fa   In esecuzione da circa un minuto   8080/tcp             uptimer

$ cat << EOF > uptimer.conf
server {
    listen 80;
    location / {
        proxy_pass http://uptimer:8080;
    }
}
EOF

$ docker cp ./uptimer.conf reverse-proxy:/etc/nginx/conf.d/default.conf

$ docker exec reverse-proxy nginx -s reload
2020/06/23 20:51:03 [notice] 31#31: signal process started

$ wget -qSO- http://localhost
  HTTP/1.1 200 OK
  Server: nginx/1.19.0
  Date: Sab, 22 Ago 2020 19:56:24 GMT
  Content-Type: text/html
  Transfer-Encoding: chunked
  Connection: keep-alive
<h2>Uptimer v1.0 è in esecuzione da 104,1 secondi.</h2>

Deploy senza interruzioni

Distribuiremo una nuova versione dell'applicazione (con un raddoppio delle prestazioni all'avvio) e cercheremo di effettuare il deploy senza interruzioni.

Tecniche interessanti

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' — Scrivere il testo my text nel file /my-file.txt all'interno del container my-container.
  • cat > /my-file.txt — Scrivere nel file il contenuto dell'input standard /dev/stdin.

Stampa

$ sed -i "s/app_version = 1/app_version = 2/" uptimer.py

$ docker build --tag uptimer .
Invio contesto di build al daemon Docker  39.94kB
Passo 1/4 : FROM python:alpine
 ---&gt; 8ecf5a48c789
Passo 2/4 : EXPOSE 8080
 ---&gt; Uso della cache
 ---&gt; cf92d174c9d3
Passo 3/4 : COPY uptimer.py app.py
 ---&gt; 3eca6a51cb2d
Passo 4/4 : CMD [ "python", "-u", "./app.py" ]
 ---&gt; Esecuzione in 8f13c6d3d9e7
Rimozione del contenitore intermedio 8f13c6d3d9e7
 ---&gt; 1d56897841ec
Costruito con successo 1d56897841ec
Taggato con successo uptimer:latest

$ docker run --detach --rm --name uptimer_BLUE --network web-gateway uptimer
96932d4ca97a25b1b42d1b5f0ede993b43f95fac3c064262c5c527e16c119e02

$ docker logs uptimer_BLUE
Uptimer v2.0 (carica in 5 sec.) avviato.

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer_BLUE:8080
<h2>Uptimer v2.0 è in esecuzione da 23.9 secondi.</h2>

$ sed s/uptimer/uptimer_BLUE/ uptimer.conf | docker exec --interactive reverse-proxy sh -c 'cat &gt; /etc/nginx/conf.d/default.conf'

$ docker exec reverse-proxy cat /etc/nginx/conf.d/default.conf
server {
    listen 80;
    location / {
        proxy_pass http://uptimer_BLUE:8080;
    }
}

$ docker exec reverse-proxy nginx -s reload
2020/06/25 21:22:23 [notice] 68#68: il processo del segnale è avviato

$ wget -qO- http://localhost
<h2>Uptimer v2.0 è in esecuzione da 63.4 secondi.</h2>

$ docker rm -f uptimer
uptimer

$ wget -qO- http://localhost
<h2>Uptimer v2.0 è in esecuzione da 84.8 secondi.</h2>

$ docker ps
ID CONTENITORE        IMMAGINE               COMANDO                  CREATO              STATO              PORTE                NOMI
96932d4ca97a        uptimer             "python -u ./app.py"     Circa un minuto fa   In esecuzione da circa un minuto   8080/tcp             uptimer_BLUE
80695a822c19        nginx:alpine        "/docker-entrypoint.…"   8 minuti fa        In esecuzione da 8 minuti        0.0.0.0:80-&gt;80/tcp   reverse-proxy

A questo punto, l'immagine viene costruita direttamente sul server, il che richiede che i sorgenti dell'applicazione siano presenti lì e carica il server di lavoro superfluo. Il passo successivo sarà l'assegnazione della costruzione dell'immagine a una macchina separata (ad esempio, in un sistema CI) e la successiva trasmissione al server.

Trasferimento delle immagini

Sfortunatamente, non ha senso trasferire immagini da localhost a localhost, quindi questa sezione può essere esplorata solo avendo a disposizione due host con Docker. In minima parte, si presenta così:

$ ssh production-server docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

$ docker image save uptimer | ssh production-server 'docker image load'
Loaded image: uptimer:latest

$ ssh production-server docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
uptimer             latest              1d56897841ec        5 minutes ago       78.9MB

Team docker save salva i dati dell'immagine in un archivio .tar, il che significa che pesa circa 1,5 volte di più di quanto potrebbe pesare in forma compressa. Quindi, comprimiamolo in nome del risparmio di tempo e traffico:

$ docker image save uptimer | gzip | ssh production-server 'zcat | docker image load'
Loaded image: uptimer:latest

Inoltre, è possibile monitorare il processo di trasferimento (anche se per questo è necessaria un'utilità esterna):

$ docker image save uptimer | gzip | pv | ssh production-server 'zcat | docker image load'
25,7MiB 0:01:01 [ 425KiB/s] [                   <=    ]
Loaded image: uptimer:latest

Suggerimento: Se per connetterti al server tramite SSH hai bisogno di molti parametri, potrebbe essere che non stai usando il file ~/.ssh/config.

Trasferimento dell'immagine tramite docker image save/load è il metodo più minimale, ma non l'unico. Ce ne sono altri:

  1. Container Registry (standard di settore).
  2. Connettersi al docker daemon del server da un altro host:
    1. Variabile d'ambiente DOCKER_HOST.
    2. Parametro della riga di comando -H o --host uno strumento docker-compose.
    3. contesto docker

Il secondo metodo (con tre varianti di implementazione) è ben descritto nell'articolo How to deploy on remote Docker hosts with docker-compose.

deploy.sh

Ora uniremo tutto ciò che abbiamo fatto manualmente in un unico script. Iniziamo con la funzione principale, poi daremo un'occhiata alle altre utilizzate in essa.

Tecniche interessanti

  • ${parameter?err_msg} — uno degli incantesimi della magia bash (aka sostituzione dei parametri). Se il parametro non è specificato, mostrare err_msg ed uscire con codice 1.
  • docker --log-driver journald — per impostazione predefinita, il driver di logging di docker è un file di testo senza alcuna rotazione. Con questo approccio, i log riempiono rapidamente l'intero disco, quindi per un ambiente di produzione è necessario cambiare il driver con uno più intelligente.

Script di deployment

deploy() {
    local usage_msg="Usage: ${FUNCNAME[0]} image_name"
    local image_name=${1?$usage_msg}

    ensure-reverse-proxy || return 2
    if get-active-slot $image_name
    then
        local OLD=${image_name}_BLUE
        local new_slot=GREEN
    else
        local OLD=${image_name}_GREEN
        local new_slot=BLUE
    fi
    local NEW=${image_name}_${new_slot}
    echo "Deploying '$NEW' in place of '$OLD'..."
    docker run 
        --detach 
        --restart always 
        --log-driver journald 
        --name $NEW 
        --network web-gateway 
        $image_name || return 3
    echo "Container started. Checking health..."
    for i in {1..20}
    do
        sleep 1
        if get-service-status $image_name $new_slot
        then
            echo "New '$NEW' service seems OK. Switching heads..."
            sleep 2  # Ensure service is ready
            set-active-slot $image_name $new_slot || return 4
            echo "'$NEW' service is live!"
            sleep 2  # Ensure all requests were processed
            echo "Killing '$OLD'..."
            docker rm -f $OLD
            docker image prune -f
            echo "Deployment successful!"
            return 0
        fi
        echo "New '$NEW' service is not ready yet. Waiting ($i)..."
    done
    echo "New '$NEW' service did not raise, killing it. Failed to deploy T_T"
    docker rm -f $NEW
    return 5
}

Funzioni utilizzate:

  • ensure-reverse-proxy — Si assicura che il reverse proxy sia attivo (utile per il primo deployment)
  • get-active-slot service_name — Determina quale slot è attualmente attivo per il servizio specificato (BLUE o GREEN)
  • get-service-status service_name deployment_slot — Verifica se il servizio è pronto a gestire le richieste in ingresso
  • set-active-slot service_name deployment_slot — Modifica la configurazione di nginx nel container reverse proxy

In ordine:

assicurati-ritorno-proxy() {
    è-contenitore-in-esecuzione ritornare-proxy && return 0
    echo "Distribuendo ritorno-proxy..."
    docker network create web-gateway
    docker run 
        --detached 
        --restart sempre 
        --log-driver journald 
        --name ritorno-proxy 
        --network web-gateway 
        --publish 80:80 
        nginx:alpine || return 1
    docker exec --interactive ritorno-proxy sh -c "> /etc/nginx/conf.d/default.conf"
    docker exec ritorno-proxy nginx -s reload
}

è-contenitore-in-esecuzione() {
    local contenitore=${1?"Utilizzo: ${FUNCNAME[0]} nome_contenitore"}

    [ -n "$(docker ps -f name=${contenitore} -q)" ]
    return $?
}

ottieni-slot-attivo() {
    local servizio=${1?"Utilizzo: ${FUNCNAME[0]} nome_servizio"}

    if è-contenitore-in-esecuzione ${servizio}_BLU && è-contenitore-in-esecuzione ${servizio}_VERDE; then
        echo "Collisione rilevata! Arrestando ${servizio}_VERDE..."
        docker rm -f ${servizio}_VERDE
        return 0  # BLU
    fi
    if è-contenitore-in-esecuzione ${servizio}_BLU && ! è-contenitore-in-esecuzione ${servizio}_VERDE; then
        return 0  # BLU
    fi
    if ! è-contenitore-in-esecuzione ${servizio}_BLU; then
        return 1  # VERDE
    fi
}

ottieni-stato-servizio() {
    local msg_utilizzo="Utilizzo: ${FUNCNAME[0]} nome_servizio slot_distribuzione"
    local servizio=${1?msg_utilizzo}
    local slot=${2?$msg_utilizzo}

    case $servizio in
        # Aggiungi percorsi di controllo salute specifici per i tuoi servizi qui
        *) local percorso_controllo_salute=":8080/" ;;
    esac
    local indirizzo_controllo_salute="http://${servizio}_${slot}${percorso_controllo_salute}"
    echo "Richiesta '$indirizzo_controllo_salute' all'interno della rete 'web-gateway' docker:"
    docker run --rm --network web-gateway alpine 
        wget --timeout=1 --quiet --server-response $indirizzo_controllo_salute
    return $?
}

imposta-slot-attivo() {
    local msg_utilizzo="Utilizzo: ${FUNCNAME[0]} nome_servizio slot_distribuzione"
    local servizio=${1?$msg_utilizzo}
    local slot=${2?$msg_utilizzo}
    [ "$slot" == BLU ] || [ "$slot" == VERDE ] || return 1

    ottieni-config-nginx $servizio $slot | docker exec --interactive ritorno-proxy sh -c "cat > /etc/nginx/conf.d/$servizio.conf"
    docker exec ritorno-proxy nginx -t || return 2
    docker exec ritorno-proxy nginx -s reload
}

Funzione get-active-slot richiede alcune spiegazioni:

Perché restituisce un numero invece di stampare una stringa?

In ogni caso, nella funzione chiamante controlliamo il risultato del suo lavoro, e controllare il codice di uscita tramite bash è molto più semplice rispetto a una stringa. Inoltre, ottenerne una stringa è molto facile:
get-active-slot service && echo BLUE || echo GREEN.

Ma tre condizioni sono davvero sufficienti per distinguere tutti gli stati?

Blue-Green Deployment semplificato

Anche due sarebbero sufficienti, l'ultima è qui solo per completezza, per non scrivere else.

Rimane indefinita solo la funzione che restituisce le configurazioni nginx: get-nginx-config service_name deployment_slot. Analogamente al health check, qui è possibile impostare qualsiasi configurazione per qualsiasi servizio. Interessante è solo cat <<- EOF, che consente di rimuovere tutte le tabulazioni all'inizio. Tuttavia, il prezzo di una formattazione decente è che sono mischiate tabulazioni e spazi, il che oggi è considerato un grave difetto. Ma bash forza le tabulazioni, e anche nella configurazione nginx sarebbe utile avere una buona formattazione. In ogni caso, qui l'accoppiamento di tabulazioni con spazi sembra davvero la migliore soluzione tra le peggiori. Tuttavia, nel frammento qui sotto non lo vedrete, poiché Habr 'fa bene', cambiando tutte le tabulazioni in 4 spazi e rendendo EOF non valido. Ecco, qui è evidente.

Per non dover ripetere, vi racconterò subito di cat << 'EOF', che incontreremo più avanti. Se si scrive semplicemente cat << EOF, all'interno dell'heredoc avviene l'interpolazione della stringa (vengono espansi le variabili ($foo), le chiamate di comando ($(bar)), ecc., e se si racchiude il delimitatore di fine documento tra virgolette singole, allora l'interpolazione è disattivata e il carattere $ viene stampato così com'è. Questo è necessario per inserire uno script all'interno di un altro script.

get-nginx-config() {
    local usage_msg="Uso: ${FUNCNAME[0]} nome_servizio slot_distribuzione"
    local service=${1?$usage_msg}
    local slot=${2?$usage_msg}
    [ "$slot" == BLUE ] || [ "$slot" == GREEN ] || return 1

    local container_name=${service}_${slot}
    case $service in
        # Aggiungi configurazioni nginx specifiche per i tuoi servizi qui
        *) nginx-config-simple-service $container_name:8080 ;;
    esac
}

nginx-config-simple-service() {
    local usage_msg="Uso: ${FUNCNAME[0]} proxy_pass"
    local proxy_pass=${1?$usage_msg}

cat << EOF
server {
    listen 80;
    location / {
        proxy_pass http://$proxy_pass;
    }
}
EOF
}

Questo è tutto lo script. Ecco il gist con questo script per il download tramite wget o curl.

Esecuzione di script parametrizzati su un server remoto

È il momento di contattare il server di destinazione. Questa volta localhost andrà benissimo:

$ ssh-copy-id localhost
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
himura@localhost's password: 

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'localhost'"
and check to make sure that only the key(s) you wanted were added.

Abbiamo scritto uno script di deployment che trasferisce un'immagine precompilata sul server di destinazione e sostituisce senza interruzioni il container del servizio. Ma come eseguirlo su una macchina remota? Lo script ha degli argomenti, essendo universale e in grado di gestire più servizi per un unico reverse proxy (le configurazioni di nginx possono gestire quale servizio utilizzare a seconda dell'URL). Non possiamo conservare lo script sul server, poiché in questo modo non riusciremo ad aggiornarlo automaticamente (per bug fix e per l'aggiunta di nuovi servizi), e in generale, lo stato è male.

Soluzione 1: Conservare lo script sul server, ma copiarlo ogni volta tramite scp. Poi connettersi tramite ssh ed eseguire lo script con gli argomenti richiesti.

Contro:

  • Due azioni anziché una
  • Il percorso dove copi si potrebbe non esistere, o non si potrebbe avere accesso, oppure lo script potrebbe essere eseguito durante la sostituzione.
  • È preferibile pulire dopo di sé (rimuovere lo script).
  • Già tre azioni.

Soluzione 2:

  • Nello script tenere solo le definizioni delle funzioni e non eseguire nulla.
  • Con sed aggiungere alla fine la chiamata della funzione.
  • Inviare tutto questo direttamente a shh attraverso pipe (|)

Vantaggi:

  • Veramente senza stato.
  • Nessuna entità boilerplate.
  • Sembra interessante.

Bene, facciamo senza Ansible. Sì, tutto è già stato inventato. Sì, la bicicletta. Guardate, che bicicletta semplice, elegante e minimalista:

$ cat < deploy.sh
#!/bin/bash

usage_msg="Usage: $0 ssh_address local_image_tag"
ssh_address=${1?$usage_msg}
image_name=${2?$usage_msg}

echo "Connecting to '$ssh_address' via ssh to seamlessly deploy '$image_name'..."
( sed "$a deploy $image_name" | ssh -T $ssh_address ) << 'END_OF_SCRIPT'
deploy() {
    echo "Yay! The '${FUNCNAME[0]}' function is executing on '$(hostname)' with argument '$1'"
}
END_OF_SCRIPT
EOF

$ chmod +x deploy.sh

$ ./deploy.sh localhost magic-porridge-pot
Collegamento a localhost...
Evviva! La funzione 'deploy' sta eseguendo su 'hut' con argomento 'magic-porridge-pot'

Tuttavia, non possiamo essere certi che sul server remoto ci sia un bash adeguato, quindi aggiungiamo un piccolo controllo all'inizio (questo invece di shellbang):

if [ "$SHELL" != "/bin/bash" ]
then
    echo "La shell '$SHELL' non è supportata da 'deploy.sh'. Imposta una shell '/bin/bash' per '$USER@$HOSTNAME'."
    exit 1
fi

E ora tutto è reale:

$ docker exec reverse-proxy rm /etc/nginx/conf.d/default.conf

$ wget -qO deploy.sh https://git.io/JUURc

$ chmod +x deploy.sh

$ ./deploy.sh localhost uptimer
Invio dell'immagine compressa 'uptimer' a 'localhost' tramite ssh...
Immagine caricata: uptimer:latest
Connessione a 'localhost' tramite ssh per distribuire senza interruzioni 'uptimer'...
Distribuzione di 'uptimer_GREEN' al posto di 'uptimer_BLUE'...
06f5bc70e9c4f930e7b1f826ae2ca2f536023cc01e82c2b97b2c84d68048b18a
Contenitore avviato. Verifica dello stato...
Richiesta a 'http://uptimer_GREEN:8080/' all'interno della rete docker 'web-gateway':
  HTTP/1.0 503 Servizio non disponibile
wget: il server ha restituito errore: HTTP/1.0 503 Servizio non disponibile
Il nuovo servizio 'uptimer_GREEN' non è ancora pronto. Attendere (1)...
Richiesta a 'http://uptimer_GREEN:8080/' all'interno della rete docker 'web-gateway':
  HTTP/1.0 503 Servizio non disponibile
wget: il server ha restituito errore: HTTP/1.0 503 Servizio non disponibile
Il nuovo servizio 'uptimer_GREEN' non è ancora pronto. Attendere (2)...
Richiesta a 'http://uptimer_GREEN:8080/' all'interno della rete docker 'web-gateway':
  HTTP/1.0 200 OK
  Server: BaseHTTP/0.6 Python/3.8.3
  Data: Sab, 22 Ago 2020 20:15:50 GMT
  Content-Type: text/html

Il nuovo servizio 'uptimer_GREEN' sembra OK. Switching heads...
nginx: il file di configurazione /etc/nginx/nginx.conf ha una sintassi corretta
nginx: il file di configurazione /etc/nginx/nginx.conf ha superato il test con successo
2020/08/22 20:15:54 [notice] 97#97: processo segnale avviato
Il servizio 'uptimer_GREEN' è attivo!
Terminazione di 'uptimer_BLUE'...
uptimer_BLUE
Spazio totale recuperato: 0B
Distribuzione riuscita!

Ora puoi aprire http://localhost/ nel browser, riavviare il deployment e verificare che avvenga senza problemi aggiornando la pagina a intervalli regolari durante la distribuzione.

Non dimentichiamo di pulire dopo il lavoro :3

$ docker rm -f uptimer_GREEN reverse-proxy 
uptimer_GREEN
reverse-proxy

$ docker network rm web-gateway 
web-gateway

$ cd ..

$ rm -r blue-green-deployment

Fonte: habr.com

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