2021年8月21日 星期六

Application deployment with systemctl

 我們在佈署應用程式時,通常有幾種佈署方式,像是 Node.js 生態可能首選就會找 Forever, PM2, 之類的工具,對於更普遍的應用應用程式來說,可能就會考慮 Launchd, Systemctl, Nohup,本文章實作了 systemctl 的方式來作應用程式佈署的選項。


以往,我都是使用 nohup 來對應用程式進行佈署,特別是使用 golang 的應用程式,每一次做應用程式佈署,我就會用到這些指令:


  1. go build
  2. 先 check port usage 然後把 pid 刪掉: sudo lsof -i -P -n | grep LISTEN
  3.  sudo kill -15 <pid>
  4. 然後再做應用程式佈署 nohup ./xxxxx &

可是每次進行這樣的步驟,會有很大的風險,基本上 downtime 控制的不是很好,寫成腳本可能也跟這個 proejct directory 挷在一起。

寫成腳本感覺是好了一些,但是關鍵點就是只使用 nohup,他的 log 是存在 file 中,其實是蠻陽春的 deamon 工具。

我們其實可以善用 Linux 系統的 process 管理工具,把 app deploy 的更 general 一些。


以下是必要了解的方針:

  1. 寫一個 .service 檔案,讓 systemctl 可以跑
  2. 寫一個 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。

  3. 使用 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:

  1.  放到 ~/go/src/xxx.com/xxxuser/test-gin 目錄
  2.  進去目錄,做 go mod init
  3.  執行 deps 處理: go mod tidy
  4.  做 go build 編譯看看
  5.  直接跑 ./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


沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014