我們在佈署應用程式時,通常有幾種佈署方式,像是 Node.js 生態可能首選就會找 Forever, PM2, 之類的工具,對於更普遍的應用應用程式來說,可能就會考慮 Launchd, Systemctl, Nohup,本文章實作了 systemctl 的方式來作應用程式佈署的選項。
以往,我都是使用 nohup 來對應用程式進行佈署,特別是使用 golang 的應用程式,每一次做應用程式佈署,我就會用到這些指令:
- go build
- 先 check port usage 然後把 pid 刪掉: sudo lsof -i -P -n | grep LISTEN
- sudo kill -15 <pid>
- 然後再做應用程式佈署 nohup ./xxxxx &
以下是必要了解的方針:
- 寫一個 .service 檔案,讓 systemctl 可以跑
- 寫一個 deploy.sh 抽換檔案,給每次做 build 的時候執行 (CI/CD) 只需要執行這個腳本即可
*remark: 正式的 production deploy 程序可能會有 build.sh, deploy.sh 兩個檔案, build.sh 檔案會做 git checkout -- . 以及 git pull 然後進去目錄 build,而 deploy.sh 檔案則是會把 build 出來的 artifacts 檔案放到 system lib 目錄,而且做 release 版本分隔,最後重新啟動 systemctl service。 - 使用 journalctl -xefu <app_name> 來做 log 監看
首先,假設你已經有一個專案,叫做 test-gin:
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") }
在 go 端,這個專案是直接用 go mod 來跑,先取名專案叫做 test-gin,所以要先把這個專案放到 go path:
- 放到 ~/go/src/xxx.com/xxxuser/test-gin 目錄
- 進去目錄,做 go mod init
- 執行 deps 處理: go mod tidy
- 做 go build 編譯看看
- 直接跑 ./test-gin 看看
go 測試專案完成了,要開始進入佈署階段了,現在的終端機應該是要切到 golang 專案目錄下,繼續做這些事情。
現在要寫一個 systemctl 讀取使用的 .service 檔,叫做 test-gin.service:
[Unit] Description=Test Gin After=local-fs.target network.target [Service] Type=simple User=<你的帳號> # e.g: root Group=<group 或你的帳號> # e.g: root LimitNOFILE=65535 Restart=always SyslogIdentifier=test-gin # 系統 log 辨別的關鍵字 WorkingDirectory=/home/<你的帳號>/go/src/exp.com/<帳號>/test-gin # go path 的專案目錄 ExecStart=/home/<你的帳號>/go/src/exp.com/<帳號>/test-gin/test-gin # go 執行程式的位置 Envoronment=PORT=443 [Install] WantedBy=multi-user.target
範例:
[Unit] Description=Test Gin After=local-fs.target network.target [Service] Type=simple User=hpcslag Group=hpcslag LimitNOFILE=65535 Restart=always SyslogIdentifier=test-gin WorkingDirectory=/home/hpcslag/go/src/exp.com/hpcslag/test-gin ExecStart=/home/hpcslag/go/src/exp.com/hpcslag/test-gin/test-gin # 可以設定多個環境變數,程式可以直接吃到 Envoronment=PORT=443 # Envoronment=HTTPPORT=80 # Envoronment=GCPServiceAccountCredentials=~/xxxx.json [Install] WantedBy=multi-user.target
然後,把佈署這個 .service 腳本的整個命令寫成腳本,讓它可以自動在改完 .service 的時候,自動更新原來的 .service 檔案,方便除錯 .service:
sudo rm -rf /usr/lib/systemd/system/test-gin.service sudo cp test-gin.service /lib/systemd/system/. sudo chmod 755 /lib/systemd/system/test-gin.service sudo systemctl enable test-gin.service systemctl start test-gin systemctl status test-gin
現在,只要直接執行 sudo sh ./apply_new_service.sh,就會看到現在執行這個應用程式是否成功,不成功則要修正。
我的檔案叫做 ./deply.sh 是誤會,事實上應該改名叫做 apply_new_service.sh。
現在只要做 go build 之後,做 systemctl restart test-gin 就可以重啟。
然後,使用 journalctl -xefu test-gin 在另一個終端機監控 systemctl restart test-gin 這個指令執行,就可以看到變化。
但是,如果要變得更正式,恐怕把 runtime binary 放在這個專案目錄不太好,我們可以改用更正式的作法,區分 current 和 past release 檔案們。
現在,我希望分出 runtime 目錄還有留存一些歷史紀錄的 runtime 資料夾, runtime 目錄稱為 current,歷史 runtime 叫做 releases,裡面的目錄應該都是用 timestamp 來命名,反正可以依照日期新舊排序。
如果要這麼做,那就要一次連,專案佈署流程都一起做完,打造一條龍的服務。
現在,要直接改掉剛才的 service 檔案,因為如果新的方法套用上去,不能讓 systemctl 去跑 golang 專案目錄的 binary 檔案,而是要讀系統目錄的檔案。
要把剛才的 test-gin.service 改動為:
[Unit] Description=Test Gin After=local-fs.target network.target [Service] Type=simple User=hpcslag Group=hpcslag LimitNOFILE=65535 Restart=always SyslogIdentifier=test-gin # 目錄要改成放到 /usr/local/lib/________ 下 WorkingDirectory=/usr/local/lib/test-gin/current ExecStart=/usr/local/lib/test-gin/current/test-gin # 如果想要在執行程式的一開始先做 migrations: # ExecStartPre=/usr/local/lib/test-gin/current/test-gin migrate\ # 做完 migrations 再 start # ExecStart=/usr/local/lib/test-gin/current/test-gin start Envoronment=PORT=443 [Install] WantedBy=multi-user.target
然後,要寫一個 deploy.sh 檔案,可以把這個專案目錄的 build 檔案全部移動到 /usr/local/lib 然後重新啟動 systemctl 的 shell 檔案:
#!/usr/bin/env bash # 換專案只要改這裡即可 export ENV=production # 設定環境變數是 Production APP_NAME=test-gin # systemctl 名稱,要跟剛才 .service 一致 DEPLOY_USER=_______ # 設定 deploy 的名稱 (範例: 目前帳號或 root) APP_GROUP=_______ # 設定 app group 名稱 (範例:目前帳號或 root) ARTIFACT_ROOT="$PWD" # 要佈署的 build 目錄 (目前預設是專案目前這個目錄) # 結束自訂區域 DESTDIR="/usr/local/lib/$APP_NAME/" CURRENT_LINK="${DESTDIR}current" MAX_PAST_RELEASES=2 # Exit on errors set -e # set -o errexit -o xtrace . ~/.bashrc cd $ARTIFACT_ROOT CURDIR="$PWD" TIMESTAMP=$(date +%Y%m%d%H%M%S) RELEASE_DIR="${DESTDIR}releases/${TIMESTAMP}" mkdir -p "$RELEASE_DIR" cp -r "${ARTIFACT_ROOT}/." "${RELEASE_DIR}" sudo chown -R ${DEPLOY_USER}:${APP_GROUP} "${RELEASE_DIR}" sudo chmod -R 777 ${RELEASE_DIR} # given permission for application folder echo "Linking new release to $CURRENT_LINK" if [[ -L "$CURRENT_LINK" ]]; then rm "$CURRENT_LINK" fi ln -s "$RELEASE_DIR" "$CURRENT_LINK" # Ensure that app OS user can use group permissions to execute files in releases echo "Setting permissions for release executables" sudo chown -R $DEPLOY_USER:$APP_GROUP "$CURRENT_LINK" find -H $CURRENT_LINK -executable -type f -exec chmod g+x {} \; sudo /bin/systemctl restart "$APP_NAME" # Remove old releases, leaving only the most recent echo "Removing past releases" find ${DESTDIR}releases/ -maxdepth 1 -mindepth 1 -type d | sort -n | head -n -$MAX_PAST_RELEASES | xargs rm -rf echo "Deployment successful" exit 0
完成後,使用 sudo sh ./deploy.sh 執行,就會看到整個程式幫你移動到 current 去,而且,每次執行時,都會把上一個 deploy 好的版本,移動到 releases 目錄下,用 timestamp 分類,而現在的程式則是跑在 current 這個目錄下。
可以搭配 journalctl -xefu test-gin 跟執行 deploy.sh 為兩個不同的 terminal 視窗,執行 deploy.sh 就會看到隔壁視窗的佈署訊息改變了。
Remark 2021/09/19:
Vultr 的 Server 會有 SELinux 的問題導致你的程式開不起來,可以試著把它關閉。
以下的流程可以針對設定一個 Application 到新的主機上的 install.sh 腳本:
#!/bin/sh set -e # stop selinux https://blog.yowko.com/linux-service-status-203/ getenforce setenforce 0 && sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config sudo rm -rf /usr/lib/systemd/system/test-gin.service sudo cp test-gin.service /lib/systemd/system/. sudo chmod 755 /lib/systemd/system/test-gin.service systemctl daemon-reload # 更新 .service 檔案就要這麼做 sudo systemctl enable test-gin.service systemctl start test-gin systemctl status test-gin
然後可以在程式目錄下寫一個 Makefile:
deploy:
go build
systemctl restart test-gin
journalctl -xefu test-gin
沒有留言:
張貼留言