容器化部署愈來愈多的應用於企業的生產環境中,如何構建安全可靠、最小化的 Docker
鏡像也就愈來愈重要。本文將針對該問題,經過原理加實踐的方式,從頭到腳幫你擼一遍。文章比較長,主要經過五個部分對容器鏡像進行講解。分別是:html
讀者能夠根據各自狀況選擇性閱讀。原文發佈在個人我的站點: GitDiG.com. 原文鏈接:構建安全可靠、最小化的 Docker 鏡像.node
手動構建 Docker
鏡像的流程圖,以下:git
如今依次按照流程採用命令行的方式手動構建一個簡單的Docker
鏡像。github
取busybox
做爲本次試驗的基礎鏡像,由於它足夠小,大小才 1.21MB
。golang
$: docker run -it busybox:latest sh
/ # touch /newfile
/ # exit
複製代碼
經過以上的操做,咱們完成了流程圖的前三步。建立了一個新容器,並在該容器上建立了一個新問題。只是,咱們退出容器後,容器也不見了。固然容器不見了,並不表示容器不存在了,Docker
已經自動保存了該容器。若是在建立時,未顯示設置容器名稱,能夠經過如下方式查找該消失的容器。docker
# 列出最近建立的容器
$: docker container ls -n 1
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c028c091f964 busybox:latest "sh" 13 minutes ago Exited (0) 27 seconds ago upbeat_cohen
# 查詢容器的詳情
$: docker container inspect c028c091f964
...
複製代碼
手動構建鏡像,很簡單。先找到發生變動的容器對象,對其變動進行提交。提交完成後,鏡像也就生成了。不過此時的鏡像只有一個自動生成的序列號惟一標識它。爲了方便鏡像的檢索,須要對鏡像進行命名以及標籤化處理。安全
命令行操做以下:bash
# 提交變動, 構建鏡像完成
$: docker commit -a JayL -m "add newfile" c028c091f964
sha256:01603f50694eb62e965e85cae2e2327240e4a68861bd0e98a4fb4ee27b403e6d
# 對鏡像進行命名, 原鏡像ID取前幾位就能夠了
$: docker image tag 01603f50694eb62e9 busybox:manual
# 驗證新鏡像
$: docker run busybox:manual ls -al newfile
-rw-r--r-- 1 root root 0 Jun 15 05:25 newfile
複製代碼
經過以上兩步過程就完成了Docker
鏡像手動建立。很是簡單是否是。可是也很是麻煩,必須先建立新容器在提交變動,生成鏡像。整個過程徹底能夠經過腳本化處理,這也是下節要說的,自動化構建Docker
鏡像。網絡
自動化構建Docker
鏡像,Docker
公司提供的不是SHELL腳本的方式,而是經過定義一套獨立的語法來描述整個構建過程, 經過該語法編輯的文件,稱爲 Dockerfile
。 自動化構建鏡像就是經過編寫Dockerfile
文件構建的。app
一樣完成上面的工做,用Dockerfile
寫出來就是:
FROM busybox:latest
RUN touch /newfile 複製代碼
至於更加詳細的Dockerfile
語法,請參見官方指南。
完成Dockerfile
編寫後,經過命令觸發構建。整個過程,腳本化出來就是:
$: mkdir autobuild && cd autobuild
$: cat <<EOF > Dockerfile
FROM busybox:latest
RUN touch /newfile
EOF
$: docker build -t busybox:autobuild .
複製代碼
Docker 鏡像是由一組只讀的鏡像層
Image Layer
組成的。而Docker 容器則是在Docker 鏡像的基礎之上,增長了一層:容器層Container Layer
。容器層Container Layer
是可讀寫的。若是對該容器層Container Layer
進行commit
提交操做,該層就變成了新的鏡像層Image Layer
。新的Docker Image
也就構建出來了。
$: mkdir layer && cd layer && touch newfile
$: cat <<EOF > Dockerfile
FROM scratch
ADD newfile .
EOF
$: docker build -t layer .
複製代碼
如下官網提供的圖示能夠很清楚的看出鏡像與容器之間的聯繫與區別:
具體某個鏡像的組成Layer
能夠經過以下命令進行查詢:
# 鏡像的構建層歷史
$: docker history busybox:autobuild
IMAGE CREATED CREATED BY SIZE COMMENT
845cc5130d2c 17 minutes ago /bin/sh -c touch /newfile 0B
ef46e0caa533 4 days ago /bin/sh -c #(nop) CMD ["sh"] 0B
<missing> 4 days ago /bin/sh -c #(nop) ADD file:1067e5a... in / 1.21MB
複製代碼
不難看出,鏡像busybox:autobuild
一共執行了從底往上的三次層構建。具體構建的指令能夠經過第三列的命令得出。<missing>
的意思是:該層是在其它系統上構建的,在本地是不可用的。只須要忽略就好。
要了解 Docker 鏡像的存儲首先必須瞭解聯合文件系統 UnionFS
(Union File System),所謂UnionFS
就是把不一樣物理位置的目錄合併mount
到同一個目錄中。UnionFS
的具體實現有不少種:
具體Docker
宿主機上使用那種UnionFS
文件系統驅動,能夠經過以下命令查詢:
$: docker info | grep Storage
Storage Driver: overlay2
複製代碼
overlay2
是一種更現代的聯合文件系統 UnionFS
,它比overlay
的早期版本在穩定與性能上都有很大提高。因此通常最新的Docker
採用的存儲驅動使用的都是overlay2
。
爲了方便演示UnionFS
文件系統,若是是MacOS系統,建議安裝Docker Machine
開啓一臺新的虛擬機操做,排除由於Docker for MacOS
運行在虛擬機上的各類環境干擾。具體Docker Machine
的安裝請自行查閱相關文檔。
首先建立一臺新的Docker Machine
:
# 建立
$: docker-machine create ufs
...
Docker is up and running!
# 登陸
$: docker-machine ssh ufs
... ok
# 查詢 overlay
$: cat /proc/filesystems | grep overlay
nodev overlay
複製代碼
經過確認,這臺Docker Machine
是支持UnionFS
文件系統的,使用的是overlay
存儲驅動。 既然UnionFS
就是把不一樣物理位置的目錄合併mount到同一個目錄中.如今咱們經過命令行的方式實現一下Docker
官網提供UnionFS
的原理圖。
從圖中能夠看出,咱們須要提供兩個目錄,分別表明Container Layer
和Image Layer
。目錄名稱,取圖示右部的名稱:
upper
, 表明Container Layer
lower
, 表明Image Layer
除了這兩個目錄之外,經過UnionFS
掛載目錄還須要兩個目錄:
merged
, 表明掛載目錄,即合併後的目錄work
, 必須爲空目錄,是overlay
存儲驅動掛載所需的工做目錄。經過命令行實現圖示中的文件夾結構:
# 建立一個測試目錄
$: mkdir demo && cd demo
# 建立子目錄與文件
$: mkdir upper lower merged work
$: touch lower/file1 lower/file2 lower/file3
$: touch upper/file2 upper/file4
# 經過文件內容區分如下file2
$: echo lower > lower/file2
$: echo upper > upper/file2
# 未掛載
$: ls merged
複製代碼
迄今爲止,一切都是常規文件系統操做。如今經過mount
命令進行UnionFS
文件系統的目錄掛載.
# 目錄合併掛載到merged
$: sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged
# 掛載完成後
$: ls merged
file1 file2 file3 file4
# file2 使用的是頂層 upper 的file2 文件
$: cat merged/file2
upper
複製代碼
下面再分別經過文件的增刪改加深對UnionFS
文件系統的理解:
# 新增文件
$: touch merges/file5
$: ls merged/
file1 file2 file3 file4 file5
# 新增文件寫在頂層的 upper 文件夾
$: ls upper/
file2 file4 file5
$: ls lower/
file1 file2 file3
複製代碼
# 修改文件 CoW 技術
$: echo mod > merged/file1
$: ls upper/
file1 file2 file4 file5
$: cat upper/file1
mod
$: cat lower/file1
複製代碼
# 刪除文件
$: rm merged/file1
$: ls -al upper | grep file1
c--------- 1 root root 0, 0 Jun 17 10:41 file1
$: ls -al lower | grep file1
-rw-r--r-- 1 docker staff 0 Jun 17 10:15 file1
複製代碼
實際操做完成以上過程,相信你對於UnionFS
文件系統有了更加直觀的感覺。你可能會問, Docker Image
的底層鏡像是由一組Layer
組成的,多個底層目錄在UnionFS
中如何掛載?其實很簡單,只須要經過:
分隔便可。
# 多層目錄: lower1 / lower2 / lower3
$: sudo mount -t overlay overlay -olowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
複製代碼
掛載完成後,lower1
/ lower2
/ lower3
之間的層疊順序又是怎樣,讀者能夠自行測試一下。
最後,咱們查詢一下系統的掛載列表,
mount | grep overlay
overlay on /home/docker/demo/merged type overlay (rw,relatime,lowerdir=lower,upperdir=upper,workdir=work)
複製代碼
從現有輸出可知目前咱們docker-machine
中僅掛載了一個overlay
目錄。
如今咱們在這臺新的docker-machine
上構建一個1.2
中所描述的Docker
鏡像: busybox:autobuild
。
$: mkdir autobuild && cd autobuild
$: cat <<EOF > Dockerfile
FROM busybox:latest
RUN touch /newfile
EOF
$: docker build -t busybox:autobuild .
# 完成構建後,如今系統中有兩個docker image
$: docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox autobuild 2e32da74b3ad 4 seconds ago 1.22MB
busybox latest e4db68de4ff2 2 days ago 1.22MB
複製代碼
構建完成後,咱們直接看一下docker-machine
上的文件系統的掛載狀況:
# docker 無容器運行
$: mount
...
/dev/sda1 on /mnt/sda1/var/lib/docker type ext4 (rw,relatime,data=ordered)
# docker 運行容器時
# 從新開啓新會話,運行一個容器實例 `docker run -it busybox:autobuild sh`
$: mount
...
/dev/sda1 on /mnt/sda1/var/lib/docker type ext4 (rw,relatime,data=ordered)
overlay on /mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/merged type overlay (rw,relatime,lowerdir=/mnt/sda1/var/lib/docker/overlay2/l/KLGL6INSJ2UBLMAUP5B4IORUTG:/mnt/sda1/var/lib/docker/overlay2/l/BGIT3WQZVII4Z2THF35I6T5V5O:/mnt/sda1/var/lib/docker/overlay2/l/6GZ2NT4UQT6EQK3IT4IGMBXU4T,upperdir=/mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/diff,workdir=/mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/work)
shm on /mnt/sda1/var/lib/docker/containers/e50f19c5bde3fe53cde3729de92f75b74323f7ebb506b0635eb76dd5b81e080a/mounts/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)
nsfs on /var/run/docker/netns/3c464f8003e8 type nsfs (rw)
複製代碼
對比輸出,可以很明顯的看到,暫僅關注 overlay
掛載狀況。得出:
/mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/merged
/mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/diff
/mnt/sda1/var/lib/docker/overlay2/l/KLGL6INSJ2UBLMAUP5B4IORUTG
/mnt/sda1/var/lib/docker/overlay2/l/BGIT3WQZVII4Z2THF35I6T5V5O
/mnt/sda1/var/lib/docker/overlay2/l/6GZ2NT4UQT6EQK3IT4IGMBXU4T
其中鏡像Layer使用的是軟鏈接。一樣的信息,咱們能夠通docker inspect
查詢出來。
$: docker inspect <container-id> -f '{{.GraphDriver.Data.MergedDir}}'
$: docker inspect <container-id> -f '{{.GraphDriver.Data.UpperDir}}'
$: docker inspect <container-id> -f '{{.GraphDriver.Data.LowerDir}}'
複製代碼
輸出的路徑就是具體Docker
鏡像的存儲位置。
最小化 Docker
鏡像的緣由可總結出如下幾條:
按 1.3
、1.4
中所討論的鏡像的組成原理與存儲, 最小化 Docker
鏡像的主要途徑總結下來也就兩條:
先從簡單的減小鏡像Layer的層數開始。
在定義Dockerfile
的時候,每一條指令都會對應一個新的鏡像層。經過docker history
命令就能夠查詢出具體Docker
鏡像構建的層以及每層使用的指令。爲了減小鏡像的層數,在實際構建鏡像時,經過使用&&
鏈接命令的執行過程,將多個命令定義到一個構建指令中執行。如:
FROM debian:stable
WORKDIR /var/www
RUN apt-get update && \ apt-get -y --no-install-recommends install curl \ ca-certificates && \ apt-get purge -y curl \ ca-certificates && \ apt-get autoremove -y && \ apt-get clean 複製代碼
除了經過將多命令經過&&
鏈接到一個構建指令外,在Docker
鏡像的構建過程當中,還能夠經過--squash
的方式,開啓鏡像層的壓縮功能,將多個變化的鏡像層,壓縮成一個新的鏡像層。
具體命令就以下:
$: docker build --squash -t <image> .
複製代碼
縮減Layer的大小須要從頭開始,即選擇什麼樣的基礎鏡像做爲初始鏡像。通常狀況下,你們都會從如下三個基礎鏡像開始。
鏡像 busybox 經過busybox
程序提供一些基礎的Linux系統操做命令,鏡像 alpine則是在次基礎上提供了apk
包管理命令,方便安裝各種工具及依賴包。普遍使用的鏡像基本都是鏡像 alpine。鏡像 busybox更適合一些快速的實驗場景。而鏡像 scratch空鏡像,由於不提供任何輔助工具,對於不依賴任何第三方庫的程序是合適的。由於鏡像 scratch空鏡像自己不提供任何container OS
,因此程序是運行在Docker Host
即宿主機上的,只是利用了Docker
技術提供的隔離技術而已。
細心的讀者可能會發現,在MacOS
上編譯的程序,採用鏡像 scratch空鏡像時,容器運行會報錯:。那是由於,Docker for Mac
是運行在Linux
虛擬機上的緣故。因此不能夠直接構建MacOS
格式的可執行程序在Docker for Mac
上採用空鏡像的方式運行。
多階段構建 Multi-Stage Build
是 Docker 17.05
版本開始引入的新特性。經過將原先僅一個階段構建的鏡像查分紅多個階段。之因此多階段構建鏡像可以縮減鏡像的大小,是由於發佈程序在編譯期相關的依賴包以及臨時文件並非最終發佈鏡像所須要的。經過劃分不一樣的階段,構建不一樣的鏡像,最終鏡像則取決於咱們真正須要發佈的實體是什麼。
FROM golang:1.11-alpine3.7 AS builder
WORKDIR /app COPY main.go . RUN go build -o server .
FROM alpine:3.7
WORKDIR /app COPY --from=builder /app .
CMD ["./server"] 複製代碼
如上的Dockerfile
就是多階段構建,在builder
階段使用的基礎鏡像是golang:1.11-alpine3.7
,顯然是由於編譯期的須要,對於發佈真正的server
程序是徹底不必的。經過多階段構建鏡像的方式就能夠僅僅打包須要的實體構成鏡像。
除了多階段構建之外,若是你還想忽略鏡像中一些冗餘文件,還能夠經過.dockerignore
的方式在文件中定義出來。功能和.gitignore
相似。
最小化Docker
鏡像的構建完成了,可是,咱們的工做卻仍未結束。咱們還須要對鏡像進行加固處理。
鏡像內容可尋址標識符(Content addressable image identifiers), 能夠對來源基礎鏡像內容進行校驗,確保沒有被第三方篡改。具體的操做方式,就是在構建本身鏡像的同時,對基礎鏡像內容進行內容的sha256
摘要值進行設置,防止在不知情的狀況下被篡改。
首先,得出具體鏡像的正確sha256
摘要值.
# 經過命令查詢出具體鏡像的sha256摘要
$: docker inspect busybox:autobuild -f "{{.RepoDigests}}"
sha256:9b63a0eaaed5e677bb1e1b29c1a97268e6c9e6fee98b48badf0f168ae72a51dc
複製代碼
再在Dockerfile
定義時,設置基礎鏡像的sha256
摘要值
FROM busybox@sha256:9b63a0eaaed5e677bb1e1b29c1a97268e6c9e6fee98b48badf0f168ae72a51dc
...
複製代碼
注意:鏡像內容可尋址標識符的獲取必須通過一次 push 或者 pull 操做,即在鏡像註冊服務上發佈後,才能夠經過以上 inspect 命令查詢出結果。若是僅僅是本地的鏡像,沒法經過 inpect 命令獲取。固然僅僅是本地使用的鏡像,鏡像內容可尋址標識符也是不必的。
容器一旦建立出來,其默認使用的用戶是能夠在鏡像中進行設置的。經過設置必要的鏡像默認用戶,能夠限制其在容器中的執行權限。在某種程度上也就進行提高了鏡像的安全級別。不過,這須要根據具體的業務發佈狀況進行設置,常規狀況下,基礎鏡像都仍是root用戶做爲默認用戶 。
安全原則:構建鏡像自己是爲了特定的應用定製的,默認狀況下應該儘量的下降用戶權限。
除了鏡像自己設置必要的默認用戶之外,在鏡像中,還會存在一類程序,即便是經過普通用戶執行,但在運行時會以更高級別的權限執行。就是系統針對可執行文件與目錄提供的SUID與SGID特殊權限。
經過對可執行文件設置SUID或SGID屬性,本來執行命令的用戶會切換成爲命令的全部者或是所屬組的權限進行執行。也就是提高了執行命令的權限。
在實際的鏡像構建中,應該儘量的避免此類權限提高形成的可能的漏洞。建議鏡像構建時,掃描鏡像內是否存在此類執行文件,若是存在儘量的刪除。刪除命令可參考:
# 鏡像構建過程當中增長對特殊權限可執行文件的掃描並刪除
RUN for i in $(find / -type f \( -perm +6000 -o -perm +2000 \)); \ do chmod ug-s $i; done 複製代碼
正如Code Review
同樣,代碼審查能夠大大提高企業項目的質量。容器鏡像一樣做爲開發人員或是運維人員的產出物,對其進行審查也是必要的。
雖然咱們能夠經過docker
命令結合文件系統瀏覽的方式進行容器鏡像的審查,但其過程須要人工參與,很難作到自動化,更別提將鏡像審查集成到CI過程當中了。但一個好的工具能夠幫咱們作到這點。
推薦一個很是棒的開源項目dive,具體安裝請參考其項目頁。它不但能夠方便咱們查詢具體鏡像層的詳細信息,還能夠做爲CI
持續集成過程當中的鏡像審查之用。使用它能夠大大提高咱們審查鏡像的速度,而且能夠將這個過程作成自動化。
該項目的具體動態操做圖示以下:
若是做爲鏡像審查以後,能夠進行以下命令操做:
$: CI=true dive <image-id>
Fetching image... (this can take a while with large images)
Parsing image...
Analyzing image...
efficiency: 95.0863 %
wastedBytes: 671109 bytes (671 kB)
userWastedPercent: 8.2274 %
Run CI Validations...
Using default CI config
PASS: highestUserWastedPercent
SKIP: highestWastedBytes: rule disabled
PASS: lowestEfficiency
複製代碼
從輸出信息能夠獲得不少有用的信息,集成的CI
過程也就很是容易了。 dive
自己就提供了.dive-ci
做爲項目的CI
配置:
rules:
# If the efficiency is measured below X%, mark as failed.
# Expressed as a percentage between 0-1.
lowestEfficiency: 0.95
# If the amount of wasted space is at least X or larger than X, mark as failed.
# Expressed in B, KB, MB, and GB.
highestWastedBytes: 20MB
# If the amount of wasted space makes up for X% or more of the image, mark as failed.
# Note: the base image layer is NOT included in the total image size.
# Expressed as a percentage between 0-1; fails if the threshold is met or crossed.
highestUserWastedPercent: 0.20
複製代碼
集成到CI
中,增長如下命令便可:
$: CI=true dive <image-id>
複製代碼
鏡像審查和代碼審查相似,是一件開始抵制,開始後就欲罷不能的事。這件事宜早不宜遲。對於企業與我的而言均百利而無一害。
以前發佈文章如何對Docker Image進行審查,出自此節。