Docker

Docker

2019 年 9 月 28 日

Docker 以其易用性和預建映像的登錄資料庫,讓 Linux 容器大為普及,並開始經常與「Linux 容器」一詞互換。

Docker 映像包含多個層,會在執行時期合併,組成容器的文件系統。Docker 透過執行 Dockerfile 中的命令來建立這些層,每條命令會建立一個新層。這些層在映像之間共用,以節省空間,並可用於快取,以加速映像建置。與其他選項(例如虛擬機器 (VM))相比,由於不會在映像中包含 Linux 核心,因此可以節省更多空間。映像的大小只比我們部署的封裝版 Erlang 發行版略大。

能夠以一般的 Linux 程序執行(不會啟動新的核心)的輕巧體積,開機速度比傳統的 VM 隔離快很多,而且消耗的資源更少。由於耗用額外的資源很低,因此將程式封裝和執行的隔離優勢當成標準實務,而非必須為每個程式執行 VM 的負擔。

在文件系統和網路隔離下執行的容器,其優勢在於不需在程式未隔離的狀況下,執行常見的操作

  • 預先安裝共用程式庫
  • 更新組態
  • 尋找開啟的埠
  • 尋找節點名稱的唯一名稱

注意事項

您可能會注意到,使用 Docker 時,我們完全不會使用 latest 標籤。這個標籤常被誤解或誤用。它是指派給沒有具體標籤的最後一個映像,而不是最新建立的映像。除非您真的不在乎要使用哪個映像版本,否則很少或根本不需要仰賴它。

在本章中,我們將介紹如何有效地為執行 service_discovery 專案建置映像,以及如何為執行測試和 dialyzer 建置映像。接著,我們將更新持續整合程序,以建置並發布新的映像。

執行本章所需的 Docker 最低版本為 19.03,且安裝了 buildx。可以執行以下命令安裝 buildx

$ export DOCKER_BUILDKIT=1
$ docker build --platform=local -o . git://github.com/docker/buildx
$ mv buildx ~/.docker/cli-plugins/docker-buildx

建立映像

官方 Erlang Docker images 針對每個新的 OTP 版本發佈。包含 Rebar3,並以 AlpineDebian 風格呈現,每次 Rebar3 和 Alpine/Debian 有新版本,這些 images 也會更新。由於標記的 images 會針對新的版本更新,強烈建議同時使用 image 的 sha256 消化(digest),並將所使用的 images 鏡像至您自己的儲存庫,即使您的儲存庫也在 Docker Hub 上。存有複本可確保基本 image 沒有在開發人員介入的情況下更改,而且在與 Docker Hub 分離的註冊中建立鏡像表示您不依賴其可用性。這項最佳範例就是,在接下來的範例和 service_discovery 儲存庫從 ghcr.io/adoptingerlang/service_discovery/us.gcr.io/adoptingerlang/ 中使用 images。

私密依存項

在建置 Docker images 時,許多人在工作環境都會遇到的第一個絆腳石是存取私有依存項。如果您有私有 git 儲存庫或 Hex 組織套件 作為依存項,Docker 容器在建置期間將無法擷取這些依存項。這通常會讓人們將 _build 納入 .dockerignore,並冒著讓建置程式受在地端人工製品污染,可能無法在其他地方複製的風險,以便在執行 docker build 之前使用 Rebar3 擷取依存項。另一個選項是將主機 SSH 憑證和/或 Hex apikey 複製到建置容器中,但建議不要這麼做,因為它會保留在 Docker 層中,而且會在您推送 image 的任何地方洩漏。最近的 Docker 版本(18.06 和更新版本)改用以安全的方式掛接機密和 SSH 代理連線或金鑰的能力。資料不會洩漏到最終 image 或未明確掛接的任何命令。

由於 service_discovery 沒有任何私密依存項,在開始針對 service_discovery 建置 images 之前,我們將討論如何支援這些依存項。

Hex 依存項

Rebar3 將私有 Hex 依存項的存取金鑰保存在 ~/.config/rebar3/hex.config 檔案中。使用實驗性的 Dockerfile 語法 --mount=type=secret,可以在編譯命令的同時,將設定掛接到容器中。此檔案掛接到一個獨立的 tmpfs 檔案系統,並排除在建置快取之外。

# syntax=docker/dockerfile:1.2
RUN --mount=type=secret,id=hex.config,target=/root/.config/rebar3/hex.config rebar3 compile

在執行 docker build 時,要掛接主機的 hex.config,只要傳遞含有相符 id 及檔案 src 路徑的機密即可。

$ docker build --secret id=hex.config,src=~/.config/rebar3/hex.config .

Git 依存項

您可以在上一個區段中使用秘密掛接來掛接 SSH 金鑰,但 Docker 加入了一項更好的解決方案,使用專門處理 SSH 的掛接類型。需要 SSH 存取權的 RUN 命令可以使用 --mount=type=ssh

# syntax=docker/dockerfile:1.2
RUN apt install --no-cache openssh-client git && \
    mkdir -p -m 0600 ~/.ssh && \
    ssh-keyscan github.com >> ~/.ssh/known_hosts && \
    git config --global url."[email protected]:".insteadOf "https://github.com/"
WORKDIR /src
COPY rebar.config rebar.lock .
RUN --mount=type=ssh rebar3 compile

首先,RUN 指令會安裝必要的相依性,SSH 和 git。接著,ssh-keyscan 用來下載 Github 的目前的公開金鑰並將其新增到 known_hosts。這個公開金鑰在 known_hosts 中表示 SSH 將不會嘗試提示詢問您是否接受主機的公開金鑰。接著,git 設定確保即使在 rebar.config 中的 git 網址使用 https 它反而將使用 SSH。如果私人儲存庫不在 Github,則此網址替換必須更改為適當的位置。

將先前的片段新增到 Dockerfile 後,我們將在本章稍後看到,您還需要在執行和設定 DOCKER_BUILDKIT 時在建置指令新增 --ssh default

$ export DOCKER_BUILDKIT=1
$ docker build --ssh default .

SSH mount 型別的額外資訊和選項可以在Moby 文件中找到- Moby 是構成 Docker 核心功能的專案名稱。

高效快取

基本指令排序

Dockerfile 中指令的順序對於建置時間和它所建立的映像的大小非常重要。Dockerfile 中每項指令將建立一層,這層在未來建立時再次利用,以在沒有變更的狀況下略過指令。透過建立一層包含專案已建置相依性,我們利用 Rebar3 來達成這個目的

COPY rebar.config rebar.lock .
RUN rebar3 compile

COPY 指令僅當 rebar.configrebar.lock 與先前建立的層不同時,才會使執行 rebar3 編譯(和檔案中的後續指令)的指令快取失效。由於專案的程式碼沒有被複製,而且 Rebar3 僅建置相依性,這會產生僅包含在 _build/default/lib 下已建置相依性的層。

在相依性建置完成並快取後,我們可以複製專案的其餘部分並編譯它

COPY . .
RUN rebar3 compile

由於 Dockerfile 中的操作順序,每個 docker build . 的執行僅編譯專案的來源,假設有變更,否則在此也會使用現有的層。任何不需要在專案有變更時重新執行的指令,都需要在任何一個 COPY 指令之前。例如,安裝 Debian 套件,設定工作目錄的 RUN apt 安裝 gitWORKDIR /app/src

不建議使用 COPY . .,因為它會提高快取無效化的機率。若有可能,最好只複製建置所需檔案和目錄,或使用 .dockerignore 檔案篩選出不需要的檔案。.git 目錄就是一例,由於其大小,而且其中的內容變更不會影響建置產出,因此可以忽略。然而,在 service_discovery 中,我們依賴 git 指令來設定發佈版本和組成該發佈版本的應用程式。對於不需要 Rebar3 此項功能的專案,建議將 .git 新增至 .dockerignore

實驗性 Mount 語法

在建置 Docker 映像檔時,複製檔案到映像檔和快取圖層不再是提高效率的唯一選項。在圖層中快取已建置的依存關係很好,但該圖層也包含 Rebar3 在 ~/.cache/rebar3/hex 中建立的 Hex 套件快取。對 rebar.configrebar.lock 的任何變更都將導致所有套件必須重新建置,且必須從 Hex 再次擷取。此外,複製整個專案的指示會建立額外的圖層,其中包含所有來源,這是浪費行為,因為我們只關心建置產出。

截至 Docker 19.03,這些問題已透過實驗性語法解決,用於將檔案掛載到 RUN 指令的內容。若要啟用實驗性語法,必須設定環境變數 DOCKER_BUILDKIT 或在 /etc/docker/daemon.json 中設定 {"features":{"buildkit": true}},並將 # syntax=docker/dockerfile:1.2 用作 Dockerfile 的第一行

# syntax=docker/dockerfile:1.2

[...]

WORKDIR /app/src

ENV REBAR_BASE_DIR /app/_build

# build and cache dependencies as their own layer
COPY rebar.config rebar.lock .
RUN --mount=id=hex-cache,type=cache,sharing=locked,target=/root/.cache/rebar3 \
    rebar3 compile

RUN --mount=target=. \
    --mount=id=hex-cache,type=cache,sharing=locked,target=/root/.cache/rebar3 \
    rebar3 compile

在這一組新的指示中,建置將 WORKDIR 設定為 /app/src,這將成為後續指令的目前工作目錄。環境變數 REBAR_BASE_DIR 則已設定為 /app/_build。基本目錄是 Rebar3 會輸出所有建置產出的位置,預設上是專案根目錄的 _build/ 目錄,在此情況下,如果沒有環境變數,路徑會是 /app/src/_build

Rebar3 組態和鎖定檔案的 COPY 保持不變,但下列 RUN 已變更,包含具備類型 cache--mount 選項。它會指示 Docker 在 Docker 圖層之外建立一個快取目錄,並將其儲存在主機的本機。此快取會保存在 docker build 的執行期間,因此,即使已變更組態或鎖定檔案,以後執行的 docker build 本機會掛載此快取,且僅會從 Hex 擷取所需的新套件。

接下來,與用於建構專案的其他指令不同,指令 COPY . . 已被移除。取而代之的是,已使用目標為 . 的掛載類型 bind(預設值)。與 cache 掛載不同,bind 掛載表示 Docker 會從建構環境中掛載到容器中,提供與 COPY . . 相同的結果,但不會從複製檔案中建立層,讓建構更加快速且規模更小。透過 COPY 指令,會從主機建立兩個副本:建構環境中的副本和建構容器中的副本。每次使用 COPY 執行 build 時,都需要將整個專案從建構環境再次複製到建構容器中。

預設上,掛載是不可變的,這表示如果寫入任何內容至 /app/src,建構將會發生錯誤。因此,Rebar3 基礎目錄已設定為 /app/_build。有一個選項可以採用「唯讀」模式進行掛載,但不會保留寫入內容,且移除建構容器建構環境資料副本優化措施的相關功能。可以在 Dockerfile 前端實驗語法中取得更多 Buildkit 文件中關於掛載選項的資訊。

最終產出的結果是含有已編譯相依項目的層 /app/_build(以及 /app/src/rebar.*,但這些相依項目的份量幾近於零),接著是含有已編譯項目的層 /app/_build,但 /app/src 中則沒有任何內容。另外,有一個快取儲存所有已下載的六角形套件。

本機快取與遠端快取

我們在這個區段使用了幾種快取類型,而這兩種快取都仰賴在同一主機上進行的建構,才有存取快取的權限。若是hex-cacheRUN 中進行掛載,這僅為本機快取功能,無法從註冊表中匯出或匯入。然而,可以從註冊表中匯入 Dockerfile 中各項指令的建構層。

在使用持續整合或任何類型的建構伺服器進行建構時,設定建構以使用遠端快取會特別有用。除非僅有一個節點在執行建構,否則會浪費時間重新建構 Dockerfile 的每一步驟。為了解決此問題,可以透過 --cache-from 告知 docker build 在何處尋找層,包括遠端註冊表中的映像檔。

--cache-from 有兩個版本,由於較新的版本從技術角度來看仍是名為 buildx 的「技術預覽」的一部分,因此我們將涵蓋這兩個版本。然而,由於 buildx 更為有效率、更容易使用,且在我們需要的功能範疇中顯得相對穩定,因此 service_discovery 專案將預設使用此功能。

舊的 --cache-from 並未「意識到多階段運作」,這意味著使用者必須手動建置並推送多階段 Dockerfile 中的每個階段。在建置時,會透過 --cache-from 參照建置於先前階段之階段的映像,並會從儲存庫中提取該映像。

有了 buildx,會建置一個快取清單,其中包含有關多階段建置中先前階段的資訊。--cache-to 參數允許以各種方式匯出此快取。我們將使用 inline 選項,此選項會將快取清單直接寫入映像的元資料。可將映像推送到儲存庫,然後透過 --cache-from 在後續建置中參照它。新快取清單的獨特之處在於,只會下載快取命中之層,而在舊表單中,在透過 --cache-from 參照時會下載先前階段的完整映像。

快取及安全性更新

使用層快取時,有一個安全性疑慮必須注意。例如,由於 RUN 指令僅在指令文字變更,或前一層使快取失效時才重新執行,因此任何已安裝系統套件仍會維持在相同版本,即使已經發布安全性修正程式。基於這個原因,建議偶爾使用 --no-cache 執行 Docker,如此一來,建置映像時不會重複使用任何層。

多階段建置

針對一個 Erlang 專案,我們需要包含已建置版本之映像,而且此映像不應包含執行版本所需的任何東西。像是 Rebar3、建置專案所使用的 Erlang/OTP 版本、用於從 github 快取相依項目的 Git 等,都必須移除。與其在建置完成後移除項目,可以使用多階段 Dockerfile 從建置階段將最終版本(它會將 Erlang 執行時期打包)複製到具有 Debian 基礎,且僅具有執行版本所需之共享函式庫(例如 OpenSSL)的階段。

我們將逐一檢視 service_discovery 專案中的 Dockerfile 各個階段。第一個階段命名為 builder

# syntax=docker/dockerfile:1.2
FROM ghcr.io/adoptingerlang/service_discovery/erlang:26.0.2 as builder

WORKDIR /app/src
ENV REBAR_BASE_DIR /app/_build

RUN rm -f /etc/apt/apt.conf.d/docker-clean

# Install git for fetching non-hex depenencies.
# Add any other Debian libraries needed to compile the project here.
RUN --mount=target=/var/lib/apt/lists,id=apt-lists,type=cache,sharing=locked \
    --mount=type=cache,id=apt,target=/var/cache/apt \
    apt update && apt install --no-install-recommends -y git

# build and cache dependencies as their own layer
COPY rebar.config rebar.lock .
RUN --mount=id=hex-cache,type=cache,target=/root/.cache/rebar3 \
    rebar3 compile

FROM builder as prod_compiled

RUN --mount=target=. \
    --mount=id=hex-cache,type=cache,target=/root/.cache/rebar3 \
    rebar3 as prod compile

builder 階段從基礎映像 erlang:26.0.2 開始。as builder 命名此階段,這樣我們才能使用它作為基礎映像,也就是在後續階段中 FROM 指令所用。

舊版的 Docker 快取

在使用舊版 --cache-from 進行遠端快取時(如上節所述),builder 階段會建置並加上標籤以供識別,這樣我們便能根據其所含的 Rebar3 相依項目參照此映像。為執行此動作,我們可以使用 rebar.configrebar.lock 上的 cksum 指令。此動作與 Docker 在決定是否失效其快取前執行的動作類似。

$ CHKSUM=$(cat rebar.config rebar.lock | cksum | awk ‘{print $1}’)
$ docker build –target builder -t service_discovery:builder-${CHKSUM} .
$ docker push service_discovery:builder-${CHKSUM}

在建置任何使用 FROM builder 的階段時,我們會包含 --cache-from=service_discovery:builder-${CHKSUM} 以提取先前建置的相依項目。

開發人員經常處理同一個專案的多個並發分支,可能會隨著不同相依項,在定義要作為快取來使用的映像時,使用目前 Rebar3 組態的雜湊值及鎖定檔案,可快取專案的組態集,並在建置時使用正確的組態集。

名為 releaser 的下一個階段,使用 prod_compiled 影像作為其基礎

FROM prod_compiled as releaser

WORKDIR /app/src

# create the directory to unpack the release to
RUN mkdir -p /opt/rel

# build the release tarball and then unpack
# to be copied into the image built in the next stage
RUN --mount=target=. \
    --mount=id=hex-cache,type=cache,target=/root/.cache/rebar3 \
    rebar3 as prod tar && \
    tar -zxvf $REBAR_BASE_DIR/prod/rel/*/*.tar.gz -C /opt/rel

此階段使用 prod 設定檔建構發行的 tarball 檔

{profiles, [{prod, [{relx, [{dev_mode, false},
                            {include_erts, true},
                            {include_src, false},
                            {debug_info, strip}]}]
            }]}.

設定檔將 include_erts 設定為 true,表示 tarball 檔包含 Erlang 執行時期,並且可以在未安裝 Erlang 的目標上執行。最後,tarball 檔解壓縮到 /opt/rel,因此將發行版從 releaser 階段複製出去的階段,不需要安裝 tar

為什麼要將發發行版 tar 起來?

您可能會注意到,只會建立發行版的 tarball 檔,以便立即解壓縮。這是出於兩個原因,而不是複製發行版目錄的內容。首先,它確保只有明確定義為發行版此版本中包含的內容才會被使用。由於在 docker 映像中建置時,先前建立的版本都不會在 _build/prod/rel 目錄中,因此這是比較不重要的步驟,但仍然有必要執行。其次,在 tar 時對發佈版本進行了一些變更,使用像 release_handler 的工具時是必要的,例如引導腳本從 RelName.boot 重新命名為 start.boot。詳細內容請參閱 systools 文件。

最後,可部署映像使用規則的 OS 映像 (debian:bullseye) 作為基礎,而非先前的階段。首先安裝執行發行版所需的任何共用程式庫,然後將 releaser 階段中解壓縮的版本複製到 /opt/service_discovery

FROM ghcr.io/adoptingerlang/service_discovery/debian:bullseye as runner

WORKDIR /opt/service_discovery

ENV COOKIE=service_discovery \
    # write files generated during startup to /tmp
    RELX_OUT_FILE_PATH=/tmp \
    # service_discovery specific env variables to act as defaults
    DB_HOST=127.0.0.1 \
    LOGGER_LEVEL=debug \
    SBWT=none

RUN rm -f /etc/apt/apt.conf.d/docker-clean

# openssl needed by the crypto app
RUN --mount=target=/var/lib/apt/lists,id=apt-lists,type=cache,sharing=locked \
    --mount=type=cache,id=apt,sharing=locked,target=/var/cache/apt \
    apt update && apt install --no-install-recommends -y openssl ncurses-bin

COPY --from=releaser /opt/rel .

ENTRYPOINT ["/opt/service_discovery/bin/service_discovery"]
CMD ["foreground"]

ENV 指令中,我們設定了版本執行時使用的環境變數的一些有用的預設值。版本啟動指令碼會將 RELX_OUT_FILE_PATH=/tmp 用作輸出此指令碼所建立的任何檔案的目錄。會這麼做是因為執行此版本時,需要從它們各自的 .src 檔案產生 sys.configvm.args,而預設情況下,這些檔案會放置在與原始 .src 檔案相同的目錄中。我們不希望將這些檔案寫入版本目錄(其中包含 .src 檔案),這是因為容器檔案系統的最佳範例是不寫入。如果此映像寫入 /tmp,則任何使用者都可以執行,但如果需要寫入 /opt/service_discovery 下的任何位置,則必須以 root 身分執行。因此,寫入 /tmp 可遵循另一個最佳範例,即不以 root 身分執行容器。我們可以進一步採取動作,讓執行時期檔案系統變成唯讀,我們將在 執行容器 中看到這一點。

/opt/service_discovery 屬於 root 所擁有,建議不要以 root 身分執行容器。如果已設定 RELX_OUT_FILE_PATH,則會使用其位置。在此,ENV 指令用於確保在執行容器時環境變數 RELX_OUT_FILE_PATH 已設定為 /tmp

$ docker buildx build -o type=docker --target runner --tag service_discovery:$(git rev-parse HEAD) .

或使用 CircleCI 所包含的 service_discovery 指令碼來建立並推送映像

ci/build_images.sh -l

此指令碼也會標記映像兩次,一次使用 git 參照 git rev-parse HEAD,如手動指令中所述,另一次使用分支名稱 git symbolic-ref --short HEAD。分支標籤用於透過 --cache-from 參照建立明細快取。此指令碼會在可用的時候使用標記為 master 分支和目前分支的映像作為快取,而且必須在建立指令中包含 --cache-to=type=inline 才能做到這一點。

使用目前的「分支名稱」和「主程式」來檢查映像並尋找快取命中率,與僅包含建置相依項目的階段映像中的 rebar.configrebar.lock 檢查和比對不如使用後者精確。有些情況並不會阻止建置建立並推送標記為檢查和比對的明確映像,並將其用作其中一個 --cache-from 映像。但至少對於這個專案來說,無需處理額外映像的便利性(由於 Buildkit 快取明細會記錄所有階段),卻減輕了對相依項目的快取遺漏產生問題,而另一種佈局並不會發生這種情況的機率。

最後,請注意腳本在 CircleCI 中的使用方式,請參閱 在 CI 中建立和發布映像,與此處相比,-l 選項就是一個例子。在 CI 中,我們只關心將映像傳輸到遠端登錄,因此透過未將已建立的映像載入 Docker daemon 可以節省時間。在本地端建立映像時,可能會想要執行該映像,而且我們會在下一節 執行容器,這樣一來,就必須載入 Docker daemon。

執行容器

現在我們有映像了,可以使用 docker run 啟動讓出,以進行本機驗證和測試。預設情況下,CMD(前景)會傳遞給發佈啟動腳本,透過 ENTRYPOINT 設定為 /opt/service_discovery/bin/service_discovery 進行設定。如果將最後一個參數傳遞給 docker run,則可以覆寫 CMD。當容器執行時,使用 console 指令會產生一個互動式外殼程式

$ docker run -ti service_discovery console
[...]
(service_discovery@localhost)1>

-ti 選項會告訴 docker 我們想要一個互動式外殼程式。這對於映像的本機測試很有用,因為使用者想要一個外殼程式來檢查正在執行的發佈。由 Dockerfile runner 階段中的 CMD 設定的預設值將使用 foreground。這裡不需要使用 -ti,因此可以刪除,而指令也只會是

$ docker run service_discovery
Exec: /opt/service_discovery/erts-10.5/bin/erlexec -noshell -noinput +Bd -boot /opt/service_discovery/releases/8ec119fc36fa702a8c12a8c4ab0349b392d05515/start -mode embedded -boot_var ERTS_LIB_DIR /opt/service_discovery/lib -config /tmp/sys.config -args_file /tmp/vm.args -- foreground
Root: /opt/service_discovery
/opt/service_discovery

為了防止意外關閉,您將無法使用 Ctrl-c 停止這個容器,所以要停止容器,請使用 docker kill <container id>

請注意,foreground 是預設值,因為這就是它在生產中執行的模式,儘管它會在背景執行

$ docker run -d service_discovery
3c45b7043445164d713ab9ecc03e5dbfb18a8d801e1b46e291e1167ab91e67f4

使用 -d 執行是 --detach 的簡稱,而輸出則是容器 ID。寫入 stdout 的記錄可以使用 docker log <container id> 觀看,而且我們將在 下一章節 中看到,在 Kubernetes 中如何將記錄路由到您選擇的記錄儲存裝置。即使是當

也可以使用 docker exec、容器 ID(在 docker run -d 的輸出中可以看到或使用 docker ps 找到)和指令 remote_console 附加到執行中的節點。容器是用 console 還是 foreground 啟動都沒有關係,但這當然最有用於當您需要一個外殼程式供尚未使用 console 啟動的節點使用時。由於 exec 不使用映像中定義的 ENTRYPOINT,所以執行指令必須以發佈啟動腳本 bin/service_discovery 開頭

$ docker exec -ti 3c45 bin/service_discovery remote_console
[...]
(service_discovery@localhost)1>

或者,可以使用 docker exec -ti 3c45 /bin/sh 執行 Linux 外殼程式,這會將您帶到 /opt/service_discovery,然後您可以在此連接一個 remote_console,或檢查執行中容器的其他面向。

若要離開遠端主控台,請勿執行 q()。,因為這會關閉 Erlang 節點和 Docker 容器。請使用 Ctrl-g 並輸入 qCtrl-g 會讓 shell 進入所謂的作業控制模式。若要深入了解如何使用此 shell 模式,請參閱 作業控制模式 (JCL 模式) 文件

在某些情況下(例如無法啟動發行版本時),覆寫 ENTRYPOINT 並取得要嘗試啟動發行版本的容器中的 shell 可能很有用。

$ docker run -ti --entrypoint /bin/sh service_discovery
/opt/service_discovery #

最後,在上一節中,我們看過如何將 RELX_OUT_FILE_PATH 設為 /tmp,這樣就不會有檔案嘗試寫入發行版本目錄,而該目錄應該保持唯讀狀態。Docker 有 diff 命令,可以顯示映像檔系統和目前正在執行容器的檔案系統之間的差異

$ docker container diff 3c45
C /tmp
A /tmp/sys.config
A /tmp/vm.args

如果您遇到問題或想要驗證發行版本未執行不該執行的動作時,這項命令可能有助於輕鬆檢查發行版本寫入磁碟的內容。如果發行版本有大量寫入磁碟的動作,最好是掛載 磁碟區,並將所有寫入動作指向該磁碟區,但對於這 2 個小型組態檔案來說,這並非必要。除非在透過範本建立組態檔案時使用機密資料,或者您想以 --read-only 執行容器。在這些情況下,建議使用 tmpfs 掛載。在 Linux 上,只要將 --tmpfs /tmp 新增至 docker run 命令即可。這樣一來,/tmp 就會變成容器中可寫入層的一部分,而不會變成一個獨立的磁碟區,該磁碟區只存在於記憶體中,而且會在容器停止時銷毀。

小心喪屍進程!

從 Erlang/OTP 19.3 起,當收到 TERM 訊號(Docker 和 Kubernetes 用它來關閉容器)時,Erlang 節點將會以 init:stop() 優雅關閉。

但在容器中,您仍然會遇到喪屍進程的潛在問題。當使用 docker exec 執行 remote_console 或任何其他發行版本指令碼命令(例如 ping)時,如果未透過 Docker 提供給您在進入點之前啟動的小型 init 的引數 --init 來啟動容器,這些動作會留下喪屍進程。

這通常不是問題,但和原子一樣,如果使用不受限,它絕對會變成問題。為了避免這個問題,以下是一項範例:除非使用 --init 或將 PID 0 設定為其他小型 init,否則請勿將 ping 用作健康檢查,而這種檢查會在容器執行期間由容器執行時間定期執行。像這樣長期執行的容器最終會讓核心行程表用完槽位,而且將無法建立新行程。

在 CI 中建置和發布映像檔

由於對於每個版本手動建置並發佈影像到儲存庫很繁瑣,因此一般都會將影像建置納入持續整合流程的一部分。通常這僅限制發生在併入 master 或建立新標籤時,但有時為了測試目的,建置分支影像也很有用。在本章節中,我們將說明幾個自動化此流程的選項,但無論您使用什麼 CI 工具,都能執行類似的工作。

CircleCI

測試章節(即將推出…)中,我們介紹 CircleCI 來執行測試。若要建置並發佈服務_偵測的 Docker 影像,會新增一個稱為「docker-build-push」的新工作。它使用 VM(而非 Docker 影像)作為執行器,並先安裝最新的 Docker 版本,在撰寫本文時,預設可用的版本不支援服務_偵測 Dockerfile 中所使用的功能。

jobs:
  docker-build-and-push:
    executor: docker/machine
    steps:
      - run:
          name: Install latest Docker
          command: |
            sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
            sudo apt-get update

            # upgrade to latest docker
            sudo apt-get install docker-ce
            docker version

            # install buildx
            mkdir -p ~/.docker/cli-plugins
            curl https://github.com/docker/buildx/releases/download/v0.3.0/buildx-v0.3.0.linux-amd64  --output ~/.docker/cli-plugins/docker-buildx
            chmod a+x ~/.docker/cli-plugins/docker-buildx            
      - checkout
      - gcp-gcr/gcr-auth
      - run:
          name: Build and push images
          command: |
            ci/build_image.sh -p -t runner -r gcr.io/adoptingerlang            

安裝最新 Docker 之後,會簽出服務_偵測儲存庫的程式碼,由於這是使用 Google Cloud 作為儲存庫和 Kubernetes,因此它會使用儲存庫進行驗證。最後,會呼叫位於服務_偵測的 ci/ 目錄中的指令碼,以建置和發佈影像。該指令碼使用本章節前面討論的 docker build 指令來建置各個階段,並使用 --cache-from 參考各個階段作為每個建置期間的快取。

若要僅在通過測試後執行此工作,可以將它透過 rebar3/ct 上的 requires 約束新增至 CircleCI 工作流程。

workflows:
  build-test-maybe-publish:
    jobs:

      [...]

    - docker-build-and-push:
        requires:
        - rebar3/ct

Google Cloud Build

在 2018 年,Google 推出了一個影像建置工具 Kaniko,它在使用者空間中執行,而不依賴於守護程式,這些功能可以在像 Kubernetes 集群這樣的環境中建置容器影像。Kaniko 應當作為影像執行,即 gcr.io/kaniko-project/executor,並且可用作 Google Cloud Build 中的步驟。

Kaniko 為 RUN 指令建立的每個圖層提供遠端快取。在建置任何圖層之前,建置會檢查影像儲存庫中的圖層快取是否有符合項。然而,我們在服務_偵測 Dockerfile 中使用的 Buildkit 功能在 Kaniko 中不可用,因此在 Google Cloud Build 設定檔 (cloudbuild.yaml) 中會使用一個獨立的 Dockerfile,即 ci/Dockerfile.cb

steps:
- name: 'gcr.io/kaniko-project/executor:latest'
  args:
  - --target=runner
  - --dockerfile=./ci/Dockerfile.cb
  - --build-arg=BASE_IMAGE=$_BASE_IMAGE
  - --build-arg=RUNNER_IMAGE=$_RUNNER_IMAGE
  - --destination=gcr.io/$PROJECT_ID/service_discovery:$COMMIT_SHA
  - --cache=true
  - --cache-ttl=8h

substitutions:
  _BASE_IMAGE: gcr.io/$PROJECT_ID/erlang:22
  _RUNNER_IMAGE: gcr.io/$PROJECT_ID/alpine:3.10

由於 Kaniko 的工作原理是檢查儲存庫快取目錄中的每個指示,因此無需指示它使用特定的影像作為快取,就像我們對 Docker 的 --cache-from 所做的那樣。Docker 的建置快取可以透過設定它,將快取中繼資料匯出到儲存庫並匯出所有階段的圖層(--cache-to=type=registry,mode=max)來更類似於 Kaniko,但在撰寫本文時,大部分儲存庫並不支援這項功能,因此本文未予說明。

有關如何在 Google Cloud Build 中使用 Kaniko 的更多詳細資訊,請參閱他們的使用 Kaniko 快取文件。

後續步驟

在本章中,我們為服務建立了 image,並透過在儲存庫中的資料變更時持續建立並發布這些 image,來完成建立持續整合管線。在下一章,我們將介紹如何從這些 image 建立對 Kubernetes 的部署。在 Kubernetes 中執行後,下一章將介紹可觀察性,例如連接到正在執行的節點、結構化良好的記錄、報告指標和分散式追蹤。