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,並以 Alpine 及 Debian 風格呈現,每次 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.config
或 rebar.lock
與先前建立的層不同時,才會使執行 rebar3 編譯
(和檔案中的後續指令)的指令快取失效。由於專案的程式碼沒有被複製,而且 Rebar3 僅建置相依性,這會產生僅包含在 _build/default/lib
下已建置相依性的層。
在相依性建置完成並快取後,我們可以複製專案的其餘部分並編譯它
COPY . .
RUN rebar3 compile
由於 Dockerfile 中的操作順序,每個 docker build .
的執行僅編譯專案的來源,假設有變更,否則在此也會使用現有的層。任何不需要在專案有變更時重新執行的指令,都需要在任何一個 COPY
指令之前。例如,安裝 Debian 套件,設定工作目錄的 RUN apt 安裝 git
和 WORKDIR /app/src
。
不建議使用 COPY . .
,因為它會提高快取無效化的機率。若有可能,最好只複製建置所需檔案和目錄,或使用 .dockerignore
檔案篩選出不需要的檔案。.git
目錄就是一例,由於其大小,而且其中的內容變更不會影響建置產出,因此可以忽略。然而,在 service_discovery
中,我們依賴 git 指令來設定發佈版本和組成該發佈版本的應用程式。對於不需要 Rebar3 此項功能的專案,建議將 .git
新增至 .dockerignore
。
實驗性 Mount 語法
在建置 Docker 映像檔時,複製檔案到映像檔和快取圖層不再是提高效率的唯一選項。在圖層中快取已建置的依存關係很好,但該圖層也包含 Rebar3 在 ~/.cache/rebar3/hex
中建立的 Hex 套件快取。對 rebar.config
或 rebar.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-cache
在 RUN
中進行掛載,這僅為本機快取功能,無法從註冊表中匯出或匯入。然而,可以從註冊表中匯入 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.config
和 rebar.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.config
和 vm.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.config
和 rebar.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
並輸入 q
。Ctrl-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 中執行後,下一章將介紹可觀察性,例如連接到正在執行的節點、結構化良好的記錄、報告指標和分散式追蹤。