持续集成案例学习:Docker、Java与Maven

简介: 本文讲的是持续集成案例学习:Docker、Java与Maven,【编者的话】对于使用Java技术栈的企业,Maven往往是其持续集成的核心工具,在当前的Docker化的运动中,要如何把Docker镜像的构建也加入到传统的Maven构建为基础的持续集成流程中呢?Alooma公司在本文中分享了他们使用Maven对Docker镜像构建进行持续集成的经验。
本文讲的是持续集成案例学习:Docker、Java与Maven 【编者的话】对于使用Java技术栈的企业,Maven往往是其持续集成的核心工具,在当前的Docker化的运动中,要如何把Docker镜像的构建也加入到传统的Maven构建为基础的持续集成流程中呢?Alooma公司在本文中分享了他们使用Maven对Docker镜像构建进行持续集成的经验。

在Alooma,我们非常非常非常喜爱Docker。真的, 我们想完全容器化我们的应用。 虽然容器化应用有非常多的好处,但在这里,我并不是要说服你用Docker。我们只是认为你和我们一样喜欢这东西。

接下来,让我们谈谈Alooma是如何在生产环境使用Docker来精简开发流程并快速push代码的。

概述

Docker允许你把你的基础架构当作代码一样来对待。这个代码就是你的Dockerfile。

像其它代码一样,我们想要使用一个紧密的 改变->提交->构建->测试 的周期(一个完整的持续集成解决方案)。为了实现这个目标,我们需要构建一个流畅的DevOps流水线。

让我们把目标分解为更加详细的需求:
  • 在版本控制系统中管理Dockerfile
  • 在CI服务器上为每个commit构建Docker镜像
  • 上传构件并打标签(这个构件要能够简单的部署)

我们的工作流

我们的DevOps流水线围绕GitHub、Jenkins和Maven构建。下面是它的工作流程:
  1. GitHub将repo的每一个push通知给Jenkins
  2. Jenkins触发一个Maven build
  3. Maven 构建所有的东西,包括Docker镜像
  4. 最后,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)的回环。

我们现在已经使用这个构建过程一段时间了,并且对它非常满意。然而仍然有提高的空间,如果你有任何关于让这个过程更加流畅的建议,我很乐意在评论中听到你的想法。

原文链接:Continuous Integration for Dockers: Case study (翻译:陈光)

原文发布时间为:2015-08-12
本文作者:Casgy
本文来自云栖社区合作伙伴DockerOne,了解相关信息可以关注DockerOne。
原文标题:持续集成案例学习:Docker、Java与Maven
目录
相关文章
|
17天前
|
NoSQL Java Linux
《docker高级篇(大厂进阶):2.DockerFile解析》包括:是什么、DockerFile构建过程解析、DockerFile常用保留字指令、案例、小总结
《docker高级篇(大厂进阶):2.DockerFile解析》包括:是什么、DockerFile构建过程解析、DockerFile常用保留字指令、案例、小总结
177 75
|
2天前
|
存储 监控 Java
JAVA线程池有哪些队列? 以及它们的适用场景案例
不同的线程池队列有着各自的特点和适用场景,在实际使用线程池时,需要根据具体的业务需求、系统资源状况以及对任务执行顺序、响应时间等方面的要求,合理选择相应的队列来构建线程池,以实现高效的任务处理。
77 12
|
5天前
|
存储 Ubuntu 关系型数据库
《docker基础篇:7.Docker容器数据卷》包括坑、回顾下上一讲的知识点,参数V、是什么、更干嘛、数据卷案例
《docker基础篇:7.Docker容器数据卷》包括坑、回顾下上一讲的知识点,参数V、是什么、更干嘛、数据卷案例
29 13
|
1月前
|
存储 缓存 监控
Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
本文介绍了Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
129 7
|
2月前
|
监控 前端开发 Java
【技术开发】接口管理平台要用什么技术栈?推荐:Java+Vue3+Docker+MySQL
该文档介绍了基于Java后端和Vue3前端构建的管理系统的技术栈及功能模块,涵盖管理后台的访问、登录、首页概览、API接口管理、接口权限设置、接口监控、计费管理、账号管理、应用管理、数据库配置、站点配置及管理员个人设置等内容,并提供了访问地址及操作指南。
|
2月前
|
jenkins Java 测试技术
如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例详细说明
本文介绍了如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例,详细说明了从 Jenkins 安装配置到自动构建、测试和部署的全流程。文中还提供了一个 Jenkinsfile 示例,并分享了实践经验,强调了版本控制、自动化测试等关键点的重要性。
88 3
|
2月前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
73 2
|
2月前
|
存储 Java 开发者
成功优化!Java 基础 Docker 镜像从 674MB 缩减到 58MB 的经验分享
本文分享了如何通过 jlink 和 jdeps 工具将 Java 基础 Docker 镜像从 674MB 优化至 58MB 的经验。首先介绍了选择合适的基础镜像的重要性,然后详细讲解了使用 jlink 构建自定义 JRE 镜像的方法,并通过 jdeps 自动化模块依赖分析,最终实现了镜像的大幅缩减。此外,文章还提供了实用的 .dockerignore 文件技巧和选择安全、兼容的基础镜像的建议,帮助开发者提升镜像优化的效果。
|
2月前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
47 2
|
2月前
|
存储 缓存 Java
Java应用瘦身记:Docker镜像从674MB优化至58MB的实践指南
【10月更文挑战第22天】 在容器化时代,Docker镜像的大小直接影响到应用的部署速度和运行效率。一个轻量级的Docker镜像可以减少存储成本、加快启动时间,并提高资源利用率。本文将分享如何将一个Java基础Docker镜像从674MB缩减到58MB的实践经验。
141 1