對(duì)構(gòu)建系統(tǒng)進(jìn)行容器化的指南
搭建一個(gè)通過(guò)容器分發(fā)應(yīng)用的可復(fù)用系統(tǒng)可能很復(fù)雜,但這兒有個(gè)好方法。
一個(gè)用于將源代碼轉(zhuǎn)換成可運(yùn)行的應(yīng)用的構(gòu)建系統(tǒng)是由工具和流程共同組成。在轉(zhuǎn)換過(guò)程中還涉及到代碼的受眾從軟件開發(fā)者轉(zhuǎn)變?yōu)樽罱K用戶,無(wú)論最終用戶是運(yùn)維的同事還是部署的同事。
在使用容器搭建了一些構(gòu)建系統(tǒng)后,我覺(jué)得有一個(gè)不錯(cuò)的可復(fù)用的方法值得分享。雖然這些構(gòu)建系統(tǒng)被用于編譯機(jī)器學(xué)習(xí)算法和為嵌入式硬件生成可加載的軟件鏡像,但這個(gè)方法足夠抽象,可用于任何基于容器的構(gòu)建系統(tǒng)。
這個(gè)方法是以一種易于使用和維護(hù)的方式搭建或組織構(gòu)建系統(tǒng),但并不涉及處理特定編譯器或工具容器化的技巧。它適用于軟件開發(fā)人員構(gòu)建軟件,并將可維護(hù)鏡像交給其他技術(shù)人員(無(wú)論是系統(tǒng)管理員、運(yùn)維工程師或者其他一些頭銜)的常見(jiàn)情況。該構(gòu)建系統(tǒng)被從終端用戶中抽象出來(lái),這樣他們就可以專注于軟件。
為什么要容器化構(gòu)建系統(tǒng)?
搭建基于容器的可復(fù)用構(gòu)建系統(tǒng)可以為軟件團(tuán)隊(duì)帶來(lái)諸多好處:
- 專注:我希望專注于應(yīng)用的開發(fā)。當(dāng)我調(diào)用一個(gè)工具進(jìn)行“構(gòu)建”時(shí),我希望這個(gè)工具集能生成一個(gè)隨時(shí)可用的二進(jìn)制文件。我不想浪費(fèi)時(shí)間在構(gòu)建系統(tǒng)的查錯(cuò)上。實(shí)際上,我寧愿不了解,或者說(shuō)不關(guān)心構(gòu)建系統(tǒng)。
- 一致的構(gòu)建行為:無(wú)論在哪種使用情況下,我都想確保整個(gè)團(tuán)隊(duì)使用相同版本的工具集并在構(gòu)建時(shí)得到相同的結(jié)果。否則,我就得不斷地處理“我這咋就是好的”的麻煩。在團(tuán)隊(duì)項(xiàng)目中,使用相同版本的工具集并對(duì)給定的輸入源文件集產(chǎn)生一致的輸出是非常重要。
- 易于部署和升級(jí):即使向每個(gè)人都提供一套詳細(xì)說(shuō)明來(lái)安裝一個(gè)項(xiàng)目的工具集,也可能會(huì)有人翻車。問(wèn)題也可能是由于每個(gè)人對(duì)自己的 Linux 環(huán)境的個(gè)性化修改導(dǎo)致的。在團(tuán)隊(duì)中使用不同的 Linux 發(fā)行版(或者其他操作系統(tǒng)),情況可能還會(huì)變得更復(fù)雜。當(dāng)需要將工具集升級(jí)到下一版本時(shí),問(wèn)題很快就會(huì)變得更糟糕。使用容器和本指南將使得新版本升級(jí)非常簡(jiǎn)單。
對(duì)我在項(xiàng)目中使用的構(gòu)建系統(tǒng)進(jìn)行容器化的這些經(jīng)驗(yàn)顯然很有價(jià)值,因?yàn)樗梢跃徑馍鲜鰡?wèn)題。我傾向于使用 Docker 作為容器工具,雖然在相對(duì)特殊的環(huán)境中安裝和網(wǎng)絡(luò)配置仍可能出現(xiàn)問(wèn)題,尤其是當(dāng)你在一個(gè)使用復(fù)雜代理的企業(yè)環(huán)境中工作時(shí)。但至少現(xiàn)在我需要解決的構(gòu)建系統(tǒng)問(wèn)題已經(jīng)很少了。
漫步容器化的構(gòu)建系統(tǒng)
我創(chuàng)建了一個(gè)教程存儲(chǔ)庫(kù),隨后你可以克隆并檢查它,或者按照本文內(nèi)容進(jìn)行操作。我將逐個(gè)介紹存儲(chǔ)庫(kù)中的文件。這個(gè)構(gòu)建系統(tǒng)非常簡(jiǎn)單(它運(yùn)行 gcc
),從而可以讓你專注于這個(gè)構(gòu)建系統(tǒng)結(jié)構(gòu)上。
構(gòu)建系統(tǒng)需求
我認(rèn)為構(gòu)建系統(tǒng)中有兩個(gè)關(guān)鍵點(diǎn):
-
標(biāo)準(zhǔn)化構(gòu)建調(diào)用:我希望能夠指定一些形如
/path/to/workdir
的工作目錄來(lái)構(gòu)建代碼。我希望以如下形式調(diào)用構(gòu)建:./build.sh /path/to/workdir
為了使得示例的結(jié)構(gòu)足夠簡(jiǎn)單(以便說(shuō)明),我將假定輸出也在
/path/to/workdir
路徑下的某處生成。(否則,將增加容器中顯示的卷的數(shù)量,雖然這并不困難,但解釋起來(lái)比較麻煩。) -
通過(guò) shell 自定義構(gòu)建調(diào)用:有時(shí),工具集會(huì)以出乎意料的方式被調(diào)用。除了標(biāo)準(zhǔn)的工具集調(diào)用
build.sh
之外,如果需要還可以為build.sh
添加一些選項(xiàng)。但我一直希望能夠有一個(gè)可以直接調(diào)用工具集命令的 shell。在這個(gè)簡(jiǎn)單的示例中,有時(shí)我想嘗試不同的gcc
優(yōu)化選項(xiàng)并查看效果。為此,我希望調(diào)用:./shell.sh /path/to/workdir
這將讓我得到一個(gè)容器內(nèi)部的 Bash shell,并且可以調(diào)用工具集和訪問(wèn)我的工作目錄(
workdir
),從而我可以根據(jù)需要嘗試使用這個(gè)工具集。
構(gòu)建系統(tǒng)的架構(gòu)
為了滿足上述基本需求,這是我的構(gòu)架系統(tǒng)架構(gòu):
Container build system architecture
在底部的 workdir
代表軟件開發(fā)者用于構(gòu)建的任意軟件源碼。通常,這個(gè) workdir
是一個(gè)源代碼的存儲(chǔ)庫(kù)。在構(gòu)建之前,最終用戶可以通過(guò)任何方式來(lái)操縱這個(gè)存儲(chǔ)庫(kù)。例如,如果他們使用 git
作為版本控制工具的話,可以使用 git checkout
切換到他們正在工作的功能分支上并添加或修改文件。這樣可以使得構(gòu)建系統(tǒng)獨(dú)立于 workdir
之外。
頂部的三個(gè)模塊共同代表了容器化的構(gòu)建系統(tǒng)。最左邊的黃色模塊代表最終用戶與構(gòu)建系統(tǒng)交互的腳本(build.sh
和 shell.sh
)。
在中間的紅色模塊是 Dockerfile 和相關(guān)的腳本 build_docker_image.sh
。開發(fā)運(yùn)營(yíng)者(在這個(gè)例子中指我)通常將執(zhí)行這個(gè)腳本并生成容器鏡像(事實(shí)上我多次執(zhí)行它直到一切正常為止,但這是另一回事)。然后我將鏡像分發(fā)給最終用戶,例如通過(guò)容器信任注冊(cè)庫(kù)進(jìn)行分發(fā)。最終用戶將需要這個(gè)鏡像。另外,他們將克隆構(gòu)建系統(tǒng)的存儲(chǔ)庫(kù)(即一個(gè)與教程存儲(chǔ)庫(kù)等效的存儲(chǔ)庫(kù))。
當(dāng)最終用戶調(diào)用 build.sh
或者 shell.sh
時(shí),容器內(nèi)將執(zhí)行右邊的 run_build.sh
腳本。接下來(lái)我將詳細(xì)解釋這些腳本。這里的關(guān)鍵是最終用戶不需要為了使用而去了解任何關(guān)于紅色或者藍(lán)色模塊或者容器工作原理的知識(shí)。
構(gòu)建系統(tǒng)細(xì)節(jié)
把教程存儲(chǔ)庫(kù)的文件結(jié)構(gòu)映射到這個(gè)系統(tǒng)結(jié)構(gòu)上。我曾將這個(gè)原型結(jié)構(gòu)用于相對(duì)復(fù)雜構(gòu)建系統(tǒng),因此它的簡(jiǎn)單并不會(huì)造成任何限制。下面我列出存儲(chǔ)庫(kù)中相關(guān)文件的樹結(jié)構(gòu)。文件夾 dockerize-tutorial
能用構(gòu)建系統(tǒng)的其他任何名稱代替。在這個(gè)文件夾下,我用 workdir
的路徑作參數(shù)調(diào)用 build.sh
或 shell.sh
。
dockerize-tutorial/
├── build.sh
├── shell.sh
└── swbuilder
├── build_docker_image.sh
├── install_swbuilder.dockerfile
└── scripts
└── run_build.sh
請(qǐng)注意,我上面特意沒(méi)列出 example_workdir
,但你能在教程存儲(chǔ)庫(kù)中找到它。實(shí)際的源碼通常存放在單獨(dú)的存儲(chǔ)庫(kù)中,而不是構(gòu)建工具庫(kù)中的一部分;本教程為了不必處理兩個(gè)存儲(chǔ)庫(kù),所以我將它包含在這個(gè)存儲(chǔ)庫(kù)中。
如果你只對(duì)概念感興趣,本教程并非必須的,因?yàn)槲覍⒔忉屗形募?。但是如果你繼續(xù)本教程(并且已經(jīng)安裝 Docker),首先使用以下命令來(lái)構(gòu)建容器鏡像 swbuilder:v1
:
cd dockerize-tutorial/swbuilder/
./build_docker_image.sh
docker image ls # resulting image will be swbuilder:v1
然后調(diào)用 build.sh
:
cd dockerize-tutorial
./build.sh ~/repos/dockerize-tutorial/example_workdir
下面是 build.sh 的代碼。這個(gè)腳本從容器鏡像 swbuilder:v1
實(shí)例化一個(gè)容器。而這個(gè)容器實(shí)例映射了兩個(gè)卷:一個(gè)將文件夾 example_workdir
掛載到容器內(nèi)部路徑 /workdir
上,第二個(gè)則將容器外的文件夾 dockerize-tutorial/swbuilder/scripts
掛載到容器內(nèi)部路徑 /scripts
上。
docker container run \
--volume $(pwd)/swbuilder/scripts:/scripts \
--volume $1:/workdir \
--user $(id -u ${USER}):$(id -g ${USER}) \
--rm -it --name build_swbuilder swbuilder:v1 \
build
另外,build.sh
還會(huì)用你的用戶名(以及組,本教程假設(shè)兩者一致)去運(yùn)行容器,以便在訪問(wèn)構(gòu)建輸出時(shí)不出現(xiàn)文件權(quán)限問(wèn)題。
請(qǐng)注意,shell.sh 和 build.sh
大體上是一致的,除了兩點(diǎn)不同:build.sh
會(huì)創(chuàng)建一個(gè)名為 build_swbuilder
的容器,而 shell.sh
則會(huì)創(chuàng)建一個(gè)名為 shell_swbuilder
的容器。這樣一來(lái),當(dāng)其中一個(gè)腳本運(yùn)行時(shí)另一個(gè)腳本被調(diào)用也不會(huì)產(chǎn)生沖突。
兩個(gè)腳本之間的另一處關(guān)鍵不同則在于最后一個(gè)參數(shù):build.sh
傳入?yún)?shù) build
而 shell.sh
則傳入 shell
。如果你看了用于構(gòu)建容器鏡像的 Dockerfile,就會(huì)發(fā)現(xiàn)最后一行包含了下面的 ENTRYPOINT
語(yǔ)句。這意味著上面的 docker container run
調(diào)用將使用 build
或 shell
作為唯一的輸入?yún)?shù)來(lái)執(zhí)行 run_build.sh
腳本。
# run bash script and process the input command
ENTRYPOINT [ "/bin/bash", "/scripts/run_build.sh"]
run_build.sh 使用這個(gè)輸入?yún)?shù)來(lái)選擇啟動(dòng) Bash shell 還是調(diào)用 gcc
來(lái)構(gòu)建 helloworld.c
項(xiàng)目。一個(gè)真正的構(gòu)建系統(tǒng)通常會(huì)使用 Makefile 而非直接運(yùn)行 gcc
。
cd /workdir
if [ $1 = "shell" ]; then
echo "Starting Bash Shell"
/bin/bash
elif [ $1 = "build" ]; then
echo "Performing SW Build"
gcc helloworld.c -o helloworld -Wall
fi
在使用時(shí),如果你需要傳入多個(gè)參數(shù),當(dāng)然也是可以的。我處理過(guò)的構(gòu)建系統(tǒng),構(gòu)建通常是對(duì)給定的項(xiàng)目調(diào)用 make
。如果一個(gè)構(gòu)建系統(tǒng)有非常復(fù)雜的構(gòu)建調(diào)用,則你可以讓 run_build.sh
調(diào)用 workdir
下最終用戶編寫的特定腳本。
關(guān)于 scripts 文件夾的說(shuō)明
你可能想知道為什么 scripts
文件夾位于目錄樹深處而不是位于存儲(chǔ)庫(kù)的頂層。兩種方法都是可行的,但我不想鼓勵(lì)最終用戶到處亂翻并修改里面的腳本。將它放到更深的地方是一個(gè)讓他們更難亂翻的方法。另外,我也可以添加一個(gè) .dockerignore
文件去忽略 scripts
文件夾,因?yàn)樗皇侨萜鞅匦璧牟糠?。但因?yàn)樗苄?,所以我沒(méi)有這樣做。
簡(jiǎn)單而靈活
盡管這一方法很簡(jiǎn)單,但我在幾個(gè)相當(dāng)不同的構(gòu)建系統(tǒng)中使用過(guò),發(fā)現(xiàn)它相當(dāng)靈活。相對(duì)穩(wěn)定的部分(例如,一年僅修改數(shù)次的給定工具集)被固定在容器鏡像內(nèi)。較為靈活的部分則以腳本的形式放在鏡像外。這使我能夠通過(guò)修改腳本并將更改推送到構(gòu)建系統(tǒng)存儲(chǔ)庫(kù)中,輕松修改調(diào)用工具集的方式。用戶所需要做的是將更改拉到本地的構(gòu)建系統(tǒng)存儲(chǔ)庫(kù)中,這通常是非常快的(與更新 Docker 鏡像不同)。這種結(jié)構(gòu)使其能夠擁有盡可能多的卷和腳本,同時(shí)使最終用戶擺脫復(fù)雜性。