Docker容器映象瘦身的三種構建方法整理
1 基本的構建方式,就是依賴各種資源的傳統方式構建,分層較多,一個容器至少在600M左右,但是各方的依賴工具都很全面,對於除錯,以及內部操作都 相對簡單方便。
2 使用Distroless移除容器中的所有累贅
目前的映象不僅含有Node.js,還含有yarn、npm、bash以及大量其他二進位制檔案。同時,它是基於Ubuntu的。因此擁有一個完整的作業系統以及所有的二進位制檔案和實用程式。
這些在執行容器時都不是必需的。我們唯一的依賴項是Node.js。
Docker容器應封裝在單一程序中,且只包含執行所需的最精簡內容。我們不需要一個作業系統。
實際上,除了Node.js,其他都可以移除。
那麼要怎麼做呢?
幸運的是,Google也有同樣的想法,他們帶來了GoogleCloudPlatform/distroless[1]。
有如其倉庫說明所述:
“Distroless”映象只包含應用程式及其執行時依賴。不包含包管理器、Shell以及其他標準Linux發行版中能找到的其他程式。
這正是我們所需要的!
我們可以調整Dockerfile檔案來使用這個新的基礎映象:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM gcr.io/distroless/nodejs COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
然後像平常那樣編譯映象:
$ docker build -t node-distroless .
應用程式應能正常執行。要驗證這一點,可以像這樣執行容器:
$ docker run -p 3000:3000 -ti --rm --init node-distroless
訪問http://localhost:3000頁面即可。
這個未包含額外程式的映象會多小呢?
$ docker images | grep node-distroless
node-distroless 7b4db3b7f1e5 76.7MB
僅僅76.76MB!
比前一個映象少了600MB!
真是個好訊息!不過在使用Distroless時有些事項需要注意。
容器執行時,如果想對其進行檢查,可以這麼做:
$ docker exec -ti <替換成_docker_id> bash
上述命令將附加到容器中並執行bash,這與發起一個SSH會話相近。
不過由於Distroless是原始作業系統的精簡版本,不包含額外的程式。容器裡並沒有Shell!
如果沒有Shell,要如何附加到執行的容器中呢?
好訊息和壞訊息是,做不到。
壞訊息是我們只能執行容器中的二進位制程式。這裡能執行的只有Node.js:
$ docker exec -ti <替換成_docker_id> node
好訊息是因為沒有Shell,如果黑客入侵了我們的應用程式並獲取了容器的訪問許可權,他也無法造成太大的損害。也就是說,程式越少則尺寸越小也越安全。不過,代價是除錯更麻煩。
需要注意的是,我們不應該在生產環境中附加到容器中進行除錯,而應依靠正確的日誌和監控。
如果我們既希望能除錯,又關心尺寸大小,又該怎麼辦?
3 使用Alpine作為更小的基礎映象
我們可以使用Alpine取代Distroless來作為基礎映象。
Alpine Linux[2]是:
一個基於musl libc[3]和busybox[4]、面向安全的輕量級Linux發行版。
換言之,它是一個尺寸更小、更安全的Linux發行版。
是否言過其實,我們來檢查一下這個映象是否更小。
修改之前的Dockerfile並使用node:8-alpine:
FROM node:8 as build
WORKDIR /app
COPY package.json index.js ./
RUN npm install
FROM node:8-alpine
COPY --from=build /app /
EXPOSE 3000
CMD ["npm", "start"]
構建該映象:
$ docker build -t node-alpine .
現在看一下它的大小:
$ docker images | grep node-alpine
node-alpine aa1f85f8e724 69.7MB
69.7MB!
甚至比Distroless映象還要小!
我們來看看能不能附加執行中的容器。
首先,啟動容器:
$ docker run -p 3000:3000 -ti --rm --init node-alpine
Example app listening on port 3000!
現在附加到容器中:
$ docker exec -ti 9d8e97e307d7 bash
OCI runtime exec failed: exec failed: container_linux.go:296: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown
運氣不佳。但或許容器有sh這個Shell?
$ docker exec -ti 9d8e97e307d7 sh
/ #
很好!我們既可以附加到執行的容器中,得到的映象尺寸也很小。
聽起來很棒,不過有一個小問題。
Alpine基礎映象是基於muslc的,這是一個C的替代標準庫。
但是,多數Linux發行版,比如Ubuntu、Debian及CentOS都是基於glibc的。這兩個庫照理應該實現了相同的介面。
不過,它們的目標不同:
-
glibc最常用,速度更快
-
muslc佔用空間更少,以安全為核心
在編譯應用程式時,多數情況下是使用某個libc來編譯的。如果想在其他libc中使用,只能重新編譯。
也就是說,使用Alpine映象來構建容器可能會造成不可預期的問題,因為使用的是不同的C標準庫。
特別是在處理預編譯的二進位制檔案時,比如Node.js的C++擴充套件,這個差異更明顯。
舉個例子,PhantomJS預置包就無法在Alpine中工作。
怎麼選擇基礎映象?
Alpine、Distroless或是原生映象到底用哪個?
如果是在生產環境中執行,並且注重安全性, Distroless映象可能會更合適。
Docker映象中每增加一個二進位制程式,就會給整個應用程式帶來一定的風險。
在容器中只安裝一個二進位制程式即可降低整體風險。
舉個例子,如果黑客在運行於Distroless的應用中發現了一個漏洞,他也無法在容器中建立Shell,因為根本就沒有。
注意:最小化攻擊面是OWASP的推薦做法[5]。
如果更在意要是大小,則可以換成Alpine基礎映象。
這兩個都很小,代價是相容性。Alpine用了一個稍稍有點不一樣的C標準庫——muslc。時不時會碰到點相容性的問題。比如這個[6]和這個[7]。
原生基礎映象非常適合用於測試和開發。
它的尺寸比較大,不過用起來就像你主機上安裝的Ubuntu一樣。並且,你能訪問該作業系統裡有的所有二進位制程式。
下面,回顧一下各個映象大小:
node:8 681MB
node:8結合多階段構建 678MB
gcr.io/distroless/nodejs 76.7MB
node:8-alpine 69.7MB
相關連結:
-
https://github.com/GoogleCloudPlatform/distroless
-
https://alpinelinux.org
-
https://www.musl-libc.org
-
https://www.busybox.net
-
https://www.owasp.org/index.php/Minimize_attack_surface_area
-
https://github.com/grpc/grpc/issues/8528
-
https://github.com/grpc/grpc/issues/6126