最低工資藍綠部署

在這篇文章中我們使用 打壞, SSH, 搬運工人 и nginx的 我們將組織 Web 應用程式的無縫佈局。 藍綠部署 是一種允許您立即更新應用程式而無需拒絕單一請求的技術。 它是零停機部署策略之一,最適合具有一個實例但能夠載入附近第二個可立即運行的實例的應用程式。

假設您有一個 Web 應用程序,許多客戶端正在使用該應用程序,並且它絕對不可能停下來幾秒鐘。 您確實需要推出庫更新、錯誤修復或新的酷功能。 在正常情況下,您需要停止該應用程序,替換它並重新啟動它。 對於docker,您可以先替換它,然後重新啟動它,但仍然會有一段時間不會處理對應用程式的請求,因為通常應用程式需要一些時間來初始載入。 如果啟動了但結果無法運行怎麼辦? 這就是問題所在,讓我們用最少的手段,盡可能優雅地解決它。

免責聲明:本文的大部分內容均以實驗格式呈現 - 以控制台會話錄製的形式。 希望這不會太難理解,並且程式碼能夠充分記錄自己。 為了營造氣氛,想像這些不僅僅是程式碼片段,而是來自「鐵」電傳打字機的紙張。

最低工資藍綠部署

每節的開頭都描述了僅透過閱讀程式碼很難透過 Google 搜尋到的有趣技術。 如果還有什麼不清楚的,可以穀歌一下並檢查一下。 解釋shell (幸運的是,由於電報的解鎖,它再次起作用)。 如果您無法透過 Google 搜尋任何內容,請在評論中提問。 我很樂意添加到相應的“有趣的技術”部分。

讓我們開始吧。

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

服務

讓我們製作一個實驗性服務並將其放入容器中。

有趣的技巧

  • cat << EOF > file-name (這里文件 + 輸入/輸出重定向) 是一種使用一個指令建立多行檔案的方法。 bash 讀取的所有內容 /dev/stdin 在此行之後和該行之前 EOF 將被記錄在 file-name.
  • wget -qO- URL (解釋shell) — 將透過 HTTP 接收到的文件輸出到 /dev/stdout (類似物 curl URL).

列印出

我特意打破了該程式碼片段以啟用 Python 的突出顯示。 最後還會有另一部這樣的作品。 考慮一下,在這些地方,紙張被切割並發送到突出顯示部門(其中代碼是用螢光筆手工著色的),然後這些碎片被粘回去。

$ 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 網路並透過主機網路將應用程式連接到反向代理,並轉送連接埠 應用 範圍 --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 系統),然後將其傳輸到伺服器。

傳輸影像

不幸的是,將映像從本機傳輸到本機是沒有意義的,因此只有當您手邊有兩台帶有 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 守護程式伺服器:
    1. 環境變量 DOCKER_HOST.
    2. 命令列選項 -H--host 工具 docker-compose.
    3. docker context

文章中詳細描述了第二種方法(具有三種實作選項) 如何使用 docker-compose 在遠端 Docker 主機上部署.

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 — 決定給定服務目前處於活動狀態的插槽(BLUEGREEN)
  • 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,因此我們將在開頭添加一個小檢查(這代替 砲轟):

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