構建 Java 鏡像的 10 個最佳實踐

你想構建一個 Java 應用程序並在 Docker 中運行它嗎?你知道在使用 Docker 構建 Java 容器有哪些最佳實踐?

在下面的速查表中,我將爲你提供構建生產級 Java 容器的最佳實踐,旨在優化和保護要投入生產環境中的 Docker 鏡像。

構建一個簡單的 Java 容器鏡像

讓我們從簡單的 Dockerfile 開始,在構建 Java 容器時,我們經常會有如下類似的內容:

FROM maven
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN mvn clean install
CMD "mvn" "exec:java"
$ docker build . -t java-application
$ docker run -p 8080:8080 java-application

這很簡單,而且有效。但是,此鏡像充滿錯誤。

我們不僅應該瞭解如何正確使用 Maven,而且還應避免像上述示例那樣構建 Java 容器。

下面,讓我們開始逐步改進這個 Dockerfile,使你的 Java 應用程序生成高效,安全的 Docker 鏡像。

1. Docker 鏡像使用確定性的標籤

當使用 Maven 構建 Java 容器鏡像時,我們首先需要基於 Maven 鏡像。但是,你知道使用 Maven 基本鏡像時實際上引入了哪些內容嗎?

當你使用下面的代碼行構建鏡像時,你將獲得該 Maven 鏡像的最新版本:

FROM maven

這似乎是一個有趣的功能,但是這種採用 Maven 默認鏡像的策略可能存在一些潛在問題:

如何解決?

讓我們用這些知識更新我們的 Dockerfile:

FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN mvn clean package -DskipTests

2. 在 Java 鏡像中僅安裝需要的內容

以下命令會在容器中構建 Java 程序,包括其所有依賴項。這意味着源代碼和構建系統都將會是 Java 容器的一部分。

RUN mvn clean package -DskipTests

我們都知道 Java 是一種編譯語言。這意味着我們只需要由你的構建環境創建的工件,而不需要代碼本身。這也意味着構建環境不應成爲 Java 鏡像的一部分。

要運行 Java 鏡像,我們也不需要完整的 JDK。一個 Java 運行時環境(JRE)就足夠了。因此,從本質上講,如果它是可運行的 JAR,那麼只需要使用 JRE 和已編譯的 Java 構件來構建鏡像。

使用 Maven 在 CI 流水線中都構建編譯程序,然後將 JAR 複製到鏡像中,如下面的更新的 Dockerfile 中所示:

FROM openjdk:11-jre-slim@sha256:31a5d3fa2942eea891cf954f7d07359e09cf1b1f3d35fb32fedebb1e3399fc9e
RUN mkdir /app
COPY ./target/java-application.jar /app/java-application.jar
WORKDIR /app
CMD "java" "-jar" "java-application.jar"

3. 使用多階段構建 Java 鏡像

在本文的前面,我們談到了我們不需要在容器中構建 Java 應用程序。但是,在某些情況下,將我們的應用程序構建爲 Docker 鏡像的一部分很方便。

我們可以將 Docker 鏡像的構建分爲多個階段。我們可以使用構建應用程序所需的所有工具來構建鏡像,並在最後階段創建實際的生產鏡像。

FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c AS build
RUN mkdir /project
COPY . /project
WORKDIR /project
RUN mvn clean package -DskipTests
FROM adoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f
RUN mkdir /app
COPY --from=build /project/target/java-application.jar /app/java-application.jar
WORKDIR /app
CMD "java" "-jar" "java-application.jar"

4. 防止敏感信息泄漏

在創建 Java 應用程序和 Docker 鏡像時,很有可能需要連接到私有倉庫,類似 settings.xml 的配置文件經常會泄露敏感信息。但在使用多階段構建時,你可以安全地將 settings.xml 複製到你的構建容器中。帶有憑據的設置將不會出現在你的最終鏡像中。此外,如果將憑據用作命令行參數,則可以在構建鏡像中安全地執行此操作。

使用多階段構建,你可以創建多個階段,僅將結果複製到最終的生產鏡像中。這種分離是確保在生產環境中不泄漏數據的一種方法。

哦,順便說一句,使用 docker history 命令查看 Java 鏡像的輸出:

$ docker history java-application

輸出僅顯示來自容器鏡像的信息,而不顯示構建鏡像的過程。

5. 不要以 root 用戶運行容器

創建 Docker 容器時,你需要應用最小特權原則,防止由於某種原因攻擊者能夠入侵你的應用程序,則你不希望他們能夠訪問所有內容。

擁有多層安全性,可以幫助你減少系統威脅。因此,必須確保你不以 root 用戶身份運行應用程序。

但默認情況下,創建 Docker 容器時,你將以 root 身份運行它。儘管這對於開發很方便,但是你不希望在生產鏡像中使用它。假設由於某種原因,攻擊者可以訪問終端或可以執行代碼。在那種情況下,它對正在運行的容器具有顯著的特權,並且訪問主機文件系統。

解決方案非常簡單。創建一個有限特權的特定用戶來運行你的應用程序,並確保該用戶可以運行該應用程序。最後,在運行應用程序之前,不要忘記使用新創建的用戶。

讓我們相應地更新我們的 Dockerfile。

FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c AS build
RUN mkdir /project
COPY . /project
WORKDIR /project
RUN mvn clean package -DskipTests
FROM adoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f
RUN mkdir /app
RUN addgroup --system javauser && adduser -S -s /bin/false -G javauser javauser
COPY --from=build /project/target/java-application.jar /app/java-application.jar
WORKDIR /app
RUN chown -R javauser:javauser /app
USER javauser
CMD "java" "-jar" "java-application.jar"

6. Java 應用程序不要使用 PID 爲 1 的進程

在許多示例中,我看到了使用構建環境來啓動容器化 Java 應用程序的常見錯誤。

上面,我們瞭解了要在  Java 容器中使用 Maven 或 Gradle 的重要性,但是使用如下命令,會有不同的效果:

在 Docker 中運行應用程序時,第一個應用程序將以進程 ID 爲 1(PID=1)運行。Linux 內核會以特殊方式處理 PID 爲 1 的進程。通常,進程號爲 1 的 PID 上的過程是初始化過程。如果我們使用 Maven 運行 Java 應用程序,那麼如何確定 Maven 將類似 SIGTERM 信號轉發給 Java 進程呢?

如果像下面的示例,那樣運行 Docker 容器,則 Java 應用程序將具有 PID 爲 1 的進程。

CMD "java" "-jar" "application.jar"

請注意,docker kill 和 docker stop 命令僅向 PID 爲 1 的容器進程發送信號。例如,如果你正在運行 Java 應用的 shell 腳本,/bin/sh 不會將信號轉發給子進程。

更爲重要的是,在 Linux 中,PID 爲 1 的容器進程還有一些其他職責。因此,在某些情況下,你不希望應用程序成爲 PID 爲 1 的進程,因爲你不知道如何處理這些問題。一個很好的解決方案是使用 dumb-init。

RUN apk add dumb-init
CMD "dumb-init" "java" "-jar" "java-application.jar"

當你像這樣運行 Docker 容器時,dumb-init 會佔用 PID 爲 1 的容器進程並承擔所有責任。你的 Java 流程不再需要考慮這一點。

我們更新後的 Dockerfile 現在看起來像這樣:

FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c AS build
RUN mkdir /project
COPY . /project
WORKDIR /project
RUN mvn clean package -DskipTests
FROM adoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f
RUN apk add dumb-init
RUN mkdir /app
RUN addgroup --system javauser && adduser -S -s /bin/false -G javauser javauser
COPY --from=build /project/target/java-code-workshop-0.0.1-SNAPSHOT.jar /app/java-application.jar
WORKDIR /app
RUN chown -R javauser:javauser /app
USER javauser
CMD "dumb-init" "java" "-jar" "java-application.jar"

7. 優雅下線 Java 應用程序

當你的應用程序收到關閉信號時,理想情況下,我們希望所有內容都能正常關閉。根據你開發應用程序的方式,中斷信號(SIGINT)或 CTRL + C 可能導致立即終止進程。

這可能不是你想要的東西,因爲諸如此類的事情可能會導致意外行爲,甚至導致數據丟失。

當你將應用程序作爲 Payara 或 Apache Tomcat 之類的 Web 服務器的一部分運行時,該 Web 服務器很可能會正常關閉。對於某些支持可運行應用程序的框架也是如此。例如,Spring Boot 具有嵌入式 Tomcat 版本,可以有效地處理關機問題。

當你創建一個獨立的 Java 應用程序或手動創建一個可運行的 JAR 時,你必須自己處理這些中斷信號。

解決方案非常簡單。添加一個退出鉤子(hook),如下面的示例所示。收到類似 SIGINT 信號後,優雅下線應用程序的進程將會被啓動。

Runtime.getRuntime().addShutdownHook(new Thread() {
   @Override
   public void run() {
       System.out.println("Inside Add Shutdown Hook");
   }
});

誠然,與 Dockerfile 相關的問題相比,這是一個通用的 Web 應用程序問題,但在容器環境中更重要。

8. 使用 .dockerignore 文件

爲了防止不必要的文件污染 git 存儲庫,你可以使用 .gitignore 文件。

對於 Docker 鏡像,我們有類似的東西—— .dockerignore 文件。類似於 git 的忽略文件,它是爲了防止 Docker 鏡像中出現不需要的文件或目錄。同時,我們也不希望敏感信息泄漏到我們的 Docker 鏡像中。

請參閱以下示例的 .dockerignore:

.dockerignore
**/*.logDockerfile
.git
.gitignore

使用 .dockerignore 文件的要點是:

9. 確保 Java 版本支持容器

Java 虛擬機(JVM)是一件了不起的事情。它會根據其運行的系統進行自我調整。有基於行爲的調整,可以動態優化堆的大小。但是,在 Java 8 和 Java 9 等較舊的版本中,JVM 無法識別容器設置的 CPU 限制或內存限制。這些較舊的 Java 版本的 JVM 看到了主機系統上的全部內存和所有 CPU 容量。Docker 設置的限制將被忽略。

隨着 Java 10 的發佈,JVM 現在可以感知容器,並且可以識別容器設置的約束。該功能 UseContainerSupport 是 JVM 標誌,默認情況下設置爲活動狀態。Java 10 中發佈的容器感知功能也已移植到 Java-8u191。

對於 Java 8 之前的版本,你可以手動嘗試使用該 -Xmx 標誌來限制堆大小,但這是一個痛苦的練習。緊接着,堆大小不等於 Java 使用的內存。對於 Java-8u131 和 Java 9,容器感知功能是實驗性的,你必須主動激活。

-XX:+ UnlockExperimentalVMOptions -XX:+ UseCGroupMemoryLimitForHeap

最好的選擇是將 Java 更新到 10 以上的版本,以便默認情況下支持容器。不幸的是,許多公司仍然嚴重依賴 Java 8。這意味着你應該在 Docker 鏡像中更新到 Java 的最新版本,或者確保至少使用 Java 8 update 191 或更高版本。

10. 謹慎使用容器自動化生成工具

你可能會偶然發現適用於構建系統的出色工具和插件。除了這些插件,還有一些很棒的工具可以幫助你創建 Java 容器,甚至可以根據需要自動發佈應用。

從開發人員的角度來看,這看起來很棒,因爲你不必在創建實際應用程序時,還要花費精力維護 Dockerfile。

這樣的插件的一個例子是 JIB。如下所示,我只需要調用 mvn jib:dockerBuild 命令可以構建鏡像:

<plugin>
   <groupId>com.google.cloud.tools</groupId>
   <artifactId>jib-maven-plugin</artifactId>
   <version>2.7.1</version>
   <configuration>
       <to>
           <image>myimage</image>
       </to>
   </configuration></plugin>

它將爲我構建一個具有指定名稱的 Docker 鏡像,而沒有任何麻煩。

使用 2.3 及更高版本時,可以通過調用 mvn 命令進行操作:

mvn spring-boot:build-image

在這種情況下,系統都會自動爲我創建一個 Java 鏡像。這些鏡像還比較小,那是因爲他們正在使用非發行版鏡像或 buildpack 作爲鏡像的基礎。但是,無論鏡像大小如何,你如何知道這些容器是安全的?你需要進行更深入的調查,即使這樣,你也不確定將來是否會保持這種狀態。

我並不是說你在創建 Java Docker 時不應使用這些工具。但是,如果你打算髮布這些鏡像,則應研究 Java 鏡像所有方面的安全。鏡像掃描將是一個好的開始。從安全性的角度來看,我的觀點是,以完全控制和正確的方式創建 Dockerfile,是創建鏡像更好,更安全的方式。

轉自:程序員朱朱,

鏈接:toutiao.com/article/6959742944421200387/

關注「ImportNew」,提升 Java 技能

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Vppyh_deoL6Wq25RHcmYOA