基于.Net Core的项目部署升级脚本的设计和实现
团队早期后台开发都是基于.Net技术,目前都转向.Net Core,发布到Linux服务器上为主。
1. 问题描述和背景
我们通常部署环境分三种:
- 开发环境:开发人员随意部署和调试,测试人员不会接触这个环境。
- UAT环境:在我们团队就是指集成和测试环境,部署人员安装系统,测试人员在这个环境上测试,开发人员不能接触服务器。
- 生产环境:只能是部署人员安装和部署。
目前要解决的问题是,尽可能减少部署人员安装项目和升级项目的手动操作,减少出错可能。另外EF Core不再支持代码级别的动态同步数据库,对于部署人员增加了一个手动同步数据库的操作又增加了出错的风险。
2. 设计
从而设计一个部署升级的自动化过程,流程如下:

这点重点提一下图上的配置,项目build和发布的文件包含2部分
- dll文件及相关文件,这些在任何环境上都是一样的,在测试,发布,UAT环境上都没有变化。
- 配置相关文件,这些在所有环境上都不一致,在.Net Core项目里配置通常包含
appsetting.json
和Migrations
目录,在我们团队大家用同一个内部框架,所以还有一个ConfigFile
目录。
UAT特别是生产环境的配置信息是不应该让开发人员知道的,比如数据库连接信息,如果开发者直接能连上生产数据这非常危险。
所以配置信息由部署人员单独保留在另外一个git服务器上,由部署人员来手动或通过自动部署脚本来修改这些配置信息。开发者只对源码修改和提交。
以后每次源码发生改变,部署人员只需要在登陆到相应环境的服务器上手动执行一下部署脚本就可以了。后期可以考虑进一步设计,让部署人员通过web管理的方式执行部署,无需远程登陆。
3. 实现
考虑使用bash脚本来编写部署逻辑,部署人员只需要修改源码git和配置git相关信息就可以。每个项目在每个环境下都需要拷贝和修改一份脚本文件projupdate.sh
:
#!/bin/bash
#请务必确保最后部署的项目
#1. 目录结构如下:
# -projectname
# -bin: 存放.NetCore项目编译发布后的所有dll及相关文件
# -update 存放当前projupdate.sh文件以及升级会自动生成相关目录和文件
#2. bin目录下的配置文件只包含appsetting.json以及自定义的ConfigFile目录
#3. bin目录下的所有日志豆都放在log目录下,这个目录是不需要备份的
#----------------------项目部署者需要修改的部分-----------------------#
#定义项目名称
proj_name="xxx项目的uat版本"
#定义项目源代码的git地址,包含用户名和密码,如果不考虑安全性,可直接写死密码在sh文件里
proj_src_git="http://tester:$1@xxx.yyy.zzz:8080/r/tools/NetCoreProjectAutoupdate.git"
#定义项目源代码的git分支名,这个尽量不要用主分支
proj_src_git_branch="master"
#定义项目源代码的所在子目录,包含.csproj文件所在目录,前面包含/,后面不包含
proj_scr_git_path="/TestSample/src/MigrationsSample/MigrationsSample"
#定义项目配置的git地址,包含用户名和密码,如果不考虑安全性,可直接写死密码在sh文件里
proj_config_git="http://tester:$2@xxx.yyy.zzz:8080/r/tools/NetCoreProjectAutoupdate.git"
#定义项目配置的所在子目录,前面包含/,后面不包含
proj_config_git_path="/TestSample/config/uat"
#---------------------------------------------------------------------#
#----------------------变量定义---------------------------------------#
#脚本的版本,格式为v.v.yyyymmdd
version="1.0.20180308"
#执行升级会记录日志文件,文件名为当日日期yyyy_mm_dd.log
logpath="./logs"
#转换为绝对路径
logname="`cd ${logpath};pwd`/`date +%Y_%m_%d`.log"
binpath="../bin"
backpath="./backup"
#---------------------------------------------------------------------#
#----------------------函数定义---------------------------------------#
#打印到命令行,并追加到日志文件
funecho(){
echo $1
if [ ! -d ${logpath} ];
then
mkdir ${logpath}
fi
echo $1 >>${logname}
}
gitclone(){
# $1: git地址 $2: 分支 $3: 保存的目录
#先删除历史clone留下的目录
delfile $3
funecho "git clone -b $2 $1 $3 2>&1|tee -a ${logname}"
git clone -b $2 $1 $3 2>&1|tee -a ${logname}
funecho " "
}
delfile(){
if [ -d $1 ];
then
funecho "删除文件或目录 $1"
rm -rf $1
fi
}
copyfile(){
funecho "拷贝文件或目录从$1到$2"
cp -r $1 $2
}
migration(){
cd ./src${proj_scr_git_path}
delfile "./bin"
delfile "./obj"
funecho "dotnet build"
dotnet build 2>&1| tee -a ${logname}
funecho "创建mirgration"
funecho "dotnet ef migrations add update`date +%Y_%m_%d_%H_%M_%S`"
dotnet ef migrations add update`date +%Y_%m_%d_%H_%M_%S` 2>&1| tee -a ${logname}
funecho "更新mirgration到数据库"
funecho "dotnet ef database update"
dotnet ef database update 2>&1| tee -a ${logname}
cd -
}
push2configgit(){
delfile "./config${proj_config_git_path}/Migrations"
copyfile "./src${proj_scr_git_path}/Migrations" "./config${proj_config_git_path}/Migrations"
cd ./config
funecho "git add -A"
git add -A 2>&1| tee -a ${logname}
funecho "git commit -m \"自动提交\""
git commit -m "自动提交" 2>&1| tee -a ${logname}
funecho "git push"
git push 2>&1| tee -a ${logname}
cd -
}
backup(){
if [ ! -d ${backpath} ];
then
mkdir ${backpath}
fi
if [ ! -d ${binpath} ];
then
mkdir ${binpath}
else
funecho "tar -zcvf ${backpath}/`date +%Y_%m_%d_%H_%M_%S`.tar.gz --exclude=${binpath}/log ${binpath}"
tar -zcvf ${backpath}/`date +%Y_%m_%d_%H_%M_%S`.tar.gz --exclude=${binpath}/log ${binpath} 2>&1| tee -a ${logname}
fi
}
publish(){
cd ./src${proj_scr_git_path}
funecho "dotnet publish"
dotnet publish 2>&1| tee -a ${logname}
cd -
funecho "cp -r -f ./src${proj_scr_git_path}/bin/Debug/netcoreapp2.0/publish/* ${binpath}"
cp -r -f ./src${proj_scr_git_path}/bin/Debug/netcoreapp2.0/publish/* ${binpath} 2>&1| tee -a ${logname}
}
#---------------------------------------------------------------------#
#----------------------执行过程----------------------------------------#
funecho " "
funecho "`date` 开始执行升级......"
funecho "1. 下载项目源码文件"
gitclone ${proj_src_git} ${proj_src_git_branch} "./src"
funecho "2. 下载项目配置文件,只考虑主分支"
gitclone ${proj_config_git} "master" "./config"
funecho "3. 删除项目源码文件中的相关配置目录和文件以及开发者自己创建的Migrations"
delfile "./src${proj_scr_git_path}/appsettings.json"
delfile "./src${proj_scr_git_path}/ConfigFile"
delfile "./src${proj_scr_git_path}/Migrations"
funecho "4. 拷贝git上下载的项目配置文件和目录到src下"
copyfile "./config${proj_config_git_path}/appsettings.json" "./src${proj_scr_git_path}/appsettings.json"
copyfile "./config${proj_config_git_path}/ConfigFile" "./src${proj_scr_git_path}/ConfigFile"
copyfile "./config${proj_config_git_path}/Migrations" "./src${proj_scr_git_path}/Migrations"
funecho "5. 在src下执行数据库迁移"
migration
funecho "6. Migrations变化后,需拷贝并push回配置git"
push2configgit
funecho "7. 备份旧的发布文件,log目录不备份"
backup
funecho "8. 重新发布并更新到bin目录"
publish
funecho "`date` 执行升级完成!"
#---------------------------------------------------------------------#
read noexit
需要说明的几点是:
-
脚本很多是基于我们自己内部开发框架的一些约定来设置的,协同开发的团队还是需要统一的内部开发框架更适合做一些自动化的操作。
-
开发者提交的源码不能只有一个分支,必须是开发的分支和发布的分支是完全分开的,部署脚本只下载发布分支。
-
不同的项目有一些特殊性,部署人员需要理解脚本然后相应做一些定制化的修改。
-
不管是第一次安装还是每次升级,都需要使用脚本去操作,不要手工介入。
-
最后提一下,sh文件在win10的powershell下运行毫无问题,应该是内嵌linux的原因。所以如果项目部署到winserver2016之类的服务器上也应该能跑,不需要把sh翻译成bat脚本。
设计和实现都是给大家作为一个参考,欢迎讨论,相关脚本和测试项目可以从git上下载。