Jenkins实战

Jenkins是一个用于自动化执行软件构建、测试和部署等一系列工作的持续集成工具,它把软件开发中的频繁重复的编译、发布和测试等工作统一自动化管理,为开发工作节省了大量的时间。本文主要内容是关于Jenkins的安装配置和自动构建部署基于maven管理的SpringBoot和SpringMVC的项目,Jenkins和部署服务器都是基于Docker制作,最终的目标是使用Jenkins拉取GIT仓库的项目自动构建并发布的远程的Tomcat服务器。在实践的过程中碰到不少困难,也踩了一些坑。

Jenkins的安装

由于Jenkins是基于Java开发,我采用了通用的war包运行方式来安装Jenkins,可以到官网的下载地址http://mirrors.jenkins.io/war/latest/jenkins.war下载最新的war包。我使用的是Windows操作系统,但本次实践的目标是在Linux环境下,由于开虚拟机比较麻烦,最终采用Docker容器的方式来开启linux服务器。在此之前,我已经下载好了一个Tomcat镜像。

1
2
3
PS C:\Users\sunfl> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hub.c.163.com/library/tomcat latest 40ab38c1ce33 3 weeks ago 367 MB

我在Windows系统上已经下载好了Jenkins的war包,为了能让容器访问到Windows的磁盘,需要对Docker for Windows进行磁盘共享配置。

war包放在了桌面,所以把C盘设置为共享磁盘,设置完毕后,启动一个tomcat容器,将8080端口映射到本地的8084端口,并指定将本地桌面目录映射到容器中的/home/desktop目录。

配置共享磁盘

1
docker run -d -p 8084:8080 -v C:/Users/sunfl/Desktop:/home/desktop hub.c.163.com/library/tomcat

进入容器并,复制桌面上的Jenkins.war到tomcat的webapps目录中,当然这个过程其实也可以通过编写Dockerfile自定义镜像完成此流程,而且更方便能被再次利用。

1
2
docker exec -it e6 bash
cp /home/desktop/jenkins.war /usr/local/tomcat/webapps/

之后重启容器

1
docker restart e6

然后访问本地的8084端口访问Jenkinshttp://localhost:8084/jenkins进入Jenkins应用

Jenkins初始配置

刚进入系统,会要求用户输入初始密码来解锁Jenkins,密码默认放在了/root/.jenkins/secrets/initialAdminPassword文件中。我们可以进入容器查看此文件获取到密码。通过容器执行以下命令获取文件中初始管理密码:

1
2
PS C:\Users\sunfl> docker exec -it e6 cat /root/.jenkins/secrets/initialAdminPassword
a5ddd6a36e424ca1bd855fe4be6a5642

打印出来的就是密码,将得到的初始密码输入,确认之后跳转到了自定义Jenkins界面,Jenkins提供两种选择自定义方式,第一种是安装官方推荐的常用插件,第二种是自己选择需要安装的插件。为了更加的简单,我选择第一种安装推荐的插件。

插件安装

选择之后开始下载插件并进行安装,由于插件的下载源是国外站点,可能会遇到由于网络因素造成插件下载安装失败,可以重试多次下载安装。

重新尝试安装插件

可以看到有一个插件没有安装成功,可以点击Retry再次尝试获取安装。

安装完毕,Jenkins要求创建一个管理员用户,根据情况输入信息,填写完毕继续,点击保存并完成。

创建一个管理员

完成之后,我们可以新建第一个Jinkins任务,或者直接点Save and Finish跳过进入首页。

之后可能遇到这样的情况,有报错提示,我们可以直接Retry跳过转到首页。但是点击 系统管理发现系统存在报错信息。这时由于刚才安装了插件,需要重启Jenkins才能生效。

可以重启服务器或者访问http://localhost:8084/jenkins/restart进入Jenkins的重启界面点击确认重启。之后等待Jenkins重启完成。输入刚才新建立的管理员进入Jenkins控制台中心,可以发现,刚才的报错信息没有了。

Jenkins首页

完善Jenkins服务器环境

由于我们使用到了Maven和Git软件,默认的tomcat镜像中没有的,需要自行安装下载,为了保证软件源最新,可以更新以下系统源。

1
2
apt-get update
apt-get upgrade

安装Git

首先安装git客户端,可以通过apt-get安装,如果系统的源比较旧,可以使用wget下载下来进行文件安装

1
apt-get install git

安装完成后可以看到git的版本是:

1
2
root@e6eb4b7811f7:/usr/local/tomcat# git --version
git version 2.1.4

安装Maven

下载Maven

1
wget http://apache.fayea.com/maven/maven-3/3.5.0/binaries/apache-maven-3.5.0-bin.tar.gz

解压:

1
tar -zxvf apache-maven-3.5.0-bin.tar.gz

解压之后,需要把maven配置到系统环境变量中才能全局执行,编辑~/.bashrc文件,追加环境变量:

1
2
3
export M2_HOME=/home/apache-maven-3.5.0
export MAVEN=$M2_HOME/bin
export PATH=$MAVEN:$PATH

配置完成之后,使用以下命令使配置立即生效:

1
source .bashrc

之后可以验证以下maven是否安装成功,可以尝试获取maven的版本信息,能获取成功,说明安装已经完成了。

1
2
3
4
5
6
7
root@e6eb4b7811f7:~# mvn -version
Apache Maven 3.5.0 (ff8f5e7444045639af65f6095c62210b5713f426; 2017-04-03T19:39:06Z)
Maven home: /home/apache-maven-3.5.0
Java version: 1.8.0_121, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: en, platform encoding: UTF-8
OS name: "linux", version: "4.9.13-moby", arch: "amd64", family: "unix"

配置Maven的源镜像和本地仓库

由于Maven默认获取依赖的远程仓库地址使国外站点,为了保证访问速度,把默认的镜像地址切换到由阿里免费提供的镜像。为了自定义Maven下载依赖的本地存储仓库位置,也需要个性化配置。具体的配置文件在Maven目录下的conf文件夹里的settings.xml文件,修改以下内容:

把镜像指向阿里镜像

1
2
3
4
5
6
7
8
9
10
<mirror>
<id>nexus</id>
<mirrorOf>*</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
<mirror>
<id>nexus-public-snapshots</id>
<mirrorOf>public-snapshots</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/repositories/snapshots/</url>
</mirror>

把本地存储仓库指向自定义位置

1
<localRepository>/usr/local/repo</localRepository>

建立测试项目

本次用于测试的项目分别基于Spring BootSpring MVC,它们的启动方式稍微不同。

Spring Boot项目

新建的Spring Boot项目中有一个HelloController,附带了一个info方法,为了更能突出Jenkins的测试过程,我专门编写了一个测试用例,这个测试用例,测试用例中测试的是访问Hello控制的info方法返回的状态是否正常,且返回的内容是类型是否为指定的字符串。在本地运行项目运行成功,测试用例失败,符合预期。

Spring Boot项目

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
package com.oopsguy;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringRunner.class)

@SpringBootTest
public class SpringBootDemoApplicationTests {

@Autowired
private WebApplicationContext context;

private MockMvc mock;

@Before
public void initMock() {
mock = MockMvcBuilders.webAppContextSetup(context).build();
}

@Test
public void testHelloInfo() throws Exception {
mock.perform(MockMvcRequestBuilders.get("/hello/info"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType("application/html"));
}

}

测试用例

完成demo上传到远程版本库上,由于我本地没有搭建gitlab,所以使用了访问速度比较快的国内git托管站码云http://git.oschina.net

上传版本库

Spring MVC项目

SpringMVC项目比较简单,没有编写测试用例,去除Spring MVC框架,其实与普通的Java EE项目没多大区别。项目中跟Spring Boot项目一样的套路,一个Hello控制器之中有一个info方法,不同的是这里的方法返回的是一个Jsp视图,而Spring Boot项目返回的是Json信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.oopsguy.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/hello")
@Controller
public class HelloController {

@RequestMapping("/info")
public String info() {

return "info";
}

}

配置Jenkins构建项目

为了确保能够建立一个maven项目并且配合Git,需要确保安装了Maven Integration pluginGit plugin这两个插件。

在新建任务之前,需要配置以下Jenkins的Maven配置,在系统管理中选择Global Toos Configuration,找到Maven配置板块,配置以下Maven的配置文件路径,即settings.xml文件的存放位置。然后再配置Maven的安装配置,填写Maven的安装目录

Maven配置

Maven安装配置

可以通过Jenkins首页的新建任务来新建一个项目,项目的类型选择Maven项目,任务的名称可以随意。

确认之后,跳转到任务详情信息填写页面,在源码管理区域,勾选Git作为版本控制系统,之后在下方完善仓库地址,由于仓库是私有的,需要进行用户身份认证才能访问,点击Credentials右边的Add按钮添加一个身凭证信息。

任务信息填写

凭证信息

默认的身份方案是使用账户和密码认证,也可以通过选择凭证类型来改变认证方式,比如选择SSH,这里我选择普通的用户名密码方式。

确认之后,返回刚才的任务详情填写页面,在Credentials选择刚才添加的凭证,之后直接保存,其它配置暂且默认。

返回Jenkins首页,可以看新添加的任务,点击最右边的按钮即可执行任务。

在执行任务过程中,其实是Jenkins通过用户提供的身份凭证从指定仓库下载代码之后执行mvn命令对项目进行编译测试打包。可以看到构建的过程正在下载依赖,而且使用了刚才在maven的settings.xml配置的依赖源。

但是。。。。构建失败了!看到构建记录得知,原来jenkins服务器没有配置jdk,怪不得无法编译代码。

无JDK,构建失败

由于我使用的是tomcat容器,本身自带了java的runtime环境,我想里面有没有内置由jdk呢,通过输出系统的环境变量可以找到预先配置的JAVA_HOME地址:

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
root@e6eb4b7811f7:/usr/local/repo# env
TOMCAT_ASC_URL=https://www.apache.org/dist/tomcat/tomcat-8/v8.5.13/bin/apache-tomcat-8.5.13.tar.gz.asc
HOSTNAME=e6eb4b7811f7
TOMCAT_VERSION=8.5.13
TERM=xterm
CATALINA_HOME=/usr/local/tomcat
OLDPWD=/usr/local
LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib
CA_CERTIFICATES_JAVA_VERSION=20161107~bpo8+1
PATH=/home/apache-maven-3.5.0/bin:/usr/local/tomcat/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 61B832AC2F1C5A90F0F9B00A1C506407564C17A3 713DA88BE50911535FE716F5208B0AB1D63011C7 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23
PWD=/usr/local/repo
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/jre
LANG=C.UTF-8
JAVA_VERSION=8u121
TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib
M2_HOME=/home/apache-maven-3.5.0
SHLVL=1
HOME=/root
JAVA_DEBIAN_VERSION=8u121-b13-1~bpo8+1
TOMCAT_MAJOR=8
OPENSSL_VERSION=1.1.0e-1
MAVEN=/home/apache-maven-3.5.0/bin
TOMCAT_TGZ_URL=https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-8/v8.5.13/bin/apache-tomcat-8.5.13.tar.gz
_=/usr/bin/env

可以看到JAVA_HOME指向的地址是JRE,系统中并不存在JDK,我们需要手动下载,在容器里使用wget方法很慢,我在Windows使用迅雷下载好了JDK放置桌面,可以复制到容器中。

将JDK复制到/home目录下,解压,可以看到结构目录:

1
2
root@e6eb4b7811f7:/home/jdk1.8.0_131# ls
COPYRIGHT LICENSE README.html THIRDPARTYLICENSEREADME-JAVAFX.txt THIRDPARTYLICENSEREADME.txt bin db include javafx-src.zip jre lib man release src.zip

进入Jenkins的全局配置面板配置JDK,在JAVA_HOME处填写具体的JDK目录就可以了。

配置完成,再次执行构建任务,构建任务成功!!构建过程也可以看到大大的Spring Boot启动界面。

构建成功

但是看到底部发现的完成状态是

1
Finished: UNSTABLE

回看构建输出的控制台信息,可以发现是有一个测试用例测试失败,也就是之前在项目中预留的失败的测试用例。

1
2
3
4
5
6
7
8
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 3.488 sec <<< FAILURE! - in com.oopsguy.SpringBootDemoApplicationTests
testHelloInfo(com.oopsguy.SpringBootDemoApplicationTests) Time elapsed: 0.129 sec <<< FAILURE!
java.lang.AssertionError: Content type expected:<application/html> but was:<application/json;charset=UTF-8>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:54)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:81)
at org.springframework.test.web.servlet.result.ContentResultMatchers$1.match(ContentResultMatchers.java:82)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:171)
at com.oopsguy.SpringBootDemoApplicationTests.testHelloInfo(SpringBootDemoApplicationTests.java:36)

在任务列表中也可以查看具体的信息

测试失败

回到Spring Boot项目,修改测试用例:

1
2
3
4
5
6
@Test
public void testHelloInfo() throws Exception {
mock.perform(MockMvcRequestBuilders.get("/hello/info"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType("application/json;charset=UTF-8"));
}

版本提交

更新代码到远程git仓库后,再次执行构建任务,构建成功SUCCESS!!。

构建成功

每一次构建都会有历史记录

构建历史

远程部署Tomcat

为了模拟远程tomcat,需要新建立一台Tomcat服务器,使用同一个镜像新建一个tomcat容器,把容器中的8080端口映射到本地的8085端口:

1
docker run -d -p 8085:8080 hub.c.163.com/library/tomcat

由于需要远程登录tomcat服务器,需要进行账号登录,配置tomcatconf目录下的tomcat-users.xml文件

能终端远程登录需要一个manager-script角色身份。

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>

<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="manager-script"/>
<user username="tomcat" password="oopsguy" roles="manager-script"/>
</tomcat-users>

如果没有配置manager-script角色,构建的就会产生如下错误:

没有权限

保存之后重启服务器,接下来就是配置Jenkins远程部署。

在配置之前需要已经确保安装了Deploy to container Plugin插件。

进入原来任务的配置界面,在构建后操作的板块中的增加构建后操作步骤选择Deploy war/ear to a container

这里有点小坑,在Add Container中列举了多种服务器,在中文界面,tomcat的版本只有到6.x,但选择6.x部署不了tomcat8版本,最低也要7.x,所以我切换回了英文语言,直接在地址栏把localhost换成了本机的IP地址。

以下有几个配置项:

  1. WAR/EAR files 指的是需要部署的打包之后的文件的位置
  2. Context path 部署之后的上下文,访问需要加上的上下文目录
  3. Containers 具体的部署容器

部署容器配置

选择Containers为tomcat之后,填写刚才在tomcat-users.xml配置的用户信息和远程tomcat的访问地址。确认之后保存。

执行构建任务,发现构建失败!有一个错误,大概是没有访问权限。

从构建记录中可以看到Jenkins试图访问manager项目,被拒绝访问,我们需要为manager配置一个访问白名单。

进入tomcat中为webapps下的manager下的META-INF中,编辑context.xml,把当前访问的IP地址加上,但是由于我们使用的是本地机器访问,不能够直接写本地地址,可以通过查看tomcat的访问日志来确认实际的访问地址。

1
cat localhost_access_log.2017-04-26.txt

通过输出的访问日志获取访问ip为172.17.0.1,接下来配置context.xml,由于我这是私人使用,直接把原来的白名单注释了,直接写固定的IP地址就可以了。

1
2
3
4
5
6
<Context antiResourceLocking="false" privileged="true" >
<!--<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />-->
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="172.17.0.1" />
</Context>

保存之后,重启服务器。

再次进行构建任务,尼玛!,还是失败!

具体的报错是路径不存在!

1
Caused by: org.codehaus.cargo.container.tomcat.internal.TomcatManagerException: FAIL - Encountered exception javax.management.MBeanException: Cannot find operation isDeployed

后来翻墙查阅才知道这特么是tomcat的锅!!我使用的Tomcat是8.5.13版本,这个bug官方已经收到反馈,并且在8.5.14已经修复,尼玛!!!!!,悲剧了,我的tomcat镜像恰好是8.5.13版本。

这个bug的反馈链接: https://bz.apache.org/bugzilla/show_bug.cgi?id=60949

没办法,我只能升级容器中tomcat了。
用wget下载最新版的tomcat后,把原来的tomcat备份,解压新的tomcat覆盖。具体的执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@b4cd737416c3:/usr/local# mv ./tomcat ./tomcatr_temp
root@b4cd737416c3:/usr/local# ls
bin etc games include lib man sbin share src tomcat_temp
root@b4cd737416c3:/usr/local# cd tomcat_temp/
root@b4cd737416c3:/usr/local/tomcat_temp# ls
LICENSE NOTICE RELEASE-NOTES RUNNING.txt bin conf include lib logs native-jni-lib temp webapps work
root@b4cd737416c3:/usr/local/tomcat_temp# cd ..
root@b4cd737416c3:/usr/local# cd /home/
root@b4cd737416c3:/home# ls
apache-tomcat-8.5.14 apache-tomcat-8.5.14.tar.gz
root@b4cd737416c3:/home# mv ./apache-tomcat-8.5.14 /usr/local/tomcat
root@b4cd737416c3:/home# ls
apache-tomcat-8.5.14.tar.gz
root@b4cd737416c3:/home# cd /usr/local/tomcat
root@b4cd737416c3:/usr/local/tomcat# ls
LICENSE NOTICE RELEASE-NOTES RUNNING.txt bin conf lib logs temp webapps work

之后重新启动容器,恢复tomcat,把原来配置tomcat的流程在搞一遍T_T。运行之后,可以看到版本提升了。

升级Tomcat

再次进行构建成功!!并且发布到了远程的tomcat服务器!

构建部署成功

来看看部署机器上的tomcat的webapps目录下已经存在了生成的war包。

1
2
3
root@b4cd737416c3:/usr/local/tomcat/webapps# ls
ROOT ROOT.war docs examples host-manager manager
root@b4cd737416c3:/usr/local/tomcat/webapps# java -war ROOT.war

Spring Boot运行

我们使用java执行Spring Boot命令可以看到运行起来了,这里有点不妥就是Spring Boot应用本身默认自带了Tomcat插件,比较适合使用SSH链接远程主机直接发布后远程执行启动脚本。

由于项目配置了8088端口,容器运行的适合没有曝露8088端口,只能在容器内访问测试,可以通过curl获取接口数据。

1
2
root@b4cd737416c3:/usr/local/tomcat# curl http://127.0.0.1:8088/hello/info
{"gender":"male","author":"Oopsguy","CI":"jenkins","language":"java"}

执行结果没问题!

之后还有一个Spring MVC项目,步骤差不多。

新建一个任务,类型为Maven项目,选择版本管理系统为Git,填写仓库名称和凭证信息,远程部署的配置也跟之前的一样,我就直接掠过了。之后对项目执行任务。有了之前的教训,第二个项目的部署比较顺利,完美部署到tomcat,在tomcat可以直接访问到刚才部署上去的Spring MVC项目。

Spring MVC运行

总结

这次实践花费了我不少时间,踩了不少的坑,特别是那个tomcat 8.5.13版本的bug,折腾了很久。总结下来,这次也学到了很多,基本的Jenkins配置技能是必须的,持续集成的流程大概了解了。但是还有很多不足,比如远程部署服务器这方面的基础还比较弱,linux的操作技能也不太熟,还是需要经常实践才行。本次实践基于Java项目,但是本人有转向PHP领域的决心,Jenkins与PHP方面的内容还不是了解很多,粗略地看到有介绍Jenkins-PHP这个项目,有时间再实战一番!