การปรับใช้สีน้ำเงิน-เขียวด้วยค่าแรงขั้นต่ำ

ในบทความนี้เราใช้ ทุบตี, SSH, นักเทียบท่า и Nginx เราจะจัดเค้าโครงเว็บแอปพลิเคชันที่ไร้รอยต่อ การปรับใช้สีน้ำเงินเขียว เป็นเทคนิคที่ช่วยให้คุณสามารถอัปเดตแอปพลิเคชันได้ทันทีโดยไม่ต้องปฏิเสธคำขอแม้แต่คำเดียว เป็นหนึ่งในกลยุทธ์การปรับใช้การหยุดทำงานเป็นศูนย์ และเหมาะที่สุดสำหรับแอปพลิเคชันที่มีอินสแตนซ์เดียว แต่มีความสามารถในการโหลดอินสแตนซ์ตัวที่สองที่พร้อมใช้งานในบริเวณใกล้เคียง

สมมติว่าคุณมีเว็บแอปพลิเคชันที่ลูกค้าจำนวนมากใช้งานอยู่ และไม่มีทางที่จะนอนลงได้สักสองสามวินาทีอย่างแน่นอน และคุณจำเป็นต้องเปิดตัวการอัปเดตไลบรารี การแก้ไขข้อบกพร่อง หรือฟีเจอร์ใหม่เจ๋งๆ ในสถานการณ์ปกติ คุณจะต้องหยุดแอปพลิเคชัน เปลี่ยนใหม่ และเริ่มใหม่อีกครั้ง ในกรณีของนักเทียบท่า คุณสามารถแทนที่ก่อนแล้วจึงรีสตาร์ท แต่จะยังคงมีช่วงเวลาที่คำขอไปยังแอปพลิเคชันจะไม่ได้รับการประมวลผล เนื่องจากโดยปกติแล้วแอปพลิเคชันจะใช้เวลาพอสมควรในการโหลดครั้งแรก จะเกิดอะไรขึ้นถ้ามันสตาร์ทแต่กลับใช้การไม่ได้? นี่คือปัญหา มาแก้ไขกันด้วยวิธีที่น้อยที่สุดและสวยงามที่สุดเท่าที่จะเป็นไปได้

การปฏิเสธความรับผิด: บทความส่วนใหญ่นำเสนอในรูปแบบทดลอง - ในรูปแบบของการบันทึกเซสชันคอนโซล หวังว่านี่จะไม่ใช่เรื่องยากเกินไปที่จะเข้าใจ และโค้ดจะมีการจัดทำเอกสารในตัวเองอย่างเพียงพอ สำหรับบรรยากาศ ลองจินตนาการว่าสิ่งเหล่านี้ไม่ใช่แค่ตัวอย่างโค้ด แต่เป็นกระดาษจากโทรพิมพ์ "เหล็ก"

การปรับใช้สีน้ำเงิน-เขียวด้วยค่าแรงขั้นต่ำ

เทคนิคน่าสนใจที่ยากสำหรับ 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 ตอนจบก็จะมีแบบนี้อีก พิจารณาว่าในสถานที่เหล่านี้ กระดาษถูกตัดเพื่อส่งไปยังแผนกไฮไลต์ (โดยที่รหัสถูกลงสีด้วยมือด้วยปากกาเน้นข้อความ) จากนั้นชิ้นส่วนเหล่านี้ก็ติดกาวกลับเข้าไป

$ 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 в โหมดพร็อกซีย้อนกลับ. Reverse proxy ถูกสร้างขึ้นระหว่างไคลเอนต์และแอปพลิเคชัน ยอมรับคำขอจากไคลเอนต์และส่งต่อไปยังแอปพลิเคชันและส่งต่อการตอบสนองของแอปพลิเคชันไปยังไคลเอนต์

แอปพลิเคชันและพร็อกซีย้อนกลับสามารถเชื่อมโยงภายในนักเทียบท่าได้โดยใช้ เครือข่ายนักเทียบท่า. ดังนั้นคอนเทนเนอร์ที่มีแอปพลิเคชันจึงไม่จำเป็นต้องส่งต่อพอร์ตบนระบบโฮสต์ด้วยซ้ำ ซึ่งช่วยให้แอปพลิเคชันสามารถแยกออกจากภัยคุกคามภายนอกได้สูงสุด

หากพร็อกซีย้อนกลับอยู่บนโฮสต์อื่น คุณจะต้องละทิ้งเครือข่ายนักเทียบท่าและเชื่อมต่อแอปพลิเคชันกับพร็อกซีย้อนกลับผ่านเครือข่ายโฮสต์ โดยส่งต่อพอร์ต ปพลิเคชัน พารามิเตอร์ --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. Container Registry (มาตรฐานอุตสาหกรรม)
  2. เชื่อมต่อกับเซิร์ฟเวอร์ docker daemon จากโฮสต์อื่น:
    1. ตัวแปรสภาพแวดล้อม DOCKER_HOST.
    2. ตัวเลือกบรรทัดคำสั่ง -H หรือ --host เครื่องมือ docker-compose.
    3. docker context

วิธีที่สอง (มีสามตัวเลือกสำหรับการนำไปใช้) ได้อธิบายไว้อย่างดีในบทความ วิธีปรับใช้บนโฮสต์ Docker ระยะไกลด้วย docker-compose.

deploy.sh

ตอนนี้เรามารวบรวมทุกสิ่งที่เราทำด้วยตนเองไว้ในสคริปต์เดียว เริ่มจากฟังก์ชันระดับบนสุดก่อน แล้วค่อยดูฟังก์ชันอื่นๆ ที่ใช้ในฟังก์ชันนั้น

เทคนิคที่น่าสนใจ

  • ${parameter?err_msg} - หนึ่งในคาถาเวทย์มนตร์ทุบตี (aka การทดแทนพารามิเตอร์). ถ้า parameter ไม่ระบุ, เอาท์พุต err_msg และออกด้วยรหัส 1
  • docker --log-driver journald — ตามค่าเริ่มต้น ไดรเวอร์การบันทึกนักเทียบท่าจะเป็นไฟล์ข้อความที่ไม่มีการหมุนใดๆ ด้วยวิธีนี้ บันทึกจะเต็มดิสก์ทั้งหมดอย่างรวดเร็ว ดังนั้นสำหรับสภาพแวดล้อมการใช้งานจริง จำเป็นต้องเปลี่ยนไดรเวอร์ให้เป็นไดรเวอร์ที่ชาญฉลาดยิ่งขึ้น

สคริปต์การปรับใช้

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ซึ่งช่วยให้คุณสามารถลบแท็บทั้งหมดที่จุดเริ่มต้นได้ จริงอยู่ที่ราคาของการจัดรูปแบบที่ดีคือแท็บผสมกับช่องว่างซึ่งปัจจุบันถือเป็นรูปแบบที่แย่มาก แต่แท็บทุบตีบังคับและคงจะดีถ้ามีการจัดรูปแบบปกติในการกำหนดค่า 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_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/ ในเบราว์เซอร์ ให้รันการปรับใช้อีกครั้ง และตรวจสอบให้แน่ใจว่าการทำงานราบรื่นโดยการอัปเดตเพจตามซีดีระหว่างเลย์เอาต์

หลังเลิกงานอย่าลืมทำความสะอาด :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

ที่มา: will.com