前言

圍繞著 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:

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 的內容 :

#!/bin/sh

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

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

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

  1. 安裝必要以及基本系統套件
apt-get update -q && apt-get install -qy busybox-static runit bash pwgen
  1. 建立基本根檔案系統結構 ,
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 來執行 。

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:

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) 是必要手段 。 也就是說 : 容器映像檔本身直接被當作是應用或服務發行套件 。 山姆鍋認為在使用任何一種容器技術時 , 釐清 「 容器不是虛擬機 」 這個概念 , 對於整體架構設計至關重要 。