构建基于Springboot+Docker+Jenkins+Github+Maven的持续集成系统

最近在做基于Docker+Jenkins+Github+Maven的持续集成环境,目的是自动化构建springboot项目并发布到生产环境。小公司没有自动化构建系统,项目发布过程是这样的:

1、在本地写好代码、测试,测试通过后打成war包或jar包;
2、将war包或jar包拷贝到云服务器上;
3、重启服务。

整个过程,特别是第2、3步靠手工操作可能会引入错误(比如命令行下不小心可能会删掉某个文件),不利于项目的可靠性。为此,想起上家公司采用了Jenkins实现了项目的自动化构建,不需要本地打包、上传,因此干脆就做一回运维,花点时间自己搭建一个持续集成系统,以实现:
只需本地写好代码并完成测试,然后将代码push到Github,后面的编译、打包、发布等工作交由持续集成系统自动完成。

恩,理想很丰满,现实嘛骨感。以前工作中都是使用现成,第一次自己搭建踩了无数的坑。好在经过一段时间的学习、实操,今天终于初步达成了目标。在此,要对Google搜索同学提出表扬。

由于比较忙,先大致说一下整个过程。

本地开发环境:
操作系统:Ubuntu 16.04
Java开发集成环境:Intellij idea 2019.1.3(Ultimate Edition)
JDK版本:1.8.0_191
数据库:Mongodb 4.0.10、Redis 3.0.6
Maven:3.6.1
Tomcat:8.5.41

云服务器环境:
操作系统:阿里云ECS,Ubuntu 16.04
JDK版本:使用镜像openjdk8,目前版本1.8.0_212
数据库:Mongodb 4.0.10、Redis 3.0.6
Maven:3.6.1
Tomcat:2.1.1,Springboot自带

大致步骤(后面有时间再补充):
注意:我的账户默认是root,因此下面的命令均不需要sudo前缀
第1步:安装Docker(按照官网安装即可)
(1)apt-get update
(2)apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
(3)curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
(4)add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
(5)apt-get update
(6)apt-get install docker-ce docker-ce-cli containerd.io
安装好后,运行“docker run hello-world”看是否安装正确。

第2步:安装Jenkins
这里通过Jenkins镜像来安装。注意:Jenkins镜像版本越高越好,否则可能会出现某些插件不兼容的问题。安装过程见我的另一篇博客https://www.jianshu.com/p/15c1addd1733。这篇博客中的汉化方法有些问题,重启Jenkins后会出现部分中文部分英文的情况,问题还没有找到。在此建议先不要汉化,因为很多问题可以在google上搜索,汉化后搜索结果的质量你懂的。

第3步:配置Jenkins和Github
这一步的目的是,当我们将本地项目push到Github后,会主动触发Jenkins从Github上拉取该项目,然后运行配置好的脚本。具体配置过程见我的另一篇博客https://www.jianshu.com/p/29d2a339a57a

第4步:配置Springboot项目
这一步主要是在项目pom.xml中引入dockerfile-maven-plugin插件,在该插件中可以指定项目打包后的名称、版本以及本地JAR包的位置。另外,还需要在项目根目录下编写Dockerfile以及build.sh、run.sh脚本。
Dockerfile:用于构建Springboot项目的镜像
build.sh:包含编译Dockerfile的命令
run.sh:用于基于项目镜像启动Docker容器,即运行项目
注意:这三个文件在项目根目录下,与src同级,见下图


image.png

4.1 引入dockerfile-maven-plugin插件
4.1.1 在pom.xml的plugins下插入一下配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>

<mainClass>com.company.testproject.TestProjectApplication</mainClass>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>

        <plugin>
            <groupId>com.spotify</groupId>
            <artifactId>dockerfile-maven-plugin</artifactId>
            <version>1.4.10</version>
            <configuration>
                <repository>testproject</repository>
                <tag>20190628-1.0</tag>
                <buildArgs>
                    <JAR_FILE>/target/testproject-0.0.1-SNAPSHOT.jar</JAR_FILE>
                </buildArgs>
            </configuration>
        </plugin>

注意:如果你原来将项目打包为war包,那么需要将pox.xml中的<packaging>war</packaging>注释掉(如果有的话)
4.1.2 配置Tomcat
由于在本地开发及测试时使用的是单独安装的Tomcat,没有使用Springboot自带的Tomcat,因此在pom.xml中是将该自带的Tomcat依赖去掉了的,即原配置如下:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>-->
            <exclusion>-->
                <groupId>org.springframework.boot</groupId>-->
                <artifactId>spring-boot-starter-tomcat</artifactIyincid>-->
            </exclusion>-->
        </exclusions>-->
    </dependency>

由于阿里云内存空间有限,为了减少不必要的内存开销,同时启动/停止项目均通过容器来实现,不需要显式执行Tomcat的startup.sh脚本来启动项目,因此没必要单独安装Tomcat。故将上面的依赖修改为:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

同时,因为使用内置的Tomcat,因此还要增加:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
    </dependency>

4.2 编写Dockerfile
Dockerfile用于构建项目镜像。在项目的根目录下新建文件Dockerfile,内容如下:

FROM openjdk:8
ARG JAR_FILE
RUN apt-get update
RUN apt-get install vim -y
RUN echo "Asia/Shanghai" > /etc/timezone
RUN dpkg-reconfigure -f noninteractive tzdata
RUN mkdir /testproject
ADD ${JAR_FILE} /testproject
EXPOSE 9081
ENTRYPOINT ["java","-jar","/testproject/testproject-0.0.1-SNAPSHOT.jar"]

这里,我没有使用宿主机中安装的JDK,而是基于基础镜像openjdk:8来构建项目镜像。
这里简要说明该Dockerfile的内容:
第1行:表示当前要构建的镜像的基础镜像是openjdk:8
第2行:设置环境变量,暂未指定值,在后面指定
第3、4行:可选。这两行目的是更新并安装vim,这两行主要是为了安装vim,因为基础镜像中除了jdk之外,没有其他软件可用。为了容器启动后可用对某些配置进行编辑或查看(当然也可以用cat命令查看,但我习惯了用vim)。如果你不需要vim,可以不安装
第5、6行:设置容器中的时区为东八区(北京时间)。容器的默认时区不是东八区,因此需要进行设置。需要注意的是,通过这两行设置后的时间也早于当前时间8小时。网上有方法是在启动容器时,加上参数-v /etc/localtime:/etc/localtime -v /etc/timezone:/etc/timezone,但我没有验证此方法。
第7行:在阿里云服务器上创建目录testproject,用于保存testproject-0.0.1-SNAPSHOT.jar
第8行:将环境变了JAR_FILE指向/testproject
第9行:将容器的端口9081暴露到容器外,即在宿主机可以访问该端口(即访问服务的端口)
第10行:指定容器启动时执行的程序及参数。这里表示在容器启动后就发布项目,如果不想立即发布项目,可以替换为其他命令,比如ENTRYPOINT ["ls", "-l", "/testproject"]

4.3 编写build.sh
这里不多说,见下面的代码:

mvn clean
mvn package -DskipTests
docker rmi -f testproject:20190628-1.0
mvn Dockerfile:build
docker images

其中,第3行表示删除上一个版本的镜像(可选,如果要考虑回滚的话就不要删除);第四行是根据Dockerfile重新生成新镜像。

4.4 编写run.sh
这个脚本主要是用于启动容器,代码如下:

docker ps -a

aa-remove-unknown

docker stop ruleparser
docker rm -f ruleparser
docker run -d --name ruleparser --network jids --network-alias jids -p 9081:9081 -v /var/jenkins_home/workspace/java_tale/data:/data -v /etc/localtime:/etc/localtime -v /etc/timezone:/etc/timezone ruleparser:20190628-1.0

这里要稍微解释一下:
第2行:防止第3行命令执行后出现”cannot stop container”的情况
第3、4行:停止前一版本的容器,并删除该容器(不删的话,第5行的docker run命令会因存在相同名称的容器而失败)
第5行:启动容器。参数说明如下:
--name:指定了容器名称;
--network:指定该容器所在的网桥,该网桥要与后文的mongodb容器所在网桥相同,否则容器不能访问mongod;
--network-alias:表示网桥的别名,如果没有别名的话不需要此参数;
-p:端口映射,格式为"-p 宿主机端口:容器端口";
-v /var/jenkins_home/workspace/java_tale/data:/data:表示将宿主机的目录/var/jenkins_home/workspace/java_tale/data映射到容器目录/data。宿主机目录存放的是项目需要的数据,在项目中位于data目录下,data目录与src目录同级,见第4步附图。这样配置后,需要在项目的properties文件中,制定数据路径,如下图:


image.png

-v /etc/localtime:/etc/localtime -v /etc/timezone:/etc/timezone:指定容器中的时区,主要是方便查看日志
ruleparser:20190628-1.0:镜像名称及标签,注意:名称及标签要与pom.xml中的配置相同。

第5步:mongodb容器及配置主从节点
参考博客https://outmanzzq.github.io/2019/01/30/docker-mongo-replica/
这里贴出我的配置过程。

5.1 运行mongodb replica的三个容器
docker run -dit -p 27000:27017 --name mongo-master -v /server/data/mongodb-docker/db/master:/data/db --network jids --network-alias jids mongo mongod --replSet rs

docker run -dit -p 27001:27017 --name mongo-slave-1 -v /server/data/mongodb-docker/db/slave-1:/data/db --network jids --network-alias jids mongo mongod --replSet rs

docker run -dit -p 27002:27017 --name mongo-slave-2 -v /server/data/mongodb-docker/db/slave-2:/data/db --network jids --network-alias jids mongo mongod --replSet rs

其中:
-p 27000:27017 表示映射宿主机27000端口到容器的27017端口
--name mongo-master 表示容器名为mongo-master
-v /server/data/mongodb-docker/db/master:/data/db 表示映射宿主机目录到mongodb容器/data/db目录 (mongodb容器运行时的默认目录),两个目录可以自定义
--net jids 指定容器网络为local-mongo-cluster
mongo 容器使用的mongodb镜像
mongod --replSet rs 执行mongod命令,将该实例(容器)添加到名为rs的副本集

5.2 进入mongo-master容器的mongo命令行
docker exec -it mongo-master mongo
(进入容器的命令是docker exec -it mongo-master /bin/bash,不要搞混)

5.3 配置主从节点
use jids
(1)确定主从配置
config = {
"_id" : "rs",
"members" : [
{
"_id" : 0,
"host" : "mongo-master:27017"
},
{
"_id" : 1,
"host" : "mongo-slave-1:27017"
},
{
"_id" : 2,
"host" : "mongo-slave-2:27017"
}
]
}
(2)初始化
rs.initiate(config)
(3)切换到MASTER节点(如果切换后还是SECONDARY,可能由于MASTER连接问题,多尝试几次即可)
rs.config()

5.4 从宿主机导入数据到容器
5.4.1 首先进入容器并在/root下创建data文件夹
docker exec -it mongo-master /bin/bash
mkdir /root/data
5.4.2 然后退出容器回到宿主机,复制数据到容器。命令格式为:docker cp 数据文件夹 容器名:容器内保存数据的目录。例如:
//假设在宿主机中保存bson和json文件的jids文件夹在/root/jids/data下
docker cp /root/jids/data/jids mongo-master:/root/data

然后就可以验证主从节点是否有效了(没有包括更多验证,如某个节点宕机),附常用命令:
(1)创建数据库
use 数据库名
(2)创建集合
db.createCollection("集合名")
(3)显示所有数据库
show dbs
注意:当使用show dbs时,会提示Error: listDatabases failed:..."errmsg" : "not master and slaveOk=false"。此时执行一下命令rs.slaveOk()即可
(4)显示所有集合
show collections
(5)显示集合中的所有文档
db.集合名.find()

第6步:在项目中配置mongodb和redis
修改项目的properties文件,根据宿主机及容器情况配置mongodb和redis的IP和Port
6.1 配置mongodb
spring.data.mongodb.uri=mongodb://172.17.0.1:27000,172.17.0.1:27001,172.17.0.1:27002/jids?replicaSet=rs
这里的172.17.0.1是宿主机中输入ifconfig后,docker0对应的虚拟IP地址。这个地址被所有容器共享,相当于容器组成的虚拟网络的localhost。注意,不要使用127.0.0.1来试图使用宿主机的服务,因为容器所在的虚拟网络与宿主机所在的网络是隔断的。

mongodb.png

6.2 配置redis
redis.host=172.17.0.1
redis.port=6379
这里的host同6.1中的IP。

redis.png

第7步:访问项目
这里有个坑。假如测试项目的controller如下


controller.png

要访问的功能模块如下:


method.png

如果将war包扔在tomcat的webapps中,那么访问该模块时需要采用"IP:PORT/testproject/test_parser?"这样的URI。然而,由于我们在Docker中使用的是Springboot内置的Tomcat,访问的URI变为了"IP:PORT/test_parser?"。这一点需要注意。

到此,整个构建过程基本完成了。过程这么长,可以想象遇到了多少坑。上面的流程都是在填完坑后的总结,其中的心酸就不提了。还有很多细节还可以优化,只有待后面熟悉Docker和Jenkins后再做打算了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271