持续集成案例学习:Docker、Java与Maven
在Alooma,我们非常非常非常喜爱Docker。真的, 我们想完全容器化我们的应用。 虽然容器化应用有非常多的好处,但在这里,我并不是要说服你用Docker。我们只是认为你和我们一样喜欢这东西。
接下来,让我们谈谈Alooma是如何在生产环境使用Docker来精简开发流程并快速push代码的。
概述
Docker允许你把你的基础架构当作代码一样来对待。这个代码就是你的Dockerfile。
像其它代码一样,我们想要使用一个紧密的改变->提交->构建->测试
的周期(一个完整的持续集成解决方案)。为了实现这个目标,我们需要构建一个流畅的DevOps流水线。
让我们把目标分解为更加详细的需求:
- 在版本控制系统中管理
Dockerfile
- 在CI服务器上为每个commit构建Docker镜像
- 上传构件并打标签(这个构件要能够简单的部署)
我们的工作流
我们的DevOps流水线围绕GitHub、Jenkins和Maven构建。下面是它的工作流程:
- GitHub将repo的每一个push通知给Jenkins
- Jenkins触发一个Maven build
- Maven 构建所有的东西,包括Docker镜像
- 最后,Maven会把镜像推送到私有的Docker Registry。
这个工作流的好处是它允许我们能够很容易的为每个发布版本打标签(所有的commit都被构建并且在我们的Docker Registry中准备好了)。然后我们可以非常容易地通过pull和run这些Docker镜像进行部署。
事实上这个部署过程是非常简单的,我们通过发送一个命令给我们信任的Slack机器人:”Aloominion”(关于我们的机器人朋友的更多情况将在未来的文章中发表)开始这个过程。
你可能对这个工作流中的其他元素非常熟悉,因为它们都很常见。所以,让我们来深入了解如何使用Maven构建Docker镜像。
深入Docker 构建
Alooma是一个Java公司。我们已经使用Maven作为我们构建流水线的中心工具,所以很自然的想到把构建Docker的过程也加入到我们的Maven构建过程中去。
当搜索和Docker交互的Maven插件时,出现了3个选项。我们选择使用Spotify的maven-docker-plugin —— 虽然rhus的和alexec的同名插件看起来也是一个不错的选择。
另一个我们的构建计划依赖的Maven插件是maven-git-commit-id-plugin。我们使用这个插件,所以我们的Docker镜像能使用git的commit ID来打标签 —— 这在部署过程中非常有帮助,我们可以了解运行的是哪个版本。
给我看代码!
每一个docker镜像有它自己的Maven模块(所有上面提到的docker-maven 插件在一个模块一个Dockerfile时都能顺利地工作)
让我们从Spotify插件的一个简单配置开始:
<plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.basedir}</dockerDirectory> <imageName>alooma/${project.artifactId}</imageName> </configuration> </plugin>
我们看到这里我们把插件的build目标和Maven的package阶段绑定,我们也指导它去在我们模块的根目录下来寻找Dockerfile(使用dockerDirectory 元素来指定),我们还把镜像名称用它的构件Id来命名(用”alloma/”做前缀)。
我们注意到的第一件事情是这个镜像没有被push到任何地方,我们可以通过加入<pushImage>true</pushImage>到配置中来解决这个问题。
但是现在这个镜像会被push到默认的Docker Hub Registry上。糟糕。
为了解决这个问题,我们定义了一个新的Maven属性<docker.registry>docker-registry.alooma.io:5000/</docker.registry>并且把镜像名称imageName
改为${docker.registry}alooma/${project.artifactId}。 你可能会想,“为什么需要为Docker Registry设置一个属性?”, 你是对的!但是有这个属性可以使我们在Regsitry URL改变的时候能够更方便的修改。
有一个更重要的事情我们还没有处理——我们想让每一个镜像用它的git commit ID来打标签。这可以通过改变imageName为${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev}来实现。
${git.commit.id.abbrev}属性是通过我上面提到的maven-git-commit-id-plugin
插件来实现的。
所以,现在我们的插件配置看起来像下面这样:
<plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.basedir}</dockerDirectory> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> <pushImage>true</pushImage> </configuration> </plugin>
我们的下一个挑战是在我们的pom.xml
中表达我们的Dockerfile
的依赖。一些我们的Docker镜像在构建时使用了 FROM
其它的Docker 镜像作为基础镜像(也在同一个构建周期中构建)。例如,我们的webgate
镜像(是我们的机遇Tomcat的WebApp)基于我们的base
镜像(包含Java 8、更新到最新的 apt-get、等等)。
这些镜像在同一个构建过程中构建意味着我们不能简单的使用FROM docker-registry.alooma.io/alooma/base:some-tag因为我们需要这个标签编程当前构建的标签(即 git commit ID)。
为了在Dockerfile
中获得这些属性,我们使用了Maven的resource filtering功能。这在一个资源文件中替换Maven 的属性。
<resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource>
在Dockerfile的内部我们有一个这样的FROM
:
FROM ${docker.registry}alooma/base:${git.commit.id.abbrevs}
一些更多的事情…….我们需要的是我们的配置来找到正确的Dockerfile(过滤过之后的),这可以在target/classes文件夹内找到,所以我们把dockerDirectory改为${project.build.directory}/classes。
这意味着现在我们的配置文件长这样:
<resources> <resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.build.directory}/classes</dockerDirectory> <pushImage>true</pushImage> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> </configuration> </plugin> </plugins> </pluginManagement>
此外,我们还要添加base
构件作为webgate
模块的一个Maven依赖来保证正确的Maven构建顺序。
但是我们还有另一个挑战:我们如何把我们编译和打包了的源文件添加到我们的Docker镜像中呢?我们的Dockerfile依赖于很多其它文件,它们通过ADD
或COPY
命令插入。(你可以在这里读到更多的关于Dockerfile的指导。)
为了让这些文件可以被获取,我们需要使用插件配置的resources
标签。
<resources> <resource> <targetPath>/</targetPath> <directory>${project.basedir}</directory> <excludes> <exclude>target/**/*</exclude> <exclude>pom.xml</exclude> <exclude>*.iml</exclude> </excludes> </resource> </resources>
注意到我们排除了一些文件。
记住这个resources
标签不应该和通常的Maven resources
标签弄混,看看下面的例子,它来自于我们的pom.xml的一部分:
<resources> <!-- general Maven resources --> <resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.build.directory}/classes</dockerDirectory> <pushImage>true</pushImage> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> <resources> <!-- Dockerfile building resources --> <resource> <targetPath>/</targetPath> <directory>${project.basedir}</directory> <excludes> <exclude>target/**/*</exclude> <exclude>pom.xml</exclude> <exclude>*.iml</exclude> </excludes> </resource> </resources> </configuration> </plugin> </plugins> </pluginManagement>
前一个添加在我们想添加一些静态资源到镜像时工作,但是如果我们想要添加一个在同一个构建中构建的构件时需要更多的调整。
例如,我们的webgate
Docker镜像包含了我们的webgate.war
,这是由另一个模块构建的。
为了添加这个war作为资源,我们首先必须把它作为我们的Maven依赖加进来,然后使用maven-dependency-plugin
插件的copy
目标来把它加到我们当前的构建目录中。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <goals> <goal>copy</goal> </goals> <configuration> <artifactItems> <artifactItem> <groupId>com.alooma</groupId> <artifactId>webgate</artifactId> <version>${project.parent.version}</version> <type>war</type> <outputDirectory>${project.build.directory}</outputDirectory> <destFileName>webgate.war</destFileName> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin>
现在这允许我们简单的把这个文件加到Docker插件的resources中去。
<resources> <resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.build.directory}/classes</dockerDirectory> <pushImage>true</pushImage> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> <resources> <resource> <targetPath>/</targetPath> <directory>${project.basedir}</directory> <excludes> <exclude>target/**/*</exclude> <exclude>pom.xml</exclude> <exclude>*.iml</exclude> </excludes> </resource> <rescource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>webgate.war</include> </rescource> </resources> </configuration> </plugin> </plugins> </pluginManagement>
我们需要做的最后一件事情是让我们的CI服务器(Jenkins)真的将镜像push到Docker Registry上。请记住本地构件默认是不会push镜像的。
为了push这些镜像,我们改变我们的<pushImage>标签的值从true
变为${push.image}
属性,这默认是被设置为false
,并且只会在CI服务器上设置为true
。(译注:这里的意思是,由于开发人员也要在本地构建然后测试之后才会提交,而测试的镜像不应该被提交到Registry,所以<pushImage>应该使用一个属性,默认为false,在CI服务器上覆盖为true在构建后去push镜像。)
这就完成了!让我们看一下最终的代码:
<resources> <resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.build.directory}/classes</dockerDirectory> <pushImage>${push.image}</pushImage> <!-- true when Jenkins builds, false otherwise --> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> <resources> <resource> <targetPath>/</targetPath> <directory>${project.basedir}</directory> <excludes> <exclude>target/**/*</exclude> <exclude>pom.xml</exclude> <exclude>*.iml</exclude> </excludes> </resource> <rescource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>webgate.war</include> </rescource> </resources> </configuration> </plugin> </plugins> </pluginManagement>
性能
这个过程有两个能够提高你的构建和部署的性能的改进地方:
- 让你的基础的机器镜像(在EC2的例子下是AMI)包含一些你的Docker镜像的基础版本。这样会使得
docker pull
只去pull那些改变了的层,即增量(相对于整个镜像来说要小得多)。 - 在Docker Registry的前端放一个Redis缓存。这可以缓存标签和元数据,减少和真实存储(在我们的例子下是S3)的回环。
我们现在已经使用这个构建过程一段时间了,并且对它非常满意。然而仍然有提高的空间,如果你有任何关于让这个过程更加流畅的建议,我很乐意在评论中听到你的想法。