Blue-Green Deployment leicht gemacht

In diesem Artikel werden wir bash, ssh, docker und nginx eine nahtlose Bereitstellung einer Webanwendung organisieren. Blue-Green Deployment ist eine Technik, die es ermöglicht, eine Anwendung sofort zu aktualisieren, ohne Anfragen abzulehnen. Es ist eine der Strategien für Zero Downtime Deployment und eignet sich am besten für Anwendungen mit einer Instanz, aber mit der Möglichkeit, eine zweite, betriebsbereite Instanz daneben zu laden.

Angenommen, Sie haben eine Webanwendung, die von vielen Kunden aktiv genutzt wird, und es darf absolut nicht für ein paar Sekunden ausfallen. Sie müssen dringend eine Bibliothek aktualisieren, einen Bugfix einspielen oder ein neues cooles Feature bereitstellen. In der Regel müsste die Anwendung gestoppt, ersetzt und wieder gestartet werden. Im Falle von Docker kann man zunächst ersetzen und dann neu starten, aber es gibt trotzdem einen Zeitraum, in dem keine Anfragen an die Anwendung bearbeitet werden, denn normalerweise benötigt die Anwendung einige Zeit zum initialen Laden. Und wenn sie gestartet wird, sich aber als nicht funktionsfähig herausstellt? Das ist das Problem, lassen Sie uns dies mit minimalen Mitteln und maximal elegant lösen.

HAFTUNGSAUSSCHLUSS: Der Großteil des Artikels liegt in einem experimentellen Format vor – in Form einer Aufzeichnung einer Konsolensitzung. Ich hoffe, es wird nicht zu kompliziert sein, dies zu verstehen, und dieser Code dokumentiert sich selbst in ausreichendem Maße. Für die Atmosphäre stellen Sie sich vor, dass dies nicht nur Code-Snippets sind, sondern Papier vom „Eisen“-Telegraphen.

Blue-Green Deployment leicht gemacht

Interessante Techniken, die man nicht einfach durch das Lesen des Codes googeln kann, werden am Anfang jedes Abschnitts beschrieben. Wenn etwas unklar ist, googeln Sie und überprüfen Sie in explainshell (glücklicherweise funktioniert es wieder, da Telegram freigeschaltet wurde). Was sich nicht googeln lässt – fragen Sie in den Kommentaren. Ich ergänze gerne den entsprechenden Abschnitt "Interessante Techniken".

Legen wir los.

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

Dienst

Erstellen wir einen Testdienst und setzen ihn in einen Container.

Interessante Techniken

  • cat < file-name (Here Document + I/O Redirection) – eine Methode, um eine mehrzeilige Datei mit einem Befehl zu erstellen. Alles, was bash ab /dev/stdin dieser Zeile und bis zur Zeile EOF geschrieben wird, wird in file-name.
  • wget -qO- URL (explainshell) – die erhaltene HTTP-Dokumentation in /dev/stdout (analog curl URL).

Druckausgabe

Ich breche absichtlich den Snippet auf, um die Hervorhebung für Python einzuschließen. Am Ende wird es noch einen ähnlichen Abschnitt geben. Stellen Sie sich vor, dass an diesen Stellen das Papier für den Highlighting-Abteilung (wo der Code manuell mit Textmarkern hervorgehoben wurde) geschnitten und dann diese Stücke wieder eingeklebt wurden.

$ 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} läuft seit {t:3.1f} Sekunden.</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} (lädt in {loading_seconds} Sek.) gestartet.')
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
 ---&gt; 8ecf5a48c789
Step 2\/4 : EXPOSE 8080
 ---&gt; Using cache
 ---&gt; cf92d174c9d3
Step 3\/4 : COPY uptimer.py app.py
 ---&gt; a7fbb33d6b7e
Step 4\/4 : CMD [ "python", "-u", ".\/app.py" ]
 ---&gt; Running in 1906b4bd9fdf
Removing intermediate container 1906b4bd9fdf
 ---&gt; 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-&gt;8080\/tcp   uptimer

$ docker logs uptimer
Uptimer v1.0 (lädt in 10 Sek.) gestartet.

$ wget -qSO- http:\/\/localhost:8080
  HTTP\/1.0 503 Dienst nicht verfügbar
  Server: BaseHTTP\/0.6 Python\/3.8.3
  Datum: Sa, 22 Aug 2020 19:52:40 GMT
  Verbindung: geschlossen
  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
  Datum: Sa, 22 Aug 2020 19:52:45 GMT
  Content-Type: text\/html
<h2>Uptimer v1.0 läuft seit 15,4 Sekunden.</h2>

$ docker rm --force uptimer
uptimer

Reverse-Proxy

Damit unsere Anwendung sich unbemerkt ändern kann, muss davor eine Entität stehen, die ihre Modifikation verbirgt. Dies könnte ein Webserver sein. nginx in im Reverse-Proxy-Modus. Der Reverse-Proxy wird zwischen dem Client und der Anwendung installiert. Er empfängt Anfragen von den Clients und leitet sie an die Anwendung weiter, während er die Antworten der Anwendung an die Clients zurücksendet.

Die Anwendung und der Reverse-Proxy können innerhalb von Docker mit docker networkverknüpft werden. So kann dem Container mit der Anwendung sogar der Port im Host-System erspart werden, was es ermöglicht, die Anwendung maximal vor externen Bedrohungen zu isolieren.

Wenn der Reverse-Proxy auf einem anderen Host lebt, muss man auf das docker network verzichten und die Anwendung mit dem Reverse-Proxy über das Host-Netzwerk verbinden, indem der Port weitergeleitet wird. der Anwendung Parameter --publish, wie bei der ersten Ausführung und wie beim Reverse-Proxy.

Den Reverse-Proxy werden wir auf Port 80 starten, da dies die Entität ist, die externe Anfragen annehmen sollte. Wenn Port 80 auf Ihrem Testhost belegt ist, ändern Sie den Parameter --publish 80:80 auf --publish ANY_FREE_PORT:80.

Interessante Techniken

Druckausgabe

$ 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 läuft seit 11,5 Sekunden.</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.…"   vor 27 Sekunden       Läuft seit 25 Sekunden       0.0.0.0:80-&gt;80\/tcp   reverse-proxy
a1105f1b583d        uptimer             "python -u .\/app.py"     Vor etwa einer Minute   Läuft seit etwa einer 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 läuft seit 104,1 Sekunden.</h2>

Nahtloses Deployment

Wir werden eine neue Version der Anwendung (mit einer zweifachen Steigerung der Startup-Leistung) veröffentlichen und versuchen, sie nahtlos zu deploien.

Interessante Techniken

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' — Text schreiben my text in die Datei /my-file.txt im Container my-container.
  • cat > /my-file.txt — Den Inhalt des Standard-Input in die Datei schreiben /dev/stdin.

Druckausgabe

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

$ docker build --tag uptimer .
Sende den Build-Kontext an den Docker-Daemon  39.94kB
Schritt 1/4 : VON python:alpine
 ---&gt; 8ecf5a48c789
Schritt 2/4 : EXPONIEREN 8080
 ---&gt; Aus dem Cache verwenden
 ---&gt; cf92d174c9d3
Schritt 3/4 : KOPIEREN uptimer.py app.py
 ---&gt; 3eca6a51cb2d
Schritt 4/4 : CMD [ "python", "-u", ". /app.py" ]
 ---&gt; Ausgeführt in 8f13c6d3d9e7
Entferne Zwischencontainer 8f13c6d3d9e7
 ---&gt; 1d56897841ec
Erfolgreich erstellt 1d56897841ec
Erfolgreich getaggt uptimer:latest

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

$ docker logs uptimer_BLUE
Uptimer v2.0 (lädt in 5 Sek.) gestartet.

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer_BLUE:8080
<h2>Uptimer v2.0 läuft seit 23,9 Sekunden.</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 [Hinweis] 68#68: Signalverarbeitung gestartet

$ wget -qO- http://localhost
<h2>Uptimer v2.0 läuft seit 63,4 Sekunden.</h2>

$ docker rm -f uptimer
uptimer

$ wget -qO- http://localhost
<h2>Uptimer v2.0 läuft seit 84,8 Sekunden.</h2>

$ docker ps
CONTAINER ID        IMAGE               KOMMANDO                  ERSTELLT              STATUS              PORTS                NAMES
96932d4ca97a        uptimer             "python -u ./app.py"     Vor etwa einer Minute   Hoch Vor etwa einer Minute   8080/tcp             uptimer_BLUE
80695a822c19        nginx:alpine        " /docker-entrypoint.…"   Vor 8 Minuten          Hoch 8 Minuten        0.0.0.0:80-&gt;80/tcp   reverse-proxy

In diesem Schritt wird das Image direkt auf dem Server gebaut, was die Anwesenheit der Anwendungsourcecodes dort erfordert und den Server zusätzlich belastet. Der nächste Schritt wird die Auslagerung des Builds auf eine separate Maschine (zum Beispiel in ein CI-System) mit anschließender Übertragung auf den Server sein.

Image-Übertragung

Leider macht es keinen Sinn, Images von localhost auf localhost zu übertragen, daher kann dieser Abschnitt nur getestet werden, wenn man zwei Hosts mit Docker zur Hand hat. Im Minimalfall sieht es ungefähr so aus:

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

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

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

Team docker save speichert die Bilddaten in einem .tar-Archiv, das also ungefähr 1,5-mal so viel wiegen kann, wie es komprimiert wiegen würde. Also lasst uns das im Sinne der Zeit- und Datenreduzierung komprimieren:

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

Außerdem kann man den Übertragungsprozess beobachten (man benötigt allerdings ein externes Tool dazu):

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

Tipp: Wenn Sie für die Verbindung zum Server über SSH viele Parameter benötigen, verwenden Sie möglicherweise nicht die Datei ~/.ssh/config.

Die Übertragung des Images erfolgt über docker image save/load – das ist die minimalistischste Methode, aber nicht die einzige. Es gibt noch andere:

  1. Container Registry (Industrie-Standard).
  2. Verbinden Sie sich mit dem Docker-Daemon des Servers von einem anderen Host aus:
    1. Umgebungsvariable DOCKER_HOST.
    2. Befehlszeilenparameter -H oder --host einem Tool docker-compose.
    3. docker context

Die zweite Methode (mit drei Varianten ihrer Umsetzung) wird gut in dem Artikel beschrieben Wie man auf remote Docker-Hosts mit docker-compose bereitstellt.

deploy.sh

Jetzt fassen wir alles, was wir manuell gemacht haben, in ein Script zusammen. Wir beginnen mit der Top-Level-Funktion und schauen uns dann die anderen an, die darin verwendet werden.

Interessante Techniken

  • ${parameter?err_msg} – eines der Zauberwesen der Bash-Magie (aka Parameterersetzung). Wenn parameter nicht gesetzt ist, zeigt es err_msg an und beendet mit dem Code 1.
  • docker --log-driver journald – standardmäßig ist der Docker-Logging-Treiber eine Textdatei ohne Rotation. Mit diesem Ansatz füllen die Logs schnell die gesamte Festplatte, daher muss der Treiber für Produktionsumgebungen auf einen intelligenteren gewechselt werden.

Deployment-Script

deploy() {
    local usage_msg="Verwendung: ${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' im Austausch für '$OLD'..."
    docker run 
        --detach 
        --restart always 
        --log-driver journald 
        --name $NEW 
        --network web-gateway 
        $image_name || return 3
    echo "Container gestartet. Überprüfen der Gesundheit..."
    for i in {1..20}
    do
        sleep 1
        if get-service-status $image_name $new_slot
        then
            echo "Neuer '$NEW' Dienst scheint in Ordnung zu sein. Umschalten..."
            sleep 2  # Sicherstellen, dass der Dienst bereit ist
            set-active-slot $image_name $new_slot || return 4
            echo "'$NEW' Dienst ist live!"
            sleep 2  # Sicherstellen, dass alle Anfragen bearbeitet wurden
            echo "'$OLD' töten..."
            docker rm -f $OLD
            docker image prune -f
            echo "Bereitstellung erfolgreich!"
            return 0
        fi
        echo "Neuer '$NEW' Dienst ist noch nicht bereit. Warten ($i)..."
    done
    echo "Neuer '$NEW' Dienst hat nicht hochgefahren, töten... Bereitstellung fehlgeschlagen T_T"
    docker rm -f $NEW
    return 5
}

Verwendete Funktionen:

  • ensure-reverse-proxy — Stellt sicher, dass der Reverse-Proxy läuft (nützlich für die erste Bereitstellung)
  • get-active-slot service_name — Bestimmt, welcher Slot derzeit aktiv ist für den angegebenen Dienst (BLUE oder GREEN)
  • get-service-status service_name deployment_slot — Bestimmt, ob der Dienst bereit ist, eingehende Anfragen zu bearbeiten
  • set-active-slot service_name deployment_slot — Ändert die Nginx-Konfiguration im Reverse-Proxy-Container

In der Reihenfolge:

ensure-reverse-proxy() {
    is-container-up reverse-proxy && return 0
    echo "Reverse-Proxy wird bereitgestellt..."
    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?"Verwendung: ${FUNCNAME[0]} container_name"}

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

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

    if is-container-up ${service}_BLUE && is-container-up ${service}_GREEN; then
        echo "Kollision erkannt! Stoppe ${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="Verwendung: ${FUNCNAME[0]} service_name deployment_slot"
    local service=${1?usage_msg}
    local slot=${2?$usage_msg}

    case $service in
        # Fügen Sie hier spezifische Health-Check-Pfade für Ihre Dienste hinzu
        *) local health_check_port_path=":8080/" ;;
    esac
    local health_check_address="http://${service}_${slot}${health_check_port_path}"
    echo "Fordere '$health_check_address' im Docker-Netzwerk 'web-gateway' an:"
    docker run --rm --network web-gateway alpine 
        wget --timeout=1 --quiet --server-response $health_check_address
    return $?
}

set-active-slot() {
    local usage_msg="Verwendung: ${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
}

Funktion get-active-slot benötigt einige Erläuterungen:

Warum gibt sie eine Zahl zurück und nicht einen String?

Trotzdem überprüfen wir das Ergebnis in der aufrufenden Funktion, und den Exit-Code mit Bash zu überprüfen ist viel einfacher als einen String. Außerdem ist es ganz einfach, einen String daraus zu erhalten:
get-active-slot service && echo BLUE || echo GREEN.

Reichen die drei Bedingungen aus, um alle Zustände zu unterscheiden?

Blue-Green Deployment leicht gemacht

Zwei Bedingungen reichen sogar aus, die letzte ist hier nur der Vollständigkeit wegen. else.

Die Funktion, die die Nginx-Konfigurationen zurückgibt, ist noch unbestimmt: get-nginx-config service_name deployment_slot. Entsprechend dem Healthcheck kann hier jede Konfiguration für jeden Dienst festgelegt werden. Interessant ist lediglich, dass cat <<- EOF, was es ermöglicht, alle Tabs am Anfang zu entfernen. Leider ist der Preis für eine anständige Formatierung — gemischte Tabs mit Leerzeichen, was heute als sehr schlechter Stil gilt. Aber bash erzwingt Tabs, und auch in der nginx-Konfiguration wäre es schön, eine ordentliche Formatierung zu haben. Kurz gesagt, hier scheint das Mischen von Tabs und Leerzeichen wirklich die beste Lösung aus den schlechten Optionen zu sein. Allerdings werden Sie das im folgenden Snippet nicht sehen, da habr «gut macht», alle Tabs in 4 Leerzeichen umwandelt und EOF ungültig macht. Hier ist deutlich.

Um es nicht zweimal zu wiederholen, sage ich gleich etwas über cat << 'EOF', das später noch auftauchen wird. Wenn man einfach schreibt cat << EOF, erfolgt innerhalb des heredocs die Interpolation der Zeichenfolge (Variablen werden aufgelöst ($foo), Befehlaufrufe ($(bar)) usw.), und wenn man das Ende des Dokuments in einfache Anführungszeichen setzt, wird die Interpolation deaktiviert und das Zeichen $ wird unverändert ausgegeben. Das ist notwendig, um ein Skript in ein anderes Skript einzufügen.

get-nginx-config() {
    local usage_msg="Verwendung: ${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
        # Fügen Sie hier spezifische nginx-Konfigurationen für Ihre Dienste hinzu
        *) nginx-config-simple-service $container_name:8080 ;;
    esac
}

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

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

Das ist das gesamte Skript. Und hier ist das Gist mit diesem Skript zum Herunterladen über wget oder curl.

Ausführung parametrisierten Skripte auf einem entfernten Server

Es ist an der Zeit, auf den Zielserver zuzugreifen. Diesmal localhost passt gut:

$ ssh-copy-id localhost
/usr/bin/ssh-copy-id: INFO: versucht, sich mit den neuen Schlüssel(n) einzuloggen, um alle zu filtern, die bereits installiert sind
/usr/bin/ssh-copy-id: INFO: 1 Schlüssel(n) bleiben zu installieren -- wenn Sie jetzt aufgefordert werden, geschieht dies, um die neuen Schlüssel zu installieren
himura@localhost's Passwort: 

Anzahl der hinzugefügten Schlüssel: 1

Versuchen Sie jetzt, sich mit der Maschine einzuloggen, mit:   "ssh 'localhost'"
und überprüfen Sie, ob nur die Schlüssel hinzugefügt wurden, die Sie wollten.

Wir haben ein Deployment-Skript geschrieben, das ein vorab zusammengestelltes Image auf den Zielserver überträgt und nahtlos den Service-Container austauscht. Aber wie führt man es auf einer entfernten Maschine aus? Das Skript hat Argumente, da es universell ist und mehrere Dienste unter einem Reverse-Proxy gleichzeitig bereitstellen kann (mit Nginx-Konfigurationen lässt sich regeln, welcher Dienst unter welcher URL läuft). Das Skript kann nicht auf dem Server gespeichert werden, da wir es nur im Falle von Bugfixes und der Hinzufügung neuer Dienste nicht automatisch aktualisieren können. Außerdem, der Zustand = das Böse.

Lösung 1: Das Skript auf dem Server speichern, es aber jedes Mal über scpzu kopieren. Dann per ssh eine Verbindung herstellen und das Skript mit den erforderlichen Argumenten ausführen.

Nachteile:

  • Zwei Aktionen statt einer.
  • Der Ort, an den Sie kopieren, könnte nicht existieren, oder es könnte keinen Zugang dazu geben, oder das Skript könnte während des Austauschs ausgeführt werden.
  • Es wäre wünschenswert, es zu entfernen (das Skript zu löschen).
  • Schon drei Aktionen.

Lösung 2:

  • Im Skript nur Funktionsdefinitionen halten und überhaupt nichts ausführen.
  • Mit Hilfe von sed Am Ende einen Funktionsaufruf hinzufügen.
  • Alles direkt in shh über pipe senden (|)

Vorteile:

  • Wirklich zustandslos.
  • Keine Boilerplate-Entitäten.
  • Fühlt sich cool an.

Lass uns dabei nur ohne Ansible auskommen. Ja, alles wurde schon erdacht. Ja, das Fahrrad. Schaut euch dieses einfache, elegante und minimalistische Fahrrad an:

$ 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
Verbindung zu localhost...
Juhu! Die 'deploy'-Funktion wird auf 'hut' mit dem Argument 'magic-porridge-pot' ausgeführt.

Wir können jedoch nicht sicher sein, dass auf dem entfernten Host ein adäquates Bash vorhanden ist. Deshalb fügen wir zu Beginn eine kleine Überprüfung hinzu (das ist anstelle von shellbang.):

if [ "$SHELL" != "/bin/bash" ]
then
    echo "Die '$SHELL'-Shell wird von 'deploy.sh' nicht unterstützt. Stellen Sie eine '/bin/bash'-Shell für '$USER@$HOSTNAME' ein."
    exit 1
fi

Und jetzt geht es wirklich los:

$ 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
Sende das komprimierte Image 'uptimer' an 'localhost' per ssh...
Geladenes Image: uptimer:latest
Verbindung zu 'localhost' via ssh, um 'uptimer' nahtlos bereitzustellen...
Bereitstellung von 'uptimer_GREEN' anstelle von 'uptimer_BLUE'...
06f5bc70e9c4f930e7b1f826ae2ca2f536023cc01e82c2b97b2c84d68048b18a
Container gestartet. Überprüfe die Gesundheit...
Anfrage an 'http://uptimer_GREEN:8080/' im Docker-Netzwerk 'web-gateway':
  HTTP/1.0 503 Dienst nicht verfügbar
wget: Server hat Fehler zurückgegeben: HTTP/1.0 503 Dienst nicht verfügbar
Der neue Dienst 'uptimer_GREEN' ist noch nicht bereit. Warte (1)...
Anfrage an 'http://uptimer_GREEN:8080/' im Docker-Netzwerk 'web-gateway':
  HTTP/1.0 503 Dienst nicht verfügbar
wget: Server hat Fehler zurückgegeben: HTTP/1.0 503 Dienst nicht verfügbar
Der neue Dienst 'uptimer_GREEN' ist noch nicht bereit. Warte (2)...
Anfrage an 'http://uptimer_GREEN:8080/' im Docker-Netzwerk 'web-gateway':
  HTTP/1.0 200 OK
  Server: BaseHTTP/0.6 Python/3.8.3
  Datum: Sa, 22. Aug 2020 20:15:50 GMT
  Content-Type: text/html

Der neue Dienst 'uptimer_GREEN' scheint in Ordnung zu sein. Köpfe wechseln...
nginx: die Konfigurationsdatei /etc/nginx/nginx.conf hat die Syntax überprüfung bestanden
nginx: die Konfigurationsdatei /etc/nginx/nginx.conf hat den Test erfolgreich bestanden
2020/08/22 20:15:54 [Hinweis] 97#97: Signalprozess gestartet
Der Dienst 'uptimer_GREEN' ist online!
Killing 'uptimer_BLUE'...
uptimer_BLUE
Gesamt wiedergewonnenen Speicher: 0B
Bereitstellung erfolgreich!

Jetzt kann man öffnen http://localhost/ im Browser, das Deployment noch einmal starten und sicherstellen, dass es nahtlos verläuft, indem man die Seite während der Bereitstellung durch einen Klick aktualisiert.

Vergessen Sie nicht, nach der Arbeit aufzuräumen :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

Quelle: habr.com

60GB SSD 8Gb DDR4