Γαλαζοπράσινη ανάπτυξη με ελάχιστους μισθούς

Σε αυτό το άρθρο χρησιμοποιούμε βίαιο χτύπημα, ssh, λιμενεργάτης и nginx Θα οργανώσουμε μια απρόσκοπτη διάταξη της διαδικτυακής εφαρμογής. Γαλαζοπράσινη ανάπτυξη είναι μια τεχνική που σας επιτρέπει να ενημερώνετε άμεσα μια εφαρμογή χωρίς να απορρίπτετε ούτε ένα αίτημα. Είναι μία από τις στρατηγικές ανάπτυξης μηδενικού χρόνου διακοπής λειτουργίας και είναι η πλέον κατάλληλη για εφαρμογές με μία παρουσία, αλλά τη δυνατότητα φόρτωσης μιας δεύτερης, έτοιμης για εκτέλεση παρουσίας κοντά.

Ας υποθέσουμε ότι έχετε μια εφαρμογή Ιστού με την οποία εργάζονται ενεργά πολλοί πελάτες και δεν υπάρχει απολύτως κανένας τρόπος να ξαπλώσει για μερικά δευτερόλεπτα. Και πρέπει πραγματικά να διαθέσετε μια ενημέρωση βιβλιοθήκης, μια επιδιόρθωση σφαλμάτων ή μια νέα ενδιαφέρουσα λειτουργία. Σε μια κανονική κατάσταση, θα χρειαστεί να σταματήσετε την εφαρμογή, να την αντικαταστήσετε και να την ξεκινήσετε ξανά. Στην περίπτωση του docker, μπορείτε πρώτα να το αντικαταστήσετε και μετά να το επανεκκινήσετε, αλλά θα υπάρχει ακόμα μια περίοδος κατά την οποία τα αιτήματα προς την εφαρμογή δεν θα διεκπεραιωθούν, επειδή συνήθως η εφαρμογή χρειάζεται λίγο χρόνο για να φορτώσει αρχικά. Τι γίνεται αν ξεκινήσει, αλλά αποδειχθεί ότι δεν λειτουργεί; Αυτό είναι το πρόβλημα, ας το λύσουμε με ελάχιστα μέσα και όσο πιο κομψά γίνεται.

ΑΠΟΠΟΙΗΣΗ ΕΥΘΥΝΗΣ: Το μεγαλύτερο μέρος του άρθρου παρουσιάζεται σε πειραματική μορφή - με τη μορφή εγγραφής μιας περιόδου λειτουργίας κονσόλας. Ας ελπίσουμε ότι αυτό δεν θα είναι πολύ δύσκολο να γίνει κατανοητό και ο κώδικας θα τεκμηριωθεί επαρκώς. Για την ατμόσφαιρα, φανταστείτε ότι αυτά δεν είναι απλώς αποσπάσματα κώδικα, αλλά χαρτί από έναν «σιδερένιο» τηλετύπο.

Γαλαζοπράσινη ανάπτυξη με ελάχιστους μισθούς

Στην αρχή κάθε ενότητας περιγράφονται ενδιαφέρουσες τεχνικές που δυσκολεύονται στο Google μόνο με την ανάγνωση του κώδικα. Αν κάτι άλλο είναι ασαφές, ψάξτε το στο google και ελέγξτε το. εξηγώ (ευτυχώς λειτουργεί ξανά, λόγω ξεμπλοκαρίσματος του τηλεγραφήματος). Εάν δεν μπορείτε να αναζητήσετε τίποτα στο Google, ρωτήστε στα σχόλια. Θα χαρώ να προσθέσω στην αντίστοιχη ενότητα «Ενδιαφέρουσες τεχνικές».

Ας αρχίσουμε.

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

Υπηρεσίες

Ας κάνουμε μια πειραματική υπηρεσία και ας την τοποθετήσουμε σε ένα δοχείο.

Ενδιαφέρουσες τεχνικές

  • cat << EOF > file-name (Εδώ Έγγραφο + Ανακατεύθυνση I/O) είναι ένας τρόπος δημιουργίας ενός αρχείου πολλαπλών γραμμών με μία εντολή. Όλα bash διαβάζει από /dev/stdin μετά από αυτή τη γραμμή και πριν από τη γραμμή EOF θα καταγραφεί σε file-name.
  • wget -qO- URL (εξηγώ) — έξοδος ενός εγγράφου που ελήφθη μέσω HTTP σε /dev/stdout (αναλογικό curl URL).

Εκτύπωσε

Σπάω συγκεκριμένα το απόσπασμα για να ενεργοποιήσω την επισήμανση για την Python. Στο τέλος θα υπάρχει άλλο ένα τέτοιο κομμάτι. Σκεφτείτε ότι σε αυτά τα σημεία το χαρτί κόπηκε για να σταλεί στο τμήμα επισήμανσης (όπου ο κωδικός ήταν χρωματισμένος στο χέρι με highlighters), και στη συνέχεια αυτά τα κομμάτια κολλήθηκαν πίσω.

$ 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

Αντίστροφος διακομιστής μεσολάβησης

Για να μπορεί η εφαρμογή μας να αλλάξει απαρατήρητη, είναι απαραίτητο να υπάρχει κάποια άλλη οντότητα μπροστά της που θα κρύβει την αντικατάστασή της. Θα μπορούσε να είναι ένας διακομιστής ιστού nginx в αντίστροφη λειτουργία διακομιστή μεσολάβησης. Δημιουργείται ένας αντίστροφος διακομιστής μεσολάβησης μεταξύ του πελάτη και της εφαρμογής. Δέχεται αιτήματα από πελάτες και τα προωθεί στην εφαρμογή και προωθεί τις απαντήσεις της εφαρμογής στους πελάτες.

Η εφαρμογή και ο αντίστροφος διακομιστής μεσολάβησης μπορούν να συνδεθούν μέσα στο docker χρησιμοποιώντας δίκτυο docker. Έτσι, το κοντέινερ με την εφαρμογή δεν χρειάζεται καν να προωθήσει μια θύρα στο κεντρικό σύστημα· αυτό επιτρέπει στην εφαρμογή να απομονωθεί στο μέγιστο από εξωτερικές απειλές.

Εάν ο αντίστροφος διακομιστής μεσολάβησης ζει σε άλλο κεντρικό υπολογιστή, θα πρέπει να εγκαταλείψετε το δίκτυο docker και να συνδέσετε την εφαρμογή στον αντίστροφο διακομιστή μεσολάβησης μέσω του δικτύου κεντρικού υπολογιστή, προωθώντας τη θύρα εφαρμογές παράμετρος --publish, όπως στην πρώτη εκκίνηση και όπως με τον αντίστροφο διακομιστή μεσολάβησης.

Θα εκτελέσουμε τον αντίστροφο διακομιστή μεσολάβησης στη θύρα 80, επειδή αυτή είναι ακριβώς η οντότητα που πρέπει να ακούσει το εξωτερικό δίκτυο. Εάν η θύρα 80 είναι απασχολημένη στον κεντρικό υπολογιστή δοκιμής, αλλάξτε την παράμετρο --publish 80:80 επί --publish ANY_FREE_PORT:80.

Ενδιαφέρουσες τεχνικές

Εκτύπωσε

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

Απρόσκοπτη ανάπτυξη

Ας παρουσιάσουμε μια νέα έκδοση της εφαρμογής (με διπλάσια ενίσχυση της απόδοσης εκκίνησης) και ας προσπαθήσουμε να την αναπτύξουμε απρόσκοπτα.

Ενδιαφέρουσες τεχνικές

  • echo 'my text' | docker exec -i my-container sh -c 'cat > /my-file.txt' — Γράψτε κείμενο my text να αρχειοθετήσω /my-file.txt μέσα στο δοχείο my-container.
  • cat > /my-file.txt — Γράψτε τα περιεχόμενα της τυπικής εισαγωγής σε ένα αρχείο /dev/stdin.

Εκτύπωσε

$ 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

Σε αυτό το στάδιο, η εικόνα δημιουργείται απευθείας στον διακομιστή, κάτι που απαιτεί να υπάρχουν οι πηγές της εφαρμογής και επίσης φορτώνει τον διακομιστή με περιττή εργασία. Το επόμενο βήμα είναι να εκχωρήσετε το συγκρότημα εικόνας σε ένα ξεχωριστό μηχάνημα (για παράδειγμα, σε ένα σύστημα CI) και στη συνέχεια να το μεταφέρετε στον διακομιστή.

Μεταφορά εικόνων

Δυστυχώς, δεν έχει νόημα να μεταφέρετε εικόνες από localhost σε localhost, επομένως αυτή η ενότητα μπορεί να εξερευνηθεί μόνο εάν έχετε δύο κεντρικούς υπολογιστές με Docker στο χέρι. Τουλάχιστον μοιάζει κάπως έτσι:

$ 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

Ομάδα docker save αποθηκεύει τα δεδομένα της εικόνας σε ένα αρχείο .tar, που σημαίνει ότι ζυγίζει περίπου 1.5 φορές περισσότερο από ό,τι θα ζύγιζε σε συμπιεσμένη μορφή. Ας το ταρακουνήσουμε λοιπόν στο όνομα της εξοικονόμησης χρόνου και κίνησης:

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

Μπορείτε επίσης να παρακολουθείτε τη διαδικασία λήψης (αν και αυτό απαιτεί ένα βοηθητικό πρόγραμμα τρίτου μέρους):

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

Συμβουλή: Εάν χρειάζεστε μια δέσμη παραμέτρων για να συνδεθείτε σε έναν διακομιστή μέσω SSH, ενδέχεται να μην χρησιμοποιείτε το αρχείο ~/.ssh/config.

Μεταφορά της εικόνας μέσω docker image save/load - Αυτή είναι η πιο μινιμαλιστική μέθοδος, αλλά όχι η μοναδική. Υπάρχουν και άλλα:

  1. Μητρώο εμπορευματοκιβωτίων (βιομηχανικό πρότυπο).
  2. Σύνδεση σε διακομιστή docker daemon από άλλο κεντρικό υπολογιστή:
    1. μεταβλητή περιβάλλοντος DOCKER_HOST.
    2. Επιλογή γραμμής εντολών -H ή --host εργαλείο docker-compose.
    3. docker context

Η δεύτερη μέθοδος (με τρεις επιλογές για την υλοποίησή της) περιγράφεται καλά στο άρθρο Πώς να αναπτύξετε σε απομακρυσμένους κεντρικούς υπολογιστές Docker με docker-compose.

deploy.sh

Τώρα ας συγκεντρώσουμε όλα όσα κάναμε χειροκίνητα σε ένα σενάριο. Ας ξεκινήσουμε με τη συνάρτηση ανώτατου επιπέδου και, στη συνέχεια, ας δούμε τις άλλες που χρησιμοποιούνται σε αυτήν.

Ενδιαφέρουσες τεχνικές

  • ${parameter?err_msg} - ένα από τα μαγικά ξόρκια bash (γνωστός και ως αντικατάσταση παραμέτρων). Αν parameter δεν προσδιορίζεται, έξοδος err_msg και βγείτε με τον κωδικό 1.
  • docker --log-driver journald — από προεπιλογή, το πρόγραμμα οδήγησης καταγραφής docker είναι ένα αρχείο κειμένου χωρίς καμία περιστροφή. Με αυτήν την προσέγγιση, τα αρχεία καταγραφής γεμίζουν γρήγορα ολόκληρο τον δίσκο, επομένως για ένα περιβάλλον παραγωγής είναι απαραίτητο να αλλάξετε το πρόγραμμα οδήγησης σε πιο έξυπνο.

Σενάριο ανάπτυξης

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
}

Χαρακτηριστικά που χρησιμοποιούνται:

  • ensure-reverse-proxy — Βεβαιωθείτε ότι ο αντίστροφος διακομιστής μεσολάβησης λειτουργεί (χρήσιμος για την πρώτη ανάπτυξη)
  • get-active-slot service_name — Καθορίζει ποια υποδοχή είναι ενεργή αυτήν τη στιγμή για μια δεδομένη υπηρεσία (BLUE ή GREEN)
  • get-service-status service_name deployment_slot — Καθορίζει εάν η υπηρεσία είναι έτοιμη να επεξεργαστεί εισερχόμενα αιτήματα
  • set-active-slot service_name deployment_slot — Αλλάζει τη διαμόρφωση nginx στο κοντέινερ αντίστροφου διακομιστή μεσολάβησης

Προκειμένου:

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
}

Λειτουργία get-active-slot θέλει μια μικρή εξήγηση:

Γιατί επιστρέφει έναν αριθμό και δεν βγάζει μια συμβολοσειρά;

Τέλος πάντων, στη συνάρτηση κλήσης ελέγχουμε το αποτέλεσμα της εργασίας της και ο έλεγχος του κωδικού εξόδου χρησιμοποιώντας το bash είναι πολύ πιο εύκολος από τον έλεγχο μιας συμβολοσειράς. Επιπλέον, η λήψη μιας συμβολοσειράς από αυτό είναι πολύ απλή:
get-active-slot service && echo BLUE || echo GREEN.

Είναι πράγματι τρεις προϋποθέσεις για να διακρίνουν όλα τα κράτη;

Γαλαζοπράσινη ανάπτυξη με ελάχιστους μισθούς

Ακόμα και δύο θα είναι αρκετά, το τελευταίο είναι εδώ μόνο για πληρότητα, για να μην γράψω else.

Μόνο η συνάρτηση που επιστρέφει τις παραμέτρους nginx παραμένει απροσδιόριστη: get-nginx-config service_name deployment_slot. Κατ' αναλογία με τον έλεγχο υγείας, εδώ μπορείτε να ορίσετε οποιαδήποτε ρύθμιση παραμέτρων για οποιαδήποτε υπηρεσία. Από τα ενδιαφέροντα πράγματα - μόνο cat <<- EOF, το οποίο σας επιτρέπει να αφαιρέσετε όλες τις καρτέλες στην αρχή. Είναι αλήθεια ότι η τιμή της καλής μορφοποίησης είναι ανάμεικτες καρτέλες με κενά, που σήμερα θεωρείται πολύ κακή μορφή. Αλλά το bash επιβάλλει τις καρτέλες και θα ήταν επίσης ωραίο να υπάρχει κανονική μορφοποίηση στη διαμόρφωση nginx. Εν ολίγοις, η ανάμειξη καρτελών με κενά εδώ φαίνεται πραγματικά σαν η καλύτερη λύση από τα χειρότερα. Ωστόσο, αυτό δεν θα το δείτε στο παρακάτω απόσπασμα, καθώς ο Habr "τα καταφέρνει καλά" αλλάζοντας όλες τις καρτέλες σε 4 κενά και καθιστώντας το EOF άκυρο. Και εδώ γίνεται αντιληπτό.

Για να μην σηκωθώ δύο φορές, θα σας το πω αμέσως cat << 'EOF', που θα συναντήσουμε αργότερα. Αν γράφεις απλά cat << EOF, τότε μέσα στο heredoc η συμβολοσειρά παρεμβάλλεται (οι μεταβλητές επεκτείνονται ($foo), κλήσεις εντολών ($(bar)) κ.λπ.), και αν βάλετε το τέλος του εγγράφου σε μονά εισαγωγικά, τότε η παρεμβολή απενεργοποιείται και το σύμβολο $ εμφανίζεται ως έχει. Τι χρειάζεστε για να εισαγάγετε ένα σενάριο μέσα σε ένα άλλο σενάριο.

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
}

Αυτό είναι ολόκληρο το σενάριο. Και έτσι ουσία με αυτό το σενάριο για λήψη μέσω wget ή curl.

Εκτέλεση παραμετροποιημένων σεναρίων σε απομακρυσμένο διακομιστή

Ήρθε η ώρα να χτυπήσετε τον διακομιστή στόχο. Αυτή τη φορά localhost αρκετά κατάλληλο:

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

Έχουμε γράψει ένα σενάριο ανάπτυξης που κατεβάζει μια προ-ενσωματωμένη εικόνα στον διακομιστή προορισμού και αντικαθιστά απρόσκοπτα το κοντέινερ υπηρεσίας, αλλά πώς μπορούμε να το εκτελέσουμε σε απομακρυσμένο μηχάνημα; Το σενάριο έχει ορίσματα, καθώς είναι καθολικό και μπορεί να αναπτύξει πολλές υπηρεσίες ταυτόχρονα κάτω από έναν αντίστροφο διακομιστή μεσολάβησης (μπορείτε να χρησιμοποιήσετε παραμέτρους nginx για να καθορίσετε ποια διεύθυνση URL θα είναι ποια υπηρεσία). Το σενάριο δεν μπορεί να αποθηκευτεί στον διακομιστή, αφού σε αυτήν την περίπτωση δεν θα μπορούμε να το ενημερώσουμε αυτόματα (με σκοπό την επιδιόρθωση σφαλμάτων και την προσθήκη νέων υπηρεσιών) και γενικά, κατάσταση = κακό.

Λύση 1: Εξακολουθεί να αποθηκεύετε το σενάριο στον διακομιστή, αλλά να το αντιγράφετε κάθε φορά scp. Στη συνέχεια, συνδεθείτε μέσω ssh και εκτελέστε το σενάριο με τα απαραίτητα ορίσματα.

Μειονεκτήματα:

  • Δύο ενέργειες αντί για μία
  • Μπορεί να μην υπάρχει μέρος όπου αντιγράφετε ή να μην υπάρχει πρόσβαση σε αυτό ή το σενάριο μπορεί να εκτελεστεί τη στιγμή της αντικατάστασης.
  • Συνιστάται να καθαρίσετε τον εαυτό σας (διαγράψτε το σενάριο).
  • Ήδη τρεις δράσεις.

Λύση 2:

  • Διατηρήστε μόνο ορισμούς συναρτήσεων στο σενάριο και δεν εκτελέστε τίποτα απολύτως
  • Με sed προσθέστε μια κλήση συνάρτησης στο τέλος
  • Στείλτε τα όλα απευθείας στο shh μέσω σωλήνα (|)

Πλεονεκτήματα:

  • Πραγματικά ανιθαγενής
  • Χωρίς οντότητες λέβητα
  • Αίσθημα δροσιάς

Ας το κάνουμε χωρίς το Ansible. Ναι, όλα έχουν ήδη εφευρεθεί. Ναι, ένα ποδήλατο. Δείτε πόσο απλό, κομψό και μινιμαλιστικό είναι το ποδήλατο:

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

Ωστόσο, δεν μπορούμε να είμαστε σίγουροι ότι ο απομακρυσμένος κεντρικός υπολογιστής έχει επαρκή bash, επομένως θα προσθέσουμε έναν μικρό έλεγχο στην αρχή (αυτό είναι αντί για shellbang):

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

Και τώρα είναι αληθινό:

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

Τώρα μπορείτε να ανοίξετε http://localhost/ στο πρόγραμμα περιήγησης, εκτελέστε ξανά την ανάπτυξη και βεβαιωθείτε ότι εκτελείται απρόσκοπτα ενημερώνοντας τη σελίδα σύμφωνα με το CD κατά τη διάρκεια της διάταξης.

Μην ξεχάσετε να καθαρίσετε μετά τη δουλειά :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

Πηγή: www.habr.com