一種讓執行在CentOS下的.NET CORE的Web專案簡單方便易部署的自動更新方案

2022-06-30 12:01:53

一、專案執行環境

專案採用的是.NET5開發的Web系統,獨立部署在省內異地多臺CentOS伺服器上,它們執行在甲方專網環境中(不接觸網際網路),甲方進行業務運作時(一段時間內)會要求異地伺服器開機上線,同時要求我們在總部進行駐場運維和技術支援。

二、自動更新需求

每年(次)的業務流程甲方會要求做出一些調整,要求線上的伺服器可以自動更新。

異地伺服器對使用人員處於黑盒模式,同時專案可以執行在root許可權下。

三、自動升級方案對比

1、Jenkins+Gitlab+自動程式碼審查+人工程式碼審查+人工釋出更新;

2、Docker構建私有源,上游更新映象後下遊拉取新映象啟動;

3、國人開發的AntDeploy(https://github.com/yuzd/AntDeploy)

  • 支援docker一鍵部署(支援netcore)
  • 支援iis一鍵部署(支援netcore和framework)
  • 支援windows服務一鍵部署(支援netcore和framework)
  • 支援linux服務一鍵部署(支援netcore)
  • (支援增量釋出)(支援一鍵回滾)(支援點火)(支援選擇特定檔案發布)(支援檢視釋出記錄)
  • 支援脫離Visual Studio獨立使用(跨平臺支援windows系統和mac系統)
  • 支援Agent批次更新

 4、國人開發的GeneralUpdate(https://gitee.com/Juster-zhu/GeneralUpdate)

GeneralUpdate寓意為通用更新力致於成為全平臺更新元件,包含常見個人、企業專案所需特性。並提供GeneralUpdate.PacketTool更新包打包工具。不過目前好像尚不支援.NET CORE的更新。

因為我們只有在甲方業務執行期間才有伺服器的使用權,異地部署的伺服器的使用人員不掌握伺服器密碼和不具備Linux操作能力,同時由於種種原因我們不能也不方便在甲方內網中部署Jenkins和Docker服務,加上現有的幾種自動更新(持續交付)方案對我們來說比較複雜,所以我們只有另闢蹊徑尋找一種對我們來說簡單實用易部署的方案。

四、使用的自動升級方案

在我們開發另外一套使用者端程式的時候,整合過一套自動更新元件(SimpleUpdater),簡單描述一下就是它可以在使用者端程式啟動後到指定的http地址下載更新摘要檔案和本地對比,如果遠端版本高於本地版本則提示更新,更新過程就是從遠端web伺服器下載下來更新包解壓後按照規則替換當前程式目錄下的檔案,從而實現更新的目的。

基於這個流程,通過試驗我們實現了這種基於HTTP伺服器提供更新服務,可以讓Web專案自動更新自己的解決方案。

方案搭建起來相當簡單,只需要架設一臺提供HTTP服務的伺服器(IIS、Nginx等都可以),然後Web伺服器上放一個Json檔案和更新壓縮包(zip格式),Json檔案中包含當前Web系統的版本號和下載地址。

當異地伺服器啟動,使用人員存取系統的時候,後臺會開一個程序通過HTTP請求的方式到升級伺服器(appsettings.json中可設定地址)存取約定的Json檔案,存取成功後解析得到伺服器端的版本號,然後和本地版本號做對比,如果伺服器版本號較新,就呼叫一個sh指令碼下載Json檔案中指定路徑的更新包,

sh指令碼下載成功後停止當前Web系統,進行解壓覆蓋,覆蓋完成後重新啟動Web服務。(我們的Web專案採用的是Kestrel提供代理服務,supervisor進行守護。)

這樣就簡單方便快捷的實現了基於CentOS的.NET CORE專案的自動更新。

五、升級流程及程式碼

1、部署一臺提供升級的伺服器,提供HTTP服務,我們使用了Windows伺服器+IIS模式,和甲方約定這臺伺服器的IP地址為升級專用,不分配給其它伺服器使用。

2、.NET CORE專案的appsettings.json中設定伺服器IP地址。

3、在專案的登入頁後臺程式碼中標識當前版本,同時在存取的時候開程序去存取升級伺服器(前臺存取後臺檢測升級介面,同時可以採用遮罩阻止使用者登入),進行升級檢測流程。

後臺程式碼:

public class LoginController : Controller
    {
        private readonly ILogger<LoginController> _logger;
        private readonly int _webVer = 1001;//當前執行中的系統版本號
        public LoginController(ILogger<LoginController> logger)
        {
            _logger = logger;
        }

        public IActionResult Login()
        {        
            //其它業務程式碼   
            return View();
        } 
        #region 檢測更新
        public async Task<JsonResult> CheckUpdateAsync()
        {
            await Task.Delay(1000);
            //AppSettings為讀取appsettings.json中相關設定的實體類,這裡是虛擬碼
            if (!AppSettings.ContainsKey("key1") || string.IsNullOrWhiteSpace(AppSettings["key1"])) return new JsonResult(new { code = 300, msg = "未能獲取到更新設定" });

            try
            {
                var restClient = new RestClient($"{AppSettings["key1"]}/update.json");
                var restRequest = new RestRequest("", Method.GET);

                var cancelToken = new CancellationTokenSource(TimeSpan.FromSeconds(15));

                var response = await restClient.ExecuteGetAsync(restRequest, cancelToken.Token);
                if (response.StatusCode != HttpStatusCode.OK)
                {
                    _logger.LogError($"檢測升級失敗,伺服器狀態:{response.StatusCode}");
                    return new JsonResult(new { code = 300, msg = "檢測升級失敗" });
                }

                var responseContent = response.Content;
                if (string.IsNullOrWhiteSpace(responseContent))
                {
                    _logger.LogInformation("更新內容為空");
                    return new JsonResult(new { code = 300, msg = "升級更新內容為空" });
                }

                var content = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseContent);
                if (content == null)
                {
                    _logger.LogInformation("更新內容序列化後為空");
                    return new JsonResult(new { code = 300, msg = "更新內容序列化後為空" });
                }
                #region 喚醒更新指令碼
                var argument = content;

                if (!argument.ContainsKey("webver") || !argument.ContainsKey("weburl"))
                {
                    _logger.LogError("檢測升級失敗,升級檔案中沒有獲取到必須的Web專案。");
                    return new JsonResult(new { code = 300, msg = "升級項中不包含本專案" }); ;
                }
                await Task.Factory.StartNew(async () =>
                {
                    TryParse(argument["webver"], out var ver);
                    if (ver > 0 && ver > _webVer)
                    {
                        await Task.Delay(1000);
                        var sh = $@"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}update.sh";
                        _logger.LogError("檢測到升級條件,開始喚醒升級指令碼");
                        try
                        {
                            await Process.Start(sh, $" {argument["weburl"]}")?.WaitForExitAsync()!;
                        }
                        catch (Exception e)
                        {
                            _logger.LogError($"更新指令碼執行失敗:{e}");
                        }
                    }
                }, TaskCreationOptions.LongRunning);
                #endregion

            }
            catch (Exception e)
            {
                _logger.LogError($"載入更新資料失敗:{e}");
                return new JsonResult(new { code = 300, msg = "載入更新資料失敗" });
            }

            return new JsonResult(new { code = 200, msg = "檢測成功" });
        }
        #endregion
    }

  

前臺檢測更新程式碼(基於LayUI)

 1     <script type="text/javascript">
 2         layui.use('layer');
 3         $(document).ready(function () {
 4             var index =  layer.open({
 5                 type: 1,
 6                 area: ['400px', '260px'],
 7                 id: 'layer_update',
 8                 resize: false,
 9                 title: '正在檢測更新',
10                 closeBtn: 0,
11                 shadeClose: false,
12                 content: '<div class="layui-field-box">正在從伺服器獲取新版本資訊,請勿重複重新整理頁面。</div>'
13             });
14             $.ajax({
15                 type: 'POST',
16                 url: '/login/CheckUpdate',
17                 data:'',
18                 dataType: "json",
19                 success: function (result) {
20                     if (result != '' && result != 'undefined') {
21                         if (result.code != "200") {
22                             layer.alert(result.msg, {
23                                 title: '錯誤'
24                             });
25                             return;
26                         }
27                     }
28                     else {
29                         layer.alert('返回資料錯誤。', {
30                             title: '錯誤'
31                         });
32                     }
33                 },
34                 complete: function (xhr, ts) {
35                     layer.close(index);
36                 }
37             });
38         });        
39     </script>
View Code

4、執行更新操作的sh指令碼程式碼,指令碼執行後面的第一個引數即為更新包下載地址,注意指令碼不要用記事本編輯,最好使用vscode來編輯。

 1 #!/usr/bin/env bash
 2 
 3 source  /etc/profile
 4 date=$(date)
 5 
 6 if [ -z $1 ];then
 7     echo "請新增下載路徑"
 8     exit
 9 fi
10 
11 wget -P /tmp $1 -O /tmp/www.zip
12 if [ $? -ne 0 ] ;then
13 echo "----------------下載失敗---------------" >> /root/update.log
14 exit
15 else
16 echo "----------------下載成功---------------" >> /root/update.log
17 fi
18 
19 mkdir /tmp/Webupdate
20 cd /tmp/Webupdate 
21 unzip /tmp/www.zip
22 
23 if [ $? -ne 0 ] ;then
24 echo "-----解壓失敗-----" >> /root/update.log
25 rm -rf /tmp/Webupdate
26 #WebName 是自己定義的用supervisor守護的web服務名稱
27 supervisorctl start WebName
28 exit
29 else
30 echo "-----解壓成功-----" >> /root/update.log
31 #WebName 是自己定義的用supervisor守護的web服務名稱
32 supervisorctl stop WebName
33 #/usr/local/www/ 是web所在目錄
34 mv -f /tmp/Webupdate/* /usr/local/www/
35 
36 supervisorctl start WebName
37 supervisorctl status WebName
38 rm -rf /tmp/Webupdate
39 rm -rf /tmp/www.zip
40 fi
View Code

5、Json檔案結構

{
    "webver":1009,
    "weburl":"http://192.168.12.25/web.zip"
}

6、後續

因為mv命令在使用中沒辦法移動目錄去覆蓋程式目錄,比如壓縮包中有個 wwwroot/abc.js,在使用中發現mv命令好像沒有辦法把wwwroot/abc.js移動覆蓋專案中的同路徑檔案,所以後續我們在更新指令碼中使用了rsync命令,這個命令需要單獨安裝才可以使用。

可以先去下載安裝這個元件後把sh指令碼中的mv命令換成rsync即可