许多人正在使用容器包装他们的Spring Boot应用程序,而构建容器并不是一件容易的事。这是Spring Boot应用程序开发人员的指南,容器对于开发人员而言并非总是很好的抽象-它们会迫使您学习和思考非常低级的问题-但是有时您会被要求创建或使用容器,因此有必要了解这些基本要素。在这里,我们旨在向您展示一些您需要创建自己的容器时可以做出的选择。

我们将假定您知道如何创建和构建基本的Spring Boot应用程序。如果不这样做,请转到入门指南之一,例如有关构建REST服务的指南。从此处复制代码,并使用以下一些想法进行练习。

Docker上也有一个入门指南,这也是一个很好的起点,但是它没有涵盖我们这里所选择的范围,也没有详细介绍。

基本的Dockerfile

Spring Boot应用程序很容易转换为可执行的JAR文件。所有入门指南都这样做,从Spring Initializr下载的每个应用程序都将具有一个创建可执行JAR的构建步骤。和Maven./mvnw install和Gradle和你在一起./gradlew build。然后,在项目的顶层,运行该JAR的基本Dockerfile如下所示:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

JAR_FILE可在作为的一部分被传递docker命令(这将是Maven和摇篮不同)。例如,Maven:

$ docker build --build-arg JAR_FILE=target/*.jar -t myorg/myapp .

对于Gradle:

$ docker build --build-arg JAR_FILE=build/libs/*.jar -t myorg/myapp .

当然,一旦选择了构建系统,就不需要ARG-只需对jar位置进行硬编码即可。例如,Maven:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

然后我们可以简单地用

$ docker build -t myorg/myapp .

并像这样运行它:

$ docker run -p 8080:8080 myorg/myapp
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.2.RELEASE)

Nov 06, 2018 2:45:16 PM org.springframework.boot.StartupInfoLogger logStarting
INFO: Starting Application v0.1.0 on b8469cdc9b87 with PID 1 (/app.jar started by root in /)
Nov 06, 2018 2:45:16 PM org.springframework.boot.SpringApplication logStartupProfileInfo
...

如果您想在图像内部四处浏览,可以像这样打开其中的外壳(基本图像没有bash):

$ docker run -ti --entrypoint /bin/sh myorg/myapp
/ # ls
app.jar  dev      home     media    proc     run      srv      tmp      var
bin      etc      lib      mnt      root     sbin     sys      usr
/ #
我们在示例中使用的高山基础容器没有bash,因此这是一个ash外壳。它具有某些功能,bash但不是全部。

如果您有一个正在运行的容器,并且想窥视它,docker exec可以使用此方法:

$ docker run --name myapp -ti --entrypoint /bin/sh myorg/myapp
$ docker exec -ti myapp /bin/sh
/ #

其中myapp--name传递给docker run命令。如果您不使用--namedocker,则会为其分配一个助记符名称,您可以从的输出中抓取该助记符名称docker ps。您也可以使用容器的SHA标识符代替名称,该名称也可以从中看到docker ps

入口点

使用了Dockerfile的exec形式ENTRYPOINT因此没有包装Java进程的外壳。优点是java进程将响应KILL发送到容器的信号。例如,在实践中,这意味着,如果您是docker run本地图像,则可以使用停止它CTRL-C。如果命令行有点长,您可以COPY在运行它之前将其提取到shell脚本中并提取到映像中。例子:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY run.sh .
COPY target/*.jar app.jar
ENTRYPOINT ["run.sh"]

请记住使用exec java …​启动Java进程(以便它可以处理KILL信号):

run.sh

#!/bin/sh
exec java -jar /app.jar

入口点另一个有趣的方面是您是否可以在运行时将环境变量注入到Java进程中。例如,假设您希望具有在运行时添加java命令行选项的选项。您可以尝试执行以下操作:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","${JAVA_OPTS}","-jar","/app.jar"]

$ docker build -t myorg/myapp .
$ docker run -p 9000:9000 -e JAVA_OPTS=-Dserver.port=9000 myorg/myapp

这将失败,因为${}替换需要外壳程序。exec表单不使用外壳程序来启动进程,因此不会应用选项。您可以通过将入口点移动到脚本(如run.sh上面的示例),或在入口点中显式创建外壳来解决这个问题。例如:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]

然后,您可以使用以下命令启动该应用

$ docker run -p 8080:8080 -e "JAVA_OPTS=-Ddebug -Xmx128m" myorg/myapp
...
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.0.RELEASE)
...
2019-10-29 09:12:12.169 DEBUG 1 --- [           main] ConditionEvaluationReportLoggingListener :


============================
CONDITIONS EVALUATION REPORT
============================
...

(显示由Spring BootDEBUG生成的全部输出的一部分-Ddebug。)

ENTRYPOINT与上面的显式shell一起使用,意味着您可以将环境变量传递到java命令中,但是到目前为止,您还不能为Spring Boot应用程序提供命令行参数。此技巧无法在端口9000上运行应用程序:

$ docker run -p 9000:9000 myorg/myapp --server.port=9000
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.0.RELEASE)
...
2019-10-29 09:20:19.718  INFO 1 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080

它不起作用的原因是因为docker命令(该--server.port=9000部分)传递给了入口点(sh),而不是传递给了它启动的Java进程。要解决此问题,您需要将命令行从添加CMDENTRYPOINT

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar ${0} ${@}"]
$ docker run -p 9000:9000 myorg/myapp --server.port=9000
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.0.RELEASE)
...
2019-10-29 09:30:19.751  INFO 1 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 9000

注意${0}“命令”(在此情况下为第一个程序参数)和${@}“命令参数”(其余程序参数)的用法。如果您使用脚本作为入口点,则不需要${0}/app/run.sh在上面的示例中)。例子:

run.sh

#!/bin/sh
exec java ${JAVA_OPTS} -jar /app.jar ${@}

到目前为止,docker配置非常简单,并且生成的映像不是很有效。docker映像只有一个文件系统层,其中包含胖子罐,我们对应用程序代码所做的每一次更改都会更改该层,该层可能为10MB或更大(某些应用程序甚至多达50MB)。我们可以通过将JAR分为多个层来改善这一点。

较小的图像

请注意,以上示例中的基本图像为openjdk:8-jdk-alpine。这些alpine图像比Dockerhub中的标准openjdk库图像。尚无Java 11的正式高山图像(AdoptOpenJDK已有一段时间,但不再出现在其Dockerhub页面上)。您还可以通过使用“ jre”标签而不是“ jdk”在基本映像中节省大约20MB。并非所有的应用程序都可以与JRE一起使用(而不是JDK),但是大多数的应用程序都可以,并且确实有些组织会强制执行每个应用程序必须遵循的规则,因为存在滥用某些JDK功能(例如编译)的风险。

可以使您缩小图像的另一个技巧是使用JLink,它与OpenJDK 11捆绑在一起。JLink允许您从完整JDK中的模块子集构建自定义JRE分发,因此您不需要JRE或JDK在基本图像中。原则上,这将使您的总映像大小小于使用openjdk官方docker映像。实际上,您还无法使用alpine带有JDK 11的基本映像,因此您对基本映像的选择将受到限制,并且可能会导致最终映像的尺寸更大。另外,您自己的基本映像中的自定义JRE无法在其他应用程序之间共享,因为它们将需要不同的自定义。因此,对于所有应用程序来说,它们可能都有较小的映像,但是它们仍需要更长的启动时间,因为它们无法从缓存JRE层中受益。

最后一点突出了图像构建者的一个真正重要的关注点:目标不一定总是要构建尽可能小的图像。较小的图像通常是一个好主意,因为它们只需要花费较少的时间就可以上传和下载,但是前提是它们中的所有层都没有被缓存。如今,图像注册表非常复杂,通过尝试巧妙地构建图像,您很容易失去这些功能的优势。如果使用公共基础层,则图像的总大小将不再是问题,随着注册管理机构和平台的发展,图像的总大小可能甚至会减少。话虽如此,尝试并优化我们的应用程序映像中的各层仍然很重要且有用,但是目标始终应该是将变化最快的东西放在最高层中,

更好的Dockerfile

由于罐子本身的包装方式,Spring Boot胖子罐子自然具有“层”。如果我们先拆包,它将已经分为内部和外部依赖关系。要在Docker构建中一步一步做到这一点,我们需要先打开jar的包装。例如(坚持使用Maven,但Gradle版本非常相似):

$ mkdir target/dependency
$ (cd target/dependency; jar -xf ../*.jar)
$ docker build -t myorg/myapp .

有了这个 Dockerfile

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

现在共有3层,所有应用程序资源都位于后面的2层中。如果应用程序依存关系不变,则第一层(from BOOT-INF/lib)不会改变,因此构建会更快,并且只要基本层已被缓存,容器在运行时的启动也将更快。

我们使用了一个硬编码的主应用程序类hello.Application。对于您的应用程序,这可能会有所不同。如果需要,可以与另一个参数化ARG。您也可以将Spring Boot fat复制JarLauncher到映像中,并使用它来运行应用程序-它可以工作,并且您不需要指定主类,但是启动时会慢一些。

调整

如果您想尽快启动应用程序(大多数人都这样做),则可以考虑一些调整。这里有一些想法:

  • 使用spring-context-indexer指向docs的链接)。对于小型应用程序,它不会增加太多,但对您有所帮助。

  • 如果您负担不起,请不要使用执行器

  • 使用Spring Boot 2.1和Spring 5.1。

  • 使用(命令行参数或系统属性等)修复Spring Boot配置文件的位置spring.config.location

  • 关闭JMX-您可能不需要在容器中使用- spring.jmx.enabled=false

  • 使用运行JVM -noverify。也要考虑-XX:TieredStopAtLevel=1(这将在以后降低JIT速度,但会节省启动时间)。

  • 使用Java 8的容器内存提示:-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap。对于Java 11,默认情况下是自动的。

您的应用程序在运行时可能不需要完整的CPU,但需要多个CPU才能尽快启动(至少2、4个更好)。如果您不介意启动速度较慢,则可以将CPU的速度限制在4以下。如果您被迫以少于4个CPU的速度启动,则可能会有所帮助,-Dspring.backgroundpreinitializer.ignore=true因为它阻止了Spring Boot创建可能不会创建的新线程。能够使用(适用于Spring Boot 2.1.0及更高版本)。

多阶段构建

Dockerfile上述假设脂肪JAR已经建在命令行上。您也可以使用多阶段构建在Docker中执行此步骤,将结果从一个映像复制到另一个映像。使用Maven的示例:

Dockerfile

FROM openjdk:8-jdk-alpine as build
WORKDIR /workspace/app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src

RUN ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

第一个图像标记为“ build”,它用于运行Maven并构建胖罐,然后将其解压缩。拆包也可以由Maven或Gradle完成(这是《入门指南》中采用的方法)-确实没有太大区别,只是必须编辑构建配置并添加插件。

请注意,源代码已分为4层。较后的层包含构建配置和应用程序的源代码,较早的层包含构建系统本身(Maven包装器)。这是一个很小的优化,这也意味着我们不必将target目录复制到Docker映像,即使是用于构建的临时映像也是如此。

源代码更改的每个构建都将变慢,因为必须在第一RUN部分中重新创建Maven缓存。但是您拥有一个完全独立的构建,只要拥有docker,任何人都可以运行它来使您的应用程序运行。在某些环境中(例如,您需要与不懂Java的人共享代码),这可能非常有用。

实验功能

Docker 18.06带有一些“实验性”功能,其中包括一种缓存构建依赖项的方法。要打开它们,您需要在守护程序(dockerd)中有一个标志,并且在运行客户端时还需要一个环境变量,然后可以在第一行中添加魔术第一行Dockerfile

Dockerfile

# syntax=docker/dockerfile:experimental

然后该RUN指令接受一个新标志--mount。这是一个完整的例子:

Dockerfile

# syntax=docker/dockerfile:experimental
FROM openjdk:8-jdk-alpine as build
WORKDIR /workspace/app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src

RUN --mount=type=cache,target=/root/.m2 ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

然后运行它:

$ DOCKER_BUILDKIT=1 docker build -t myorg/myapp .
...
 => /bin/sh -c ./mvnw install -DskipTests              5.7s
 => exporting to image                                 0.0s
 => => exporting layers                                0.0s
 => => writing image sha256:3defa...
 => => naming to docker.io/myorg/myapp

使用实验性功能,您可以在控制台上获得不同的输出,但是可以看到,一旦缓存变热,Maven构建现在仅需几秒钟而不是几分钟。

Dockerfile配置的Gradle版本非常相似:

Dockerfile

# syntax=docker/dockerfile:experimental
FROM openjdk:8-jdk-alpine AS build
WORKDIR /workspace/app

COPY . /workspace/app
RUN --mount=type=cache,target=/root/.gradle ./gradlew clean build
RUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/build/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
虽然这些功能处于试验阶段,但打开和关闭buildkit的选项取决于docker您所使用的版本。检查文档以了解您拥有的版本(上面的示例对于docker18.0.6是正确的)。

安全方面

就像在传统的VM部署中一样,不应在具有root权限的情况下运行进程。相反,映像应包含运行该应用程序的非root用户。

在中Dockerfile,可以通过添加另一层来添加(系统)用户和组,然后将其设置为当前用户(而不是默认用户root)来实现:

Dockerfile

FROM openjdk:8-jdk-alpine

RUN addgroup -S demo && adduser -S demo -G demo
USER demo

...

如果有人设法突破您的应用程序并在容器内运行系统命令,这将限制他们的功能(最小特权原则)。

一些其他Dockerfile命令仅以root用户身份运行,因此也许您必须将USER命令进一步向下移动(例如,如果您打算将更多软件包安装到仅以root用户身份使用的容器中)。
不使用的其他方法Dockerfile可能更合适。例如,在稍后描述的buildpack方法中,默认情况下,大多数实现将使用非root用户。

另一个考虑因素是,大多数应用程序在运行时可能不需要完整的JDK,因此一旦我们进行了多阶段构建,我们就可以安全地切换到JRE基础映像。因此,在上面的多阶段构建中,我们可以使用

Dockerfile

FROM openjdk:8-jre-alpine

...

以获得最终的可运行图像。如上所述,这还节省了映像中的一些空间,这些空间将由运行时不需要的工具占用。

构建插件

如果您不想docker直接在构建中调用,则可以使用Maven和Gradle丰富的插件集来为您完成此工作。这里仅仅是少数。

Spring Boot插件

使用Spring Boot 2.3,您可以选择直接使用Spring Boot从Maven或Gradle构建映像。只要您已经在构建Spring Boot jar文件,您只需要直接调用插件即可。使用Maven

$ ./mvnw spring-boot:build-image

并与Gradle

$ ./gradlew bootBuildImage

它使用本地docker守护程序(因此必须安装),但不需要Dockerfile。结果是docker.io/<group>/<artifact>:latest默认调用的图像。您可以使用以下方法在Maven中修改图像名称:

<project>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<image>
						<name>myorg.demo</name>
					</image>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

并在Gradle中使用

bootBuildImage {
	imageName = "myorg/demo"
}

该映像是使用Cloud Native Buildpacks构建的,其中默认构建器针对Spring Boot应用程序进行了优化(您可以对其进行自定义,但是默认设置很有用)。像上面的示例一样,可以有效地对图像进行分层。它还使用CF内存计算器根据运行时的可用容器资源在运行时调整JVM的大小,因此在运行映像时,您将看到内存计算器报告其结果:

$ docker run -p 8080:8080 myorg/demo
Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=86557K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx450018K (Head Room: 0%, Loaded Class Count: 12868, Thread Count: 250, Total Memory: 1073741824)
...

Spotify Maven插件

Spotify的Maven插件是一个受欢迎的选择。它要求应用程序开发人员编写a Dockerfile,然后docker为您运行,就像您在命令行上一样。docker image标签和其他内容有一些配置选项,但它使您的应用程序中的docker知识集中在Dockerfile,很多人都喜欢。

对于真正的基本用法,它无需额外配置即可直接使用:

$ mvn com.spotify:dockerfile-maven-plugin:build
...
[INFO] Building Docker context /home/dsyer/dev/demo/workspace/myapp
[INFO]
[INFO] Image will be built without a name
[INFO]
...
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.630 s
[INFO] Finished at: 2018-11-06T16:03:16+00:00
[INFO] Final Memory: 26M/595M
[INFO] ------------------------------------------------------------------------

这将构建一个匿名docker镜像。我们现在可以docker在命令行上用标记,或者使用Maven配置将其设置为repository。示例(不更改pom.xml):

$ mvn com.spotify:dockerfile-maven-plugin:build -Ddockerfile.repository=myorg/myapp

或在pom.xml

pom.xml

<build>
    <plugins>
        <plugin>
            <groupId>com.spotify</groupId>
            <artifactId>dockerfile-maven-plugin</artifactId>
            <version>1.4.8</version>
            <configuration>
                <repository>myorg/${project.artifactId}</repository>
            </configuration>
        </plugin>
    </plugins>
</build>

Palantir Gradle插件

真知晶球摇篮插件工作有Dockerfile,它也能产生一个Dockerfile适合你,然后它运行docker,如果你是在命令行中运行它。

首先,您需要将插件导入到您的build.gradle

build.gradle

buildscript {
    ...
    dependencies {
        ...
        classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0')
    }
}

最后,您应用该插件并调用其任务:

build.gradle

apply plugin: 'com.palantir.docker'

group = 'myorg'

bootJar {
    baseName = 'myapp'
    version =  '0.1.0'
}

task unpack(type: Copy) {
    dependsOn bootJar
    from(zipTree(tasks.bootJar.outputs.files.singleFile))
    into("build/dependency")
}
docker {
    name "${project.group}/${bootJar.baseName}"
    copySpec.from(tasks.unpack.outputs).into("dependency")
    buildArgs(['DEPENDENCY': "dependency"])
}

在此示例中,我们选择将Spring Boot胖子罐解压缩到build目录中的特定位置,该目录是docker构建的根目录。然后,上面的多层(不是多阶段)Dockerfile将起作用。

Spring Boot Maven和Gradle插件

MavenGradle的Spring Boot构建插件可用于创建容器映像。插件docker build使用Cloud Native Buildpacks创建OCI映像(与创建的格式相同)。您不需要a,Dockerfile但确实需要docker守护程序,可以是本地的(使用docker构建时使用的守护程序),也可以通过DOCKER_HOST环境变量远程使用。

Maven的示例(不更改pom.xml):

$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myorg/myapp

和Gradle一起:

$ ./gradlew bootBuildImage --imageName=myorg/myapp

第一次构建可能要花很长时间,因为它必须下载一些容器映像和JDK,但是后续的构建会很快。

如果您运行图像:

$ docker run -p 8080:8080 -t myorg/myapp
Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=86381K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx450194K (Head Room: 0%, Loaded Class Count: 12837, Thread Count: 250, Total Memory: 1073741824)
....
2015-03-31 13:25:48.035  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-31 13:25:48.037  INFO 1 --- [           main] hello.Application

您会看到它正常启动。您可能还会注意到,JVM内存需求是在容器内部计算并设置为命令行选项的。这与Cloud Foundry构建包中使用多年的内存计算相同。它代表了对各种JVM应用程序(包括但不限于Spring Boot应用程序)的最佳选择的重大研究,其结果通常比JVM的默认设置要好得多。您可以自定义命令行选项,并使用环境变量覆盖内存计算器。

Jib Maven和Gradle插件

Google有一个名为Jib的开源工具,它相对较新,但出于多种原因却很有趣。可能最有趣的事情是您不需要docker来运行它-它使用与获得的图像相同的标准输出来构建映像,docker build但是docker除非您要求使用,否则不会使用-因此它可以在没有docker的环境中工作已安装(在构建服务器中并不罕见)。您也不需要Dockerfile(无论如何都会被忽略),也不需要任何东西pom.xml来获得在Maven中构建的图像(Gradle要求您至少在中安装插件build.gradle)。

Jib的另一个有趣的功能是,它对图层有独到见解,并且以与Dockerfile上面创建的多层略有不同的方式优化了它们。就像在胖子罐中一样,Jib将本地应用程序资源与依赖项分离开来,但它走得更远,而且还将快照依赖项放入一个单独的层中,因为它们更容易发生变化。有一些配置选项可用于进一步自定义布局。

Maven的示例(不更改pom.xml):

$ mvn com.google.cloud.tools:jib-maven-plugin:build -Dimage=myorg/myapp

要运行上述命令,您将需要具有在myorg存储库前缀下推送到Dockerhub的权限。如果您docker在命令行上通过进行了身份验证,则可以在本地~/.docker配置中使用。您还可以在您的仓库中设置Maven“服务器”身份验证~/.m2/settings.xmlid存储库的身份很重要):

settings.xml

    <server>
      <id>registry.hub.docker.com</id>
      <username>myorg</username>
      <password>...</password>
    </server>

还有其他选项,例如,您可以docker使用dockerBuild目标而不是来针对docker守护程序在本地构建(例如在命令行上运行)build。还支持其他容器注册表,对于每个容器注册表,您将需要通过docker或Maven设置来设置本地身份验证。

一旦将gradle插件包含在您的中build.gradle,它便具有类似的功能,例如

build.gradle

plugins {
  ...
  id 'com.google.cloud.tools.jib' version '1.8.0'
}

或使用入门指南中使用的较旧样式:

build.gradle

buildscript {
    repositories {
      maven {
        url "https://plugins.gradle.org/m2/"
      }
      mavenCentral()
    }
    dependencies {
        classpath('org.springframework.boot:spring-boot-gradle-plugin:2.2.1.RELEASE')
        classpath('com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:1.8.0')
    }
}

然后您可以用

$ ./gradlew jib --image=myorg/myapp

与Maven构建一样,如果docker在命令行中使用进行了身份验证,则映像推送将从您的本地~/.docker配置进行身份验证。

持续集成

如今(或应该如此),自动化已成为每个应用程序生命周期的一部分。人们用来实现自动化的工具往往非常擅长于从源代码调用构建系统。因此,如果您得到一个docker映像,并且构建代理中的环境与开发人员自己的环境充分匹配,这可能就足够了。向Docker注册表进行身份验证可能是最大的挑战,但是所有自动化工具中都有一些功能可以帮助实现这一点。

但是,有时最好将容器创建完全留给自动化层,在这种情况下,可能不需要污染用户的代码。容器创建很棘手,开发人员有时并不真正在意它。如果用户代码更简洁,则其他工具更有可能“做正确的事”,应用安全修复程序,优化缓存等。自动化有多种选择,并且这些天都将具有与容器相关的某些功能。我们只看几个。

大堂

Concourse是可用于CI和CD的基于管道的自动化平台。它在Pivotal内部大量使用,该项目的主要作者在那里工作。除CLI外,Concourse中的所有内容都是无状态的,并且所有内容都在容器中运行。由于运行容器是自动化管道的主要业务顺序,因此很好地支持创建容器。该泊坞窗图像资源负责保持你构建的输出状态更新,如果它是一个容器图像。

这是一个示例管道,为上面的示例构建一个docker映像,假设它位于github上,myorg/myapp并且Dockerfile在根目录下有一个,并且在内有一个构建任务声明src/main/ci/build.yml

resources:
- name: myapp
  type: git
  source:
    uri: https://github.com/myorg/myapp.git
- name: myapp-image
  type: docker-image
  source:
    email: {{docker-hub-email}}
    username: {{docker-hub-username}}
    password: {{docker-hub-password}}
    repository: myorg/myapp

jobs:
- name: main
  plan:
  - task: build
    file: myapp/src/main/ci/build.yml
  - put: myapp-image
    params:
      build: myapp

管道的结构非常具有声明性:您定义“资源”(输入或输出或两者兼有)和“作业”(使用并向资源应用操作)。如果任何输入资源发生更改,则会触发新的构建。如果作业期间任何输出资源发生更改,则将对其进行更新。

可以在与应用程序源代码不同的位置定义管道。对于一般的构建设置,任务声明也可以集中或外部化。如果这是滚动的方式,则可以将开发和自动化之间的关注点分离开。

詹金斯

Jenkins是另一种流行的自动化服务器。它具有广泛的功能,但在此处与其他自动化示例最接近的功能是管道功能。这是一个Jenkinsfile使用Maven构建Spring Boot项目,然后使用Dockerfile构建图像并将其推送到存储库的:

Jenkinsfile

node {
    checkout scm
    sh './mvnw -B -DskipTests clean package'
    docker.build("myorg/myapp").push()
}

对于需要在构建服务器中进行身份验证的(现实的)泊坞库,您可以使用将凭证添加到上述docker对象docker.withCredentials(…​)

构建包

Spring Boot Maven和Gradle插件使用buildpack的方式与packCLI在以下示例中使用的方式完全相同。主要区别在于,插件用于docker运行构建,pack而不需要。给定相同的输入,得到的图像是相同的。

Cloud Foundry多年来一直在内部使用容器,用于将用户代码转换为容器的技术的一部分是Build Packs,该思想最初是从Heroku借来的。当前的buildpacks(v2)生成通用二进制输出,该输出由平台组装到容器中。在新一代buildpacks的(v3)是Heroku与其他公司(包括Pivotal)之间的合作,它直接且显式地构建了容器映像。这对于开发人员和操作员而言非常有趣。开发人员不需要太在乎如何构建容器的细节,但是如果需要,他们可以轻松地创建一个容器。Buildpacks还具有许多用于缓存构建结果和依赖项的功能,因此,与本地Docker构建相比,Buildpack的运行速度通常要快得多。操作员可以扫描容器以审核其内容,并对其进行转换以修补它们以进行安全更新。您可以在本地运行构建包(例如,在开发人员机器上或在CI服务中),也可以在Cloud Foundry之类的平台上运行。

buildpack生命周期的输出是一个容器映像,但是您不需要docker或a Dockerfile,因此它对CI和自动化友好。输出映像中的文件系统层由buildpack进行控制,通常,将进行许多优化,而无需开发人员知道或关心它们。在较低层(如包含操作系统的基础映像)与较高层(如包含中间件和特定于语言的依赖关系)之间还有一个应用程序二进制接口。如果存在安全更新,那么诸如Cloud Foundry之类的平台就可以修补较低的层,而不会影响应用程序的完整性和功能。

为了让您了解buildpack的功能,这里是一个从命令行使用Pack CLI的示例(它将与我们在本指南中使用的示例应用程序一起使用,不需要Dockerfile任何特殊的构建配置):

$ pack build myorg/myapp --builder=cloudfoundry/cnb:bionic --path=.
2018/11/07 09:54:48 Pulling builder image 'cloudfoundry/cnb:bionic' (use --no-pull flag to skip this step)
2018/11/07 09:54:49 Selected run image 'packs/run' from stack 'io.buildpacks.stacks.bionic'
2018/11/07 09:54:49 Pulling run image 'packs/run' (use --no-pull flag to skip this step)
*** DETECTING:
2018/11/07 09:54:52 Group: Cloud Foundry OpenJDK Buildpack: pass | Cloud Foundry Build System Buildpack: pass | Cloud Foundry JVM Application Buildpack: pass
*** ANALYZING: Reading information from previous image for possible re-use
*** BUILDING:
-----> Cloud Foundry OpenJDK Buildpack 1.0.0-BUILD-SNAPSHOT
-----> OpenJDK JDK 1.8.192: Reusing cached dependency
-----> OpenJDK JRE 1.8.192: Reusing cached launch layer

-----> Cloud Foundry Build System Buildpack 1.0.0-BUILD-SNAPSHOT
-----> Using Maven wrapper
       Linking Maven Cache to /home/pack/.m2
-----> Building application
       Running /workspace/app/mvnw -Dmaven.test.skip=true package
...
---> Running in e6c4a94240c2
---> 4f3a96a4f38c
---> 4f3a96a4f38c
Successfully built 4f3a96a4f38c
Successfully tagged myorg/myapp:latest
$ docker run -p 8080:8080 myorg/myapp
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

2018-11-07 09:41:06.390  INFO 1 --- [main] hello.Application: Starting Application on 1989fb9a00a4 with PID 1 (/workspace/app/BOOT-INF/classes started by pack in /workspace/app)
...

--builder是一个运行buildpack生命周期的docker映像-通常,它将是所有开发人员或单个平台上所有开发人员的共享资源。您可以在命令行上设置默认构建器(在中创建文件~/.pack),然后在后续构建中省略该标志。

cloudfoundry/cnb:bionic构建器还知道如何从可执行的jar文件构建映像,因此您可以mvnw先使用构建,然后再将其指向--pathjar文件以获得相同的结果。

基尼特语

容器和平台领域的另一个新项目是Knative。Knative有很多东西,但是如果您不熟悉Knative,则可以将其视为构建无服务器平台的基础。它基于Kubernetes构建,因此最终它会使用容器映像,并将它们转换为平台上的应用程序或“服务”。但是,它的主要功能之一是能够使用源代码并为您构建容器,从而使其对开发人员和操作员更友好。基建是执行此操作的组件,它本身就是一个将用户代码转换为容器的灵活平台-您可以按照自己喜欢的任何方式进行操作。一些模板提供了常见的模式,例如Maven和Gradle构建,以及使用Kaniko的多阶段docker构建。还有一个使用Buildpacks的模板,这对我们来说很有趣,因为buildpacks一直对Spring Boot都有很好的支持。

闭幕

本指南介绍了许多用于为Spring Boot应用程序构建容器映像的选项。所有这些都是完全有效的选择,现在由您决定需要哪一个。您的第一个问题应该是“我真的需要构建容器映像吗?” 如果答案是“是”,那么您的选择可能会受到效率和可缓存性以及关注点分离的影响。您是否希望使开发人员不必过多地了解如何创建容器映像?您是否想让需要修补操作系统和中间件漏洞的开发人员负责更新映像?也许开发人员需要对整个过程进行完全控制,并且他们拥有所需的所有工具和知识。