使用 GitHub Action 實作零停機部署

在之前的文章中 (使用 GitHub Action 來做簡單的 CI/CD),我們簡單的介紹了如何使用 Github Action 完成一個簡單的 CI/CD 流程,將一個 Laravel 應用程式部署到遠端的正式環境。

CI 的部分使用熱門的 PHPUnit 測試框架來進行測試,而在 CD 的部分,是使用 SSH 連線至正式環境伺服器並依序執行下列指令來更新應用程式:

  1. 使用 git pull 更新程式碼。
  2. 使用 composer installnpm install 更新依賴套件。
  3. 最後使用 npm run prod 打包前端資源。

雖然自動部署的目的有達成,但這種更新方式其實存在著一些問題。

  • 為了更新依賴套件,需要安裝如 composernpm 這些對正式環境來說不是非常必要的工具。
  • 不論是 git pullnpm installcomposer install 指令,都會需要花上一些時間從網路上下載套件來更新,如果在更新的過程中,用戶執行的操作剛好需要正在更新的套件,就有可能因為依賴缺失,而導致應用程式出現錯誤。

假設用戶的操作需要執行 A、B 與 C 三個程式碼檔案,A 與 B 更新好了,但 C 可能因為網路問題尚未完成更新,那麼用戶執行操作時就有機會遇到問題。

本篇文章會來簡單介紹如何使用 GitHub Action 來實作零停機部署 (Zero Downtime Deployment),讓應用程式即使在更版階段,依舊能提供服務且不中斷。

使用 GitHub Action 實作零停機部署


假設我們有一個 Laravel 應用應用程式 app 放在 /var/www/ 底下,web server 使用 Nginx。

如果要實作零停機部署,我們需要修改部署的流程:

  1. 在 CD 的過程中新增一個步驟,此步驟會下載應用程式的依賴套件與打包前端資源,最後將應用程式進行壓縮生成一份壓縮檔案存放起來。
  2. 將縮檔案上傳至正式環境上並解壓縮。
  3. 建立一個軟連結,並指向解壓縮後的應用程式檔案的路徑,Nginx 的 root 設定會指向這個軟連結。

為了符合這個流程,我們需要調整  app 資料夾底下的結構。

artifacts/
    2022_04_28_12_00_23_app.tar.gz
    ...

release/
    2022_04_28_12_00_23_app/
    ...

current -> /var/www/app/release/2022_04_28_12_00_23_app

app 資料夾底下主要會有兩個資料夾一個軟連結,其用途為:

  • artifacts/:應用程式會在執行  CI/CD 的過程中下載依賴套件與打包前端資源,最後進行壓縮並生成一份壓縮檔,完整應用程式的壓縮檔會放置在此資料夾內。
  • release/:將解壓縮後的應用程式放進這個資料夾中。
  • current:最新版本應用程式的軟連結。

因爲資料夾結構的調整,我們需要修改原本 Nginx 中 root 的設定,改為指向軟連結。

將原本的 root 設定:

server {
    listen 80;
    listen [::]:80;
    server_name app.com;
    root /var/www/app/public;

    ...
}

修改為指向軟連結:

server {
    listen 80;
    listen [::]:80;
    server_name app.com;
    root /var/www/app/current/public;

    ...
}

接下來我們在 workflow 設定檔案中加上一個新的步驟 create-deployment-artifacts

name: cicd
on:
  push:
    branches:
      - main

jobs:
  test:
    # ...

  # 準備要部署的應用程式檔案
  create-deployment-artifacts:
    runs-on: ubuntu-latest
    name: Create deployment artifacts
    needs: tests
    steps:
      - uses: actions/checkout@v3

      - name: Complie CSS and JavaScript
        run: |
          npm install
          npm run prod

      - name: Configure PHP 8.1
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.1
          extensions: bcmatch, ctype, curl, dom, fileinfo, json, mbstring, openssl, pcre, pdo, tokenizer, xml, redis
          coverage: none

      - name: Install Composer dependencies
        run: composer install --no-progress --prefer-dist --optimize-autoloader --no-dev

      # 將下載好依賴套件與打包好前端資源的應用程式進行壓縮,生成一個壓縮檔案
      # 在 tar 壓縮指令中可以使用 --exclude 排除 .git/ 與 node_module/ 這些在正式環境不需要的資料夾,減少檔案的大小
      - name: Create deployment artifact
        run: tar -czf "${{ github.sha }}.tar.gz" --exclude=.git --exclude=node_modules *

      # 使用官方 action 將上一個步驟的應用程式壓縮檔上傳到 GitHub Action 上
      - name: Store artifact for distribution
        uses: actions/upload-artifact@v3
        with:
          # 在下一個步驟中,我們可以使用此名稱下載壓縮檔案
          name: app-build
          # ${{ github.sha }} 可以產生一組雜湊值
          # 整個 workflow 的 ${{ github.sha }} 都會是相同的
          path: ${{ github.sha }}.tar.gz

最後新增一個步驟 deploy-to-server,把完整的應用程式上傳至正式環境,並進行部署。

name: cicd
on:
  push:
    branches:
      - main

jobs:
  tests:
    # ...

  create-deployment-artifacts:
    # ...

  deploy-to-server:
    runs-on: ubuntu-latest
    name: Deploy to server
    # 需要完成 create-deployment-artifacts 才能執行此 job
    needs: create-deployment-artifacts
    env:
      # 使用環境變數設定應用程式資料夾在正式環境上的路徑
      ARTIFACTS_PATH: /var/www/app/artifacts
      RELEASE_PATH: /var/www/app/release
      APP_SYMBOLIC_LINK_PATH: /var/www/app/current
    steps:
      - uses: actions/checkout@v3

      # 使用官方套件下載剛剛打包好的應用程式壓縮檔
      - uses: actions/download-artifact@v3
        with:
          name: app-build

      # 將壓縮檔案上傳到指定的資料夾
      - name: Upload
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          username: ${{ secrets.SSH_USERNAME }}
          source: ${{ github.sha }}.tar.gz
          # 可以使用 ${{ env.變數名稱 }} 存取剛剛設定的環境變數
          target: ${{ env.ARTIFACTS_PATH }}

      - name: Active release
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_HOST }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          username: ${{ secrets.SSH_USERNAME }}
          script: |

            # 將檔案解壓縮到 release 資料夾底下
            mkdir ${{ env.RELEASE_PATH }}/${{ github.sha }}
            tar -xzf ${{ env.ARTIFACTS_PATH }}/${{ github.sha }}.tar.gz -C ${{ env.RELEASE_PATH }}/${{ github.sha }}

            # 產生設定檔案 .env
            printf "%s" '${{ secrets.BLOG_ENV }}' > "${{ env.RELEASE_PATH }}/${{ github.sha }}/.env"

            # 改變 current 的軟連結,使其指向 release 資料夾底下最新的應用程式資料夾
            sudo -u www-data ln -sfn ${{ env.RELEASE_PATH }}/${{ github.sha }} ${{ env.APP_SYMBOLIC_LINK_PATH }}

            # 調整應用程式資料夾的權限
            sudo chown -R www-data:www-data ${{ env.APP_PATH }}/

            # 生成 Laravel 快取檔案
            cd ${{ env.APP_PATH }}
            sudo -u www-data php artisan view:cache
            sudo -u www-data php artisan config:cache
            sudo -u www-data php artisan route:cache

            # 只保留前六個最新的壓縮檔與解壓縮檔案
            cd ${{ env.ARTIFACTS_PATH }} && ls -t -1 | tail -n +6 | xargs sudo rm -rf
            cd ${{ env.RELEASE_PATH }} && ls -t -1 | tail -n +6 | xargs sudo rm -rf

大功告成,可以看到應用程式切換到新版本的方式,就只是更換軟連結指向的資料夾而已,非常快速!

因為有保留舊版本的檔案,如果最新版本的應用程式有 Bug,你也可以快速的切回舊版本。

這種在應用程式完全準備好之後才進行版本更新的方式又稱為原子部署(Atomic Deployment),是實現零停機部署的一種方式。

參考資料


sharkHead
written by
sharkHead

後端工程師, PHP 基金會每月 5 鎂小額贊助人 稍微擅長 PHP、Python 與 Google Search,偶爾寫寫 TypeScript 對於逗號後面必須加空格有著絕對的堅持