一种让运行在CentOS下的.NET CORE的Web项目简单方便易部署的自动更新方案
<h2>一、项目运行环境</h2><p><span style="font-size: 16px">项目采用的是.NET5开发的Web系统,独立部署在省内异地多台CentOS服务器上,它们运行在甲方专网环境中(不接触互联网),甲方进行业务运作时(一段时间内)会要求异地服务器开机上线,同时要求我们在总部进行驻场运维和技术支持。</span></p>
<h2>二、自动更新需求</h2>
<p><span style="font-size: 16px">每年(次)的业务流程甲方会要求做出一些调整,要求在线的服务器可以自动更新。</span></p>
<p><span style="font-size: 16px">异地服务器对使用人员处于黑盒模式,同时项目可以运行在root权限下。</span></p>
<h2>三、自动升级方案对比</h2>
<p><span style="font-size: 16px">1、Jenkins+Gitlab+自动代码审查+人工代码审查+人工发布更新;</span></p>
<p><span style="font-size: 16px">2、Docker构建私有源,上游更新镜像后下游拉取新镜像启动;</span></p>
<p><span style="font-size: 16px">3、国人开发的AntDeploy(https://github.com/yuzd/AntDeploy)</span></p>
<ul dir="auto">
<li><span style="font-size: 14px">支持docker一键部署(支持netcore)</span></li>
<li><span style="font-size: 14px">支持iis一键部署(支持netcore和framework)</span></li>
<li><span style="font-size: 14px">支持windows服务一键部署(支持netcore和framework)</span></li>
<li><span style="font-size: 14px">支持linux服务一键部署(支持netcore)</span></li>
<li><span style="font-size: 14px">(支持增量发布)(支持一键回滚)(支持点火)(支持选择特定文件发布)(支持查看发布记录)</span></li>
<li><span style="font-size: 14px">支持脱离Visual Studio独立使用(跨平台支持windows系统和mac系统)</span></li>
<li><span style="font-size: 14px">支持Agent批量更新</span></li>
</ul>
<p><span style="font-size: 16px"> 4、国人开发的GeneralUpdate(https://gitee.com/Juster-zhu/GeneralUpdate)</span></p>
<p><span style="font-size: 14px">GeneralUpdate寓意为通用更新力致于成为全平台更新组件,包含常见个人、企业项目所需特性。并提供GeneralUpdate.PacketTool更新包打包工具。不过目前好像尚不支持.NET CORE的更新。<span style="color: rgba(255, 0, 0, 1)">据网友反馈,现在支持了.NET CORE的更新,请自行测试。</span></span></p>
<p><span style="font-size: 16px">因为我们只有在甲方业务运行期间才有服务器的使用权,异地部署的服务器的使用人员不掌握服务器密码和不具备Linux操作能力,同时由于种种原因我们不能也不方便在甲方内网中部署Jenkins和Docker服务,加上现有的几种自动更新(持续交付)方案对我们来说比较复杂,所以我们只有另辟蹊径寻找一种对我们来说简单实用易部署的方案。</span></p>
<h2>四、使用的自动升级方案</h2>
<p><span style="font-size: 16px">在我们开发另外一套客户端程序的时候,集成过一套自动更新组件(SimpleUpdater),简单描述一下就是它可以在客户端程序启动后到指定的http地址下载更新摘要文件和本地对比,如果远程版本高于本地版本则提示更新,更新过程就是从远程web服务器下载下来更新包解压后按照规则替换当前程序目录下的文件,从而实现更新的目的。</span></p>
<p><span style="font-size: 16px">基于这个流程,通过试验我们实现了这种基于HTTP服务器提供更新服务,可以让Web项目自动更新自己的解决方案。</span></p>
<p><span style="font-size: 16px">方案搭建起来相当简单,只需要架设一台提供HTTP服务的服务器(IIS、Nginx等都可以),然后Web服务器上放一个Json文件和更新压缩包(zip格式),Json文件中包含当前Web系统的版本号和下载地址。</span></p>
<p><span style="font-size: 16px">当异地服务器启动,使用人员访问系统的时候,后台会开一个进程通过HTTP请求的方式到升级服务器(appsettings.json中可配置地址)访问约定的Json文件,访问成功后解析得到服务器端的版本号,然后和本地版本号做对比,如果服务器版本号较新,就调用一个sh脚本下载Json文件中指定路径的更新包,</span></p>
<p><span style="font-size: 16px">sh脚本下载成功后停止当前Web系统,进行解压覆盖,覆盖完成后重新启动Web服务。(我们的Web项目采用的是Kestrel提供代理服务,supervisor进行守护。)</span></p>
<p><span style="font-size: 16px">这样就简单方便快捷的实现了基于CentOS的.NET CORE项目的自动更新。</span></p>
<h2>五、升级流程及代码</h2>
<p><span style="font-size: 16px">1、部署一台提供升级的服务器,提供HTTP服务,我们使用了Windows服务器+IIS模式,和甲方约定这台服务器的IP地址为升级专用,不分配给其它服务器使用。</span></p>
<p><span style="font-size: 16px">2、.NET CORE项目的appsettings.json中配置服务器IP地址。</span></p>
<p><span style="font-size: 16px">3、在项目的登录页后台代码中标识当前版本,同时在访问的时候开进程去访问升级服务器(前台访问后台检测升级接口,同时可以采用遮罩阻止用户登录),进行升级检测流程。</span></p>
<p><span style="font-size: 16px">后台代码:</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;collapse:true;;gutter:true;">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
}
</pre>
</div>
<p> </p>
<p><span style="font-size: 16px">前台检测更新代码(基于LayUI)</span></p>
<div class="cnblogs_code"><img id="code_img_closed_8d156f99-f03a-4e50-92df-b8968a24cbd7" class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img id="code_img_opened_8d156f99-f03a-4e50-92df-b8968a24cbd7" class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_8d156f99-f03a-4e50-92df-b8968a24cbd7" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <script type="text/javascript">
<span style="color: rgba(0, 128, 128, 1)"> 2</span> layui.use('layer'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span> $(document).ready(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> () {
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span> <span style="color: rgba(0, 0, 255, 1)">var</span> index =<span style="color: rgba(0, 0, 0, 1)">layer.open({
</span><span style="color: rgba(0, 128, 128, 1)"> 5</span> type: 1<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)"> 6</span> area: ['400px', '260px'<span style="color: rgba(0, 0, 0, 1)">],
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span> id: 'layer_update'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> resize: <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span> title: '正在检测更新'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">10</span> closeBtn: 0<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">11</span> shadeClose: <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">12</span> content: '<div class="layui-field-box">正在从服务器获取新版本信息,请勿重复刷新页面。</div>'
<span style="color: rgba(0, 128, 128, 1)">13</span> <span style="color: rgba(0, 0, 0, 1)"> });
</span><span style="color: rgba(0, 128, 128, 1)">14</span> <span style="color: rgba(0, 0, 0, 1)"> $.ajax({
</span><span style="color: rgba(0, 128, 128, 1)">15</span> type: 'POST'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">16</span> url: '/login/CheckUpdate'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">17</span> data:''<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">18</span> dataType: "json"<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">19</span> success: <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (result) {
</span><span style="color: rgba(0, 128, 128, 1)">20</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (result != '' && result != 'undefined'<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">21</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (result.code != "200"<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">22</span> <span style="color: rgba(0, 0, 0, 1)"> layer.alert(result.msg, {
</span><span style="color: rgba(0, 128, 128, 1)">23</span> title: '错误'
<span style="color: rgba(0, 128, 128, 1)">24</span> <span style="color: rgba(0, 0, 0, 1)"> });
</span><span style="color: rgba(0, 128, 128, 1)">25</span> <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">26</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">27</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">28</span> <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">29</span> layer.alert('返回数据错误。'<span style="color: rgba(0, 0, 0, 1)">, {
</span><span style="color: rgba(0, 128, 128, 1)">30</span> title: '错误'
<span style="color: rgba(0, 128, 128, 1)">31</span> <span style="color: rgba(0, 0, 0, 1)"> });
</span><span style="color: rgba(0, 128, 128, 1)">32</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">33</span> <span style="color: rgba(0, 0, 0, 1)"> },
</span><span style="color: rgba(0, 128, 128, 1)">34</span> complete: <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (xhr, ts) {
</span><span style="color: rgba(0, 128, 128, 1)">35</span> <span style="color: rgba(0, 0, 0, 1)"> layer.close(index);
</span><span style="color: rgba(0, 128, 128, 1)">36</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">37</span> <span style="color: rgba(0, 0, 0, 1)"> });
</span><span style="color: rgba(0, 128, 128, 1)">38</span> <span style="color: rgba(0, 0, 0, 1)"> });
</span><span style="color: rgba(0, 128, 128, 1)">39</span> </script></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p><span style="font-size: 16px">4、执行更新操作的sh脚本代码,脚本执行后面的第一个参数即为更新包下载地址,<span style="color: rgba(255, 0, 0, 1)">注意脚本不要用记事本编辑,最好使用vscode来编辑。</span></span></p>
<div class="cnblogs_code"><img id="code_img_closed_311c226b-c2f3-4465-a9ad-5f0fae934c8a" class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img id="code_img_opened_311c226b-c2f3-4465-a9ad-5f0fae934c8a" class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_311c226b-c2f3-4465-a9ad-5f0fae934c8a" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> #!/usr/bin/<span style="color: rgba(0, 0, 255, 1)">env</span><span style="color: rgba(0, 0, 0, 1)"> bash
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span>
<span style="color: rgba(0, 128, 128, 1)"> 3</span> source/etc/<span style="color: rgba(0, 0, 0, 1)">profile
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span> <span style="color: rgba(0, 0, 255, 1)">date</span>=$(<span style="color: rgba(0, 0, 255, 1)">date</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 128, 128, 1)"> 5</span>
<span style="color: rgba(0, 128, 128, 1)"> 6</span> <span style="color: rgba(0, 0, 255, 1)">if</span> [ -z $<span style="color: rgba(128, 0, 128, 1)">1</span> ];<span style="color: rgba(0, 0, 255, 1)">then</span>
<span style="color: rgba(0, 128, 128, 1)"> 7</span> <span style="color: rgba(0, 0, 255, 1)">echo</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">请添加下载路径</span><span style="color: rgba(128, 0, 0, 1)">"</span>
<span style="color: rgba(0, 128, 128, 1)"> 8</span> <span style="color: rgba(0, 0, 0, 1)"> exit
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span> <span style="color: rgba(0, 0, 255, 1)">fi</span>
<span style="color: rgba(0, 128, 128, 1)">10</span>
<span style="color: rgba(0, 128, 128, 1)">11</span> <span style="color: rgba(0, 0, 255, 1)">wget</span> -P /tmp $<span style="color: rgba(128, 0, 128, 1)">1</span> -O /tmp/www.<span style="color: rgba(0, 0, 255, 1)">zip</span>
<span style="color: rgba(0, 128, 128, 1)">12</span> <span style="color: rgba(0, 0, 255, 1)">if</span> [ $? -ne <span style="color: rgba(128, 0, 128, 1)">0</span> ] ;<span style="color: rgba(0, 0, 255, 1)">then</span>
<span style="color: rgba(0, 128, 128, 1)">13</span> <span style="color: rgba(0, 0, 255, 1)">echo</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">----------------下载失败---------------</span><span style="color: rgba(128, 0, 0, 1)">"</span> >> /root/<span style="color: rgba(0, 0, 0, 1)">update.log
</span><span style="color: rgba(0, 128, 128, 1)">14</span> <span style="color: rgba(0, 0, 0, 1)">exit
</span><span style="color: rgba(0, 128, 128, 1)">15</span> <span style="color: rgba(0, 0, 255, 1)">else</span>
<span style="color: rgba(0, 128, 128, 1)">16</span> <span style="color: rgba(0, 0, 255, 1)">echo</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">----------------下载成功---------------</span><span style="color: rgba(128, 0, 0, 1)">"</span> >> /root/<span style="color: rgba(0, 0, 0, 1)">update.log
</span><span style="color: rgba(0, 128, 128, 1)">17</span> <span style="color: rgba(0, 0, 255, 1)">fi</span>
<span style="color: rgba(0, 128, 128, 1)">18</span>
<span style="color: rgba(0, 128, 128, 1)">19</span> <span style="color: rgba(0, 0, 255, 1)">mkdir</span> /tmp/<span style="color: rgba(0, 0, 0, 1)">Webupdate
</span><span style="color: rgba(0, 128, 128, 1)">20</span> cd /tmp/<span style="color: rgba(0, 0, 0, 1)">Webupdate
</span><span style="color: rgba(0, 128, 128, 1)">21</span> <span style="color: rgba(0, 0, 255, 1)">unzip</span> /tmp/www.<span style="color: rgba(0, 0, 255, 1)">zip</span>
<span style="color: rgba(0, 128, 128, 1)">22</span>
<span style="color: rgba(0, 128, 128, 1)">23</span> <span style="color: rgba(0, 0, 255, 1)">if</span> [ $? -ne <span style="color: rgba(128, 0, 128, 1)">0</span> ] ;<span style="color: rgba(0, 0, 255, 1)">then</span>
<span style="color: rgba(0, 128, 128, 1)">24</span> <span style="color: rgba(0, 0, 255, 1)">echo</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">-----解压失败-----</span><span style="color: rgba(128, 0, 0, 1)">"</span> >> /root/<span style="color: rgba(0, 0, 0, 1)">update.log
</span><span style="color: rgba(0, 128, 128, 1)">25</span> <span style="color: rgba(0, 0, 255, 1)">rm</span> -rf /tmp/<span style="color: rgba(0, 0, 0, 1)">Webupdate
</span><span style="color: rgba(0, 128, 128, 1)">26</span> <span style="color: rgba(0, 0, 0, 1)">#WebName 是自己定义的用supervisor守护的web服务名称
</span><span style="color: rgba(0, 128, 128, 1)">27</span> <span style="color: rgba(0, 0, 0, 1)">supervisorctl start WebName
</span><span style="color: rgba(0, 128, 128, 1)">28</span> <span style="color: rgba(0, 0, 0, 1)">exit
</span><span style="color: rgba(0, 128, 128, 1)">29</span> <span style="color: rgba(0, 0, 255, 1)">else</span>
<span style="color: rgba(0, 128, 128, 1)">30</span> <span style="color: rgba(0, 0, 255, 1)">echo</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">-----解压成功-----</span><span style="color: rgba(128, 0, 0, 1)">"</span> >> /root/<span style="color: rgba(0, 0, 0, 1)">update.log
</span><span style="color: rgba(0, 128, 128, 1)">31</span> <span style="color: rgba(0, 0, 0, 1)">#WebName 是自己定义的用supervisor守护的web服务名称
</span><span style="color: rgba(0, 128, 128, 1)">32</span> <span style="color: rgba(0, 0, 0, 1)">supervisorctl stop WebName
</span><span style="color: rgba(0, 128, 128, 1)">33</span> #/usr/local/www/<span style="color: rgba(0, 0, 0, 1)"> 是web所在目录
</span><span style="color: rgba(0, 128, 128, 1)">34</span> <span style="color: rgba(0, 0, 255, 1)">mv</span> -f /tmp/Webupdate<span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> /usr/local/www/
</span><span style="color: rgba(0, 128, 128, 1)">35</span>
<span style="color: rgba(0, 128, 128, 1)">36</span> <span style="color: rgba(0, 128, 0, 1)">supervisorctl start WebName
</span><span style="color: rgba(0, 128, 128, 1)">37</span> <span style="color: rgba(0, 128, 0, 1)">supervisorctl status WebName
</span><span style="color: rgba(0, 128, 128, 1)">38</span> <span style="color: rgba(0, 128, 0, 1)">rm -rf /tmp/Webupdate
</span><span style="color: rgba(0, 128, 128, 1)">39</span> <span style="color: rgba(0, 128, 0, 1)">rm -rf /tmp/www.zip
</span><span style="color: rgba(0, 128, 128, 1)">40</span> <span style="color: rgba(0, 128, 0, 1)">fi</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p><span style="font-size: 16px">5、Json文件结构</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">{
"webver":1009,
"weburl":"http://192.168.12.25/web.zip"
}</span></pre>
</div>
<p><span style="font-size: 16px">6、后续</span></p>
<p><span style="font-size: 16px">因为mv命令在使用中没办法移动目录去覆盖程序目录,比如压缩包中有个 wwwroot/abc.js,在使用中发现mv命令好像没有办法把wwwroot/abc.js移动覆盖项目中的同路径文件,所以后续我们在更新脚本中使用了rsync命令,这个命令需要单独安装才可以使用。</span></p>
<p><span style="font-size: 16px">可以先去下载安装这个组件后把sh脚本中的mv命令换成rsync即可</span></p><br><br>
来源:https://www.cnblogs.com/wdw984/p/16426163.html
頁:
[1]