Blue-Green Deployment w wersji podstawowej

W tym artykule z pomocą bash, ssh, docker i nginx zorganizujemy bezproblemowe wdrożenie aplikacji webowej. Wdrożenie blue-green to technika, która umożliwia natychmiastową aktualizację aplikacji bez odrzucania żadnego żądania. Jest to jedna ze strategii wdrożenia bez przestojów i najlepiej sprawdza się w przypadku aplikacji z jedną instancją, ale z możliwością uruchomienia obok drugiej, gotowej do pracy instancji.

Załóżmy, że masz aplikację webową, z której aktywnie korzysta wielu klientów, i nie można jej w żaden sposób wyłączyć na kilka sekund. A bardzo potrzebujesz wprowadzić aktualizację biblioteki, naprawić błąd lub dodać nową, fajną funkcję. W normalnej sytuacji wymagałoby to zatrzymania aplikacji, wymiany i ponownego uruchomienia. W przypadku Dockera można najpierw zastąpić, a potem ponownie uruchomić, ale wciąż będzie okres, w którym żądania do aplikacji nie będą obsługiwane, ponieważ zwykle aplikacja potrzebuje trochę czasu na początkowe załadowanie. A co jeśli uruchomi się, ale okaże niesprawna? Rozwiążmy tę kwestię przy minimalnych środkach i w maksymalnie elegancki sposób.

DISCLAIMER: Większość artykułu przedstawiona jest w eksperymentalnym formacie — w postaci nagrania sesji konsolowej. Mam nadzieję, że nie będzie to zbyt trudne do zrozumienia, a ten kod wystarczająco dokumentuje siebie sam. Dla atmosfery, wyobraź sobie, że to nie tylko fragmenty kodu, ale papier z „żelaznego” teletajpu.

Blue-Green Deployment w wersji podstawowej

Interesujące techniki, które trudno znaleźć po prostu czytając kod, są opisane na początku każdego rozdziału. Jeśli coś będzie niejasne — googluj i sprawdzaj w explainshell (na szczęście znowu działa z powodu odblokowania Telegrama). Co nie jest w Google — pytaj w komentarzach. Chętnie uzupełnię odpowiedni rozdział „Interesujące techniki”.

Zaczynamy.

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

Serwis

Utworzymy próbny serwis i umieścimy go w kontenerze.

Interesujące techniki

  • cat < file-name (Dokument Here + I/O Redirection) — sposób na stworzenie wielowierszowego pliku jednym poleceniem. Wszystko, co bash odczyta z /dev/stdin po tej linii i do linii EOF zostanie zapisane w file-name.
  • wget -qO- URL (explainshell) — wyświetli otrzymany dokument HTTP w /dev/stdout (odpowiednik curl URL).

Drukowanie

Celowo przerywam snippety, aby włączyć podświetlenie dla Pythona. Na końcu będzie jeszcze jeden taki fragment. Uważaj, że w tych miejscach papier został pocięty do przekazania do działu podświetlania (gdzie kod był ręcznie kolorowany markerami), a potem te kawałki wklejono z powrotem.

$ cat < uptimer.py
z http.server import BaseHTTPRequestHandler, HTTPServer
z 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} działa od {t:3.1f} sekund.</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} (ładowanie w {loading_seconds} sek.) uruchomione.')
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 .
Wysyłanie kontekstu budowania do demona Docker  39.42kB
Krok 1/4 : FROM python:alpine
 ---&gt; 8ecf5a48c789
Krok 2/4 : EXPOSE 8080
 ---&gt; Używając pamięci podręcznej
 ---&gt; cf92d174c9d3
Krok 3/4 : COPY uptimer.py app.py
 ---&gt; a7fbb33d6b7e
Krok 4/4 : CMD [ "python", "-u", ".\/app.py" ]
 ---&gt; Uruchamianie w 1906b4bd9fdf
Usuwanie kontenera pośredniego 1906b4bd9fdf
 ---&gt; c1655b996fe8
Pomyślnie zbudowano c1655b996fe8
Pomyślnie otagowano uptimer:latest

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

$ docker ps
ID KONTENERA        OBRAZ               KOMENDA                UTWORZONE             STATUS              PORTY                    NAZWY
8f88c944b8bf        uptimer             "python -u .\/app.py"   3 sekundy temu       Uruchomione 5 sekundy        0.0.0.0:8080-&gt;8080\/tcp   uptimer

$ docker logs uptimer
Uptimer v1.0 (ładowany w 10 sek.) uruchomiony.

$ wget -qSO- http:\/\/localhost:8080
  HTTP\/1.0 503 Usługa niedostępna
  Serwer: BaseHTTP\/0.6 Python\/3.8.3
  Data: Sob, 22 Sie 2020 19:52:40 GMT
  Połączenie: zamknięte
  Typ treści: text\/html;charset=utf-8
  Długość treści: 484

$ wget -qSO- http:\/\/localhost:8080
  HTTP\/1.0 200 OK
  Serwer: BaseHTTP\/0.6 Python\/3.8.3
  Data: Sob, 22 Sie 2020 19:52:45 GMT
  Typ treści: text\/html
<h2>Uptimer v1.0 działa od 15,4 sekundy.</h2>

$ docker rm --force uptimer
uptimer

Serwer proxy zwrotny

Aby nasza aplikacja mogła niezauważalnie się zmieniać, musi być przed nią jeszcze jakaś instancja, która ukryje tę zmianę. Może to być serwer WWW nginx do w trybie proxy zwrotnym. Serwer proxy zwrotny jest ustawiony pomiędzy klientem a aplikacją. Przyjmuje żądania od klientów i przekazuje je do aplikacji, a odpowiedzi aplikacji kieruje do klientów.

Aplikację i serwer proxy zwrotny można połączyć wewnątrz Dockera za pomocą docker network. Dzięki temu kontener z aplikacją nie musi mieć przekierowanego portu w systemie gospodarza, co pozwala maksymalnie izolować aplikację od zagrożeń z zewnątrz.

Jeśli serwer proxy zwrotny będzie działał na innym hoście, trzeba będzie zrezygnować z docker network i połączyć aplikację z serwerem proxy zwrotnym przez sieć hosta, przekierowując port aplikacji parametr --publish, tak jak przy pierwszym uruchomieniu oraz jak w przypadku serwera proxy zwrotnego.

Serwer proxy zwrotny uruchomimy na porcie 80, ponieważ to właśnie ta instancja powinna słuchać zewnątrz. Jeśli port 80 jest zajęty na twoim teście, zmień parametr --publish 80:80 na --publish ANY_FREE_PORT:80.

Interesujące techniki

Drukowanie

$ 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 działa od 11,5 sekundy.</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 sekund temu       Działa 25 sekund       0.0.0.0:80-&gt;80/tcp   reverse-proxy
a1105f1b583d        uptimer             "python -u ./app.py"     Około minutę temu   Działa Około minutę   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 działa od 104,1 sekundy.</h2>

Bezproblemowe wdrożenie

Wydamy nową wersję aplikacji (z dwukrotnym wzrostem wydajności uruchamiania) i spróbujemy wdrożyć ją bezproblemowo.

Interesujące techniki

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' — Zapisz tekst my text do pliku /my-file.txt wewnątrz kontenera my-container.
  • cat > /my-file.txt — Zapisz zawartość standardowego wejścia do pliku /dev/stdin.

Drukowanie

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

$ docker build --tag uptimer .
Wysyłanie kontekstu budowy do demona Docker  39.94kB
Krok 1/4 : FROM python:alpine
 ---&gt; 8ecf5a48c789
Krok 2/4 : EXPOSE 8080
 ---&gt; Wykorzystując pamięć podręczną
 ---&gt; cf92d174c9d3
Krok 3/4 : COPY uptimer.py app.py
 ---&gt; 3eca6a51cb2d
Krok 4/4 : CMD [ "python", "-u", ".\/app.py" ]
 ---&gt; Uruchamianie w 8f13c6d3d9e7
Usuwanie pośredniego kontenera 8f13c6d3d9e7
 ---&gt; 1d56897841ec
Pomyślnie zbudowano 1d56897841ec
Pomyślnie oznaczono uptimer:latest

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

$ docker logs uptimer_BLUE
Uptimer v2.0 (ładowany w 5 sek.) rozpoczęty.

$ docker run --rm --network web-gateway alpine wget -qO- http://uptimer_BLUE:8080
<h2>Uptimer v2.0 działa od 23,9 sekundy.</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
serwer {
    listen 80;
    lokalizacja / {
        proxy_pass http://uptimer_BLUE:8080;
    }
}

$ docker exec reverse-proxy nginx -s reload
2020/06/25 21:22:23 [informacja] 68#68: sygnał proces uruchomiony

$ wget -qO- http://localhost
<h2>Uptimer v2.0 działa od 63,4 sekundy.</h2>

$ docker rm -f uptimer
uptimer

$ wget -qO- http://localhost
<h2>Uptimer v2.0 działa od 84,8 sekundy.</h2>

$ docker ps
ID KONTENERA        OBRAZ               KOMENDA                  UTWORZONO              STATUS              PORTY                NAZWY
96932d4ca97a        uptimer             "python -u .\/app.py"     Około minutę temu   Uruchomiono Około minutę   8080/tcp             uptimer_BLUE
80695a822c19        nginx:alpine        "\/docker-entrypoint.…"   8 minut temu        Uruchomiono 8 minut        0.0.0.0:80-&gt;80/tcp   reverse-proxy

Na tym etapie obraz jest budowany bezpośrednio na serwerze, co wymaga posiadania tam źródeł aplikacji oraz obciąża serwer zbędną pracą. Następnym krokiem będzie wydzielenie procesu budowy obrazu na oddzielną maszynę (np. w systemie CI) z późniejszym przesyłaniem go na serwer.

Przesyłanie obrazów

Niestety, przesyłanie obrazów z localhost na localhost nie ma sensu, więc tę sekcję można przetestować tylko mając pod ręką dwa hosty z Dockerem. W minimalnym wydaniu wygląda to mniej więcej tak:

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

$ docker image save uptimer | ssh production-server 'docker image load'
Załadowano obraz: uptimer:latest

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

Zespół docker save zapisuje dane obrazu w archiwum .tar, co oznacza, że waży około 1.5 razy więcej, niż mogłoby ważyć w formie skompresowanej. Więc spróbujmy go skompresować dla oszczędności czasu i transferu:

$ docker image save uptimer | gzip | ssh production-server 'zcat | docker image load'
Załadowano obraz: uptimer:latest

A także, można obserwować proces przesyłania (prawda, potrzebne do tego jest zewnętrzne narzędzie):

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

Porada: Jeśli potrzebujesz wielu parametrów do połączenia z serwerem przez SSH, być może nie używasz pliku ~/.ssh/config.

Przesyłanie obrazu przez docker image save/load — to najbardziej minimalistyczna metoda, ale nie jedyna. Są też inne:

  1. Container Registry (standard branżowy).
  2. Połączenie się z docker daemon serwera z innego hosta:
    1. Zmienna środowiskowa DOCKER_HOST.
    2. Parametr wiersza poleceń -H lub --host narzędzia docker-compose.
    3. docker context

Drugi sposób (z trzema wariantami jego realizacji) jest dobrze opisany w artykule Jak wdrożyć na zdalnych hostach Docker z docker-compose.

deploy.sh

Teraz zbierzmy wszystko, co robiliśmy ręcznie w jeden skrypt. Zacznijmy od funkcji najwyższego poziomu, a potem przyjrzymy się pozostałym, używanym w nim.

Interesujące techniki

  • ${parameter?err_msg} — jedno z zaklęć bash-magii (znane jako substytucja parametrów). Jeśli parametr nie jest ustawiony, wypisz err_msg i wyjdź z kodem 1.
  • docker --log-driver journald — domyślnie, drivery logowania Dockera to plik tekstowy bez żadnej rotacji. Przy takim podejściu logi szybko zapychają cały dysk, więc w środowisku produkcyjnym należy zmienić driver na mądrzejszy.

Skrypt wdrożeniowy

deploy() {
    local usage_msg="Użycie: ${FUNCNAME[0]} nazwa_obrazu"
    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 "Wdrażanie '$NEW' w miejsce '$OLD'..."
    docker run 
        --detach 
        --restart always 
        --log-driver journald 
        --name $NEW 
        --network web-gateway 
        $image_name || return 3
    echo "Kontener uruchomiony. Sprawdzanie stanu..."
    for i in {1..20}
    do
        sleep 1
        if get-service-status $image_name $new_slot
        then
            echo "Nowa usługa '$NEW' wydaje się być w porządku. Przechodzimy do zmiany nagłówków..."
            sleep 2  # Upewnij się, że usługa jest gotowa
            set-active-slot $image_name $new_slot || return 4
            echo "Usługa '$NEW' działa!"
            sleep 2  # Upewnij się, że wszystkie żądania zostały przetworzone
            echo "Zamykam '$OLD'..."
            docker rm -f $OLD
            docker image prune -f
            echo "Wdrażanie zakończone pomyślnie!"
            return 0
        fi
        echo "Nowa usługa '$NEW' nie jest jeszcze gotowa. Czekam ($i)..."
    done
    echo "Nowa usługa '$NEW' nie uruchomiła się, zamykam ją. Wdrażanie nie powiodło się T_T"
    docker rm -f $NEW
    return 5
}

Wykorzystane funkcje:

  • ensure-reverse-proxy — Upewnia się, że reverse proxy działa (przydatne przy pierwszym wdrożeniu)
  • get-active-slot nazwa_usługi — Określa, który slot jest aktualnie aktywny dla danej usługi (BLUE lub GREEN)
  • get-service-status nazwa_usługi slot_wdrożeniowy — Określa, czy usługa jest gotowa do przetwarzania przychodzących żądań
  • set-active-slot nazwa_usługi slot_wdrożeniowy — Zmienia konfigurację nginx w kontenerze reverse proxy

Po kolei:

ensure-reverse-proxy() {
    is-container-up reverse-proxy && return 0
    echo "Wdrażanie 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?"Użycie: ${FUNCNAME[0]} container_name"}

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

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

    if is-container-up ${service}_BLUE && is-container-up ${service}_GREEN; then
        echo "Wykryto kolizję! Zatrzymywanie ${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="Użycie: ${FUNCNAME[0]} service_name deployment_slot"
    local service=${1?usage_msg}
    local slot=${2?$usage_msg}

    case $service in
        # Dodaj specyficzne ścieżki healthcheck dla swoich usług tutaj
        *) local health_check_port_path=":8080/" ;;
    esac
    local health_check_address="http://${service}_${slot}${health_check_port_path}"
    echo "Wysyłam zapytanie do '$health_check_address' w ramach sieci docker '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="Użycie: ${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
}

veth_xdp_flush_bq() get-active-slot wymaga drobnych wyjaśnień:

Dlaczego zwraca liczbę, a nie wypisuje ciąg?

I tak w wywołującej funkcji sprawdzamy wynik jej działania, a sprawdzanie kodu powrotu za pomocą bash jest znacznie prostsze niż ciągu. Poza tym, uzyskanie ciągu jest bardzo łatwe:
get-active-slot service && echo BLUE || echo GREEN.

Czy te trzy warunki są wystarczające, aby rozróżnić wszystkie stany?

Blue-Green Deployment w wersji podstawowej

Dwa wystarczą, ostatnie jest tylko dla pełności, aby nie pisać inaczej.

Nieokreślona pozostaje tylko funkcja zwracająca konfiguracje nginx: get-nginx-config service_name deployment_slot. Na wzór healthcheck, tutaj można zdefiniować dowolną konfigurację dla dowolnej usługi. Z interesujących rzeczy — tylko cat <<- EOF, co pozwala usunąć wszystkie tabulatory na początku. Prawda, że cena dobrego formatowania to mieszane tabulatory z spacjami, co dzisiaj uważane jest za bardzo zły ton. Ale bash wymusza tabulatory, a w konfiguracji nginx też byłoby dobrze mieć normalne formatowanie. Krótko mówiąc, tutaj mieszanka tabulatorów z spacjami wydaje się naprawdę najlepszym rozwiązaniem z najgorszych. Jednakże, w poniższym skrypcie nie zobaczysz tego, ponieważ habr "robi dobrze", zmieniając wszystkie tabulatory na 4 spacje i czyniąc EOF niepoprawnym. A tutaj wyraźnie widać.

Żeby nie wstawać dwa razy, od razu opowiem o cat << 'EOF', który jeszcze pojawi się później. Jeśli napisać po prostu cat << EOF, to wewnątrz heredoc następuje interpolacja stringu (ujawniane są zmienne ($foo), wywołania komend ($(bar)) itd.), a jeśli zakończyć znak końca dokumentu w pojedynczych cudzysłowach, to interpolacja jest wyłączona i symbol $ wyświetlany jest jak jest. To, co potrzebne do wstawienia skryptu do innego skryptu.

get-nginx-config() {
    local usage_msg="Użycie: ${FUNCNAME[0]} nazwa_usługi slot_deploymentu"
    local service=${1?$usage_msg}
    local slot=${2?$usage_msg}
    [ "$slot" == BLUE ] || [ "$slot" == GREEN ] || return 1

    local container_name=${service}_${slot}
    case $service in
        # Dodaj odpowiednie konfiguracje nginx dla swoich usług tutaj
        *) nginx-config-simple-service $container_name:8080 ;;
    esac
}

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

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

To jest cały skrypt. I oto gist z tym skryptem do pobrania przez wget lub curl.

Wykonywanie skryptów parametryzowanych na zdalnym serwerze

Nadszedł czas, aby zapukać do docelowego serwera. Tym razem localhost idealnie pasuje:

$ ssh-copy-id localhost
/usr/bin/ssh-copy-id: INFO: attemptuje zalogować się z nowym kluczem(ami), aby odfiltrować wszystkie, które są już zainstalowane
/usr/bin/ssh-copy-id: INFO: 1 klucz(ów) pozostaje do zainstalowania -- jeśli zostaniesz teraz zapytany, to po to, aby zainstalować nowe klucze
hasło himura@localhost: 

Liczba dodanych kluczy: 1

Teraz spróbuj zalogować się do maszyny za pomocą:   "ssh 'localhost'"
i upewnij się, że tylko klucz(e), które chciałeś, zostały dodane.

Napisaliśmy skrypt wdrożeniowy, który przesyła wstępnie zbudowany obraz na docelowy serwer i bezproblemowo zamienia kontener usługi, ale jak go wykonać na zdalnej maszynie? Skrypt ma argumenty, ponieważ jest uniwersalny i może wdrażać kilka usług pod jednym odwrotnym proxy (konfiguracjami nginx można rozwiązać, jaki URL będzie jaką usługą). Skrypt nie może być przechowywany na serwerze, ponieważ w takim przypadku nie będziemy mogli go automatycznie aktualizować (w celu poprawienia błędów i dodawania nowych usług), a w ogóle, stan = zło.

Rozwiązanie 1: Przechowywać skrypt na serwerze, ale kopiować go za każdym razem przez scp. Następnie połączyć się przez ssh i wykonać skrypt z wymaganymi argumentami.

Wady:

  • Dwie czynności zamiast jednej
  • Miejsca, do których kopiujesz, może nie być albo nie mieć do niego dostępu, albo skrypt może być wykonywany w momencie zamiany.
  • Wskazane jest posprzątać po sobie (usunąć skrypt).
  • Już trzy czynności.

Rozwiązanie 2:

  • W skrypcie trzymać tylko definicje funkcji i w ogóle nic nie uruchamiać
  • Dzięki sed dopisać na końcu wywołanie funkcji
  • Wysyłać to wszystko bezpośrednio do shh przez pipe (|)

Zalety:

  • Naprawdę bezstanowy
  • Bez zbędnych encji
  • Czuję się fajnie

Tylko bez Ansible. Tak, wszystko już wymyślono. Tak, rower. Zobacz, jaki prosty, elegancki i minimalistyczny rower:

$ 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
Łączenie z localhost...
Hurra! Funkcja 'deploy' wykonuje się na 'hut' z argumentem 'magic-porridge-pot'

Jednak nie możemy być pewni, że na zdalnym hoście jest odpowiedni bash, więc dodamy na początku małą kontrolkę (zamiast shellbang):

if [ "$SHELL" != "/bin/bash" ]
then
    echo "Powłoka '$SHELL' nie jest obsługiwana przez 'deploy.sh'. Ustaw powłokę '/bin/bash' dla '$USER@$HOSTNAME'."
    exit 1
fi

A teraz wszystko naprawdę:

$ 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
Wysyłanie spakowanego obrazu 'uptimer' do 'localhost' za pośrednictwem ssh...
Załadowany obraz: uptimer:latest
Łączenie z 'localhost' za pośrednictwem ssh w celu bezproblemowego wdrożenia 'uptimer'...
Wdrożenie 'uptimer_GREEN' zamiast 'uptimer_BLUE'...
06f5bc70e9c4f930e7b1f826ae2ca2f536023cc01e82c2b97b2c84d68048b18a
Kontener uruchomiony. Sprawdzanie stanu...
Zgłaszanie 'http://uptimer_GREEN:8080/' w ramach sieci docker 'web-gateway':
  HTTP/1.0 503 Usługa niedostępna
wget: serwer zwrócił błąd: HTTP/1.0 503 Usługa niedostępna
Nowa usługa 'uptimer_GREEN' jeszcze nie jest gotowa. Oczekiwanie (1)...
Zgłaszanie 'http://uptimer_GREEN:8080/' w ramach sieci docker 'web-gateway':
  HTTP/1.0 503 Usługa niedostępna
wget: serwer zwrócił błąd: HTTP/1.0 503 Usługa niedostępna
Nowa usługa 'uptimer_GREEN' jeszcze nie jest gotowa. Oczekiwanie (2)...
Zgłaszanie 'http://uptimer_GREEN:8080/' w ramach sieci docker 'web-gateway':
  HTTP/1.0 200 OK
  Serwer: BaseHTTP/0.6 Python/3.8.3
  Data: Sat, 22 Aug 2020 20:15:50 GMT
  Typ treści: text/html

Nowa usługa 'uptimer_GREEN' wydaje się być w porządku. Przełączanie...
nginx: plik konfiguracyjny /etc/nginx/nginx.conf ma poprawną składnię
nginx: test pliku konfiguracyjnego /etc/nginx/nginx.conf zakończony sukcesem
2020/08/22 20:15:54 [ogłoszenie] 97#97: proces sygnałowy uruchomiony
Usługa 'uptimer_GREEN' działa!
Zabijanie 'uptimer_BLUE'...
uptimer_BLUE
Całkowita odzyskana przestrzeń: 0B
Wdrożenie zakończone sukcesem!

Można teraz otworzyć http://localhost/ w przeglądarce, uruchomić wdrożenie jeszcze raz i upewnić się, że przebiega bezproblemowo poprzez odświeżenie strony na KD podczas wdrożenia.

Nie zapominaj sprzątać po pracy :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

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster