Jenkins持续集成

在本小节中,我们将讨论持续集成。

持续集成指的是: 频繁地(一天多次)将代码集成到主干。这里的集成不止是代码合并,还要保证可以通过编译、单元测试、集成测试。

持续集成的主要优点是:

  • 快速发现错误。即所谓的”早发现、早治疗”。
  • 减少开发、主分支之间的冲突。更频繁的分支合并,可以降低分支冲突发生的概率。
  • 为持续部署打下基础。代码先要能集成起来,才能够进一步地部署。

针对我们当前的架构,持续集成分为如下几个步骤:

  • 从Gerrit上check out代码
  • Gradle编译项目
  • 打包Docker镜像

部署Salve打包机

在持续构建的过程中,需要迁出代码、编译、打Docker镜像等步骤,如果都放在Jenkins的容器内执行,存在一些缺点:

  • 影响Jenkins主服务性能。编译、打镜像都是非常耗费系统资源的操作。如果放到Jenkins上执行,势必会影响服务的流畅性和稳定性。
  • 耦合过紧,不方便升级维护。如果要Jenkins支持打包、编译,需要自己定制镜像,即在Jenkins基础上安装Gradle、Java等工具。如果将来任何一个工具或Jenkins需要升级版本,就需要重新开发镜像,非常繁琐。

因此,通常都会新建一个独立于Jenkins的打包机,里面集成编译打包工具。Jenkins将打包机作为Slave添加到系统中,在打包时将调用Slave机器执行打包任务。

Slave打包机可以采用物理机、虚拟机或者容器,这里我们选择容器的方式,主要优点有:

  • 故障恢复,方便运维。编译、打包的复杂程度较高,经常会导致打包机挂起,采用容器的方式,可以快速恢复故障。
  • 版本可控,方便升级。持续集成的工具链需要逐步升级。例如Java、Gradle版本,每年都会更新几次,采用容器的方式,可以更好地管理版本,更精细地控制打包流程。
  • 资源使用可控。编译、打包耗费较大资源,有时候为了保证整个系统稳定性,要限制打包使用的CPU、内存资源,使用容器技术可以轻松地做到这一点。
  • 启动快速,方便扩展。随着持续集成的规模逐渐扩大,要同时执行多个任务,甚至要在打包高峰期,动态启动若干Slave,以提升并行度。这类似于微服务的副本扩展,容器集群(如k8s)对这种副本拓展有很好地支持。

我们首先来构建一个打包机的镜像,Dockerfile如下:

  1. FROM ubuntu:18.04
  2. # apt-add-repostory zip unzip git
  3. RUN apt-get update
  4. RUN apt-get install -y apt-utils software-properties-common zip unzip git
  5. # SSH
  6. RUN apt-get install -y openssh-server \
  7. && mkdir /var/run/sshd \
  8. && sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config
  9. # Java
  10. ENV JAVA_HOME /usr/lib/jvm/java-8-oracle
  11. RUN \
  12. echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
  13. add-apt-repository -y ppa:webupd8team/java && \
  14. apt-get update && \
  15. apt-get install -y oracle-java8-installer
  16. # Gradle
  17. ENV GRADLE_HOME /opt/gradle
  18. ENV GRADLE_VERSION 4.10
  19. ARG GRADLE_DOWNLOAD_SHA256=248cfd92104ce12c5431ddb8309cf713fe58de8e330c63176543320022f59f18
  20. RUN set -o errexit -o nounset \
  21. && echo "Downloading Gradle" \
  22. && wget --no-verbose --output-document=gradle.zip "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
  23. \
  24. && echo "Checking download hash" \
  25. && echo "${GRADLE_DOWNLOAD_SHA256} *gradle.zip" | sha256sum --check - \
  26. \
  27. && echo "Installing Gradle" \
  28. && unzip gradle.zip \
  29. && rm gradle.zip \
  30. && mv "gradle-${GRADLE_VERSION}" "${GRADLE_HOME}/" \
  31. && ln --symbolic "${GRADLE_HOME}/bin/gradle" /usr/bin/gradle
  32. # Create User
  33. RUN useradd -m build
  34. RUN echo "build:build123" | chpasswd
  35. # Docker ce
  36. RUN apt-get install -y \
  37. apt-transport-https \
  38. ca-certificates \
  39. curl \
  40. software-properties-common
  41. RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
  42. RUN add-apt-repository \
  43. "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
  44. $(lsb_release -cs) \
  45. stable"
  46. RUN apt-get install -y docker-ce
  47. RUN usermod -aG docker build
  48. # Clean
  49. RUN apt-get remove -y apt-utils software-properties-common && \
  50. apt-get autoremove -y && \
  51. apt-get clean && \
  52. rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/oracle-jdk8-installer
  53. EXPOSE 22
  54. # SSH Daemon
  55. CMD ["/usr/sbin/sshd", "-D"]

如上所示,上述镜像主要完成了以下功能:

  • JDK 8的安装
  • SSHD服务的安装
  • Gradle 4.10的安装
  • build用户(密码build123)的配置
  • Docker的安装和配置(这里是Docker inside Docker,我们只安装可执行文件,通过volulme映射使用物理机的docker)

有了上述镜像后,我们启动这个slave容器:

  1. #!/bin/bash
  2. # BUILD
  3. docker build -t slave .
  4. NAME="slave"
  5. # submit to local docker node
  6. docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
  7. docker run \
  8. --name $NAME \
  9. -v /var/run/docker.sock:/var/run/docker.sock \
  10. -p 22 \
  11. --detach \
  12. --restart always \
  13. -d slave:latest

接下来,我们在Jenkins中添加这台slave机器。

使用管理员帐号登录,”Manage Jenkins” -> “Manage Nodes” -> “New Node”,然后如下图所示配置:

Jenkins配置Slave机器

主要的配置为:

  • Remote root directory 家目录/home/build
  • Usage 尽可能多使用(尽量不占用master进程)
  • Launch method 使用ssh模式
    • Host slave即上面启动的容器
    • Credentials 可以新增一个密码方式的验证build/build123
  • Availability 尽量让slave在线

配置成功默认是离线的,稍等一会,会提示”slave已经上线”。

持续集成第一步:迁出代码、编译

本节开篇已经提到,持续集成的第一步即从代码仓库中迁出代码,我们来完成这项工作。

首先在gerrit上准备一个项目,假设为lmsia-xyz,这是一个最简单的Spring Boot项目。

为了能够提交、迁出代码,需要将公钥配置到gerrit上,点击右上角的名字 -> Setting -> SSH Public Keys,填入即可完成。

准备好项目后,我们在Jenkins上新建一个”Freestyle”项目,命名为lmsia-xyz-build。

首先配置代码仓库,如下图所示:

Jenkins配置Gerrit权限

  • 在”Source Code Management”中,选择”Git”,并填写gerrit的repo地址
  • Credentials中新增一个用户,为gerrit中的用户,要填写私钥

此外,还要限制只能在slave上执行: Restrict where this project can be run中设置”slave”。

完成后点击底部的Save。

配置好后,我们执行第一次Build,在项目左侧菜单选择”Build Now”,可以在Log中查看输出如下:

  1. Building remotely on slave in workspace /home/build/workspace/lmsia-xyz-build
  2. Cloning the remote Git repository
  3. Cloning repository ssh://lihy@10.1.64.72:29418/lmsia-xyz
  4. > git init /home/build/workspace/lmsia-xyz-build # timeout=10
  5. Fetching upstream changes from ssh://lihy@10.1.64.72:29418/lmsia-xyz
  6. > git --version # timeout=10
  7. using GIT_SSH to set credentials
  8. > git fetch --tags --progress ssh://lihy@10.1.64.72:29418/lmsia-xyz +refs/heads/*:refs/remotes/origin/*
  9. > git config remote.origin.url ssh://lihy@10.1.64.72:29418/lmsia-xyz # timeout=10
  10. > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10
  11. > git config remote.origin.url ssh://lihy@10.1.64.72:29418/lmsia-xyz # timeout=10
  12. Fetching upstream changes from ssh://lihy@10.1.64.72:29418/lmsia-xyz
  13. using GIT_SSH to set credentials
  14. > git fetch --tags --progress ssh://lihy@10.1.64.72:29418/lmsia-xyz +refs/heads/*:refs/remotes/origin/*
  15. > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
  16. > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
  17. Checking out Revision eab8a79ff6cde375c017b6f9eec29dff02a0bb85 (refs/remotes/origin/master)
  18. > git config core.sparsecheckout # timeout=10
  19. > git checkout -f eab8a79ff6cde375c017b6f9eec29dff02a0bb85
  20. Commit message: "MOD: init commit"
  21. First time build. Skipping changelog.
  22. Finished: SUCCESS

如上所示,我们成功地从代码仓库迁出了代码,第一步顺利完成!

在迁出代码后,我们需要进行编译,回到lmsia-xyz-build项目的配置中,找到Build选项,新增一个”Execute shell”步骤,命令输入”gradle build”,点击底部”Save”。

再次执行”Build Now”,发现项目依然执行成功,查看日志,可以发现编译也成功地执行了!

至此,我们已经完成了代码的迁出和编译。

打包镜像并上出到私有仓库

在lmsia-xyz-build项目中新建一个Shell步骤,内容如下:

  1. $HOME/ms2docker.sh

上述脚本需要添加到slave的镜像中,直接COPY即可,这里不再赘述。

脚本内容如下:

  1. #!/bin/bash
  2. set -e
  3. # Const
  4. DOCKER_REGISTRY="10.1.64.72"
  5. PROJECT_VERSION=${BUILD_NUMBER:-1}
  6. PROJECT_NAME=$(basename `pwd`| sed -r 's/-build$//g')
  7. SERVER_NAME="$PROJECT_NAME-server"
  8. JAR_NAME="$SERVER_NAME.jar"
  9. DOCKER_FULLNAME="$PROJECT_NAME:$PROJECT_VERSION"
  10. # Copy Jar
  11. find . -name "$SERVER_NAME*.jar" -exec cp {} ./$JAR_NAME \;
  12. # Generate Dockerfile
  13. cat > ./Dockerfile <<EOF
  14. FROM anapsix/alpine-java:8_server-jre
  15. VOLUME /tmp /app
  16. WORKDIR /app
  17. EXPOSE 8080 3000
  18. COPY ${JAR_NAME} /app
  19. CMD ["java", "-jar", "/app/${JAR_NAME}"]
  20. EOF
  21. # Build
  22. docker build .
  23. docker build -t $PROJECT_NAME .
  24. docker tag $PROJECT_NAME $DOCKER_REGISTRY/$DOCKER_FULLNAME
  25. docker push $DOCKER_REGISTRY/$DOCKER_FULLNAME

简单解释一下:

  • 首先获得项目名称(根据当前执行的工作文件夹)
  • 查找生成的server.jar,并拷贝到当前目录
  • 编译一个Docker的镜像,包含Java环境和server的jar包
  • 上传Docker镜像到私有仓库

在Jenkins配置好后,点击保存,重新打包。

看一下结果:

  1. ....
  2. The push refers to repository [10.1.64.72/lmsia-xyz]
  3. ce4c6e84ae9a: Preparing
  4. c24b758e34d0: Preparing
  5. c28e906f67c9: Preparing
  6. cd7100a72410: Preparing
  7. c28e906f67c9: Layer already exists
  8. c24b758e34d0: Layer already exists
  9. cd7100a72410: Layer already exists
  10. ce4c6e84ae9a: Pushed
  11. 6: digest: sha256:86ccfb07945bdaf61c90470e3302774cde31d41b6c1a26647ea92fdb681536b3 size: 1158
  12. Finished: SUCCESS

如上所述,已经成功地打好Docker镜像并上传到私有仓库中。

至此,我们经完成了持续集成的所有步骤,它包含:

  • 从gerrit上迁出代码到本地(slave机器)
  • 调用gradle编译
  • 使用Docker打镜像并上传到私有仓库

拓展与思考

  1. 在开发过程中,微服务会频繁的打包、持续集成。这会产生大量历史镜像文件,这些历史版本并不会被使用,却浪费了大量的磁盘空间。请自行查找资料,实现”只保留最近3个最新项目镜像”这一功能。
  2. 本节的”持续集成”,主要指项目的编译部分。然而,在实际项目中,还需要在集成阶段进行单元测试、集成测试。如何在”持续集成”阶段完成测试工作,请结合实际情况思考这一问题。