圍繞著 Docker 容器 (container) 技術所引發的風潮,一個越來越成熟的生態系正在形成。相信許多人都在關注它的發展, 也可能有諸多的疑問。對於真正想把 Docker 應用在生產環境的人而言, 其中一個疑問可能是:Docker 的映像(image) 非得動則幾十,上百甚至上千 MB 嗎?的確,頻寬跟儲存空間不再像過去那樣高不可攀, 但太大的檔案或多或少還是會影響部署時間。本文山姆鍋示範其中一種製作小型 Docker 映像的方法。

對於想直接知道結果的讀者,可以參考本文所使用的 原始碼

為什麼需要在意映像檔大小

Docker 所提供的容器技術,另一種稱呼為: 軟體容器(Software Container), 是一種 作業系統層虛擬化 技術。 在不同的作業系統有不同的方法跟名稱,例如:Solaris 的 zones, FreeBSD 的 jail, Linux 的 LXC 等都是同樣概念的東西。 表面上,容器跟虛擬機(virtual machine)有許多相似之處,同樣可以區隔不同的應用與服務,避免干擾彼此的執行。 所以,您會看到許多人試圖在一個容器內同時執行多個服務或應用,需要用到排程,所以 cron 服務要啟動; 需要資料庫,所以資料庫服務器也一起執行。 嚴格來講,沒有人可以說這樣做不對。但是,容器不是虛擬機!這樣的做法並沒有發揮容器技術的潛力。 山姆鍋認同的觀點是:一個容器應該只包含一個應用或服務剛好所需(just enough)的檔案。例如:一個 web 應用,除了應用本身執行檔外, 可能需要第三方的套件,也許還需要一些系統套件才能運作,除此之外都是多餘。簡單說,容器包含的東西要盡可能的精簡,自然表示映像檔要盡量的小。

為什麼不需要在意映像檔大小

什麼?不是才說要在意映像檔大小,轉眼又說不用在意?是的沒錯,山姆鍋不是頭腦不清楚。對於多數的使用情況,您不需要太在意一個映像檔大小。 透過 Docker 這樣的產品,由於映像檔採用層層堆疊的方式組成,組成映像檔底層的資料通常會被重複使用。如此,在部署下載映像檔時, 並不會因為小小修改就需要重新下載整個映像檔。但這個前提是:那個映像檔之前已經被下載並快取在該主機中!

小型映像檔的需求

山姆鍋先說明對這個小型映像檔的需求:

  1. 要跟 Ubuntu 14.04 二進位相容: 對於網路服務或應用,山姆鍋選擇的作業系統就是 Ubuntu,所以,跟 Ubuntu 相容是必要條件。

  2. 要支援 Bash: 雖然有 Busybox 提供大多數工具程式,但還是希望使用的 Shell 是 Bash。

  3. 要有一個啟動器: 需要一個類似 Linux init 功能,但是精簡的服務。山姆鍋這裏使用的是 runit , 使用 runit 主要目的在利用它所提供的 log 以及改變執行身份的功能。

  4. 建構映像檔需要的時間必需短: 使用 buildroot 這樣的工具雖然可以建構出小型映像檔,但編譯需要的時間實在太久了。

製作小型 Docker 映像檔的方法

不管您對於該不該在意映像檔大小的結論是什麼,看到這裡,山姆鍋假設您還是想知道如何製作小型映像檔的方法。 山姆鍋採用的方法分成兩階段,第一階段產生一個根檔案 (root filesystem) 系統; 第二階段將這個根檔案系統包裝成 Docker 映像檔。

先說說比較容易理解的第二階段,底下是第二階段所使用的 Dockerfile:

1
2
3
4
5
6
7
8
9
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. 從標記 1, 這個映像以 scratch 這個映像為基礎。

  2. 標記 2 的將代表根檔案系統內容的 ‘overlayfs.tar’ 展開並複製到映像檔的根目錄。
    這裏稍微補充一下:ADD 指令在遇到 .tar 這種壓縮檔會自動解開。
  3. run.sh 這個腳本負責啟動 runit 服務。

run.sh 的內容:

1
2
3
4
#!/bin/sh

export > /etc/envvars #1
/usr/sbin/runsvdir-start

runit 有個問題是不會傳遞環境變數給它啟動的服務或應用,標號 1 的敘述就是將當下環境變數存到 /etc/envvars 這個檔案,後續可以 使用 source /etc/envvars 重新讀回。

第一階段唯一目的就是製作第二階段所需的根檔案系統 tar 檔。第二階段本身也是透過 Docker 來執行, Dockerfile 的大致步驟說明如下:

  1. 安裝必要以及基本系統套件
1
apt-get update -q && apt-get install -qy busybox-static runit bash pwgen
  1. 建立基本根檔案系統結構,
1
2
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
  1. 建立 Docker 映像檔所需的系統檔案,如 /etc/resolv.conf,並建立 root 以及 ava 帳戶。
    ava 這個身份用來執行應用,根據安全建議,您應該避免使用 root 來執行。
1
2
3
4
5
6
7
8
9
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
  1. 複製 Busybox, Bash, Runit, pwgen 及必要程式庫到適當路徑。
  2. 使用上列步驟建立的根檔案結構,建立第二階段所需要的 overlayfs.tar 檔案,並放置在所產生的映像檔的根目錄中。

整個建構流程便是利用第一階段的 Dockerfile 建立一個映像檔,其中 /overlayfs.tar 檔案是第二階段需要的根檔案系統。 最後,使用第二階段的 Dockerfile 建立真正要發行的映像檔。有提供一個 Makefile:

.make}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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/