專案採用的是.NET5開發的Web系統,獨立部署在省內異地多臺CentOS伺服器上,它們執行在甲方專網環境中(不接觸網際網路),甲方進行業務運作時(一段時間內)會要求異地伺服器開機上線,同時要求我們在總部進行駐場運維和技術支援。
每年(次)的業務流程甲方會要求做出一些調整,要求線上的伺服器可以自動更新。
異地伺服器對使用人員處於黑盒模式,同時專案可以執行在root許可權下。
1、Jenkins+Gitlab+自動程式碼審查+人工程式碼審查+人工釋出更新;
2、Docker構建私有源,上游更新映象後下遊拉取新映象啟動;
3、國人開發的AntDeploy(https://github.com/yuzd/AntDeploy)
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>
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
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即可