Blue-Green Deployment in der Einfachheit

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

Angenommen, Sie haben eine Webanwendung, die von vielen Kunden aktiv genutzt wird, und die darf unter keinen Umständen kurzzeitig ausfallen. Sie müssen jedoch ein Update der Bibliothek, einen Bugfix oder eine neue coole Funktion einspielen. In einer herkömmlichen Situation müssten Sie die Anwendung anhalten, sie ersetzen und erneut starten. Im Falle von Docker kann man zunächst ersetzen und danach neu starten, aber es gibt dennoch einen Zeitraum, in dem Anfragen an die Anwendung nicht bearbeitet werden, da die Anwendung in der Regel Zeit für den ersten Start benötigt. Und wenn sie startet, aber nicht funktioniert? Lassen Sie uns diese Herausforderung mit minimalen Mitteln und maximaler Eleganz lösen.

HAFTUNGSAUSSCHLUSS: Der Großteil des Artikels ist im experimentellen Format als Aufzeichnung einer Konsolensitzung dargestellt. Ich hoffe, das lässt sich recht einfach nachvollziehen, und dieser Code dokumentiert sich selbst in ausreichendem Maße. Um die Atmosphäre zu schaffen, stellen Sie sich vor, dass dies nicht einfach Code-Schnipsel sind, sondern Papier aus einem „eisernen“ Teletype.

Blue-Green Deployment in der Einfachheit

Interessante Techniken, die man schwer finden kann, wenn man nur den Code liest, sind am Anfang jedes Abschnitts beschrieben. Wenn etwas unklar ist, googeln Sie bitte und überprüfen Sie in explainshell (zum Glück funktioniert es wieder, da Telegram entblockt wurde). Was nicht googelt, können Sie in den Kommentaren fragen. Ich ergänze gerne den entsprechenden Abschnitt "Interessante Techniken".

Legen wir los.

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

Dienste

Lassen Sie uns einen Testdienst erstellen und diesen in einen Container packen.

Interessante Techniken

  • cat < file-name (Hier-Dokument + I/O-Umleitung) — eine Methode, um eine mehrzeilige Datei mit einem Befehl zu erstellen. Alles, was bash von /dev/stdin nach dieser Zeile und bis zur Zeile EOF wird in file-name.
  • wget -qO- URL (explainshell) — um das über HTTP erhaltene Dokument in /dev/stdout (entspricht curl URL).

Ausgabe

Ich schneide den Snippet absichtlich, um die Highlights für Python einzufügen. Am Ende wird es noch einen solchen Abschnitt geben. Stellen Sie sich vor, dass an diesen Stellen das Papier geschnitten wurde, um es an die Highlight-Abteilung weiterzugeben (wo der Code von Hand mit Markern hervorgehoben wurde), und diese Stücke wurden dann wieder eingeklebt.

$ cat < uptimer.py
von http.server importieren BaseHTTPRequestHandler, HTTPServer
von time importieren 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
  Date: Sa, 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: 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 die Möglichkeit hat, nahtlos umzuschalten, muss etwas vor ihr stehen, das die Änderung verbirgt. Das könnte ein Webserver sein. nginx in im Reverse-Proxy-Modus. Der Reverse-Proxy wird zwischen dem Client und der Anwendung eingerichtet. Er nimmt Anfragen von Clients entgegen und leitet sie an die Anwendung weiter, während die Antworten der Anwendung an die Clients geschickt werden.

Die Anwendung und der Reverse-Proxy können innerhalb von Docker über docker networkverbunden werden. Auf diese Weise muss dem Container mit der Anwendung nicht einmal ein Port im Hostsystem zugewiesen werden, was die Anwendung vor Bedrohungen von außen maximal isoliert.

Wenn der Reverse-Proxy auf einem anderen Host betrieben wird, müssen wir auf docker network verzichten und die Anwendung mit dem Reverse-Proxy über das Netzwerk des Hosts verbinden, indem wir den Port der Anwendung mit dem Parameter --publish, wie beim ersten Start und bei einem Reverse-Proxy.

Wir werden den Reverse-Proxy auf Port 80 starten, da dies die Entität ist, die nach außen hören sollte. Wenn der Port 80 auf Ihrem Testhost belegt ist, ändern Sie den Parameter --publish 80:80 findet man --publish ANY_FREE_PORT:80.

Interessante Techniken

Ausgabe

$ 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       Hoch 25 Sekunden       0.0.0.0:80-&gt;80/tcp   reverse-proxy
a1105f1b583d        uptimer             "python -u ./app.py"     Vor etwa einer Minute   Hoch Vor 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: Sa, 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 doppelt so schneller Startleistung) ausrollen und versuchen, sie nahtlos zu deployen.

Interessante Techniken

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' — Text speichern my text in die Datei /my-file.txt im Container my-container.
  • cat > /my-file.txt — Speichern Sie den Inhalt des Standard-Inputs in eine Datei /dev/stdin.

Ausgabe

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

$ docker build --tag uptimer .
Der Build-Kontext wird an den Docker-Daemon gesendet  39,94kB
Schritt 1/4: FROM python:alpine
 ---&gt; 8ecf5a48c789
Schritt 2/4: EXPOSE 8080
 ---&gt; Cache wird verwendet
 ---&gt; cf92d174c9d3
Schritt 3/4: COPY uptimer.py app.py
 ---&gt; 3eca6a51cb2d
Schritt 4/4: CMD [ "python", "-u", ".\/app.py" ]
 ---&gt; Wird ausgeführt in 8f13c6d3d9e7
Zwischencontainer 8f13c6d3d9e7 wird entfernt
 ---&gt; 1d56897841ec
Erfolgreich gebaut 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 [notice] 68#68: Prozesssignal 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               COMMAND                  CREATED              STATUS              PORTS                NAMES
96932d4ca97a        uptimer             "python -u .\/app.py"     Vor etwa einer Minute   Aktiv Vor etwa einer Minute   8080/tcp             uptimer_BLUE
80695a822c19        nginx:alpine        "\/docker-entrypoint.…"   Vor 8 Minuten        Aktiv 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 Verfügbarkeit der Quellcodes der Anwendung erfordert und den Server mit zusätzlicher Arbeit belastet. Der nächste Schritt wird die Auslagerung des Bildbaus auf eine separate Maschine (z.B. in eine CI-System) und die anschließende Übertragung auf den Server sein.

Übertragung von Images

Leider macht es keinen Sinn, Images von localhost auf localhost zu übertragen. Daher können Sie diesen Abschnitt nur ausprobieren, wenn Sie zwei Hosts mit Docker zur Verfügung haben. Im Minimalfall sieht es etwa so aus:

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

$ docker image save uptimer | ssh produktions-server 'docker image load'
Geladenes Image: uptimer:latest

$ ssh produktions-server docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
uptimer             latest              1d56897841ec        vor 5 Minuten       78,9MB

Der Befehl docker save speichert die Daten des Images in ein .tar-Archiv, was bedeutet, dass es etwa 1,5-mal größer ist als im komprimierten Format. Lassen Sie uns also im Sinne der Zeit- und Datensparsamkeit komprimieren:

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

Außerdem können Sie den Übertragungsprozess überwachen (dafür benötigen Sie jedoch ein externes Tool):

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

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

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

  1. Container-Registry (Branchensstandard).
  2. Verbinden mit dem Docker-Daemon des Servers von einem anderen Host:
    1. Umgebungsvariable DOCKER_HOST.
    2. Kommandozeilenparameter -H oder --host Werkzeug version: '1' services: simplesample-sonar: image: sonarqube:lts ports: - 9001:9000 - 9092:9092 network_mode: bridge.
    3. docker-Kontext

Eine zweite Methode (mit drei Implementierungsvarianten) ist gut in dem Artikel beschrieben Wie man auf entfernte Docker-Hosts mit docker-compose bereitstellt.

deploy.sh

Jetzt fassen wir alles, was wir manuell gemacht haben, in ein Skript zusammen. Beginnen wir mit der Hauptfunktion und schauen wir uns dann die anderen an, die darin verwendet werden.

Interessante Techniken

  • ${parameter?err_msg} — eines der Zauberformeln der Bash-Magie (aka Parameterersetzung). Wenn der Parameter nicht gesetzt ist, gebe err_msg aus und verlasse mit Code 1.
  • docker --log-driver journald — standardmäßig verwendet der Docker-Logtreiber eine Textdatei ohne Rotation. Diese Herangehensweise führt dazu, dass die Logs schnell die gesamte Festplatte füllen, weshalb der Treiber für Produktionsumgebungen auf eine intelligentere Lösung geändert werden sollte.

Deployment-Skript

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

    ensure-reverse-proxy || return 2
    if get-active-slot $image_name
    then
        local OLD=${image_name}_BLAU
        local new_slot=GRÜN
    else
        local OLD=${image_name}_GRÜN
        local new_slot=BLAU
    fi
    local NEW=${image_name}_${new_slot}
    echo "Bereitstellung von '$NEW' anstelle von '$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. Wechseln der Heads..."
            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' wird beendet..."
            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 wurde nicht hochgefahren, wird beendet. Bereitstellung fehlgeschlagen T_T"
    docker rm -f $NEW
    return 5
}

Verwendete Funktionen:

  • ensure-reverse-proxy — Stellt sicher, dass der Reverse-Proxy funktioniert (nützlich für die erste Bereitstellung)
  • get-active-slot service_name — Bestimmt, welcher Slot aktuell aktiv ist für den angegebenen Dienst (BLAU oder GRÜN)
  • get-service-status service_name deployment_slot — Bestimmt, ob der Dienst bereit ist, eingehende Anfragen zu verarbeiten
  • set-active-slot service_name deployment_slot — Ändert die Nginx-Konfiguration im Reverse-Proxy-Container

In der richtigen Reihenfolge:

ensure-reverse-proxy() {
    is-container-up reverse-proxy && return 0
    echo "Starte den 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?"Benutzung: ${FUNCNAME[0]} container_name"}

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

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

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

    case $service in
        # Fügen Sie hier spezifische Healthcheck-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 "Anfrage '$health_check_address' im Docker-Netzwerk 'web-gateway':"
    docker run --rm --network web-gateway alpine 
        wget --timeout=1 --quiet --server-response $health_check_address
    return $?
}

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

Die Funktion get-active-slot benötigt einige Erklärungen:

Warum gibt sie eine Zahl zurück und gibt keinen String aus?

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

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

Blue-Green Deployment in der Einfachheit

Auch zwei reichen aus, die letzte ist nur der Vollständigkeit halber da, um nicht zu schreiben. else.

Nur die Funktion, die die Nginx-Konfigurationen zurückgibt, bleibt unbestimmt: get-nginx-config service_name deployment_slot. Analog zum Healthcheck kann hier jede Konfiguration für jeden Dienst festgelegt werden. Interessant ist nur cat <<- EOF, was alle Tabs am Anfang entfernt. Allerdings ist der Preis für anständige Formatierung die Mischung von Tabs mit Leerzeichen, was heute als sehr schlechter Stil gilt. Aber Bash erzwingt Tabs, und in der Nginx-Konfiguration wäre es auch schön, eine ordentliche Formatierung zu haben. Kurz gesagt, hier scheint die Mischung aus Tabs und Leerzeichen tatsächlich die beste Lösung aus den schlechtesten zu sein. Doch im folgenden Snippet werden Sie das nicht sehen, da Habr „es gut macht“ und alle Tabs in 4 Leerzeichen umwandelt, wodurch EOF ungültig wird. Hier wird es klarer.

Um es einfacher zu machen, erzähle ich gleich von cat << 'EOF', das später noch vorkommen wird. Wenn man einfach schreibt cat << EOF, wird in der heredoc die Interpolation der Zeichenfolge durchgeführt (Variablen ($foo), Befehlaufrufe ($(bar)), etc. werden aufgelöst), 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 perfekt, um ein Skript in ein anderes Skript einzufügen.

get-nginx-config() {
    local usage_msg="Verwendung: ${FUNCNAME[0]} dienst_name bereitstellungs_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 die Gist mit diesem Skript zum Herunterladen über wget oder curl.

Ausführen von parametrierbaren Skripten auf einem entfernten Server

Es ist an der Zeit, sich mit dem Zielserver zu verbinden. Diesmal localhost passt es ganz gut:

$ ssh-copy-id localhost
/usr/bin/ssh-copy-id: INFO: Versuch, sich mit dem neuen Schlüssel einzuloggen, um bestehende Schlüssel herauszufiltern
/usr/bin/ssh-copy-id: INFO: 1 Schlüssel ist noch zu installieren – falls Sie jetzt aufgefordert werden, ist es zur Installation der neuen Schlüssel
himura@localhost's Passwort: 

Anzahl der hinzugefügten Schlüssel: 1

Versuchen Sie nun, sich mit folgendem Befehl auf dem Computer anzumelden: "ssh 'localhost'"
und überprüfen Sie, ob nur die gewünschten Schlüssel hinzugefügt wurden.

Wir haben ein Bereitstellungsskript geschrieben, das ein vorab erstelltes Image auf den Zielserver überträgt und nahtlos den Dienstcontainer austauscht. Aber wie führen wir es auf einer entfernten Maschine aus? Das Skript hat Parameter, da es universell ist und mehrere Dienste gleichzeitig unter einem Reverse-Proxy bereitstellen kann (mit Nginx-Configs kann geregelt werden, unter welcher URL welcher Dienst läuft). Das Skript kann nicht auf dem Server gespeichert werden, da wir es in diesem Fall nicht automatisch aktualisieren können (zum Zweck von Bugfixes und zum Hinzufügen neuer Dienste) und überhaupt, Zustand = böse.

Lösung 1: Das Skript auf dem Server speichern, aber jedes Mal über scpkopieren. Dann sich per ssh verbinden und das Skript mit den erforderlichen Parametern ausführen.

Nachteile:

  • Zwei Schritte anstelle von einem
  • Der Ort, an den Sie kopieren, könnte nicht existieren, oder der Zugriff darauf könnte fehlen, oder das Skript könnte zum Zeitpunkt des Austauschs ausgeführt werden.
  • Bitte den Bereich hinterlassen (Skript löschen).
  • Bereits drei Aktionen.

Lösung 2:

  • Im Skript nur Funktionsdefinitionen enthalten und nichts ausführen.
  • Mit sed Funktionaufruf am Ende hinzufügen.
  • Alles direkt über Pipe in shh senden (|)

Vorteile:

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

Aber bitte ohne Ansible. Ja, alles wurde schon erfunden. Ja, das Fahrrad. Schaut mal, wie einfach, elegant und minimalistisch dieses Fahrrad ist:

$ 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, also fügen wir zu Beginn einen kurzen Check hinzu (das ist anstelle von shebang.):

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

Und jetzt wird es ernst:

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

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

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

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster