Blua-Verda Deplojo ĉe minimumaj salajroj

En ĉi tiu artikolo ni uzas bash, ssh, docker и nginx Ni organizos senjuntan aranĝon de la retejo-aplikaĵo. Bluverda deplojo estas tekniko, kiu ebligas al vi tuj ĝisdatigi aplikaĵon sen malakcepti eĉ unu peton. Ĝi estas unu el la nulmalfunkciaj deplojstrategioj kaj plej taŭgas por aplikoj kun unu kazo, sed la kapablo ŝargi duan, pretan por funkciiga kazon proksime.

Ni diru, ke vi havas TTT-aplikaĵon kun kiu multaj klientoj aktive laboras, kaj estas absolute neniu maniero por ke ĝi kuŝu dum kelkaj sekundoj. Kaj vi vere bezonas lanĉi bibliotekan ĝisdatigon, korekton de cimoj aŭ novan bonegan funkcion. En normala situacio, vi devos ĉesigi la aplikaĵon, anstataŭigi ĝin kaj rekomenci ĝin. En la kazo de docker, vi povas unue anstataŭigi ĝin, poste rekomenci ĝin, sed ankoraŭ restos periodo en kiu petoj al la aplikaĵo ne estos procesitaj, ĉar kutime la aplikaĵo prenas iom da tempo por komence ŝargi. Kio se ĝi komenciĝas, sed montriĝas nefunkciebla? Jen la problemo, ni solvu ĝin per minimumaj rimedoj kaj kiel eble plej elegante.

Malgarantio: Plejparto de la artikolo estas prezentita en eksperimenta formato - en formo de registrado de konzola sesio. Espereble ĉi tio ne estos tro malfacile komprenebla kaj la kodo sufiĉe dokumentas sin. Por atmosfero, imagu, ke ĉi tiuj ne estas nur kodaj fragmentoj, sed papero el "fera" teletipo.

Blua-Verda Deplojo ĉe minimumaj salajroj

Interesaj teknikoj, kiuj malfacilas por Guglo nur per legado de la kodo, estas priskribitaj komence de ĉiu sekcio. Se io alia estas neklara, guglos ĝin kaj kontrolu ĝin. klarigi ŝelo (feliĉe ĝi funkcias denove, pro la malblokado de la telegramo). Se vi ne povas Guglo ion, demandu en la komentoj. Mi volonte aldonos al la responda sekcio "Interesaj teknikoj".

Ni komencu.

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

servo

Ni faru eksperimentan servon kaj metu ĝin en ujon.

Interesaj teknikoj

  • cat << EOF > file-name (Jen Dokumento + I/O Redirekto) estas maniero krei plurlinian dosieron per unu komando. Ĉio el bash legas /dev/stdin post ĉi tiu linio kaj antaŭ la linio EOF estos registrita en file-name.
  • wget -qO- URL (klarigi ŝelo) — eligi dokumenton ricevitan per HTTP al /dev/stdout (analogo curl URL).

Presaĵo

Mi specife rompas la fragmenton por ebligi reliefigon por Python. Je la fino estos alia peco tia. Konsideru, ke en ĉi tiuj lokoj la papero estis tranĉita por esti sendita al la elstara fako (kie la kodo estis mane kolorigita per lumigiloj), kaj tiam ĉi tiuj pecoj estis gluitaj reen.

$ cat << EOF > 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 < loading_seconds:
                    self.send_error(503)
                else:
                    self.send_response(200)
                    self.send_header('Content-Type', 'text/html')
                    self.end_headers()
                    response = f'<h2>{app_name} is running for {t:3.1f} seconds.</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} (loads in {loading_seconds} sec.) started.')
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 .
Sending build context to Docker daemon  39.42kB
Step 1/4 : FROM python:alpine
 ---> 8ecf5a48c789
Step 2/4 : EXPOSE 8080
 ---> Using cache
 ---> cf92d174c9d3
Step 3/4 : COPY uptimer.py app.py
 ---> a7fbb33d6b7e
Step 4/4 : CMD [ "python", "-u", "./app.py" ]
 ---> Running in 1906b4bd9fdf
Removing intermediate container 1906b4bd9fdf
 ---> c1655b996fe8
Successfully built c1655b996fe8
Successfully tagged uptimer:latest

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

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                    NAMES
8f88c944b8bf        uptimer             "python -u ./app.py"   3 seconds ago       Up 5 seconds        0.0.0.0:8080->8080/tcp   uptimer

$ docker logs uptimer
Uptimer v1.0 (loads in 10 sec.) started.

$ wget -qSO- http://localhost:8080
  HTTP/1.0 503 Service Unavailable
  Server: BaseHTTP/0.6 Python/3.8.3
  Date: Sat, 22 Aug 2020 19:52:40 GMT
  Connection: close
  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
  Date: Sat, 22 Aug 2020 19:52:45 GMT
  Content-Type: text/html
<h2>Uptimer v1.0 is running for 15.4 seconds.</h2>

$ docker rm --force uptimer
uptimer

Inversa prokurilo

Por ke nia aplikaĵo povu ŝanĝi nerimarkite, necesas, ke antaŭ ĝi estu iu alia ento, kiu kaŝos ĝian anstataŭaĵon. Ĝi povus esti retservilo nginx в inversa prokura reĝimo. Inversa prokurilo estas establita inter la kliento kaj la aplikaĵo. Ĝi akceptas petojn de klientoj kaj plusendas ilin al la aplikaĵo kaj plusendas la respondojn de la aplikaĵo al la klientoj.

La aplikaĵo kaj inversa prokurilo povas esti ligitaj ene de docker uzante docker-reto. Tiel, la ujo kun la aplikiĝo eĉ ne bezonas plusendi havenon sur la mastro-sistemo; tio permesas al la aplikaĵo esti maksimume izolita de eksteraj minacoj.

Se la inversa prokurilo loĝas sur alia gastiganto, vi devos forlasi la docker-reton kaj konekti la aplikaĵon al la inversa prokurilo per la gastiga reto, plusendante la havenon. apps parametro --publish, kiel ĉe la unua komenco kaj kiel ĉe la inversa prokurilo.

Ni rulos la inversan prokurilon sur la haveno 80, ĉar ĉi tio estas ĝuste la ento, kiu devus aŭskulti la eksteran reton. Se la haveno 80 estas okupata en via testa gastiganto, ŝanĝu la parametron --publish 80:80 sur --publish ANY_FREE_PORT:80.

Interesaj teknikoj

Presaĵo

$ 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 is running for 11.5 seconds.</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 seconds ago       Up 25 seconds       0.0.0.0:80->80/tcp   reverse-proxy
a1105f1b583d        uptimer             "python -u ./app.py"     About a minute ago   Up About a minute   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: Sat, 22 Aug 2020 19:56:24 GMT
  Content-Type: text/html
  Transfer-Encoding: chunked
  Connection: keep-alive
<h2>Uptimer v1.0 is running for 104.1 seconds.</h2>

Senjunta deplojo

Ni lanĉu novan version de la aplikaĵo (kun duobla ekfunkcio-akcelo) kaj provu disfaldi ĝin perfekte.

Interesaj teknikoj

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' — Skribu tekston my text arkivi /my-file.txt ene de la ujo my-container.
  • cat > /my-file.txt — Skribu la enhavon de norma enigo al dosiero /dev/stdin.

Presaĵo

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

$ docker build --tag uptimer .
Sending build context to Docker daemon  39.94kB
Step 1/4 : FROM python:alpine
 ---> 8ecf5a48c789
Step 2/4 : EXPOSE 8080
 ---> Using cache
 ---> cf92d174c9d3
Step 3/4 : COPY uptimer.py app.py
 ---> 3eca6a51cb2d
Step 4/4 : CMD [ "python", "-u", "./app.py" ]
 ---> Running in 8f13c6d3d9e7
Removing intermediate container 8f13c6d3d9e7
 ---> 1d56897841ec
Successfully built 1d56897841ec
Successfully tagged uptimer:latest

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

$ docker logs uptimer_BLUE
Uptimer v2.0 (loads in 5 sec.) started.

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer_BLUE:8080
<h2>Uptimer v2.0 is running for 23.9 seconds.</h2>

$ sed s/uptimer/uptimer_BLUE/ uptimer.conf | docker exec --interactive reverse-proxy sh -c 'cat > /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: signal process started

$ wget -qO- http://localhost
<h2>Uptimer v2.0 is running for 63.4 seconds.</h2>

$ docker rm -f uptimer
uptimer

$ wget -qO- http://localhost
<h2>Uptimer v2.0 is running for 84.8 seconds.</h2>

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
96932d4ca97a        uptimer             "python -u ./app.py"     About a minute ago   Up About a minute   8080/tcp             uptimer_BLUE
80695a822c19        nginx:alpine        "/docker-entrypoint.…"   8 minutes ago        Up 8 minutes        0.0.0.0:80->80/tcp   reverse-proxy

En ĉi tiu etapo, la bildo estas konstruita rekte sur la servilo, kio postulas ke la aplikaĵfontoj estu tie, kaj ankaŭ ŝarĝas la servilon per nenecesa laboro. La sekva paŝo estas asigni la bildan asembleon al aparta maŝino (ekzemple al CI-sistemo) kaj poste transdoni ĝin al la servilo.

Transdono de bildoj

Bedaŭrinde, ne havas sencon transdoni bildojn de loka gastiganto al loka gastiganto, do ĉi tiu sekcio nur povas esti esplorita se vi havas du gastigantojn kun Docker ĉemane. Minimume ĝi aspektas kiel ĉi tio:

$ 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

teamo docker save konservas la bildajn datumojn en .tar-arkivo, kio signifas, ke ĝi pezas ĉirkaŭ 1.5 fojojn pli ol ĝi pezus en kunpremita formo. Do ni skuu ĝin en la nomo de ŝpari tempon kaj trafikon:

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

Vi ankaŭ povas monitori la elŝutan procezon (kvankam ĉi tio postulas triapartan ilon):

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

Konsilo: Se vi bezonas amason da parametroj por konekti al servilo per SSH, vi eble ne uzas la dosieron. ~/.ssh/config.

Transdonante la bildon per docker image save/load - Ĉi tiu estas la plej minimumisma metodo, sed ne la sola. Estas aliaj:

  1. Uja Registro (industria normo).
  2. Konekti al docker daemon-servilo de alia gastiganto:
    1. mediovariablo DOCKER_HOST.
    2. Opcio de komandlinio -H--host instrumento docker-compose.
    3. docker context

La dua metodo (kun tri ebloj por ĝia efektivigo) estas bone priskribita en la artikolo Kiel disfaldi sur foraj Docker-gastigantoj kun docker-compose.

deploy.sh

Nun ni kolektu ĉion, kion ni faris permane en unu skripton. Ni komencu per la ĉefnivela funkcio, kaj poste rigardu la aliajn uzatajn en ĝi.

Interesaj teknikoj

  • ${parameter?err_msg} - unu el la bash-magiosorĉoj (alinome anstataŭigo de parametroj). Se parameter ne specifita, eligo err_msg kaj eliru per kodo 1.
  • docker --log-driver journald — defaŭlte, la docker-logging-ŝoforo estas tekstdosiero sen ajna rotacio. Kun ĉi tiu aliro, la ŝtipoj rapide plenigas la tutan diskon, do por produktadmedio necesas ŝanĝi la ŝoforon al pli inteligenta.

Deploja skripto

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
}

Trajtoj uzataj:

  • ensure-reverse-proxy — Certigas, ke la inversa prokurilo funkcias (utila por la unua deplojo)
  • get-active-slot service_name — Determinas kiu fendo estas nuntempe aktiva por antaŭfiksita servo (BLUEGREEN)
  • get-service-status service_name deployment_slot — Determinas ĉu la servo pretas prilabori envenantajn petojn
  • set-active-slot service_name deployment_slot — Ŝanĝas la nginx-agordon en la inversa prokura ujo

En ordo:

ensure-reverse-proxy() {
    is-container-up reverse-proxy && return 0
    echo "Deploying reverse-proxy..."
    docker network create web-gateway
    docker run 
        --detach 
        --restart always 
        --log-driver journald 
        --name reverse-proxy 
        --network web-gateway 
        --publish 80:80 
        nginx:alpine || return 1
    docker exec --interactive reverse-proxy sh -c "> /etc/nginx/conf.d/default.conf"
    docker exec reverse-proxy nginx -s reload
}

is-container-up() {
    local container=${1?"Usage: ${FUNCNAME[0]} container_name"}

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

get-active-slot() {
    local service=${1?"Usage: ${FUNCNAME[0]} service_name"}

    if is-container-up ${service}_BLUE && is-container-up ${service}_GREEN; then
        echo "Collision detected! Stopping ${service}_GREEN..."
        docker rm -f ${service}_GREEN
        return 0  # BLUE
    fi
    if is-container-up ${service}_BLUE && ! is-container-up ${service}_GREEN; then
        return 0  # BLUE
    fi
    if ! is-container-up ${service}_BLUE; then
        return 1  # GREEN
    fi
}

get-service-status() {
    local usage_msg="Usage: ${FUNCNAME[0]} service_name deployment_slot"
    local service=${1?usage_msg}
    local slot=${2?$usage_msg}

    case $service in
        # Add specific healthcheck paths for your services here
        *) local health_check_port_path=":8080/" ;;
    esac
    local health_check_address="http://${service}_${slot}${health_check_port_path}"
    echo "Requesting '$health_check_address' within the 'web-gateway' docker network:"
    docker run --rm --network web-gateway alpine 
        wget --timeout=1 --quiet --server-response $health_check_address
    return $?
}

set-active-slot() {
    local usage_msg="Usage: ${FUNCNAME[0]} service_name deployment_slot"
    local service=${1?$usage_msg}
    local slot=${2?$usage_msg}
    [ "$slot" == BLUE ] || [ "$slot" == GREEN ] || return 1

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

funkcio get-active-slot postulas iom da klarigo:

Kial ĝi resendas nombron kaj ne eligas ĉenon?

Ĉiuokaze, en la voka funkcio ni kontrolas la rezulton de ĝia laboro, kaj kontroli elirkodon per bash estas multe pli facila ol kontroli ĉenon. Krome, ricevi ŝnuron de ĝi estas tre simpla:
get-active-slot service && echo BLUE || echo GREEN.

Ĉu tri kondiĉoj vere sufiĉas por distingi ĉiujn ŝtatojn?

Blua-Verda Deplojo ĉe minimumaj salajroj

Eĉ du sufiĉos, la lasta estas ĉi tie nur por kompleteco, por ne skribi else.

Nur la funkcio, kiu resendas nginx-agordojn, restas nedifinita: get-nginx-config service_name deployment_slot. Analogie kun sankontrolo, ĉi tie vi povas agordi ajnan agordon por iu ajn servo. El la interesaj aferoj — nur cat <<- EOF, kiu permesas forigi ĉiujn langetojn komence. Vere, la prezo de bona formatado estas miksitaj langetoj kun spacoj, kio hodiaŭ estas konsiderata tre malbona formo. Sed bash devigas langetojn, kaj ankaŭ estus bone havi normalan formatadon en la nginx-agordo. Resume, miksi langetojn kun spacoj ĉi tie vere ŝajnas la plej bona solvo el la plej malbona. Tamen, vi ne vidos ĉi tion en la suba fragmento, ĉar Habr "faras ĝin bone" ŝanĝante ĉiujn langetojn al 4 spacoj kaj malvalidigante EOF. Kaj ĉi tie ĝi estas rimarkebla.

Por ne ellitiĝi dufoje, mi tuj rakontos al vi pri cat << 'EOF', kiu estos renkontita poste. Se vi skribas simple cat << EOF, tiam ene de heredoc la ĉeno estas interpolata (variabloj estas vastigitaj ($foo), komandvokoj ($(bar)) ktp.), kaj se vi enmetas la finon de dokumento inter unuopaj citiloj, tiam interpolado estas malŝaltita kaj la simbolo $ estas montrata kiel estas. Kion vi bezonas por enmeti skripton ene de alia skripto.

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

    local container_name=${service}_${slot}
    case $service in
        # Add specific nginx configs for your services here
        *) nginx-config-simple-service $container_name:8080 ;;
    esac
}

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

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

Ĉi tio estas la tuta skripto. Kaj tiel esenco kun ĉi tiu skripto por elŝuto per wget aŭ curl.

Efektivigo de parametrizitaj skriptoj sur fora servilo

Estas tempo frapi la celservilon. Ĉifoje localhost sufiĉe taŭga:

$ 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.

Ni skribis deplojan skripton kiu elŝutas antaŭkonstruitan bildon al la cela servilo kaj perfekte anstataŭigas la servan ujon, sed kiel ni povas ekzekuti ĝin sur fora maŝino? La skripto havas argumentojn, ĉar ĝi estas universala kaj povas disfaldi plurajn servojn samtempe sub unu inversa prokurilo (vi povas uzi nginx-agordojn por determini kiu url estos kiu servo). La skripto ne povas esti konservita en la servilo, ĉar ĉi-kaze ni ne povos ĝin aŭtomate ĝisdatigi (por korekti cimojn kaj aldoni novajn servojn), kaj ĝenerale, stato = malbono.

Solvo 1: Ankoraŭ konservu la skripton sur la servilo, sed kopiu ĝin ĉiufoje scp. Poste konekti per ssh kaj ekzekuti la skripton kun la necesaj argumentoj.

Kons:

  • Du agoj anstataŭ unu
  • Eble ne estas loko, kie vi kopias, aŭ eble ne estas aliro al ĝi, aŭ la skripto povas esti efektivigita en la momento de anstataŭigo.
  • Estas konsilinde purigi post vi mem (forigu la skripton).
  • Jam tri agoj.

Solvo 2:

  • Konservu nur funkciodifinojn en la skripto kaj rulu nenion
  • Kun la helpo de sed aldonu funkciovokon ĝis la fino
  • Sendu ĉion rekte al shh per pipo (|)

Pros:

  • Vere sennacia
  • Neniuj kaldronaj entoj
  • Sentante malvarmeta

Ni simple faru ĝin sen Ansible. Jes, ĉio jam estas inventita. Jes, biciklo. Rigardu kiel simpla, eleganta kaj minimumisma estas la biciklo:

$ cat << 'EOF' > 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
Connecting to localhost...
Yay! The 'deploy' function is executing on 'hut' with argument 'magic-porridge-pot'

Tamen, ni ne povas esti certaj, ke la fora gastiganto havas adekvatan bash, do ni aldonos malgrandan kontrolon ĉe la komenco (ĉi tio estas anstataŭe de ŝelbango):

if [ "$SHELL" != "/bin/bash" ]
then
    echo "The '$SHELL' shell is not supported by 'deploy.sh'. Set a '/bin/bash' shell for '$USER@$HOSTNAME'."
    exit 1
fi

Kaj nun ĝi estas reala:

$ 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
Sending gzipped image 'uptimer' to 'localhost' via ssh...
Loaded image: uptimer:latest
Connecting to 'localhost' via ssh to seamlessly deploy 'uptimer'...
Deploying 'uptimer_GREEN' in place of 'uptimer_BLUE'...
06f5bc70e9c4f930e7b1f826ae2ca2f536023cc01e82c2b97b2c84d68048b18a
Container started. Checking health...
Requesting 'http://uptimer_GREEN:8080/' within the 'web-gateway' docker network:
  HTTP/1.0 503 Service Unavailable
wget: server returned error: HTTP/1.0 503 Service Unavailable
New 'uptimer_GREEN' service is not ready yet. Waiting (1)...
Requesting 'http://uptimer_GREEN:8080/' within the 'web-gateway' docker network:
  HTTP/1.0 503 Service Unavailable
wget: server returned error: HTTP/1.0 503 Service Unavailable
New 'uptimer_GREEN' service is not ready yet. Waiting (2)...
Requesting 'http://uptimer_GREEN:8080/' within the 'web-gateway' docker network:
  HTTP/1.0 200 OK
  Server: BaseHTTP/0.6 Python/3.8.3
  Date: Sat, 22 Aug 2020 20:15:50 GMT
  Content-Type: text/html

New 'uptimer_GREEN' service seems OK. Switching heads...
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
2020/08/22 20:15:54 [notice] 97#97: signal process started
The 'uptimer_GREEN' service is live!
Killing 'uptimer_BLUE'...
uptimer_BLUE
Total reclaimed space: 0B
Deployment successful!

Nun vi povas malfermi http://localhost/ en la retumilo, rulu la deplojon denove kaj certigu, ke ĝi funkcias perfekte ĝisdatigante la paĝon laŭ la KD dum la aranĝo.

Ne forgesu purigi post laboro :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

fonto: www.habr.com