在本文中我們將使用 , , и 我們組織了 Web 應用程式的無縫佈局。 - 是一種允許您立即更新應用程式而不拒絕任何要求的技術。它是零停機部署策略之一,最適合具有單一實例但能夠在附近啟動第二個可立即使用的實例的應用程式。
假設您有一個被許多客戶積極使用的 Web 應用程序,它絕對不能停下來幾秒鐘。您確實需要推出庫更新、錯誤修復或新的酷炫功能。在正常情況下,您需要停止該應用程序,替換它,然後重新啟動它。對於 Docker 來說,您可以先替換它,然後重新啟動它,但仍會有一段時間無法處理對應用程式的請求,因為應用程式通常需要一些時間才能進行初始載入。如果它啟動了但無法運行怎麼辦?這就是問題所在,讓我們用最少的手段盡可能優雅地解決它。
免責聲明:本文的大部分內容以實驗形式呈現 - 作為控制台會話的記錄。希望這不會太難理解並且程式碼足夠自文檔化。為了營造氛圍,想像一下這些不只是程式碼片段,而是來自「鐵」電傳打字機的紙張。
每個部分的開頭都描述了一些有趣的技術,這些技術僅透過閱讀程式碼很難在谷歌上找到。如果還有不清楚的地方,可以穀歌搜尋並查看 (幸運的是,由於 Telegram 的封鎖解除,它又可以正常工作了)。如果您在 Google 上找不到任何內容,請在評論中提問。我很樂意補充相應的“有趣的技術”部分。
讓我們開始吧。
$ mkdir blue-green-deployment && cd $_服務
讓我們建立一個測試服務並將其放在容器中。
有趣的技術
cat << EOF > file-name( + ) - 一種使用一個指令建立多行檔案的方法。 bash 讀取的任何內容/dev/stdin在這行之後和這行之前EOF將被記錄在file-name.wget -qO- URL() — 將透過 HTTP 接收的文件輸出到/dev/stdout(類似物curl URL).
列印出
我故意破壞了該程式碼片段,以便為 Python 啟用突出顯示。最後還會有另一篇類似的文章。請考慮一下,在這些地方,紙張被剪開並轉移到突出顯示部門(在那裡用螢光筆手繪代碼),然後將這些碎片粘回去。
$ cat << EOF > uptimer.pyfrom 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反向代理
為了使我們的應用程式能夠不被注意地改變,有必要在它前面有一些其他實體來隱藏它的替換。它可能是一個網頁伺服器。 в 。客戶端和應用程式之間安裝了反向代理。它接受來自客戶端的請求並將其轉發給應用程序,並將應用程式的回應轉發給客戶端。
應用程式和反向代理可以在 Docker 內部使用 。因此,具有應用程式的容器甚至不需要在主機系統中轉送端口,從而最大限度地將應用程式與外部威脅隔離。
如果反向代理位於另一台主機上,則必須放棄 docker 網絡,並透過轉送連接埠將應用程式透過主機網路連接到反向代理 應用 範圍 --publish,就像第一次啟動時一樣,也像使用反向代理一樣。
我們將在連接埠 80 上執行反向代理,因為這是應該監聽外界的實體。如果測試主機上的 80 連接埠繁忙,請變更參數 --publish 80:80 上 --publish ANY_FREE_PORT:80.
有趣的技術
- 在使用者建立的 Docker 網路中,容器不僅可以透過 IP 位址進行連線。容器名稱也會解析為其 IP 位址。 (,Docker 規範第 5 點)。
列印出
$ 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 - 這是最簡約的方法,但不是唯一的方法。還有其他:
- 容器註冊(業界標準)。
- 從另一台主機連線到docker守護程式伺服器:
- 環境變數
DOCKER_HOST. - 命令列參數
-H或--host工具docker-compose. docker context
- 環境變數
第二種方法(有三種實作方案)在文章中有詳細描述 .
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)。腳本不能儲存在伺服器上,因為在這種情況下我們將無法自動更新它(為了修復錯誤和添加新服務),並且一般來說,state = evil。
解決方案 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_SCRIPTEOF
$ 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!現在你可以打開它 在瀏覽器中再次執行部署,並透過部署期間根據 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
