對構建系統進行容器化的指南
一個用於將源代碼轉換成可運行的應用的構建系統是由工具和流程共同組成。在轉換過程中還涉及到代碼的受眾從軟體開發者轉變為最終用戶,無論最終用戶是運維的同事還是部署的同事。
在使用容器搭建了一些構建系統後,我覺得有一個不錯的可復用的方法值得分享。雖然這些構建系統被用於編譯機器學習演算法和為嵌入式硬體生成可載入的軟體鏡像,但這個方法足夠抽象,可用於任何基於容器的構建系統。
這個方法是以一種易於使用和維護的方式搭建或組織構建系統,但並不涉及處理特定編譯器或工具容器化的技巧。它適用於軟體開發人員構建軟體,並將可維護鏡像交給其他技術人員(無論是系統管理員、運維工程師或者其他一些頭銜)的常見情況。該構建系統被從終端用戶中抽象出來,這樣他們就可以專註於軟體。
為什麼要容器化構建系統?
搭建基於容器的可復用構建系統可以為軟體團隊帶來諸多好處:
- 專註:我希望專註於應用的開發。當我調用一個工具進行「構建」時,我希望這個工具集能生成一個隨時可用的二進位文件。我不想浪費時間在構建系統的查錯上。實際上,我寧願不了解,或者說不關心構建系統。
- 一致的構建行為:無論在哪種使用情況下,我都想確保整個團隊使用相同版本的工具集並在構建時得到相同的結果。否則,我就得不斷地處理「我這咋就是好的」的麻煩。在團隊項目中,使用相同版本的工具集並對給定的輸入源文件集產生一致的輸出是非常重要。
- 易於部署和升級:即使向每個人都提供一套詳細說明來安裝一個項目的工具集,也可能會有人翻車。問題也可能是由於每個人對自己的 Linux 環境的個性化修改導致的。在團隊中使用不同的 Linux 發行版(或者其他操作系統),情況可能還會變得更複雜。當需要將工具集升級到下一版本時,問題很快就會變得更糟糕。使用容器和本指南將使得新版本升級非常簡單。
對我在項目中使用的構建系統進行容器化的這些經驗顯然很有價值,因為它可以緩解上述問題。我傾向於使用 Docker 作為容器工具,雖然在相對特殊的環境中安裝和網路配置仍可能出現問題,尤其是當你在一個使用複雜代理的企業環境中工作時。但至少現在我需要解決的構建系統問題已經很少了。
漫步容器化的構建系統
我創建了一個教程存儲庫,隨後你可以克隆並檢查它,或者按照本文內容進行操作。我將逐個介紹存儲庫中的文件。這個構建系統非常簡單(它運行 gcc
),從而可以讓你專註於這個構建系統結構上。
構建系統需求
我認為構建系統中有兩個關鍵點:
- 標準化構建調用:我希望能夠指定一些形如
/path/to/workdir
的工作目錄來構建代碼。我希望以如下形式調用構建:
./build.sh /path/to/workdir
為了使得示例的結構足夠簡單(以便說明),我將假定輸出也在 /path/to/workdir
路徑下的某處生成。(否則,將增加容器中顯示的卷的數量,雖然這並不困難,但解釋起來比較麻煩。)
- 通過 shell 自定義構建調用:有時,工具集會以出乎意料的方式被調用。除了標準的工具集調用
build.sh
之外,如果需要還可以為build.sh
添加一些選項。但我一直希望能夠有一個可以直接調用工具集命令的 shell。在這個簡單的示例中,有時我想嘗試不同的gcc
優化選項並查看效果。為此,我希望調用:
./shell.sh /path/to/workdir
這將讓我得到一個容器內部的 Bash shell,並且可以調用工具集和訪問我的工作目錄(workdir
),從而我可以根據需要嘗試使用這個工具集。
構建系統的架構
為了滿足上述基本需求,這是我的構架系統架構:
![Container build system architecture](/data/attachment/album/202005/19/085620czamgvs3hpzzyzy3.jpg "Container build system architecture")
在底部的 workdir
代表軟體開發者用於構建的任意軟體源碼。通常,這個 workdir
是一個源代碼的存儲庫。在構建之前,最終用戶可以通過任何方式來操縱這個存儲庫。例如,如果他們使用 git
作為版本控制工具的話,可以使用 git checkout
切換到他們正在工作的功能分支上並添加或修改文件。這樣可以使得構建系統獨立於 workdir
之外。
頂部的三個模塊共同代表了容器化的構建系統。最左邊的黃色模塊代表最終用戶與構建系統交互的腳本(build.sh
和 shell.sh
)。
在中間的紅色模塊是 Dockerfile 和相關的腳本 build_docker_image.sh
。開發運營者(在這個例子中指我)通常將執行這個腳本並生成容器鏡像(事實上我多次執行它直到一切正常為止,但這是另一回事)。然後我將鏡像分發給最終用戶,例如通過 容器信任註冊庫 進行分發。最終用戶將需要這個鏡像。另外,他們將克隆構建系統的存儲庫(即一個與教程存儲庫等效的存儲庫)。
當最終用戶調用 build.sh
或者 shell.sh
時,容器內將執行右邊的 run_build.sh
腳本。接下來我將詳細解釋這些腳本。這裡的關鍵是最終用戶不需要為了使用而去了解任何關於紅色或者藍色模塊或者容器工作原理的知識。
構建系統細節
把教程存儲庫的文件結構映射到這個系統結構上。我曾將這個原型結構用於相對複雜構建系統,因此它的簡單並不會造成任何限制。下面我列出存儲庫中相關文件的樹結構。文件夾 dockerize-tutorial
能用構建系統的其他任何名稱代替。在這個文件夾下,我用 workdir
的路徑作參數調用 build.sh
或 shell.sh
。
dockerize-tutorial/
├── build.sh
├── shell.sh
└── swbuilder
├── build_docker_image.sh
├── install_swbuilder.dockerfile
└── scripts
└── run_build.sh
請注意,我上面特意沒列出 example_workdir
,但你能在教程存儲庫中找到它。實際的源碼通常存放在單獨的存儲庫中,而不是構建工具庫中的一部分;本教程為了不必處理兩個存儲庫,所以我將它包含在這個存儲庫中。
如果你只對概念感興趣,本教程並非必須的,因為我將解釋所有文件。但是如果你繼續本教程(並且已經安裝 Docker),首先使用以下命令來構建容器鏡像 swbuilder:v1
:
cd dockerize-tutorial/swbuilder/
./build_docker_image.sh
docker image ls # resulting image will be swbuilder:v1
然後調用 build.sh
:
cd dockerize-tutorial
./build.sh ~/repos/dockerize-tutorial/example_workdir
下面是 build.sh 的代碼。這個腳本從容器鏡像 swbuilder:v1
實例化一個容器。而這個容器實例映射了兩個卷:一個將文件夾 example_workdir
掛載到容器內部路徑 /workdir
上,第二個則將容器外的文件夾 dockerize-tutorial/swbuilder/scripts
掛載到容器內部路徑 /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
還會用你的用戶名(以及組,本教程假設兩者一致)去運行容器,以便在訪問構建輸出時不出現文件許可權問題。
請注意,shell.sh 和 build.sh
大體上是一致的,除了兩點不同:build.sh
會創建一個名為 build_swbuilder
的容器,而 shell.sh
則會創建一個名為 shell_swbuilder
的容器。這樣一來,當其中一個腳本運行時另一個腳本被調用也不會產生衝突。
兩個腳本之間的另一處關鍵不同則在於最後一個參數:build.sh
傳入參數 build
而 shell.sh
則傳入 shell
。如果你看了用於構建容器鏡像的 Dockerfile,就會發現最後一行包含了下面的 ENTRYPOINT
語句。這意味著上面的 docker container run
調用將使用 build
或 shell
作為唯一的輸入參數來執行 run_build.sh
腳本。
# run bash script and process the input command
ENTRYPOINT [ "/bin/bash", "/scripts/run_build.sh"]
run_build.sh 使用這個輸入參數來選擇啟動 Bash shell 還是調用 gcc
來構建 helloworld.c
項目。一個真正的構建系統通常會使用 Makefile 而非直接運行 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
在使用時,如果你需要傳入多個參數,當然也是可以的。我處理過的構建系統,構建通常是對給定的項目調用 make
。如果一個構建系統有非常複雜的構建調用,則你可以讓 run_build.sh
調用 workdir
下最終用戶編寫的特定腳本。
關於 scripts 文件夾的說明
你可能想知道為什麼 scripts
文件夾位於目錄樹深處而不是位於存儲庫的頂層。兩種方法都是可行的,但我不想鼓勵最終用戶到處亂翻並修改裡面的腳本。將它放到更深的地方是一個讓他們更難亂翻的方法。另外,我也可以添加一個 .dockerignore
文件去忽略 scripts
文件夾,因為它不是容器必需的部分。但因為它很小,所以我沒有這樣做。
簡單而靈活
儘管這一方法很簡單,但我在幾個相當不同的構建系統中使用過,發現它相當靈活。相對穩定的部分(例如,一年僅修改數次的給定工具集)被固定在容器鏡像內。較為靈活的部分則以腳本的形式放在鏡像外。這使我能夠通過修改腳本並將更改推送到構建系統存儲庫中,輕鬆修改調用工具集的方式。用戶所需要做的是將更改拉到本地的構建系統存儲庫中,這通常是非常快的(與更新 Docker 鏡像不同)。這種結構使其能夠擁有儘可能多的卷和腳本,同時使最終用戶擺脫複雜性。
via: https://opensource.com/article/20/4/how-containerize-build-system
作者:Ravi Chandran 選題:lujun9972 譯者:LazyWolfLin 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive