使用 GitHub Action 實作零停機部署
在之前的文章中 (使用 GitHub Action 來做簡單的 CI/CD),我們簡單的介紹了如何使用 Github Action 完成一個簡單的 CI/CD 流程,將一個 Laravel 應用程式部署到遠端的正式環境。
CI 的部分使用熱門的 PHPUnit 測試框架來進行測試,而在 CD 的部分,是使用 SSH 連線至正式環境伺服器並依序執行下列指令來更新應用程式:
- 使用
git pull
更新程式碼。 - 使用
composer install
與npm install
更新依賴套件。 - 最後使用
npm run prod
打包前端資源。
雖然自動部署的目的有達成,但這種更新方式其實存在著一些問題。
- 為了更新依賴套件,需要安裝如
composer
與npm
這些對正式環境來說不是非常必要的工具。 - 不論是
git pull
、npm install
與composer install
指令,都會需要花上一些時間從網路上下載套件來更新,如果在更新的過程中,用戶執行的操作剛好需要正在更新的套件,就有可能因為依賴缺失,而導致應用程式出現錯誤。
假設用戶的操作需要執行 A、B 與 C 三個程式碼檔案,A 與 B 更新好了,但 C 可能因為網路問題尚未完成更新,那麼用戶執行操作時就有機會遇到問題。
本篇文章會來簡單介紹如何使用 GitHub Action 來實作零停機部署 (Zero Downtime Deployment),讓應用程式即使在更版階段,依舊能提供服務且不中斷。
使用 GitHub Action 實作零停機部署
假設我們有一個 Laravel 應用應用程式 app
放在 /var/www/
底下,web server 使用 Nginx。
如果要實作零停機部署,我們需要修改部署的流程:
- 在 CD 的過程中新增一個步驟,此步驟會下載應用程式的依賴套件與打包前端資源,最後將應用程式進行壓縮生成一份壓縮檔案存放起來。
- 將縮檔案上傳至正式環境上並解壓縮。
- 建立一個軟連結,並指向解壓縮後的應用程式檔案的路徑,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 資料夾底下
sudo mkdir ${{ env.RELEASE_PATH }}/${{ github.sha }}
sudo tar -xzf ${{ env.ARTIFACTS_PATH }}/${{ github.sha }}.tar.gz -C ${{ env.RELEASE_PATH }}/${{ github.sha }}
# 產生設定檔案 .env
sudo echo "${{ secrets.BLOG_ENV }}" | sudo tee -a ${{ env.RELEASE_PATH }}/${{ github.sha }}/.env
# 改變 current 的軟連結,使其指向 release 資料夾底下最新的應用程式資料夾
sudo ln -sfn ${{ env.RELEASE_PATH }}/${{ github.sha }} ${{ env.APP_PATH }}
# 調整應用程式資料夾的權限
sudo chown -R www-data:www-data ${{ env.APP_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
# 重新啟動所有 supervisor 程序
sudo supervisorctl restart all
# 只保留前六個最新的壓縮檔與解壓縮檔案
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),是實現零停機部署的一種方式。