前言

书接【Bug周刊】的gitlab-ci构建部分,我们已经对一个 maven 项目进行了CI构建,实现每次提交代码后自动打包为 jar 包,并在docker in docker 的镜像中 build 为 docker 镜像。避免跳转麻烦,把上文的构建内容放到了基础部分

基础(可跳过)

问题描述

需要对一个maven项目进行自动化构建,要求每次提交都会触发构建,减少运维的工作量,将构建好的jar包打包成docker镜像并推送至私有的镜像仓库。

详情如下:

1、自定义开发的common模块并不完善,也没有上传至私有的nexus仓库,需要打包的功能模块依赖于common

2、项目依赖的部分jar包需要从私有的nexus仓库下载,需要配置对应的仓库地址

3、构建时间的优化、提升

解决方案

1、在代码仓库中增加 .m2/settings.xml 文件,配置对应的私有nexus仓库地址、阿里云或者腾讯云的nexus地址提升下载速度

2、增加 localReposity 配置,告诉maven在找不到对应jar 包时,从本地读取,完成common模块的引入。由于common模块是独立开发的,故和其他模块的pom父类并不一致,各个模块也有不同的配置,在原项目根目录下并没有pom文件,所以不能通过构建根pom文件的方式完成项目的打包。

3、引入cache

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
41
42
43
44
45
46
47
variables:
RELEASE_TAG: "1.0.0"
MAVEN_CLI_OPTS: "../.m2/settings.xml --batch-mode -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"

cache:
key: ${CI_COMMIT_REF_SLUG} # cache键值对 减少mvn下载jar包的时间 key指向当前项目分支
paths:
- ~/.m2/repository/ # 缓存地址 镜像根目录下的 .m2/repository/ 文件夹
- target/

# CI构建两步
stages:
- package
- release

# 在maven镜像中构建jar包
package:
image: maven:3.6.1-jdk-8-alpine
stage: package
only:
- master # 触发构建的分支
tags:
- docker
script:
- cd test-common # 进入common模块打包
- mvn -s $MAVEN_CLI_OPTS -e package install # 打包 并将打包后生成的jar下载至 镜像根目录.m2/repository/路径下(本地仓库)
- cd ../test-app # 切出common 打包其他模块
- mvn -s $MAVEN_CLI_OPTS -e package
artifacts: # 构建好的jar文件上传 并设置过期时间
paths:
- test-app/target/test-app-application-exec.jar
expire_in: 1 hours

release: # docker in docker 在docker中构建jar为docker镜像
image: docker:20-dind
stage: release
only:
- master
tags:
- docker-slim
before_script:
- docker login --username=username -p $REPOS_PASSWORD test.com # 配置私有镜像仓库的账号 密码 地址
script:
- df -h
- docker build -t test/test-app:$RELEASE_TAG . # docker镜像标签
- docker push test.com/test-image/test-app:$RELEASE_TAG # 推送

进阶

问题描述

目前的业务需求是,在原maven项目的基础上,根目录增加了同级的模块,需要分模块构建,并且每次提交代码只对产生变更的模块进行打包

文件夹树如下:

1
2
3
4
5
6
7
8
9
10
|---.m2
|---gateway
|---moudle
|---hr
|---manage
|---adminstrive
|---.gitlab-ci.yml
|---dockerfile
|---startup.sh

解决方案

1、对变更模块进行判断,需要使用 git diff 命令

单纯的maven3.6版本的镜像没有git,同时也未安装对应的命令行工具,如apt、apk、yum等,因此无法在 before_script 阶段安装 git 工具曲线救国。只能更换原来的镜像。

2、分模块构建,需要使用通用的 dockerfile ,即在gitlab-ci.yml中对构建模块名进行判断,将此作为变量传入到dockerfile中。

docker build 命令提供了 --build-arg 的参数可以将变量传入dockerfile中。

3、明确CI文件 script 中的命令与 linux 终端命令细微的区别,避免出现标点的错误。

少年要不要来回试试,没有这些; \ \n &&符号,可能会寸步难行。

4、选择正确的镜像,满足打包和构建的使用要求。

如果在 dind 镜像中没有git命令对模块判断进行 build,不妨试试判断上一步,对产生变更的模块进行打包,是否有产物传给这一步骤,判断文件是否存在,比再安装一遍git省事多了。

5、纠正基础中的缓存地址。

配置文件

.gitlab-ci.yml

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
variables:
# 将打包文件的路径作为变量 简化后续代码长度
RELEASE_TAG: "0.0.1"
JAR_HR: "module/module-hr/target/module-hr-exec.jar"
JAR_MANAGE: "module/module-manage/target/module-manage-exec.jar"
# .m2文件夹在代码的根目录 模块在 下两层 比如 moudle/hr moudle/manage 所以需要跳出两次
MAVEN_CLI_OPTS: "../../.m2/settings.xml --batch-mode -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"

#cache:
# key: ${CI_COMMIT_REF_SLUG}
# paths:
# 这是第一次的缓存配置 路径错误 因为已经指明了maven 安装jar包的地址
# 为 maven.repo.local=$CI_PROJECT_DIR/.m2/repository
# - ~/.m2/repository/
# - target/
# - /usr/share/maven
# - /root/.m2/repository
cache:
key: m2-repo
paths:
# 这两个地址是等效的 都指向服务器的 /builds/username/projectName/.m2/repository
- .m2/repository/
- $CI_PROJECT_DIR/.m2/repository

stages:
- package
- release

package:
# 单纯maven不行 我用java带maven很合理吧 java镜像有apt-get 这很河狸吧
image: labelinsight/java-maven:3.6-jdk-8
stage: package
only:
- dev
tags:
- docker
before_script:
- apt-get install -y git
script:
# git diff 命令判断模块是否发生变更 并判断是否发生在对应的 hr manage 模块下
- if [[ -n $(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | grep '^module/module-hr/') ]]; then
cd module/module-hr;
mvn -s $MAVEN_CLI_OPTS -e package;
cd ../../;
fi;
- if [[ -n $(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | grep '^module/module-manage/') ]]; then
cd module/module-manage;
mvn -s $MAVEN_CLI_OPTS -e package;
cd ../../;
fi;
artifacts:
paths:
- module/module-hr/target/module-hr-exec.jar
- module/module-manage/target/module-manage-exec.jar

expire_in: 1 hours

release:
image: docker:20-dind
stage: release
only:
- dev
tags:
- docker-slim
before_script:
# - apk add git 判断文件是否存在 不用装git了
# $REPOS_PASSWORD 为管理员提前设置好的系统变量
- docker login --username=username -p $REPOS_PASSWORD docker.repos.cscec81.com:4433
script:
- df -h
# 打标签 --build-arg 传变量 变量名为 BUILD_JAR_NAME 对应值是开始在 variables部分 设定好的
# 推送至 私有的docker镜像仓库
- if [[ -f "module/module-hr/target/module-hr-exec.jar" ]]; then
docker build -t test/test-hr:$RELEASE_TAG --build-arg BUILD_JAR_NAME=$JAR_HR .;
docker push repos.test.com/test-image/test-hr:$RELEASE_TAG;
fi;
- if [[ -f "module/module-manage/target/module-manage-exec.jar" ]]; then
docker build -t test/test-manage:$RELEASE_TAG --build-arg BUILD_JAR_NAME=$JAR_MANAGE .;
docker push repos.test.com/test-image/test-manage:$RELEASE_TAG;
fi;

# 如果有更多模块 按照 加变量 -> 加git diff -> 加产物 -> 加判断推送 的流程,ctrl c v 就行了

dockerfile

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
41
42
43
44
45
#FROM openjdk:8-jre
FROM openjdk:8-jdk

# docker build 时传入的变量
ARG BUILD_JAR_NAME

# 配置JVM参数
ENV BASE_DIR="/app" \
JAVA_HOME="/usr/local/openjdk-8/" \
JAVA="/usr/local/openjdk-8/bin/java" \
JVM_XMS="8g" \
JVM_XMX="8g" \
JVM_XMN="3g" \
JVM_MS="128m" \
JVM_MMS="320m" \
TZ="Asia/Shanghai" \
BUILD_JAR_NAME=$BUILD_JAR_NAME

WORKDIR $BASE_DIR

# 测试环境配置 部署时注释掉 从rancher配置
# 如果使用 请换成你自己的ip 和 密码
ENV MYSQL_HOST=127.0.0.1 \
MYSQL_PORT=3306 \
MYSQL_SERVICE_DB_NAME=root \
MYSQL_SERVICE_PASSWORD=123456

# 时区配置 打印该模块路径 及 文件名
RUN rm -f /etc/localtime \
&& ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& echo "${BUILD_JAR_NAME}" \
&& JAR_FILE_NAME=$(basename "${BUILD_JAR_NAME}") \
&& echo "The jar file name is: ${JAR_FILE_NAME}"
# 注意 RUN命令的运行结果变量JAR_FILE_NAME 是局部变量 到下一层读取的话是 null
# 所以我直接将 docker build 传入的 BUILD_JAR_NAME 变量作为环境变量,在启动脚本 startup.sh 中处理了


# 拷贝jar包 及 脚本
COPY $BUILD_JAR_NAME ./startup.sh $BASE_DIR/

RUN chmod +x startup.sh

ENTRYPOINT ["sh","startup.sh"]

startup.sh

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
#!/bin/sh

# 打印启动的文件名
JAR_FILE_NAME=$(basename "${BUILD_JAR_NAME}")
echo "> JAR_FILE_NAME: ${JAR_FILE_NAME}"

JAVA_OPT="${JAVA_OPT} -server -Xms${JVM_XMS} -Xmx${JVM_XMX} -Xmn${JVM_XMN} -XX:MetaspaceSize=${JVM_MS} -XX:MaxMetaspaceSize=${JVM_MMS}"
# debug 模式下参数配置(传输、端口号、调试服务器、不在 JVM 启动时暂停,而是等待调试器连接后再开始执行。)
if [ "${MODE_DEBUG}" = "y" ]; then
JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=12345,server=y,suspend=n"
fi
# 异常处理机制 及 禁用大页面
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${BASE_DIR}/logs/java_heapdump.hprof"
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages"

# 垃圾回收 日志 轮换文件大小限制
mkdir -p "${BASE_DIR}/logs" && touch "${BASE_DIR}/logs/gc.log"
JAVA_OPT="${JAVA_OPT} -Xloggc:${BASE_DIR}/logs/gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M"

# 配置文件编码 Redis等ip和密码 启动jar包
JAVA_OPT="${JAVA_OPT} -Dfile.encoding=utf-8"
#JAVA_OPT="${JAVA_OPT} -DMYSQL_HOST=${MYSQL_HOST} -DMYSQL_PORT=${MYSQL_PORT} -DMYSQL_SERVICE_DB_NAME=${MYSQL_SERVICE_DB_NAME} -DMYSQL_SERVICE_PASSWORD=${MYSQL_SERVICE_PASSWORD}"
JAVA_OPT="${JAVA_OPT} -jar ${BASE_DIR}/${JAR_FILE_NAME}"

echo "This server is starting, you can docker logs your container"

echo ${JAVA_OPT}

exec $JAVA ${JAVA_OPT}