1. 引言
隨着越來越多的組織轉向容器和虛擬服務器,Docker 正在成為軟件開發工作流程中越來越重要的組成部分。為此,Spring Boot 2.3 中的一項強大新功能是能夠輕鬆創建 Spring Boot 應用程序的 Docker 鏡像。
在本教程中,我們將學習如何為 Spring Boot 應用程序創建 Docker 鏡像。
2. 傳統 Spring Boot Docker 構建
使用 Dockerfile 構建 Spring Boot Docker 鏡像的傳統方法是使用 Dockerfile。 下面是一個簡單的示例:
FROM openjdk:17-jdk-alpine
EXPOSE 8080
ARG JAR_FILE=target/demo-app-1.0.0.jar
ADD ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]我們可以使用 docker build 命令來創建 Docker 鏡像。 這對於大多數應用程序都有效,但存在一些缺點。
首先,我們使用的是 Spring Boot 創建的胖 JAR 包。 這可能會影響啓動時間,尤其是在容器化環境中。 通過添加 JAR 包的解壓內容,可以節省啓動時間。
其次,Docker 鏡像是分層構建的。 Spring Boot 胖 JAR 包的特性導致應用程序代碼和第三方庫都包含在同一個層級中。 這意味着即使只修改一行代碼,整個層級都需要重新構建
在構建之前解壓 JAR 包,應用程序代碼和第三方庫各自擁有自己的層級。 這樣,我們就可以利用 Docker 的緩存機制。 現在,當一行代碼發生變化時,只需要重新構建相應的層級。
考慮到這一點,讓我們看看 Spring Boot 如何改進了創建 Docker 鏡像的過程。
3. Buildpacks
Buildpacks 是一種工具,它提供框架和應用程序依賴項。
例如,對於一個 Spring Boot 胖 JAR 包,一個 Buildpack 將為我們提供 Java 運行時環境。 這使得我們能夠跳過 Dockerfile 並自動獲得一個合理的 Docker 鏡像。
Spring Boot 包含 Maven 和 Gradle 對 Buildpacks 的支持。 例如,使用 Maven 構建時,我們會運行以下命令:
./mvnw spring-boot:build-image讓我們來看一些相關的輸出,以瞭解發生了什麼:
[INFO] Building jar: target/demo-0.0.1-SNAPSHOT.jar
...
[INFO] Building image 'docker.io/library/demo:0.0.1-SNAPSHOT'
...
[INFO] > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 100%
...
[INFO] [creator] ===> DETECTING
[INFO] [creator] 5 of 15 buildpacks participating
[INFO] [creator] paketo-buildpacks/bellsoft-liberica 2.8.1
[INFO] [creator] paketo-buildpacks/executable-jar 1.2.8
[INFO] [creator] paketo-buildpacks/apache-tomcat 1.3.1
[INFO] [creator] paketo-buildpacks/dist-zip 1.3.6
[INFO] [creator] paketo-buildpacks/spring-boot 1.9.1
...
[INFO] Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT'
[INFO] Total time: 44.796 s第一行顯示我們構建了標準胖 JAR 包,就像任何典型的 Maven 包一樣。
下一行開始 Docker 鏡像構建。緊接着,我們看到構建拉取了 Packeto 構建器。
Packeto 是雲原生構建包的實現。 它分析我們的項目並確定所需的框架和庫。 在我們的案例中,它確定我們有一個 Spring Boot 項目並添加了所需的構建包。
最後,我們看到生成的 Docker 鏡像和總構建時間。 請注意,第一次構建時,我們花費了相當長的一段時間來下載構建包並創建不同的層。
構建包的偉大之處在於 Docker 鏡像具有多層結構。 因此,如果只修改我們的應用程序代碼,後續構建將會快得多:
...
[INFO] [creator] Reusing layer 'paketo-buildpacks/executable-jar:class-path'
[INFO] [creator] Reusing layer 'paketo-buildpacks/spring-boot:web-application-type'
...
[INFO] Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT'
...
[INFO] Total time: 10.591 s4. 分層罐 (Layered Jars)
在某些情況下,我們可能更傾向於不使用構建包——例如,我們的基礎設施已經與另一個工具綁定,或者我們已經有自定義的 Dockerfile 想要重用。
出於這些原因,Spring Boot 也支持使用分層罐構建 Docker 鏡像。 為了理解其工作原理,讓我們來看一個典型的 Spring Boot 胖罐 (fat jar) 佈局:
org/
springframework/
boot/
loader/
...
BOOT-INF/
classes/
...
lib/
...這個胖 JAR 包由 3 個主要區域組成:
- 用於啓動 Spring 應用程序所需的 Bootstrap 類
- 應用程序代碼
- 第三方庫
通過分層 JAR 包,結構相似,但我們得到一個名為 layers.idx 的新文件,它將胖 JAR 中的每個目錄映射到一層:
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"Spring Boot 提供四層,無需配置:
- 依賴項:來自第三方的一般依賴項
- 快照依賴項:來自第三方的一般快照依賴項
- 資源:靜態資源
- 應用程序:應用程序代碼和資源
目標是將應用程序代碼和第三方庫放入反映它們更改頻率的層級。
例如,應用程序代碼很可能是最頻繁更改的,因此它會擁有自己的層級。 此外,每個層級都可以獨立演化,只有在層級已更改時,才會為 Docker 鏡像重新構建。
現在我們理解了新的分層 JAR 結構,讓我們看看如何利用它來創建 Docker 鏡像。
4.1. 創建分層 JAR
首先,我們需要設置項目以創建分層 JAR。 使用 Maven,這意味着在 POM 的 Spring Boot 插件部分添加一個新的配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>使用此配置,Maven 的 打包 命令(以及其任何依賴命令)將使用之前提到的四層默認層生成一個新的分層 JAR 包。
4.2. 查看和提取層
接下來,我們需要從 JAR 文件中提取層,以確保 Docker 鏡像擁有正確的層。
要檢查任何分層 JAR 文件的層,可以運行以下命令:
java -Djarmode=layertools -jar demo-0.0.1.jar list然後,為了提取它們,我們將運行:
java -Djarmode=layertools -jar demo-0.0.1.jar extract4.3. 創建 Docker 鏡像
使用 Dockerfile 是將這些層整合到 Docker 鏡像中的最簡單方法:
FROM openjdk:17-jdk-alpine as builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM openjdk:17-jdk-alpine
COPY --from=builder dependencies/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]此 Dockerfile 從我們的胖 JAR 包中提取層,然後將每一層複製到 Docker 鏡像中。 每一條 COPY 指令都會在最終的 Docker 鏡像中產生一個新的層。
如果構建此 Dockerfile,我們可以看到來自分層 JAR 包的每一層都會作為單獨的層添加到 Docker 鏡像中:
...
Step 6/10 : COPY --from=builder dependencies/ ./
---> 2c631b8f9993
Step 7/10 : COPY --from=builder snapshot-dependencies/ ./
---> 26e8ceb86b7d
Step 8/10 : COPY --from=builder spring-boot-loader/ ./
---> 6dd9eaddad7f
Step 9/10 : COPY --from=builder application/ ./
---> dc80cc00a655
...5. 結論
在本教程中,我們學習了各種使用 Spring Boot 構建 Docker 鏡像的方法。通過使用 Buildpacks,我們可以獲得合適的 Docker 鏡像,無需樣板代碼或自定義配置。或者,通過付出一些額外的努力,我們可以使用分層 JAR 來獲得更定製化的 Docker 鏡像。
有關使用 Java 和 Docker 的更多信息,請查看 jib 教程。