转载

用 Docker, maven, jenkins 完成 CI

随着特征分支的演进以及git使用的增加,对于分支的持续集成成了基础架构的梦魇。Docker 可以用来消除到同一远程服务器部署生产和构建集成测试的需要。规模控制可通过 jenkins 的 slaves 同时运行一个或者多个任务来实现。

问题

Git 将主干分支合并变得简单化。一个对待开发的正常方法流程是根据每个需求特点各建立一个特征分支,当分支需求开发测试完成,即将特征分支合并到主干。对于单元测试,现在已经有了现成可行的工具可帮助你在合并分支到主干前进行自动化测试特征分支。Travis-ci 已经提供了这样的工具到 Github 工程,推荐前去尝试!

集成测试仍然存在问题。当你需要一个运行环境来进行复杂的集成测试,管理配置这些各不相同的分支和环境会变得有点困难。你可以选择为不同的特征分支分别建立虚拟机进行来回的切换测试或者尝试让这些分支共享一个环境配置。或者最糟糕的选择:集成到同一分支后再进行集成测试。这个选择的问题在于它违背了“无损害”的原则:当你知道它不必去拆分内容时你会去完成分支的合并,但现在却有了一个合并的分支却注定需要拆分?对我来说这是很不好的设计。

部署集成的旧方式

部署一个测试环境通常就意味着在服务器上部署一个新版本的应用。其他内容都需要准备就绪。我以一个以java为基础的CMS作为例子:Hippo. Hippo 包括两个 WAR 文件:cms.war 和 site.war. 这两个 WARs 通过共享一些公有的 jars 包部署到 tomcat 实例。如果你是基于标准 maven 原型构建的工程,构建过程将会产生一个压缩包,你可以顺利的将它解压到 tomcat 的运行目录。以下我运行于 jenkins 的 deploy.sh 脚本的伪代码:

  • 将工程压缩包通过 SCP 上传到服务器
  • ssh 登录到服务器
  • 停止 tomcat 进程
  • 删除 "work", "webapps", "shared", 和"common" 目录
  • 将上传的压缩包解压到 tomcat 目录
  • 启动 tomcat

操作完,我们将等待 tomcat 的启动部署 webapps. 这样的过程包含了将一个脚本复制到服务器上去运行检查在 catalina.out 文件中是否存在 "server startup in xxx" 字符串这样一个步骤。一个流程下来有些步骤需要 root 权限。虽然这些都是能够实现的并已经有很大一部分的实践先例,但增加了复杂程度,而且对于一个简单的集成化测试目标,显得也有些呆板。

Docker

Docker 能够实现在一个机器上将多个进程分别运行于各自独立的容器,而无需大量的虚拟机。它可以让进程隔离,对于进程来说就像各自运行在自己独立的环境中。 Docker 基于 Go 语言开发,是 LXC 的接口,在 linux 内核 3.8 版本发布时作为一个新的功能点。

Docker 在 ubuntu 系统中运行顺利,红帽也准备好兼容 Docker.(实际已经实现)

Docker 可以让你用 Dockerfile 文件去启动特定的容器,这些特点可以用来与适用于虚拟机的 vagrantfile 进行比较。这些 docker 文件可以由其他的 docker 文件组成,建立一种继承/复合的容器。

基于 Docker 构建集成服务器

我在 Github 上创建一个示例工程,演示了如何借助 docker 实现 Hippo 工程的集成测试。如果你使用 vagrant, 你可以发现在工程的根目录下的 vagrant 启动 jenkins 服务器。借助于 Dockerfile 的命令,我建立了一个 jdk 7 的镜像,可以作为 tomcat 的基础镜像。我又用这个 tomcat 镜像作为集成的环境基础。这个用于构建集成镜像的 Dockerfile 放置在工程的根目录下,内容如下:

FROM wouterd/tomcat
MAINTAINER Wouter Danes "https://github.com/wouterd"
ADD myhippoproject.tar.gz /tmp/app-distribution/
RUN for i in $(ls /tmp/app-distribution/) ; do mkdir -p /var/lib/tomcat6/${i} && cp -f /tmp/app-distribution/${i}/* /var/lib/tomcat6/${i}/ ; done

wouterd/tomcat 镜像构建如下:

FROM        wouterd/oracle-jre7
VOLUME ["/var/log/tomcat6"]
MAINTAINER Wouter Danes "https://github.com/wouterd"
RUN apt-get install -y tomcat6
CMD JAVA_HOME=/usr/lib/jvm/java-7-oracle CATALINA_BASE=/var/lib/tomcat6 CATALINA_HOME=/usr/share/tomcat6 /usr/share/tomcat6/bin/catalina.sh run
EXPOSE 8080

wouterd/oracle-jre7 是在纯净的 ubuntu 系统上安装 jdk 7 的镜像。这些代码都在 Github 工程 docker-images 目录中。wouterd/tomcat 镜像完成了以下这些内容:

  • FROM wouterd/oracle-jre7 表示以 wouterd/oracle-jre7 为基础镜像(包括了 ubuntu + oracle java 7)
  • VOLUME ["/var/log/tomcat6"] 告诉容器暴露这个文件路径给外界。Docker 实际上 "物理地" 将这个路径放置在这个容器外部从而使其他容器可以共享到这个文件。等会儿我会展示这将是一个很好的设计。
  • RUN apt-get install -y tomcat6 通过 ubuntu 维护的库安装 tomcat6
  • CMD [some bash] 设置容器运行时执行的命令,在这个例子中设置了两个环境变量,并运行 catalina.sh 脚本
  • EXPOSE 8080 暴露容器 8080 端口到宿主机

docker 镜像在集成测试完成两件事的过程中得到创建(不知道怎么翻译合适):

  • 得到 wouterd/tomcat 镜像
  • ADD [file] [destination] 将 maven 工程的压缩包复制到临时的目录,并进行解压
  • RUN 命令是一个比较曲折的方法去复制压缩包中的内容到 TOMCAT_HOME 目录,由于简单的添加压缩包到 TOMCAT_HOME 可能不能正确的解压。如果 TOMCAT_HOME 目录是空的,这个命令将会执行。
  • 将 wouterd/tomcat 镜像的 CMD 命令继承下来,所以当你使用 docker run 命令启动集成的容器,它将会启动 tomcat 并开始部署。

用容器实现持续集成

既然我们已经准备好建立集成服务器需要的一切,我们可以开始将他们汇总在一起进行构建。我将使用 jenkins 作为构建服务器,但完全可以用你自己的构建服务器进行替换,像 Go, Hudson 或者 Bamboo。集成测试也可以使用 mvn 运行,如果你是使用不低于 3.8 版本内核的 linux 系统或者 boot2docker-cli 的 MacOS X 系统。参考以下的工程要求部分,按照要求的步骤和软件在你自己的机器上运行 mvn。

用 boot2docker 测试

为了能够用 boot2docker 完成集成测试,你需要在命令行设置 -Dboot2docker=[IP-of-boot2docker-vm],如:mvn verify -Dboot2docker=192.168.59.103. 接下来你将看到怎么为 boot2docker 虚拟机设置 IP, 你需要设置的是 eth1 的 IP:

Wouters-MacBook-Pro-2:hippo-docker wouter$ boot2docker-cli ssh
Warning: Permanently added '[localhost]:2022' (RSA) to the list of known hosts.
docker@localhost's password:
## .
## ## ## ==
## ## ## ## ===
/""""""""""""""""/___/ ===
~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~
/______ o __/
/ / __/
/____/______/
_ _ ____ _ _
| |__ ___ ___ | |_|___ / __| | ___ ___| | _____ _ __
| '_ / / _ / / _ /| __| __) / _` |/ _ / / __| |/ / _ / '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__| < __/ |
|_.__/ /___/ /___/ /__|_____/__,_|/___/ /___|_|/_/___|_|
boot2docker: 0.8.0
docker@boot2docker:~$ ifconfig
docker0 Link encap:Ethernet HWaddr 56:84:7A:FE:97:99
inet addr:172.17.42.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::5484:7aff:fefe:9799/64 Scope:Link
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:71349 errors:0 dropped:0 overruns:0 frame:0
TX packets:119482 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:3000501 (2.8 MiB) TX bytes:169514559 (161.6 MiB)

eth0 Link encap:Ethernet HWaddr 08:00:27:F6:4F:CB
inet addr:10.0.2.15 Bcast:10.0.2.255 Mask:255.255.255.0
inet6 addr: fe80::a00:27ff:fef6:4fcb/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:415492 errors:0 dropped:0 overruns:0 frame:0
TX packets:125189 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:603256397 (575.3 MiB) TX bytes:7233415 (6.8 MiB)

eth1 Link encap:Ethernet HWaddr 08:00:27:35:F0:76
inet addr:192.168.59.103 Bcast:192.168.59.255 Mask:255.255.255.0
inet6 addr: fe80::a00:27ff:fe35:f076/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:591 errors:0 dropped:0 overruns:0 frame:0
TX packets:83 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:94986 (92.7 KiB) TX bytes:118562 (115.7 KiB)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:40 errors:0 dropped:0 overruns:0 frame:0
TX packets:40 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:8930 (8.7 KiB) TX bytes:8930 (8.7 KiB)

你可以通过在工程根目录下运行 vangrant up 创建 jenkins 服务器。在初始化和启动之后,可通过 http://localhost:8080 访问 jenkins.

在构建过程中,maven 工程完成了以下这些内容:

  • compile 编译工程
  • test 执行单元测试
  • package 建立压缩包
  • pre-integration-test 创建 docker 镜像并启动新容器,等待 tomcat 完成启动
  • integration-test 用 junit, webdriver, phantomjs 在容器中完成集成测试
  • post-integration-test 停止并删除容器以及删除创建的镜像文件

所有有趣的部分在 "myhippoproject" 工程中的 "integrationtests" 模块:

pre-integration-test 和 post-integration-test 是通过 exec-maven-plugin 插件运行 shell 脚本完成的。其实已经有了很好的 docker api java 客户端和 docker 的 maven 插件可以使用,但在我编写这些脚本时还没发现。以下是启动脚本。这个脚本可用不超过10行的 java 代码实现。

#!/bin/bash

if [[ ${BOOT_2_DOCKER_HOST_IP} ]] ; then
echo "Boot2Docker specified, this will work if you use the new boot2docker-cli VM.."
boot2docker='yes'
docker_run_args='-p 8080'
else
boot2docker=''
docker_run_args=''
fi

set -eu

work_dir="${WORK_DIR}"
docker_file="${DOCKER_FILE_LOCATION}"
distribution_file="${DISTRIBUTION_FILE_LOCATION}"
docker_build_dir="${work_dir}/docker-build"

mkdir -p ${work_dir}

mkdir -p ${docker_build_dir}

cp ${docker_file} ${distribution_file} ${docker_build_dir}/

image_id=$(docker build --rm -q=false ${docker_build_dir} | grep "Successfully built" | cut -d " " -f 3)
echo ${image_id} > ${work_dir}/docker_image.id

rm -rf ${docker_build_dir}

catalina_out="/var/log/tomcat6/catalina.$(date +%Y-%m-%d).log"

container_id=$(docker run ${docker_run_args} -d ${image_id})
echo ${container_id} > ${work_dir}/docker_container.id

container_ip=$(docker inspect --format '{{.NetworkSettings.IPAddress}}' ${container_id})

echo -n "Waiting for tomcat to finish startup..."

Give Tomcat some time to wake up...

while ! docker run --rm --volumes-from ${container_id} busybox grep -i -q 'INFO: Server startup in' ${catalina_out} ; do

sleep 5

echo -n "."

done

echo -n "done"

if [[ ${boot2docker} ]] ; then

# This Go template will break if we end up exposing more than one port, but by then this should be ported to Java

# code already (famous last words...)

tomcat_port=$(docker inspect --format '{{ range .NetworkSettings.Ports }}{{ range . }}{{ .HostPort }}{{end}}{{end}}' ${container_id})

tomcat_host_port="${BOOT_2_DOCKER_HOST_IP}:${tomcat_port}"

else

tomcat_host_port="${container_ip}:8080"

fi

echo ${tomcat_host_port} > ${work_dir}/docker_container.ip

针对远端运行docker这个脚本通过一定的技巧解决了端口问题(这里应该有问题),只不过是运行一些 docker 命令去建立镜像和启动容器。我使用 docker 自带功能去管理 tomcat 容器中的日志文件。通过 tomcat 容器中的 VOLUME 命令,我指定 /var/log/tomcat6 目录为数据卷,我可以通过 --volumes-from 启动一个新的 tomcat 容器来得到这个目录。以下这个命令在容器中执行 grep 来确定服务是否已经启动:docker run --rm --volumes-from ${container_id} busybox grep -i -q 'INFO: Server startup in' ${catalina_out} --rm 参数的作用是在启动容器后立即停止并删除容器,这个对于我们来说是好的,我们在服务启动之前将会执行很多次这样的操作。

这个脚本片段如下:

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<id>build-and-start-integration-server</id>
<goals>
<goal>exec</goal>
</goals>
<phase>pre-integration-test</phase>
<configuration>
<environmentVariables>
<WORK_DIR>${project.build.directory}/docker-work</WORK_DIR>
<DOCKER_FILE_LOCATION>${project.basedir}/src/main/assembly/Dockerfile</DOCKER_FILE_LOCATION>
<DISTRIBUTION_FILE_LOCATION>${project.build.directory}/myhippoproject.tar.gz</DISTRIBUTION_FILE_LOCATION>
</environmentVariables>
<executable>${project.basedir}/src/test/script/start-integration-server.sh</executable>
</configuration>
</execution>
</executions>
</plugin>

package 是使用 maven-assembly-plugin 插件去创建压缩包,所创建的压缩包将需要解压到 tomcat 容器中的 CATALINA_HOME 目录。

这个 integration-test 阶段是通过 webdriver 和 phantomis driver 实现的。但你可以使用你自己喜欢的测试工具用于集成测试。一个例子是使用集成的 apache CXF REST 代理去验证你创建的 rest 接口。

工程要求

  • 安装 docker
  • 安装 PhantomJS,设置好路径
  • 不要求 git,但需要能够得到这个工程
  • Maven 3.x
  • Java 7+ (Oracle JDK preferred)
  • 你需要运行 ./build-docker-images.sh 来创建 jdk 7 和 tomcat 镜像,才能进行集成测试

原文:

正文到此结束
Loading...