隨着容器技術的普及,愈來愈多的應用被容器化。人們使用容器的頻率愈來愈高,但經常忽略一個基本但又很是重要的問題 - 容器鏡像的體積。本文將介紹精簡容器鏡像的必要性並以基於 spring boot 的 java 應用爲例描述最小化容器鏡像的經常使用技巧。java
精簡容器鏡像是很是必要的,下面分別從安全性和敏捷性兩個角度進行闡釋。node
安全性python
基於安全方面的考慮,將沒必要要的組件從鏡像中移除能夠減小攻擊面、下降安全風險。雖然 docker 支持用戶經過 Seccomp 限制容器內能夠執行操做或者使用 AppArmor 爲容器配置安全策略,但它們的使用門檻較高,要求用戶具有安全領域的專業素養。git
敏捷性github
精簡的容器鏡像能提升容器的部署速度。假設某一時刻訪問流量激增,您須要經過增長容器副本數以應對突發壓力。若是某些宿主機不包含目標鏡像,須要先拉取鏡像,而後啓動容器,這時使用體積較小的鏡像能加速這一過程、縮短擴容時間。另外,鏡像體積越小,其構建速度也越快,同時還能減小存儲和傳輸的成本。spring
將一個 java 應用容器化所需的步驟可概括以下:docker
本章所用的樣例是一個基於 spring boot 的 java 應用 spring-boot-docker,所用的未經優化的 dockerfile 以下:shell
FROM maven:3.5-jdk-8 COPY src /usr/src/app/src COPY pom.xml /usr/src/app RUN mvn -f /usr/src/app/pom.xml clean package ENTRYPOINT ["java","-jar","/usr/src/app/target/spring-boot-docker-1.0.0.jar"]
因爲應用使用 maven 構建,dockerfile 中指定maven:3.5-jdk-8
做爲基礎鏡像,該鏡像的大小爲 635MB。經過這種方式最終構建出的鏡像很是大,達到了 719MB,這是由於一方面基礎鏡像自己就很大,另外一方面 maven 在構建過程當中會下載許多用於執行構建任務的 jar 包。緩存
多階段構建安全
Java 程序的運行只依賴 JRE,並不須要 maven 或者 JDK 中衆多用於編譯、調試、運行的工具,所以一個明顯的優化方法是將用於編譯構建 java 源碼的鏡像和用於運行 java 應用的鏡像分開。爲了達到這一目的,在 docker 17.05 版本以前須要用戶維護 2 個 dockerfile 文件,這無疑增長了構建的複雜性。好在自 17.05 開始,docker 引入了多階段構建的概念,它容許用戶在一個 dockerfile 中使用多個 From 語句。每一個 From 語句能夠指定不一樣的基礎鏡像並將開啓一個全新的構建流程。您能夠選擇性地將前一階段的構建產物複製到另外一個階段,從而只將必要的內容保留在最終的鏡像裏。優化後的 dockerfile 以下:
FROM maven:3.5-jdk-8 AS build COPY src /usr/src/app/src COPY pom.xml /usr/src/app RUN mvn -f /usr/src/app/pom.xml clean package FROM openjdk:8-jre ARG DEPENDENCY=/usr/src/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"]
該 dockerfile 選用maven:3.5-jdk-8
做爲第一階段的構建鏡像,選用openjdk:8-jre
做爲運行 java 應用的基礎鏡像而且只拷貝了第一階段編譯好的.claass
文件和依賴的第三方 jar 包到最終的鏡像裏。經過這種方式優化後的鏡像大小爲 459MB。
使用 distroless 做爲基礎鏡像
雖然經過多階段構建能減少最終生成的鏡像的大小,但 459MB 的體積仍相對過大。經調查發現,這是由於使用的基礎鏡像openjdk:8-jre
體積過大,到達了 443MB,所以下一步的優化方向是減少基礎鏡像的體積。
Google 開源的項目 distroless 正是爲了解決基礎鏡像體積過大這一問題。Distroless 鏡像只包含應用程序及其運行時依賴項,不包含包管理器、shell 以及在標準 Linux 發行版中能夠找到的任何其餘程序。目前,distroless 爲依賴 java、python、nodejs、dotnet 等環境的應用提供了基礎鏡像。
使用 distroless 的 dockerfile 以下:
FROM maven:3.5-jdk-8 AS build COPY src /usr/src/app/src COPY pom.xml /usr/src/app RUN mvn -f /usr/src/app/pom.xml clean package FROM gcr.io/distroless/java ARG DEPENDENCY=/usr/src/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"]
該 dockerfile 和上一版的惟一區別在於將運行階段依賴的基礎鏡像由openjdk:8-jre
(443 MB)替換成了gcr.io/distroless/java
(119 MB)。通過這一優化,最終鏡像的大小爲 135MB。
使用 distroless 的惟一不即是您沒法 attach 到一個正在運行的容器上排查問題,由於鏡像中不包含 shell。雖然 distroless 的 debug 鏡像提供 busybox shell,但須要用戶從新打包鏡像、部署容器,對於那些已經基於非 debug 鏡像部署的容器無濟於事。 但從安全角度來看,沒法 attach 容器並不徹底是壞事,由於攻擊者沒法經過 shell 進行攻擊。
使用 alpine 做爲基礎鏡像
若是您確實有 attach 容器的需求,又但願最小化鏡像的大小,能夠選用 alpine 做爲基礎鏡像。Alpine 鏡像的特色是體積很是下,基礎款鏡像的體積僅 4 MB 左右。
使用 alpine 後的 dockerfile 以下:
FROM maven:3.5-jdk-8 AS build COPY src /usr/src/app/src COPY pom.xml /usr/src/app RUN mvn -f /usr/src/app/pom.xml clean package FROM openjdk:8-jre-alpine ARG DEPENDENCY=/usr/src/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"]
這裏並未直接繼承基礎款 alpine,而是選用從 alpine 構建出的包含 java 運行時的openjdk:8-jre-alpine
(83MB)做爲基礎鏡像。使用該 dockerfile 構建出的鏡像體積爲 99.2MB,比基於 distroless 的還要小。
執行命令docker exec -ti <container_id> sh
能夠成功 attach 到運行的容器中。
distroless vs alpine
既然 distroless 和 alpine 都能提供很是小的基礎鏡像,那麼在生產環境中到底應該選擇哪種呢?若是安全性是您的首要考慮因素,建議選用 distroless,由於它惟一可運行的二進制文件就是您打包的應用;若是您更關注鏡像的體積,能夠選用 alpine。
其餘技巧
除了能夠經過上述技巧精簡鏡像外,還有如下方式:
想了解更多優化 dockerfile 的小竅門可參考教程 Best practices for writing Dockerfiles。
本文爲雲棲社區原創內容,未經容許不得轉載。