建構容器的最佳做法

本文說明建構容器的一組最佳做法。這組做法適用於多種目標,從縮短建構時間到建立較小且較具彈性的映像檔均包含在內,旨在讓容器建構過程更簡單 (例如使用 Cloud Build),以及更容易在 Google Kubernetes Engine (GKE) 上執行。

這些最佳做法的重要性並不相同。舉例來說,省略其中一些做法也能成功執行實際工作環境的工作負載,但另外有些做法則是不可或缺的重要基礎。要特別注意的是,與安全性相關的最佳做法人人不同。是否能採用這些做法須視您的環境和限制而定。

若要充分瞭解本文,您必須具備一些 Docker 和 Kubernetes 的基本知識。這裡討論的一些最佳做法也適用於 Windows 容器,但大多數做法是假設您使用 Linux 容器。有關執行及操作容器的建議,請參閱操作容器的最佳做法一文。

每一容器封裝單一應用程式

重要性:高

開始使用容器時,常見錯誤是將其視為可同時執行多個不同項目的虛擬機器。雖然容器能以這種方式運作,但容器模型大多數優點卻也因此遭到遏制。以傳統的 Apache/MySQL/PHP 堆疊為例,您可能想在單一容器中執行所有元件。不過最佳做法是使用二個或三個不同的容器:Apache 和 MySQL 各使用一個容器;如果執行 PHP-FPM,也可以讓 PHP 另外使用一個容器。

由於容器是設計成與託管應用程式擁有相同的生命週期,因此每個容器只應包含一個應用程式。當容器啟動時,應用程式也應該隨之啟動;當應用程式停止時,容器也應一併停止。下圖顯示這項最佳做法。

以圖表顯示不含自訂映像檔的啟動程序。

圖 1: 左邊的容器遵循最佳做法;右邊的容器則沒有。

如果一個容器中有多個應用程式,這些應用程式的生命週期可能各不相同,或處於不同的狀態。舉例來說,您可能會有一個執行中的容器,但其中一個核心元件當機或沒有回應。如果不進行額外的健康狀態檢查,整體容器管理系統 (Docker 或 Kubernetes) 就無法判斷容器是否處於健康狀態。如果您使用的是 Kubernetes,這代表在預設情況下系統不會視需求重新啟動容器。

您可能會在公開映像檔中看到下列動作,但請不要遵照那些範例:

  • 使用 supervisord 這類程序管理系統來管理容器中的一或多個應用程式。
  • 將 bash 指令碼做為容器的進入點,並讓其產生多個應用程式做為背景工作。有關如何在容器中正確使用 bash 指令碼的資訊,請參閱正確處理 PID 1、信號處理作業和廢止程序一節。

正確處理 PID 1、信號處理作業和廢止程序

重要性:高

Linux 信號是控管容器內程序生命週期的主要方法。為配合上一個最佳做法,並讓應用程式的生命週期與容器緊密連結,請確保您的應用程式能正確處理 Linux 信號。最重要的 Linux 信號是 SIGTERM,因為這個信號會終止程序。您的應用程式可能也會收到 SIGKILL 信號 (用於強制終止程序),或 SIGINT 信號 (系統會在您輸入 Ctrl+C 時傳送,通常視為 SIGTERM 處理)。

程序 ID (PID) 是 Linux kernel 提供給每個程序的專屬 ID。PID 設有命名空間,代表容器會有一組專屬 PID 對應到主機系統上的 PID。啟動具有 PID 1 的 Linux kernel 時,就會啟動第一個程序。以一般作業系統來說,這個程序屬於 init 系統,例如 systemd 或 SysV。同樣地,在容器中啟動的第一個程序會獲得 PID 1。Docker 和 Kubernetes 會利用信號與容器內的程序進行通訊,主要目的是終止程序。Docker 和 Kubernetes 都只能向容器內具有 PID 1 的程序傳送信號。

對容器來說,PID 和 Linux 信號會產生下列兩個需要考量的問題。

問題 1:Linux kernel 如何處理信號

對於具有 PID 1 的程序,Linux kernel 處理信號的方式會和處理其他程序時有所不同。這個程序不會自動註冊信號處理常式,代表 SIGTERM 或 SIGINT 等信號預設為沒有作用。根據預設,您必須使用 SIGKILL 終止程序,以避免任何安全關機。視您的應用程式而定,使用 SIGKILL 可能會造成使用者端發生錯誤、中斷寫入作業 (用於儲存資料),或讓監控系統產生不必要的快訊。

問題 2:傳統的 init 系統如何處理孤立程序

您也可以使用 systemd 這類傳統的 init 系統移除 (清除) 孤立的廢止程序。孤立的程序 (意指父項程序已失效的程序) 會重新連接到具有 PID 1 的程序,而具有 PID 1 的程序會在孤立程序失效時予以移除。這是一般 init 系統的做法。不過在容器中,這項工作會交由任何具有 PID 1 的程序執行。如果該程序無法正確處理這項移除作業,您就會面臨記憶體或其他資源耗盡的風險。

這些問題有幾個常見解決方案,將在以下各節中概述。

解決方案 1:以 PID 1 執行,並註冊信號處理常式

這項解決方案只適用於第一個問題。只有當應用程式能以受管控的方式產生子項程序 (通常是這種情況)、並能避免發生第二個問題時,這項解決方案才有作用。

如要實作這項解決方案,最簡單的方法就是在 Dockerfile 中使用 CMD 和/或 ENTRYPOINT 指令啟動程序。舉例來說,下列 Dockerfile 中的 nginx 是第一個也是唯一要啟動的程序。

FROM debian:9

RUN apt-get update && \
    apt-get install -y nginx

EXPOSE 80

CMD [ "nginx", "-g", "daemon off;" ]

有時候,您可能需要在容器中準備環境,才能讓程序正常執行。在這種情況下,最佳做法是讓容器在開始執行時就啟動殼層指令碼。這個殼層指令碼的工作是準備環境及啟動主程序。不過,如果您採用這種方法,具有 PID 1 的變成是殼層指令碼,而非程序。因此您必須使用內建的 exec 指令,從殼層指令碼啟動程序。exec 指令會將指令碼替換為所需的程式。接著,您的程序就會繼承 PID 1。

解決方案 2:使用特殊的 init 系統

如同在較傳統 Linux 環境中的做法,您也可以使用 init 系統處理這些問題。但如果僅針對這個用途,使用一般的 init 系統 (例如 systemd 或 SysV) 太過複雜又龐大,因此建議您使用專為容器建立的 init 系統,例如 tini

如果您使用特殊 init 系統,則 init 程序會具有 PID 1 並執行下列操作:

  • 註冊正確的信號處理常式。
  • 確保信號適用於您的應用程式。
  • 移除任何最終廢止程序。

您可以在 Docker 中利用 docker run 指令的 --init 選項使用這項解決方案。如要在 Kubernetes 中使用這項解決方案,您必須在容器映像檔中安裝 init 系統,並將其做為容器的進入點。

針對 Docker 建構快取最佳化

重要性:高

Docker 建構快取可以大幅加快容器映像檔的建構速度。映像檔為逐層建構,在 Dockerfile 中,每個指令都會在產生的映像檔中建立一個層級。在建構過程中,Docker 會盡可能重複使用先前建構的層級,並略過成本可能較高的步驟。只有當所有先前的建構步驟都使用建構快取時,Docker 才能使用其建構快取。雖然這個行為通常有助於加快建構速度,但您需要考慮一些情況。

例如,為了充分運用 Docker 建構快取的優勢,您必須將經常變動的建構步驟放在 Dockerfile 的底部。如果您將這些步驟放在頂端,Docker 就不能將其建構快取用於較少變動的建構步驟中。由於系統通常會為原始碼的每個新版本建構新的 Docker 映像檔,因此在 Dockerfile 中會盡可能最後才將原始碼加入映像檔中。在下圖中,您會看到如果變更了 STEP 1,Docker 只能重複使用 FROM debian:9 步驟中的層級。但如果您變更了 STEP 3,Docker 就能重複使用 STEP 1「和」STEP 2 的層級。

如何使用 Docker 建構快取的範例

圖 2: 如何使用 Docker 建構快取的範例。綠色是可以重複使用的層級,紅色是必須重新建立的層級。

重複使用層級會產生另一個結果:如果某個建構步驟依賴儲存在本機檔案系統中的任何快取種類,則這個快取必須在同一個建構步驟中產生。如果未產生這個快取,您的建構步驟可能會使用先前建構作業中的過期快取執行。這種行為最常發生在套件管理工具中,例如 apt 或 yum;您必須在套件安裝過程中使用同一個 RUN 指令更新存放區。

如果您變更了下列 Dockerfile 中的第二個 RUN 步驟,系統不會重新執行 apt-get update 指令,因此只能使用過期的 apt 快取。

FROM debian:9

RUN apt-get update
RUN apt-get install -y nginx

您也可以在單一 RUN 步驟中合併兩個指令:

FROM debian:9

RUN apt-get update && \
    apt-get install -y nginx

移除不必要的工具

重要性:中

如要保護應用程式不受攻擊者的侵擾,請嘗試移除任何不必要的工具,以減少應用程式的受攻擊面。舉例來說,移除可在系統中建立反向殼層的 netcat 等公用程式。如果容器中沒有 netcat,攻擊者就必須尋找其他方法。

這項最佳做法適用於任何工作負載,即使是非容器化的工作負載也一樣。不同之處在於,與搭配傳統的虛擬機器或不含作業系統的伺服器相比,這項最佳做法在容器上實作更加簡單。

其中有一些工具非常適合用來偵錯。舉例來說,如果您盡可能遵循這項最佳做法,那麼詳盡的記錄、追蹤、剖析和應用程式效能管理系統便幾乎是必備的。事實上,您也無法再依賴本機偵錯工具,因為這類工具通常極具機密性,只有少數人能存取。

檔案系統內容

這項最佳做法的第一個部分會處理容器映像檔的內容。請盡量減少映像檔中的內容。如果您可以將應用程式編譯為單一的靜態連結二進位檔,將這個二進位檔加入暫存映像檔可讓最終映像檔中「只」包含您的應用程式,不會包含其他任何內容。減少映像檔中封裝的工具數量,也可以減少潛在攻擊者可在容器中執行的操作。詳情請參閱盡可能建構最小的映像檔一節。

檔案系統安全性

映像檔中沒有工具並不足以確保安全;您必須防止潛在攻擊者安裝他們自己的工具。您可以結合下列兩種方式:

  • 避免在容器內以根權限執行作業:這個方法會提供第一層安全防護,可防止攻擊者使用映像檔中內嵌的套件管理工具來修改根擁有的檔案等 (例如 apt-getapk)。為讓這個方法有效,您必須停用或解除安裝 sudo 指令。如要進一步瞭解這個主題,請參閱避免以根權限執行作業一節。

  • 以唯讀模式啟動容器:您可以使用 docker run 指令的 --read-only 標記,或使用 Kubernetes 的 readOnlyRootFilesystem 選項來執行這項作業。您可以使用 PodSecurityPolicy 在 Kubernetes 中強制執行。

盡可能建構最小的映像檔

重要性:中

建構較小的映像檔具有縮短上傳和下載時間等多項優點,這點對於 Kubernetes 中的 Pod 冷啟動時間格外重要;映像檔越小,節點下載的速度就越快。但建構小型映像檔並不簡單,因為最終映像檔可能會包含建構依附元件或未最佳化的層級。

盡可能使用最小的基本映像檔

基本映像檔是 Dockerfile 中 FROM 指令參照的基本映像檔。Dockerfile 中的其他所有指令都以這個映像檔為建構基礎。基本映像檔越小,產生的映像檔也越小,下載速度就更快。舉例來說,alpine:3.7 映像檔的大小為 71 MB,比 centos:7 映像檔小。

您也可以使用暫存基本映像檔,在這個空白的映像檔上建構自己的執行階段環境。如果您的應用程式是靜態連結二進位檔,則可輕鬆使用暫存基本映像檔:

FROM scratch
COPY mybinary /mybinary
CMD [ "/mybinary" ]

distroless 專案提供了一些不同程式語言的最小化基本映像檔。這些映像檔只包含該程式語言的執行階段依附元件,不包含您預期 Linux 發行版會提供的許多工具,例如殼層或套件管理工具。

減少映像檔中的叢集量

如要縮減映像檔的大小,請僅安裝必要項目即可。如果您不小心安裝了額外套件,可在後續步驟中移除。只不過,這樣做仍無法將映像檔縮減回原先大小。由於 Dockerfile 的每個指令都會建立一個層級,因此在後續步驟移除映像檔的資料,並不會縮減映像檔的整體大小 (資料依舊存在,只是隱藏在更深的層級中而已)。請參閱以下範例:

不建議的 Dockerfile 建議的 Dockerfile

FROM debian:9
RUN apt-get update && \ apt-get install -y \ [buildpackage] RUN [build my app] RUN apt-get autoremove --purge \ -y [buildpackage] && \ apt-get -y clean && \ rm -rf /var/lib/apt/lists/*

FROM debian:9
RUN apt-get update && \ apt-get install -y \ [buildpackage] && \ [build my app] && \ apt-get autoremove --purge \ -y [buildpackage] && \ apt-get -y clean && \ rm -rf /var/lib/apt/lists/*

在不建議的 Dockerfile 版本中,[buildpackage]/var/lib/apt/lists/* 中檔案仍存在於與第一個 RUN 對應的層級中。這個層級是映像檔的一部分,即使該層級所含資料在產生的映像檔中無法存取,也必須與其他部分一起上傳及下載。

在建議的 Dockerfile 版本中,所有動作都在單一層級中完成,且該層級只包含了您建構的應用程式。[buildpackage]/var/lib/apt/lists/* 中的檔案不在產生的映像檔中,也並未隱藏於更深的層級中。

如需進一步瞭解映像檔層級,請參閱最佳化 Docker 建構快取一節。

另一個在映像檔中減少叢集量的好方法是使用「多階段建構」(Docker 17.05 版推出)。多階段建構可讓您在第一個「建構」容器中建構應用程式,當您使用相同的 Dockerfile 時,可將其結果用於另一個容器中。

Docker 多階段建構程序

圖 3: Docker 多階段建構程序。

在下列 Dockerfile 中,hello 二進位檔在第一個容器中建構,並注入第二個容器。由於第二個容器以暫存為基礎,因此產生的映像檔只包含 hello 二進位檔,不包含建構期間所需的來源檔案和物件檔案。二進位檔必須以靜態方式建立連結,這樣運作時暫存映像檔就不需要任何外部程式庫。

FROM golang:1.10 as builder

WORKDIR /tmp/go
COPY hello.go ./
RUN CGO_ENABLED=0 go build -a -ldflags '-s' -o hello

FROM scratch
CMD [ "/hello" ]
COPY --from=builder /tmp/go/hello /hello

嘗試使用共通層建立映像檔

如果您必須下載 Docker 映像檔,Docker 會先檢查您是否已擁有映像檔的部分層級。如果您已擁有這些層級,系統就不會重複下載。如果您先前下載的另一個映像檔與目前下載的映像檔有相同基礎,就會發生這種情況。這個做法可大幅降低第二個映像檔的下載資料量。

在機構層級,您可以提供一組共通的標準基本映像檔給開發人員,以善用這個減量優點。您的系統只需要下載每個基本映像檔一次。初始下載之後,只需要下載每個映像檔中不重複的層級即可。實際上,映像檔的相同處越多,下載速度就越快。

嘗試使用共通層建立映像檔

圖 4: 使用共通層建立映像檔。

使用 Container Registry 的安全漏洞掃描功能

重要性:中

在不含作業系統的伺服器和虛擬機器的領域中,軟體安全漏洞是眾所周知的問題。通常,解決這些安全漏洞的方式是使用集中式庫存系統,該系統會列出每個伺服器上安裝的套件。您可以訂閱上游作業系統的安全漏洞動態饋給,即可在安全漏洞影響到您的伺服器時收到通知,並進行相應的修補作業。

不過,由於容器應該不可變更 (詳情請參閱容器的無狀態性與不變性說明),因此即使您發現了安全漏洞,也請不要就地修補。最佳做法是重新建構映像檔,納入修補程式後再重新部署。與伺服器相比,容器的生命週期較短,識別資訊的定義也較不明確。因此,使用類似的集中式庫存系統來偵測容器中的安全漏洞並不是一個好方法。

為協助您解決這個問題,Container Registry 提供安全漏洞掃描功能。這項功能啟用後,會找出容器映像檔的套件安全漏洞。當您將映像檔上傳到 Container Registry 以及更新安全漏洞資料庫時,系統都會掃描映像檔。您可以透過下列幾種方式處理這項功能回報的資訊:

  • 建立一個類似 Cron 的工作,列出安全漏洞,並在有修復方案的情況下,觸發修復程序。
  • 偵測到安全漏洞後,立即使用 Cloud Pub/Sub 整合功能觸發貴機構採用的修補程序。

我們建議您設定自動執行修補程序,並使用最初用於建構映像檔的現有持續整合管道。如果您對持續部署管道有信心,則可能也會想在一切就緒時自動部署已修復的映像檔。但是,大多數人希望在部署之前手動執行驗證步驟,下列程序可達成這個願望:

  1. 將映像檔儲存在 Container Registry 中,並啟用安全漏洞掃描功能。
  2. 設定可定期從 Container Registry 擷取新安全漏洞的工作,並視需要觸發重新建構映像檔的作業。
  3. 建構新的映像檔後,由持續部署系統部署至測試環境。
  4. 手動檢查測試環境是否有問題。
  5. 如果未發現任何問題,請手動觸發部署至實際工作環境。

正確標記映像檔

重要性:中

Docker 映像檔通常以兩個元件來識別:名稱和標記。以 google/cloud-sdk:193.0.0 映像檔為例,google/cloud-sdk 是名稱,93.0.0 是標記。如果您並未在 Docker 指令中提供標記,則預設會使用 latest 標記。名稱/標記組合在任何情況下都不會重複。不過您可以視需要將標記重新指派給不同的映像檔。

建構映像檔時,您需自行正確標記。請遵循連貫一致的標記政策,並記錄您的標記政策,方便映像檔使用者清楚瞭解。

容器映像檔是封裝及發布軟體的方式。將映像檔加上標記可讓使用者識別軟體的特定版本,以便下載。因此,請將容器映像檔上的標記系統與軟體的版本政策建立緊密連結。

使用語意化版本進行標記

軟體發布工作的常見方法是使用版本編號「標記」(例如在 git tag 指令中) 原始碼的特定版本。語意化版本控制規範中說明了如何簡潔地處理版本編號。在這個系統中,軟體的版本編號分為 X.Y.Z 這三個部分,其中:

  • X 代表主要版本,只針對不相容的 API 變更遞增。
  • Y 代表次要版本,針對新功能遞增。
  • Z 代表修補程式版本,針對修正錯誤遞增。

每次遞增次要或修補程式版本編號時,變更項目都必須具有回溯相容性。

如果您使用這個系統或類似系統,請按照下列政策標記映像檔:

  • latest 標記一律是指最新 (可能穩定) 的映像檔。請在建立新的映像檔後立即移動這個標記。
  • X.Y.Z 標記是指軟體的特定版本。請勿移至其他映像檔。
  • X.Y 標記是指軟體 X.Y 次要分支版本的最新修補程式版本。請在發布新的修補程式版本時移動這個標記。
  • X 標記是指 X 主要分支版本最新次要版本的最新修補程式版本。請在發布新的修補程式版本或新的次要版本時移動這個標記。

採用此政策可讓使用者靈活選擇想要使用的軟體版本。使用者可以選擇特定的 X.Y.Z 版本,並確信映像檔不會變更;也可以選擇較不明確的標記以自動取得更新資訊。

使用 Git 修訂版本雜湊碼進行標記

如果您有進階的持續推送軟體更新系統,且經常發布軟體,則可能不會採用「語意化版本控制規範」一節中所述的版本編號。在這種情況下,常見的版本編號處理方法是使用 Git 修訂版本 SHA-1 雜湊碼 (或其簡短版本) 做為版本編號。Git 修訂版本雜湊碼設計成無法變更,並會參照軟體的特定版本。

您可以將這個修訂版本雜湊碼做為軟體的版本編號使用,也可以用來標記從軟體特定版本建構的 Docker 映像檔。這樣做會讓 Docker 映像檔可供追蹤;因為在這種情況下,映像檔標記為不可變更,您可以立即得知指定容器中執行的軟體特定版本。在持續推送軟體更新管道中,自動更新部署作業使用的版本編號。

審慎考慮是否要使用公開映像檔

重要性:不適用

Docker 的一大優勢是支援各種軟體而提供大量的公開映像檔。這些映像檔可讓您立即開始使用。但在設計貴機構的容器策略時,您可能會遇到一些限制,而公開提供的映像檔無法符合需求。以下列舉幾個可能無法使用公開映像檔的限制範例:

  • 您想要確實掌控映像檔中的內容。
  • 您不想依賴外部存放區。
  • 您希望能嚴格控管實際工作環境中的安全漏洞。
  • 您希望能在每個映像檔中使用相同的基本作業系統。

只有一個方法可以解決所有這些限制,但費用相當高昂,就是您必須建構自己的映像檔。建構自己的映像檔適合數量有限的映像檔,但這個數字很可能會快速增加。只要您有管理大規模系統的可能性,請考慮採用以下幾點:

  • 以可靠的自動化方式建構映像檔,即使是很少建構的映像檔也一樣。 使用 Cloud Build 中的自動建構觸發條件是達成此目的好方法。
  • 使用標準化基本映像檔。Google 提供一些基本映像檔供您使用。
  • 以自動化方式將基本映像檔的更新傳送至「子項」映像檔。
  • 設法解決映像檔中安全漏洞。詳情請參閱使用 Container Registry 的安全漏洞分析說明。
  • 設法在機構內不同團隊建立的映像檔上,強制執行內部標準。

有幾種工具可以協助在您建構與部署的映像檔上強制執行政策:

  • container-diff 可以分析映像檔內容,甚至比較兩個映像檔之間的內容。
  • container-structure-test 可以測試映像檔的內容是否符合您定義的一組規則。
  • Grafeas 是成果中繼資料 API,可用於儲存與映像檔有關的中繼資料,以利後續檢查這些映像檔是否遵守您的政策。
  • 在 Kubernetes 中部署工作負載之前,您可以使用 Kubernetes 的許可控制器檢查多項必備條件。
  • 您也可以使用 Kubernetes 的 Pod 安全性政策,在叢集中強制使用安全性選項。

您可能想採用混合式系統,利用 Debian 或 Alpine 這類公開映像檔做為基本映像檔,再由此建構其他部分。或是,您想在一些非關鍵的映像檔中使用公開映像檔,並在其他情況下建構自己的映像檔。這些問題沒有正確或錯誤的答案,但您必須謹慎處理。

授權注意事項

將第三方程式庫和套件加入 Docker 映像檔之前,請確定相應的授權允許這樣的行為。第三方授權可能在重新分配這個部分存有限制,您要將 Docker 映像檔發布至公開登錄檔時,就會遭遇這類限制。

後續步驟

歡迎試用其他 Google Cloud Platform 功能,並參考我們的教學課程

本頁內容對您是否有任何幫助?請提供意見:

傳送您對下列選項的寶貴意見...

這個網頁