寫一個自動部署 Web 應用的腳本
在現代 DevOps 實踐中,自動化部署不僅能減少人為錯誤,更能顯著提升開發與營運的效率。對於剛接觸 Linux 自動化的小白或資深管理員而言,撰寫一個健壯的 Shell 腳本來自動化 Web 應用(例如基於 Nginx 和 Node.js 或 Python Flask 的應用)的部署流程,是一個極佳的入門專案。
今天,我們將撰寫一個通用型的自動部署腳本。這個腳本將模擬典型的 CI/CD 流程:拉取最新程式碼、安裝依賴、停止舊服務、重新啟動新服務,並記錄日誌。我們將以 Ubuntu 22.04 或 Debian 12 為基礎環境進行示範。
腳本設計思路
一個可靠的部署腳本應該包含以下核心步驟:
- 環境檢查:確認必要工具(如
git,systemctl)是否存在。 - 變數配置:將易變的路徑、專案名稱等參數化,方便重複使用。
- 版本控制:拉取最新程式碼並記錄當前 Git Commit ID。
- 依賴安裝:根據應用類型執行
npm install或pip install。 - 服務重啟:安全地重啟應用服務並檢查狀態。
- 錯誤處理與日誌:確保任何失敗都能被捕捉並記錄,避免系統處於不一致狀態。
完整部署腳本範例
以下是一個名為 deploy.sh 的完整腳本。請將其儲存至你的專案根目錄,並賦予執行權限。
#!/bin/bash
# 自動部署 Web 應用腳本
# 適用於 Ubuntu 22.04 / Debian 12
set -e # 遇到錯誤立即退出
# --- 配置區 ---
APP_NAME="my-web-app"
DEPLOY_DIR="/opt/${APP_NAME}"
REPO_URL="https://github.com/example/${APP_NAME}.git"
SERVICE_USER="www-data"
LOG_FILE="/var/log/${APP_NAME}_deploy.log"
# --- 輔助函數 ---
log() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${timestamp}] $1" | tee -a "${LOG_FILE}"
}
error_exit() {
log "ERROR: $1"
exit 1
}
# --- 主要邏輯 ---
log "開始部署 ${APP_NAME} ..."
# 1. 環境檢查
if ! command -v git &> /dev/null; then
error_exit "未找到 git,請先執行 sudo apt install git"
fi
if ! command -v systemctl &> /dev/null; then
error_exit "未找到 systemctl,請確認系統為 systemd 環境"
fi
# 2. 建立目錄結構
if [ ! -d "${DEPLOY_DIR}" ]; then
log "建立部署目錄: ${DEPLOY_DIR}"
sudo mkdir -p "${DEPLOY_DIR}"
sudo chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DEPLOY_DIR}"
fi
# 3. 拉取最新程式碼
cd "${DEPLOY_DIR}" || error_exit "無法切換至目錄 ${DEPLOY_DIR}"
if [ -d ".git" ]; then
log "拉取最新程式碼..."
sudo -u "${SERVICE_USER}" git pull origin main || error_exit "Git pull 失敗"
else
log "初始化 Git 倉庫..."
sudo -u "${SERVICE_USER}" git init
sudo -u "${SERVICE_USER}" git remote add origin "${REPO_URL}"
sudo -u "${SERVICE_USER}" git fetch origin
sudo -u "${SERVICE_USER}" git checkout -b main origin/main || error_exit "Git checkout 失敗"
fi
# 記錄當前部署的版本
CURRENT_COMMIT=$(sudo -u "${SERVICE_USER}" git rev-parse --short HEAD)
log "當前部署版本: ${CURRENT_COMMIT}"
# 4. 安裝依賴 (以 Node.js 為例,請依實際應用調整)
log "安裝依賴..."
# 假設專案中有 package.json
if [ -f "package.json" ]; then
sudo -u "${SERVICE_USER}" npm install --production || error_exit "npm install 失敗"
elif [ -f "requirements.txt" ]; then
sudo -u "${SERVICE_USER}" pip install -r requirements.txt || error_exit "pip install 失敗"
fi
# 5. 重新啟動服務
log "重啟服務 ${APP_NAME} ..."
sudo systemctl restart "${APP_NAME}" || error_exit "服務重啟失敗"
# 6. 驗證服務狀態
sleep 2
if sudo systemctl is-active --quiet "${APP_NAME}"; then
log "部署成功!服務 ${APP_NAME} 正在運行。"
else
error_exit "服務 ${APP_NAME} 啟動後未處於 active 狀態,請檢查日誌。"
fi
log "部署流程結束。"
如何使用與設定
-
建立系統服務檔案: 為了讓腳本能透過
systemctl管理應用,你需要建立一個 systemd unit 檔案。例如/etc/systemd/system/my-web-app.service:[Unit] Description=My Web Application After=network.target [Service] User=www-data WorkingDirectory=/opt/my-web-app ExecStart=/usr/bin/node /opt/my-web-app/index.js Restart=always Environment=NODE_ENV=production [Install] WantedBy=multi-user.target -
賦予執行權限:
chmod +x deploy.sh -
執行部署: 由於腳本中使用了
sudo來操作系統資源,建議以 root 或具有 sudo 權限的使用者執行:sudo ./deploy.sh
常見問題與解決方案
1. 權限不足導致部署失敗
問題:腳本在 git pull 或 npm install 時報錯 Permission denied。
原因:Web 應用通常由非 root 使用者(如 www-data 或 node)運行,但部署過程可能需要寫入目錄或執行系統級命令。
解決方案:
- 確保腳本中所有寫入目錄的操作都使用
sudo -u <service_user>來模擬服務使用者。 - 在部署前,確認
/opt/my-web-app的擁有者是正確的服務使用者,而非 root。若為 root 擁有,後續git pull可能會因為權限衝突而失敗。
2. 服務重啟後立即失敗
問題:腳本顯示 Restart=always 成功,但 systemctl status 顯示服務處於 failed 狀態。
原因:應用啟動需要時間初始化(如連接資料庫、載入模型),腳本中的 sleep 2 可能不足,或者應用本身配置有誤。
解決方案:
- 在腳本中增加更嚴格的檢查機制,例如使用
systemctl is-active輪詢多次。 - 檢查應用日誌:
journalctl -u my-web-app -n 50 --no-pager,確認錯誤原因通常是環境變數缺失或資料庫連線超時。
小結
透過撰寫這個自動部署腳本,我們不僅實現了程式碼的自動化更新,更建立了一套標準化的部署規範。這個腳本結合了 set -e 的錯誤處理、日誌記錄以及權限管理,是構建更複雜 CI/CD 管道(如使用 GitHub Actions 或 Jenkins)的基礎。
建議後續可以將此腳本整合進 Git Hook 中,當程式碼推送到主分支時自動觸發部署,進一步實現真正的持續整合與持續部署(CI/CD)。記住,在自動化之前,先確保手動流程的穩定性,這是 Linux 系統管理的重要原則。