圍繞著 Docker 容器(container)技術所引發的風潮,一個越來越成熟的生態系正在形成。相信許多人都在關注它的發展,也可能有諸多的疑問。對於真正想把 Docker 應用在生產環境的人而言,其中一個疑問可能是:Docker 的映像(image) 非得動則幾十,上百甚至上千 MB 嗎?的確,頻寬跟儲存空間不再像過去那樣高不可攀,但太大的檔案或多或少還是會影響部署時間。本文山姆鍋示範其中一種製作小型 Docker 映像的方法。
對於想直接知道結果的讀者,可以參考本文所使用的 原始碼 ⎘ 。
2020/6/10 更新:透過 Multi-stage build, 可以在同一個 Dockerfile 就完成映像建置。
為什麼需要在意映像檔大小
Docker 所提供的容器技術,另一種稱呼為: 軟體容器(Software Container),是一種 作業系統層虛擬化 ⎘ 技術。 在不同的作業系統有不同的方法跟名稱,例如:Solaris 的 zones, FreeBSD 的 jail, Linux 的 LXC 等都是同樣概念的東西。表面上,容器跟虛擬機(virtual machine)有許多相似之處,同樣可以區隔不同的應用與服務,避免干擾彼此的執行。所以,您會看到許多人試圖在一個容器內同時執行多個服務或應用,需要用到排程,所以 cron 服務要啟動; 需要資料庫,所以資料庫服務器也一起執行。嚴格來講,沒有人可以說這樣做不對。但是,容器不是虛擬機!這樣的做法並沒有發揮容器技術的潛力。山姆鍋認同的觀點是:一個容器應該只包含一個應用或服務剛好所需(just enough)的檔案。例如:一個 web 應用,除了應用本身執行檔外,可能需要第三方的套件,也許還需要一些系統套件才能運作,除此之外都是多餘。簡單說,容器包含的東西要盡可能的精簡,自然表示映像檔要盡量的小。
為什麼不需要在意映像檔大小
什麼?不是才說要在意映像檔大小,轉眼又說不用在意?是的沒錯,山姆鍋不是頭腦不清楚。對於多數的使用情況,您不需要太在意一個映像檔大小。透過 Docker 這樣的產品,由於映像檔採用層層堆疊的方式組成,組成映像檔底層的資料通常會被重複使用。如此,在部署下載映像檔時,並不會因為小小修改就需要重新下載整個映像檔。但這個前提是:那個映像檔之前已經被下載並快取在該主機中!
小型映像檔的需求
山姆鍋先說明對這個小型映像檔的需求:
- 要跟 Ubuntu 14.04 二進位相容: 對於網路服務或應用,山姆鍋選擇的作業系統就是 Ubuntu,所以,跟Ubuntu相容是必要條件。
- 要支援 Bash: 雖然有 Busybox 提供大多數工具程式,但還是希望使用的 Shell 是 Bash。
- 要有一個啟動器: 需要一個類似 Linux init 功能,但是精簡的服務。山姆鍋這裏使用的是
runit
,使用 runit 主要目的在利用它所提供的 log 以及改變執行身份的功能。 - 建構映像檔需要的時間必需短: 使用 buildroot 這樣的工具雖然可以建構出小型映像檔,但編譯需要的時間實在太久了。
製作小型 Docker 映像檔的方法
不管您對於該不該在意映像檔大小的結論是什麼,看到這裡,山姆鍋假設您還是想知道如何製作小型映像檔的方法。山姆鍋採用的方法分成兩階段,第一階段產生一個根檔案(root filesystem)系統; 第二階段將這個根檔案系統包裝成 Docker 映像檔。
先說說比較容易理解的第二階段,底下是第二階段所使用的 Dockerfile:
FROM scratch #1
ADD overlayfs.tar / #2
ADD run.sh /run.sh #3
RUN chmod a+x /run.sh
ENV HOME /
WORKDIR /
CMD ["/run.sh"]
-
從標記 1, 這個映像以 scratch 這個映像為基礎。
-
標記 2 的將代表根檔案系統內容的
overlayfs.tar
展開並複製到映像檔的根目錄。 這裏稍微補充一下:ADD 指令在遇到 .tar 這種壓縮檔會自動解開。 -
run.sh
這個腳本負責啟動runit
服務。
run.sh
的內容:
#!/bin/sh
export > /etc/envvars #1
/usr/sbin/runsvdir-start
runit 有個問題是不會傳遞環境變數給它啟動的服務或應用,標號 1 的敘述就是將當下環境變數存到 /etc/envvars
這個檔案,後續可以 使用 source /etc/envvars
重新讀回。
第一階段唯一目的就是製作第二階段所需的根檔案系統 tar 檔。第二階段本身也是透過 Docker 來執行, Dockerfile 的大致步驟說明如下:
-
安裝必要以及基本系統套件
apt-get update -q && apt-get install -qy busybox-static runit bash pwgen
-
建立基本根檔案系統結構
RUN mkdir -p bin etc etc/service dev dev/pts lib proc sys tmp var var/lib var/run var/log \ usr usr/sbin usr/bin usr/lib root
-
建立 Docker 映像檔所需的系統檔案,如 /etc/resolv.conf,並建立root以及ava帳戶。
ava
這個身份用來執行應用,根據安全建議,您應該避免使用root
來執行。chmod a+w tmp &&\ touch etc/resolv.conf &&\ cp /etc/nsswitch.conf etc/nsswitch.conf &&\ echo root:x:0:0:root:/root:/bin/sh > etc/passwd &&\ echo root:x:0: > etc/group &&\ echo ava:x:1000:1000::/home/ava:/bin/bash >> etc/passwd &&\ echo ava:x:1000: >> etc/group &&\ echo ava:x:1000:1000::/home/ava:/bin/bash >> /etc/passwd &&\ echo ava:x:1000: >> /etc/group
其中,
- 複製 Busybox, Bash, Runit, pwgen 及必要程式庫到適當路徑。
- 使用上列步驟建立的根檔案結構,建立第二階段所需要的 overlayfs.tar 檔案,並放置在所產生的映像檔的根目錄中。
整個建構流程便是利用第一階段的 Dockerfile 建立一個映像檔,其中 /overlayfs.tar
檔案是第二階段需要的根檔案系統。 最後,使用第二階段的 Dockerfile 建立真正要發行的映像檔。有提供一個 Makefile:
GROUP=eavatar
NAME=basebox
VERSION=0.1.5
all: build tag
build: Dockerfile overlayfs.tar
docker build -t $(GROUP)/$(NAME):$(VERSION) .
overlayfs.tar:
cd overlayfs && docker build -t $(NAME)-builder .
docker run $(NAME)-builder cat /overlayfs.tar > overlayfs.tar
# docker rmi $(NAME)-builder
tag:
@if ! docker images $(GROUP)/$(NAME) | awk '{ print $$2 }' | grep -q -F $(VERSION); then echo "$(NAME) version $(VERSION) is not yet built. Please run 'make build'"; false; fi
docker tag $(GROUP)/$(NAME):$(VERSION) $(GROUP)/$(NAME):latest
clean:
rm -f overlayfs.tar
這個方法的優點在於適用所有可以在 Ubuntu 上執行的應用,缺點是必須找出應用相依的系統程式庫(libraries)。
結語
為什麼不在容器內支援套件管理功能,如 apt, opkg? 因為直接修改容器內的檔案會造成版本控管困難,導致難以理解系統目前狀態。 Docker 容器技術是達成持續交付(continuous delivery)的重要工具,而要達成持續交付的目的,使用相同的二進位套件(binary packages)是必要手段。 也就是說:容器映像檔本身直接被當作是應用或服務發行套件。山姆鍋認為在使用任何一種容器技術時, 釐清「容器不是虛擬機」這個概念,對於整體架構設計至關重要。
參考資料
runit
: http://smarden.org/runit/ ⎘
buildroot
: http://buildroot.uclibc.org/ ⎘