书籍名称:[Python3 自动化软件发布系统-Django2 实战]
软件自动化发布,是建立在手工软件发布很成熟的基础之上的,并且是对公司的软件包结构,服务器操作系统标准化之后形成的。相对于手工部署,自动化软件部署效率更高,操作更标准化,节约人力物力,并且可以记录每一次的发布细节。所以它的技能要求更高,要求系统开发人员不但要懂运维,还要懂开发。
以下是自动化软件发布系统 Manabe 的工作流:
当程序员将代码上传到 GitLab 之后,通过 在 Manabe 中新建一个 发布单,并编译此发布单,Manabe 就会通过 Jenkins 的 API,远程地从 GitLab 上拉取指定代码,并编译成软件包,上传到 Nginx 软件仓库中。
当需要发布的时候,Manabe 将通过 SaltStack 组件远程执行脚本,命令指定的服务器从 Nginx 仓库拉取指定版本的软件包,并实现每个服务器上的软件包的备份,更新,部署,服务启停等标准操作。
示例项目
在本节,我们设计一个简单的示例代码,后面将会使用这个代码演示自动化软件发布的各个方面
编写示例代码
在本示例中,我们使用 STS(Spring Tool Suite) 来开发这个应用。我们的示例是一个 maven 项目,这里只讲解主要的实现代码。
文件结构如下所示:
1 | javademo |
- Pom.xml 的文件内容如下:
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
代码解释
- 第10行,指明我们生成的软件包为 jar 包;
- 第1822行,指明我们使用的 spring-boot 版本为 2.3.5;27行,指明使用了一个依赖 starter:spring-boot-starter-web;
- 第24
- 第32~34行,spring-boot-maven-plugin 这个编译插件,是为了解决单独运行 jar 时的 MANIFEST 错误问题;
- App.java 的内容如下:
1 | package com.example.javademo; |
代码解释:
- 第11行,为了极致精简,我们将 controller 也写到了 main 函数的主文件中;
- 第1519行,使用了 @Value 注解,来读取配置文件中的变量。而配置文件,我们放在项目根目录的 config 目录中,在部署时,需要将 config 文件夹和 jar 软件放在同一个目录下,才能正常读取配置;25行,定义了一个 uri:/hello。它会将 env 变量名称和 db 变量名称显示出来;
- 第21
- config 文件夹下包括两个文件,application-test.properties 和 application-prd.properties,文件都是与环境相关的。内容如下:
1 | # application-prd.properties |
编译项目
在测试项目可以运行后,使用 maven 命令,生成可执行的 jar 包。
1 | mvn package -Dmaven.test.skip = true |
也可以在 STS 中,右击 pom.xml 文件进行编译。
手工运行
在得到 jar 软件包和 配置文件后,可以先进行手工安装测试,将 javademo-0.0.1-SNAPSHOT.jar 软件包和 config 目录转移到一个单独目录下,然后执行如下命令:
1 | java -jar javademo-0.0.1-SNAPSHOT.jar --spring.profiles.active=test |
如无意外,将会看到如下所示界面:
启动浏览器,访问网址 http://127.0.0.1:8080/hello
,可以看到如下所示输出
1 | env:'TEST' /hello, db:'TEST DATABASE' |
可以看到,由于我们启动时指定了 test 环境,所以显示的是 test 的变量;
使用 GitLab 保存源代码
GitLab 的安装和使用,可以去网上查找相关的资料,此处直接演示如何将上面的代码存入 GitLab 中;
建立用户和项目组
在 GitLab,首先建立一个 Jenkins 用户,新建一个 ZEP-BACKEND 项目组。然后将这个用户放入项目组,用这个用户来打通 GitLab 和 Jenkins 之间的互信,Jenkins 使用这个用户从 GitLab 中拉取代码进行编译。
使用管理员登录 GitLab中,单击用户头像,选择
Settings
,进入系统设置,点击顶部的一个扳手图标(Admin Area
),进入管理界面;点击右侧窗口中的 New user,选择创建用户,输入必填项,建立 jenkins 用户,如下:
在 GitLab 的正常使用用,为了安全起见,所有后台管理增加的用户,都是通过邮件发送给对方,让对方通过邮箱中的链接,来更新用户密码;由于我们测试时没有使用邮箱验证,所以直接通过
Create user
按钮中的 users 链接进入用户管理界面,更改 jenkins 用户密码;建立好用户之后,开始建立项目组。单击页面顶部的加号(
"+"
)图标,在下拉菜单中选择Group
;在建立项目组的界面里,输入 ZEP-BACKEND 项目组信息,在
Visibility Level
中选择Private
,表示这个项目只有项目组里的用户才能浏览或者更新代码,而不是任何人都有权限。然后单击Create group
按钮保存;项目组建好后,下面将 jenkins 用户加入到 ZEP-BACKEND 项目组,至此,完成了第一步的操作。jenkins 用户自动拥有了 ZEP-BACKEND项目组下所有项目的权限;
同样按照上面的操作,建立 ZEP-FRONT,ABC-BACKEND,ABC-FRONT 项目,并将 jenkins 用户加入到这些项目组中。
建立项目
接着需要在 ZEP-BACKEND 项目组中新建一个 ZEP-BACKEND-JAVA 项目,这个项目就是在上一节的示例代码。操作如下:
- 在首页右上角,单击 New Project 按钮,输入相关信息,注意,在 GitLab 服务器地址后,需要选择 ZEP-BACKEND 这个项目组,同样,在 Visibility Level 中,选择 Private 选项,允许项目组成员进入,然后单击 create Project 按钮,就建立了 ZEP-BACKEND-JAVA 项目;
- 建立好项目后,当我们进入 GitLab 界面,选顶部
Projects
–>Your projects
时,能看到自己的项目,表示已经建好此项目; - 当进入 ZEP-BACKEND-JAVA 项目右边导航,在 settings 设置下的 member 成员列表总查看用户时,会发现,我们的 Jenkins 已在其名单中了。
除了将用户加入项目组,让用户在整个项目组有权限之外,GitLab 为了灵活管理用户,还在每个项目的 member 管理页面,单独设置了增加用户的功能。值得注意的是,在单个项目下增加用户,其权限仅存在于本项目,而不会扩展到整个项目组。
将本地代码推送到 GitLab 中
获取 ZEP-BACKEND-JAVA 的 GitLab 地址,在本示例中,git 地址为:
http://gitlab.59izt.com/zep-backend/zep-backend-java.git
在本地计算机上选定一目录作为 Git 目录,然后输入以下命令将此代码仓库克隆到本地。如果有用户名和密码方面的问题,可以在 Git 地址中引入用户名,如:
http://jenkins@gitlab.59izt.com/zep-backend/zep-backend-java.git
。输入密码,就可以得到 git 代码的本地备份了。1
git clone http://jenkins@120.48.5.173:5981/zep-backend/zep-backend-java.git
将上一小节的 javademo 代码拷贝到 zep-backend-java 文件夹,然后新建 README.md 文件,添加项目说明;
执行 以下命令,将代码提交到 GitLab
1
2
3
4
5
6
7
8# 添加文件到本地 Git 仓库
git add .
# 应用变更到本地 Git
git commit -m "project init"
# 上传代码到远程仓库
git push提交成功后,进入 GitLab,就可以验证提交的代码,如下:
使用 Jenkins 编译项目
使用 Docker 版的 Jenkins
因为 Docker 版的 Jenkins 本身没有集成 Maven 软件,所以在制作一些 Java 项目时,Maven 命令就会出错。这里先是在宿主机上安装好 Maven 程序,再将宿主机的 Maven 挂载进容器(-v /usr/local/maven:/usr/local/maven). Docker 命令如下:
1 | docker run -itd -p 8080:8080 -v /jenkins-data:/var/jenkins_home -v /usr/local/maven:/usr/local/maven --name='jenkins' jenkins/jenkins:2.141-alpine |
正式使用 Jenkins 的方法
使用管理员登录 Jenkins,建立流水线型任务,名为 ZEP-BACKEND-JAVA,如下:
后期要使用 Manabe 通过 Jenkins API 传过来的参数进行构建,使用的参数如下:
参数 说明 默认值 Git_url Git 地址 http://gitlab.59izt.com/ZEP-BACKEND/ZER-BACKEND-JAVA.git
branch_build 编译版本 master package_name 软件包名 javademo-1.0.jar Zip_package_name 压缩包名 javademo-1.0.tar.gz app_name 应用名称 ZEP-BACKEND-JAVA deploy_version 发布单号 deploy_version dir_build_file 编译目录 javademo 在上面的参数设计中,我们争取让其适用于不同语言的项目,如果这个目标达不成,至少要让其适用于相同语言的项目,也至少需要对同一个语言,生成同一种软件包(war 包或 jar 包)的项目适用。绝对不可取的是,对公司每一个项目,都建立一个 Pipeline 流水线任务,这样的任务,对现实没有一定的抽象度,标准化程序不高,一有变动,就会涉及巨大的工作量。我们宁愿设计一个不用的参数,也不要在需要灵活性时,发现没有参数可用。
在 jenkins 的设置如下所示:
最重要的步骤就是制作 Pipeline。我们使用的脚本如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39pipeline {
agent {
node { label "master" }
}
stages {
stage('Prepare Git Code') {
steps {
echo "Perparing beging.."
sh "rm -rf ${WORKSPACE}/*"
git branch: '${branch_build}',
credentialsId: 'gitlab_jenkins',
url: '${git_url}'
echo 'Prepare end..'
}
}
stage('Build') {
steps {
dir("${WORKSPACE}/${dir_build_file}") {
echo 'Build begin..'
sh '/usr/local/maven/maven-3.6.3/bin/mvn package -Dmaven.test.skip=true'
sh "cp target/${package_name} ./${package_name}"
sh "tar -zcvf ${zip_package_name} ${package_name} config/"
echo 'Build end..'
}
}
}
stage('Test') {
steps {
echo 'Testing...'
}
}
stage('scp package to nginx') {
steps {
echo 'scp nginx begin..'
echo 'scp nginx end..'
}
}
}
}Pipeline 解释:
- 第3行,
label "master"
表示使用主节点编译; - 第9行,${WORKSPACE} 这种不是我们明显传入的变量,就是系统内置的一些变量;
- 第11行,credentialsId: ‘gitlab_jenkins’, 这里传入jenkins 用户的 id;(需要先新建一个 jenkins 用户与 gitlab 通信)
- 第12行,${git_url} 这种类似 shell 的变量插值,正是我们传递给 Jenkins 的变量;
- 第23行,因为我们的软件包包含配置文件,所以用 tar 将软件和配置压缩起来;
- 第18行, dir(“${WORKSPACE}/${dir_build_file}”) 这样的语法,表示的是 cd 到某个目录;
- 最后一个 stage,由于前面还没有讲到 nginx 服务器,所以没有将软件包做进一步的传送,只是简单的进行了 echo 输出。后面再回来完善此脚本;
sh “cp target/${package_name} ./${package_name}”` 这里需要注意,后面的路径不能直接写 ./,必须写成 ./${package_name}
- 第3行,
接着,进行一次模拟手工,单击 Jenkins web 界面左边的 Build with Parameters 进行编译。如下所示:
如果是第一次编译,maven 会从远程拉取 jar 包,这一过程很费时间,所以需要长时间等待,第一次拉取得到的 jar 包都会缓存在 maven 在本地的一个临时目录仓库下。下一次如果版本没有更新,就不会再次进行远程拉取了;
当编译完成之后,软件包就生成了。同时在 jenkins 的前端也提供了图表,用于显示编译的进度和时间。
使用 Nginx 作为软件仓库
将 jenkins 中编译生成的软件包传递到 nginx 服务器上,大致要经过以下几个步骤:
- 首先打通 jenkins 和 nginx 服务器之间的 ssh 免密码登录;
- 在服务器上安装好 nginx,定义好服务根目录,并设置目录浏览权限;
- jenkins 中安装对应的传送软件的插件,将 ssh 免密生成的 rsa 私钥设置在 jenkins 系统里;
- 完成 jenkins 的 Pipeline 最后部分的编写,通过测试即可。
Jenkins 和 nginx 服务器之间的免密登录
假设 jenkins 服务器的 IP 为 192.168.200.15,nginx 服务器的 IP为 192.168.200.17;
登录 jenkins 服务器,在 root 用户目录下执行如下命令,生成 rsa 算法的公钥和私钥
1
ssh-keygen -t rsa
在 jenkins 上,执行以下命令将公钥传送到 nginx 服务器上
1
ssh-copy-id -i .ssh/id_rsa.pub root@192.168.200.17
现在我们将 jenkins 上的 RSA 私钥内容拷贝出来,因为在 jenkins 的 SSH Agent 插件设置时需要用到;
安装 nginx 服务器
安装 nginx 有两种方式,yum 或 源码编译,这里为,编译安装请自行百度,以下使用 yum 安装;
登录 nginx 服务器,执行以下命令,在 yum 仓库里增加 epel 仓库
1
yum install -y epel-release
运行以下命令,安装 nginx
1
yum install -y nginx
在 nginx 服务器上创建软件存放目录
1
2mkdir -p /data/wwwroot/packages.59izt.com/packages
chown -R www.www /data/wwwroot/packages.59izt.com/packages修改 nginx 配置文件,在 server 区块中增加以下代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14root /data/wwwroot/packages.59izt.com;
index index.html index.htm index.php;
#error_page 404 /404.html;
#error_page 502 /502.html;
location /packages/ {
alias /data/wwwroot/packages.59izt.com/packages/;
autoindex on; # 开启目录浏览
autoindex_format html; # 以 HTML 风格将目录展示在浏览器中
autoindex_exact_size off; # 切换为 off 后,以可读的方式显示文件大小
autoindex_localtime on; # 以服务器的文件时间作为显示的时间
charset utf-8,gbk; # 展示中文文件名
}
...重启 nginx 服务,删除 /data/wwwroot/packages.59izt.com/packages/ 目录下除 packages 以外的所有文件;
1
systemctl restart nginx
浏览器测试访问
http://192.168.200.17/packages/
,效果如下:
安装 jenkins 插件
- 以管理员身份登录 jenkins,在
系统管理
–>插件管理
的可选插件
中,勾选SSH Agent Plugin
插件进行安装; - 由于 SSH Agent Plugin 需要做安全登录验证,所以需要先设置好凭据。在 jenkin 系统里,依次点击
凭据
–>系统
–>全局凭证
–>新建
,即可新建一个凭据;
配置 Jenkins Pipeline
根据前面的配置参数,可以完成前面未完成的 Pipeline 配置了.
ZEP-BACKEND-JAVA 的 Pipeline 全部内容如下:
1 | pipeline { |
验证
配置告一段落,现在将流程串联起来,实操一次:
在 Jenkins 的 ZEP-BACKEND-JAVA 任务中,单击 Build with Parameters,输入如下所示:
单击开始构建按钮,Jenkins 会按照我们的设置,从 Git 按代码编译 jar 包,并且上传到 Nginx;
查看控制台输出,证明 scp 命令完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40[Pipeline] { (scp package to nginx)
[Pipeline] dir
Running in /root/.jenkins/workspace/ZEP-BACKEND-JAVA/javademo
[Pipeline] {
[Pipeline] echo
scp nginx begin..
[Pipeline] sshagent
[ssh-agent] Using credentials root (connect nginx server)
[ssh-agent] Looking for ssh-agent implementation...
[ssh-agent] Exec ssh-agent (binary ssh-agent on a remote machine)
$ ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-MJ2l0CYO0Hk0/agent.7596
SSH_AGENT_PID=7599
Running ssh-add (command line suppressed)
Identity added: /root/.jenkins/workspace/ZEP-BACKEND-JAVA/javademo@tmp/private_key_2811079941907552391.key (/root/.jenkins/workspace/ZEP-BACKEND-JAVA/javademo@tmp/private_key_2811079941907552391.key)
[ssh-agent] Started.
[Pipeline] {
[Pipeline] sh
+ ssh -o StrictHostKeyChecking=no -l root 192.168.200.17 mkdir -p /data/wwwroot/packages.59izt.com/packages/ZEP-BACKEND-JAVA/2020-1112-1725-37XZ
[Pipeline] sh
+ scp javademo-1.0.tar.gz root@192.168.200.17:/data/wwwroot/packages.59izt.com/packages/ZEP-BACKEND-JAVA/2020-1112-1725-37XZ
[Pipeline] }
$ ssh-agent -k
unset SSH_AUTH_SOCK;
unset SSH_AGENT_PID;
echo Agent pid 7599 killed;
[ssh-agent] Stopped.
[Pipeline] // sshagent
[Pipeline] echo
scp nginx end..
[Pipeline] }
[Pipeline] // dir
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS访问网址
http://192.168.200.17/packages/ZEP-BACKEND-JAVA/2020-1112-1725-37XZ/
,可以看到软件包已经放到了正确的目录下。