容器的OCI標準定義了容器鏡像

規範,容器鏡像包與傳統的壓縮包(zip/tgz等)相比有兩個關鍵區別點:1)分層存儲;2)打包即部署。

分層存儲可以極大減少鏡像更新時候拉取鏡像包的時間,通常應用程序更新升級都只是更新業務層(如Java程序的jar包),而鏡像中的操作系統Lib層、運行時(如Jre)層等文件不會頻繁更新。因此新版本鏡像實質有變化的只有很小的一部分,在更新升級時候也只會從鏡像倉庫拉取很小的文件,所以速度很快。

打包即部署是指在容器鏡像製作過程包含了傳統軟體包部署的過程(安裝依賴的操作系統庫或工具、創建用戶、創建運行目錄、解壓、設置文件許可權等等),這麼做的好處是把應用及其依賴封裝到了一個相對封閉的環境,減少了應用對外部環境的依賴,增強了應用在各種不同環境下的行為一致性,同時也減少了應用部署時間。

基於分層存儲與打包即部署的特性,容器可以更快的部署、運行、保持環境一致性,這些特性都是容器相對於傳統虛擬機打包部署的優勢。這兩個點都是通過dockerfile來承載的,因此如何編寫高效、安全、規範易用的dockerfile是容器實踐中關鍵的一個環節。

1. 規範與安全

1.1. FROM

1.1.1 優先使用最小功能集的基礎鏡像

很多基礎鏡像都提供不同功能集的版本,根據實際情況選擇最小功能集的版本,功能越少體積越小,引入安全漏洞的風險越低。

如Node鏡像,最小的才不到30M,最大的超過200M。

1.1.2 顯示指定基礎鏡像的版本,禁用latest

latest 是一個不確定的版本號,依賴 latest 會導致每次構建出的鏡像是不可預知的。

1.1.3 指定依賴最具體的鏡像版本

依賴基礎鏡像版本時候,盡量指定到最具體的版本,這樣不確定性最小。如Node鏡像,版本號有 12, 12.4, 12.4.0,依賴 12.4.0 是不確定性最小的。

1.1.4 條件允許建議將依賴的基礎鏡像在本地倉庫mirror一份防止相同tag的鏡像內容不同

容器鏡像倉庫中的tag是可以被複寫的,同一個tag在不同的時間對應內容可能不同,因此如果條件允許,將依賴的鏡像mirror一份到本地,這樣可以保證

每次構建時候依賴的基礎鏡像是不變的。

1.1.5 顯示指定基礎鏡像的平台架構

Docker 1.10 版本開始,Docker官方鏡像倉庫與Docker-EE/Docker-CE都支持鏡像多平台功能,這裡的多平台包含不同類型操作系統(Windows/Linux),不同CPU體系(X86/ARM64)。

利用鏡像多平台功能,在docker pull ...命令中可以不用顯示指定平台類型,docker客戶端會根據當前的平台架構與鏡像倉庫協商(基於manifest list特性),拉取對應平台的鏡像。

通過這個特性可以保持用戶界面的簡潔性,客戶端執行 docker pull node:12 命令,在ARM64機器上,則會拉取 arm64v8/node:12 鏡像,在Linux機器上,則拉取 node:12 鏡像。

但是這種方式也引入了不確定性,不確定性表現為2方面:第一方面為不同平台上版本存在差異,可能依賴的版本只在一個平台有;第二個方面為有些時間可能在Linux環境下構建ARM鏡像,這樣會導致構建出錯誤鏡像。

1.2. LABEL

1.2.1 通過LABEL指令增加鏡像元數據(作者,時間,描述等)

通過Label增加鏡像作者,構建時間,描述等信息,讓使用者得到更多關於鏡像信息。

1.2.2 在Label中增加securitytxt規範

通過Label增加鏡像對應應用的 securitytxt 信息。

securitytxt 是 IETF 組織起草的一份規範,目的定義一套互聯網服務提供者與安全漏洞發現者之間交互的規範,使得安全漏洞能夠被閉環。

1.3. WORKDIR

1.3.1 使用WORKDIR指定工作目錄,避免絕對路徑擴散

RUN, COPY 命令都要使用到絕對路徑,定義好 WORKDIR 會使得 Dockerfile 移植性更好,更容易維護。

1.4 ENV與ARG

1.4.1 勿使用ENV與ARG傳遞敏感信息

ENV, ARG 的值都會被記錄下來,通過 docker image history 命令可以查看到,因此不要將敏感信息傳遞給ENV與ARG。

如果需要在dockerfile中使用密鑰或憑證,使用 mount secret 方式。

1.5. RUN

1.5.1 上下文依賴的命令在同一層完成(一個RUN指令)

由於 docker build cache 機制,有上下文依賴的命令放到同一個RUN指令中執行。

假如有如下dockefile片段:

...
RUN apt-get update
RUN apt-get install -y nginx
...

過了一段時間之後,需要修改一下上述dockerfile,增加一個安裝包

...
RUN apt-get update
RUN apt-get install -y nginx python
...

此時構建鏡像,docker 比對緩存,RUN apt-get update 這一層已經存在,使用緩存。這時候apt倉庫中的python或nginx可能已經有新版本了,由於沒有執行 apt-get update ,因此安裝的並不是最新版本。

1.5.2 考慮build cache的時效性,使用--no-cache參數禁用cache

通過構建參數 build --no-cache 顯示使得緩存失效,不過緩存失效會增加構建時長,需要綜合考慮。

1.5.3 使用 set -o pipefail 避免管道錯誤被忽略

假如在dockerfile中執行命令 RUN wget -O - https://some.site | wc -l > /number ,如果 wget -O - https://some.site 執行失敗了,但是 wc -l 是成功的,因此並不會報錯退出。

使用 set -o pipefail 避免管道錯誤被忽略。上述命令修改為: RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

1.6 ADD與COPY

1.6.1 優先使用COPY,比ADD更簡單明了

ADD 命令功能相對 COPY 更複雜,包含從internet下載,解壓等,優先使用 COPY 。

1.6.2 禁止使用ADD從遠程URL下載包,使用curl或wget先下載,使用ADD從本地解壓到鏡像內

不推薦使用 ADD 命令從遠程下載一個軟體包解壓到鏡像內,推薦使用 curl 下載到本地,然後再解壓到鏡像內,並將原始文件刪除。

1.7 USER

1.7.1 禁用ROOT用戶運行應用,為應用創建用戶與用戶組

root用戶運行應用程序存在安全風險,為每個應用單獨創建一個運行用戶。

1.7.2 如果應用依賴特定的UID/GID,則創建用戶/用戶組時候顯示指定

由於dockerfile中創建用戶/用戶組的 UID/GID 是不固定的,如果應用程序依賴 UID/GID,創建用戶時候顯示指定。

1.7.4 應用運行用戶的Shell設置為/sbin/nologin

應用程序運行用戶設置Shell為 /sbin/nologin 。

1.8 EXPOSE

1.8.1 使用EXPOSE指令指明Listen埠與協議

通過 EXPOSE 指令申明應用 listen 的埠與協議,讓應用運維人員者簡單明了得知埠。

EXPOSE 80/tcp

1.9 VOLUME

1.9.1 使用VOLUME申明鏡像中需要寫入數據的目錄

程序持久化或臨時文件目錄,使用 VOLUME 指令申明,讓應用運維人員簡單明了得知需要掛載的卷。

1.10 日誌

1.10.1 將標準日誌與錯誤日誌分別輸出到stdout與stderr

日誌輸出到標準輸出與錯誤輸出,方便查看與採集日誌。參考 Nginx 的 dockerfile :

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log
&& ln -sf /dev/stderr /var/log/nginx/error.log

1.11 CMD與ENTRYPOINT

1.11.1 優先使用CMD/ENTRYPOINT指令的EXEC格式設置鏡像的默認執行程序,使得應用進程PID為1

推薦將容器內的應用程序設置為PID 1,這樣對容器內應用管理相對簡單,1號進程退出後整個容器也就退出了。

在dockerfile中使用 CMD 或 ENTRYPOINT 指令設置容器鏡像的默認啟動進程, CMD/ENTRYPOINT 有兩種執行方式:exec 與 /bin/sh ,使用 exec 方式直接執行應用程序可執行文件設置PID為1,使用 /bin/sh 方式的話,1號進程就是 /bin/sh 。

exec 方式: CMD["java", "/same/args"], ENTRYPOINT ["java", "/same/args"] , /bin/sh 方式: CMD java /same/args, ENTRYPOINT java /same/args

1.11.2 使用ENTRYPOINT封裝鏡像的固定行為,使用CMD配合輸入可變參數

ENTRYPOINT 與 CMD 功能類似,他們區別是 ENTRYPOINT 不能被 docker run 的命令行參數覆蓋,而 CMD 可以; ENTRYPOINT 通常配合 CMD 使用,使用 CMD 設置 ENTRYPOINT 的默認參數,同時支持在 docker run 設置參數傳遞給 ENTRYPOINT。

參考Redis的ENTRYPOINT寫法。

對於切換用戶,推薦使用 gosu 替換 sudo 。

1.11.3 使用ENTRYPOINT執行運行前準備工作

如果在運行應用程序之前需要做一些準備工作,如檢查文件/目錄許可權等,那麼可以在 ENTRYPOINT 的腳本中完成。

1.11.4 正確理解K8S的command與args與docker鏡像的ENTRYPOINT與CMD的關係

通過K8S調度docker鏡像可以覆蓋dockerfile中的 ENTRYPOINT 與 CMD,他們之間的關係參考如下文章。

1.12 使用hadolint工具檢查dockerfile

hadolint 定義了一套規則與檢查工具,可以在項目中使用。

2. 效率

2.1 體積小

2.1.1. 選擇最小的基礎鏡像

參考 1.1.1 。

2.1.2 禁止使用 chmod -R /a/root/path,更改具體某個/類文件的許可權

由於dockerfile構建鏡像使用的是分層只讀文件系統,如果使用 chmod 更改一個大目錄許可權,相當於複製了這個目錄下所有文件,會導致鏡像體積變大。

2.1.3 分階段構建

Docker 17.05 版本中引入了分階段構建({Multi-stage builds](https://docs.docker.com/devel...),通過分階段構建可以減少鏡像大小。

2.1.4 dockerfile書寫順序按照更新頻度升序排序

Docker鏡像的分層是子層依賴父層,對應到 Docker build cache 機制中,如果某一層未命中緩存,那麼其剩下的層都不會使用緩存。

因此如果需要最大利用緩存機制,推薦將變化頻度低的層盡量放上層,變化頻度高的層放下層。

2.1.5 禁止使用類似apt-get upgrade系統級更新,更新具體需要的軟體包

apg-get upgrade 會使得鏡像大小不可控,不知道更新了多少軟體包。

2.1.6 相關度高/更新頻度一致的命令,寫到同一個RUN指令中

同樣是考慮緩存命中率,變化頻度一致的命令放到同一層。

2.2 構建快

2.2.1 使用獨立的目錄作為build context

Docker在構建鏡像時候有一個 build context 概念,build context 在 docker build 指定一個目錄,docker 會將 build context 目錄內所有文件載入到內存,作為build context。

build context 目錄內內容越少,載入速度越快,建議使用獨立的目錄作為build context,只拷貝需要的文件到 build context 目錄。

2.2.2 使用.dockerignore文件

使用 .dockerignore 文件排除目錄下不需要載入到 build context 內的文件或目錄。

2.2.3 從stdin接收dockerfile會使得build context大小為0

Docker 支持從stdin輸入 dockerfile ,這種方式不會載入任何本地文件到docker的build context中。

關注公眾號訂閱雲最佳實踐


推薦閱讀:
相关文章