容器已成为打包应用程序及其所有软件和操作系统依赖项,然后将它们交付到不同环境的首选方式。
本文介绍了容器化 Spring Boot 应用程序的不同方法:
- 使用 Docker 文件创建 Docker 映像,
- 使用 Cloud-Native Buildpack 从源创建 OCI 映像,
- 通过使用多层工具将 JAR 的各个部分分成不同的层来优化运行时图像。
代码示例
本文附有工作代码示例
容器术语
我们将从本文中使用的容器术语开始:
- 容器镜像: 特定格式的文件。 我们将通过运行构建工具将应用程序转换为容器映像。
- 容器:容器镜像的可执行实例。
- 容器引擎:负责运行容器的守护进程。
- 容器主机:容器引擎运行的主机。
- 容器注册中心:用于发布和分发容器镜像的一般位置。
- OCI标准:
开放式容器倡议(OCI) 是 Linux 基金会内部形成的一个轻量级、开放的治理结构。 OCI 镜像规范定义了容器镜像和运行时格式的行业标准,以确保所有容器引擎都可以运行任何构建工具创建的容器镜像。
为了容器化应用程序,我们将应用程序包装在容器映像中,并将该映像发布到共享注册表。 容器运行时从注册表中检索该映像,将其解压,然后在其中运行应用程序。
Spring Boot 2.3 版本提供了用于创建 OCI 镜像的插件。
以传统方式构建容器镜像
通过向 Docker 文件添加一些指令,为 Spring Boot 应用程序创建 Docker 映像非常简单。
我们首先创建一个可执行 JAR 文件,并作为 Docker 文件指令的一部分,在应用必要的设置后将可执行 JAR 文件复制到基础 JRE 映像之上。
让我们创建 Spring 应用程序 web
, lombok
и actuator
。 我们还添加了一个休息控制器来提供 API GET
方法。
创建 Dockerfile
然后,我们通过添加来容器化该应用程序 Dockerfile
:
FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/application.jar"]
我们的 Docker 文件包含来自 adoptopenjdk
,在其上复制 JAR 文件,然后打开端口, 8080
它将监听请求。
构建应用程序
首先,您需要使用 Maven 或 Gradle 创建应用程序。 这里我们使用Maven:
mvn clean package
这将为应用程序创建一个可执行 JAR 文件。 我们需要将这个可执行 JAR 转换为 Docker 镜像以在 Docker 引擎上运行。
创建容器镜像
然后,我们通过运行命令将此可执行 JAR 文件放入 Docker 映像中 docker build
从包含之前创建的 Dockerfile 的项目根目录:
docker build -t usersignup:v1 .
我们可以使用以下命令在列表中查看我们的图像:
docker images
上述命令的输出包括我们的图像 usersignup
与基础图像一起, adoptopenjdk
在我们的 Docker 文件中指定。
REPOSITORY TAG SIZE
usersignup v1 249MB
adoptopenjdk 11-jre-hotspot 229MB
查看容器镜像内的层
让我们看看图像内的图层堆栈。 我们将使用
dive usersignup:v1
以下是 Dive 命令的部分输出:
正如我们所看到的,应用程序层占据了图像大小的很大一部分。 作为优化的一部分,我们希望在以下部分中减小该层的大小。
使用 Buildpack 创建容器镜像
云构建包的优势
使用 Buildpack 创建映像的主要好处之一是 图像配置更改可以集中管理(构建器)并使用构建器传播到所有应用程序。
构建包与平台紧密耦合。 Cloud-Native Buildpacks 通过支持 OCI 镜像格式提供跨平台标准化,确保镜像可以由 Docker 引擎运行。
使用 Spring Boot 插件
Spring Boot 插件使用 Buildpack 从源代码构建 OCI 映像。 图像是使用创建的 bootBuildImage
任务(Gradle)或 spring-boot:build-image
目标 (Maven) 和本地 Docker 安装。
我们可以通过指定名称来自定义推送到 Docker 注册表所需的镜像的名称 image tag
:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>docker.io/pratikdas/${project.artifactId}:v1</name>
</image>
</configuration>
</plugin>
让我们使用 Maven 来做这件事 build-image
创建应用程序和创建容器映像的目标。 我们目前没有使用任何 Dockerfile。
mvn spring-boot:build-image
结果将是这样的:
[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Building image 'docker.io/pratikdas/usersignup:v1'
[INFO]
[INFO] > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator] Adding label 'org.springframework.boot.version'
.. [creator] *** Images (c311fe74ec73):
.. [creator] docker.io/pratikdas/usersignup:v1
[INFO]
[INFO] Successfully built image 'docker.io/pratikdas/usersignup:v1'
从输出中我们看到 paketo Cloud-Native buildpack
用于创建工作 OCI 映像。 和之前一样,我们可以通过运行以下命令来查看列为 Docker 镜像的镜像:
docker images
结论:
REPOSITORY SIZE
paketobuildpacks/run 84.3MB
gcr.io/paketo-buildpacks/builder 652MB
pratikdas/usersignup 257MB
使用 Jib 创建容器镜像
Jib 是 Google 的一个镜像创建插件,它提供了另一种从源代码创建容器镜像的方法。
配置中 jib-maven-plugin
在 pom.xml 中:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.5.2</version>
</plugin>
接下来,我们使用 Maven 命令运行 Jib 插件来构建应用程序并创建容器映像。 和以前一样,我们在这里没有使用任何 Docker 文件:
mvn compile jib:build -Dimage=<docker registry name>/usersignup:v1
执行上述 Maven 命令后,我们得到以下输出:
[INFO] Containerizing application to pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO]
[INFO] Built and pushed image as pratikdas/usersignup:v1
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete
输出显示容器映像已创建并放置在注册表中。
创建优化图像的动机和技术
我们进行优化的主要原因有两个:
- Производительность:在容器编排系统中,容器映像从映像注册表检索到运行容器引擎的主机。 这个过程称为规划。 从注册表中提取大型映像会导致容器编排系统中的调度时间较长以及 CI 管道中的构建时间较长。
- 安全:较大的图像也有较大的漏洞区域。
Docker 镜像由一堆层组成,每个层代表 Dockerfile 中的一条指令。 每层代表底层变化的增量。 当我们从注册表中拉取 Docker 映像时,它会分层拉取并缓存在主机上。
Spring Boot 使用
优化公式的重点是在单独的级别将应用程序与 Spring 框架依赖项隔离。
依赖层构成了厚 JAR 文件的大部分,仅下载一次并缓存在主机系统上。
在应用程序更新和容器调度期间,仅拉取应用程序的薄层。 如下图所示:
在以下部分中,我们将了解如何为 Spring Boot 应用程序创建这些优化的映像。
使用 Buildpack 为 Spring Boot 应用程序创建优化的容器映像
Spring Boot 2.3 通过将厚 JAR 文件的各个部分提取到单独的层中来支持分层。 分层功能默认处于禁用状态,必须使用 Spring Boot Maven 插件显式启用:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
我们将使用此配置首先使用 Buildpack 构建容器映像,然后在以下部分中使用 Docker 构建容器映像。
让我们开始吧 build-image
用于创建容器镜像的 Maven 目标:
mvn spring-boot:build-image
如果我们运行 Dive 来查看生成图像中的各层,我们可以看到与使用 fat JAR 格式得到的内容相比,应用程序层(以红色框出)在千字节范围内要小得多:
使用 Docker 为 Spring Boot 应用程序创建优化的容器映像
我们还可以使用 Docker 文件创建分层的 Docker JAR 映像,而不是使用 Maven 或 Gradle 插件。
当我们使用 Docker 时,我们需要执行两个额外的步骤来提取层并将它们复制到最终镜像中。
使用启用了分层的 Maven 构建后生成的 JAR 内容将如下所示:
META-INF/
.
BOOT-INF/lib/
.
BOOT-INF/lib/spring-boot-jarmode-layertools-2.3.3.RELEASE.jar
BOOT-INF/classpath.idx
BOOT-INF/layers.idx
输出显示了一个名为 spring-boot-jarmode-layertools
и layersfle.idx
文件。 此附加 JAR 文件提供分层处理功能,如下一节所述。
提取各个层的依赖关系
要从分层 JAR 中查看和提取层,我们使用系统属性 -Djarmode=layertools
跑 spring-boot-jarmode-layertools
JAR 而不是应用程序:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar
运行此命令会生成包含可用命令选项的输出:
Usage:
java -Djarmode=layertools -jar usersignup-0.0.1-SNAPSHOT.jar
Available commands:
list List layers from the jar that can be extracted
extract Extracts layers from the jar for image creation
help Help about any command
输出显示命令 list
, extract
и help
с help
设为默认值。 让我们运行命令 list
选项:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application
我们看到可以作为层添加的依赖项列表。
默认图层:
图层名称
内容
dependencies
版本不包含 SNAPSHOT 的任何依赖项
spring-boot-loader
JAR 加载器类
snapshot-dependencies
版本包含 SNAPSHOT 的任何依赖项
application
应用程序类和资源
层定义于 layers.idx
文件按照应添加到 Docker 映像的顺序排列。 这些图层在第一次检索后会缓存在主机中,因为它们不会更改。 仅将更新的应用层下载到主机,由于尺寸减小,速度更快 .
构建一个图像,将依赖项提取到不同的层中
我们将使用名为的方法分两个阶段构建最终图像
让我们修改 Dockerfile 以进行多阶段构建:
# the first stage of our build will extract the layers
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
# the second stage of our build will copy the extracted layers
FROM adoptopenjdk:14-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
我们将此配置保存在一个单独的文件中 - Dockerfile2
.
我们使用以下命令构建 Docker 镜像:
docker build -f Dockerfile2 -t usersignup:v1 .
运行此命令后,我们得到以下输出:
Sending build context to Docker daemon 20.41MB
Step 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pulling from library/adoptopenjdk
.
.
Successfully built a9ebf6970841
Successfully tagged userssignup:v1
我们可以看到,Docker 镜像是使用镜像 ID 创建的,然后被标记的。
最后,我们像以前一样运行 Dive 命令来检查生成的 Docker 镜像内的各层。 我们可以提供图像 ID 或标签作为 Dive 命令的输入:
dive userssignup:v1
正如您在输出中看到的,包含应用程序的层现在只有 11 KB,并且依赖项缓存在单独的层中。
提取各个层的内部依赖关系
我们可以通过将任何自定义依赖项提取到单独的层中来进一步减小应用程序层的大小,而不是通过在 yml
类似的文件名为 layers.idx
:
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "custom-dependencies":
- "io/myorg/"
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
在这个文件中 layers.idx
我们添加了一个名为的自定义依赖项, io.myorg
包含从共享存储库检索的组织依赖项。
结论
在本文中,我们研究了使用云原生 Buildpack 直接从源代码构建容器映像。 这是使用 Docker 创建容器镜像的通常方式的替代方法:首先创建一个厚的可执行 JAR 文件,然后通过在 Docker 文件中指定指令将其打包到容器镜像中。
我们还考虑通过启用分层功能来优化容器,该功能将依赖项拉入缓存在主机上的单独层中,并在容器执行引擎中的调度时加载应用程序的薄层。
您可以在以下位置找到本文中使用的所有源代码
命令参考
以下是我们在本文中使用的命令的快速概述。
上下文清除:
docker system prune -a
使用 Docker 文件创建容器镜像:
docker build -f <Docker file name> -t <tag> .
我们从源代码构建容器镜像(没有 Dockerfile):
mvn spring-boot:build-image
查看依赖层。 在构建应用程序 JAR 文件之前,请确保在 spring-boot-maven-plugin 中启用了分层功能:
java -Djarmode=layertools -jar application.jar list
提取依赖层。 在构建应用程序 JAR 文件之前,请确保在 spring-boot-maven-plugin 中启用了分层功能:
java -Djarmode=layertools -jar application.jar extract
查看容器镜像列表
docker images
查看左侧容器镜像内(确保已安装dive工具):
dive <image ID or image tag>
来源: habr.com