面向開發人員的映象和容器實踐指南

語言: CN / TW / HK

譯者 | 崔瑩峰

策劃 | 齊健

容器和開放容器計劃 (OCI) 映象是重要的開源應用程式打包和交付技術, Docker 和 Kubernetes 等專案使其流行起來。本文中,將用簡單的術語描述這項技術,重點介紹映象和容器的基本方面以供開發人員理解,然後討論開發人員可以遵循的一些最佳實踐,以使他們的容器可移植。

PART 01

什麼是映象?

映象只不過是軟體的一種打包格式。一個很好的類比是 Java 的 JAR 檔案或 Python 輪子。JAR(或 EAR 或 WAR)檔案只是具有不同副檔名的 ZIP 檔案,Python 輪子作為 gzip 壓縮包分發。它們都遵循一個標準的內部目錄結構。

映象被打包為tar.gz(gzipped tarballs),它們包括您正在構建或分發的軟體,但也就是僅有這點與 JAR 和輪子能夠類比的地方。一方面,映象不僅包含您的軟體,還包含執行您的軟體所需的所有支援依賴項,包括一個完整的作業系統。輪子和jar通常是作為依賴項構建的,但也可以是可執行的,而映象幾乎總是構建為可執行的,很少作為依賴項構建。

瞭解映象中內容的詳細資訊對於瞭解如何使用映象或為它們編寫和設計軟體並不是必要的。從軟體的角度來看,重要的是要理解您建立的映象將包含一個完整的作業系統。因為從您希望執行的軟體的角度來看,映象被打包成一個完整的作業系統,所以它們必然比以更傳統方式打包的軟體大得多。

請注意,映象是不可變的。它們一旦構建就無法更改。如果您修改映象上執行的軟體,就必須構建一個全新的映象並替換舊映象。

標籤:

在映象被建立時,通常都會使用一個唯一的雜湊值來建立,但也會使用人類可讀的名稱標識它們。例如ubi、ubi-minimal、openjdk11等。但是,每個名稱都可以有不同版本的映象,並且通常通過標籤來區分。例如,openjdk11映象可能被標記為jre-11.0.14.1_1-ubi和jre-11.0.14.1_1-ubi-minimal,分別表示openjdk11軟體包版本11.0.14.1_1基於Red Hat ubi和ubi最小映象構建的映象。

PART 02

什麼是容器?

容器是在主機系統上例項化並執行的映象。從映象到執行容器分為兩步:建立和啟動。建立獲取映象併為其提供自己的 ID 和檔案系統。Create(例如在docker Create中)可以重複多次,以建立一個映象的多個執行例項,每個例項都有自己的 ID 和檔案系統。啟動容器將在主機上啟動一個隔離的程序,在容器中執行的軟體將表現得就像執行在自己的虛擬機器中一樣。因此,容器是主機上的一個獨立的程序,具有自己的 ID 和獨立的檔案系統。

從軟體開發人員的角度來看,使用容器有兩個主要原因:一致性和可伸縮性。這些是相互關聯的,它們共同允許專案使用近年來最有前途的軟體開發創新之一,即“一次構建,多次部署”的原則。

(1)一致性

因為映象是不可變的,並且包含了從作業系統上執行軟體所需的所有依賴項,所以無論您選擇在何處部署它,都可以獲得一致性。這意味著無論您在開發、測試或任何數量的生產環境中將映象作為容器啟動,容器都將以完全相同的方式執行。作為軟體開發人員,您不必擔心這些環境是否執行在不同的主機作業系統或版本上,因為容器每次都執行相同的作業系統。這就是將軟體與其完整的執行時環境一起打包的好處,而不僅僅是因為缺乏必需的環境或依賴軟體,而無法執行的軟體。

這種一致性意味著在幾乎所有情況下,當在一個環境(例如,生產環境)中發現問題時,您可以確信自己能夠在開發或其他環境中重現該問題,因此您可以確認行為並專注於修復它。您的專案永遠不應該再次陷入“但是它可以在我的機器上工作”的可怕問題。

(2)可伸縮性

映象不僅包含您的軟體,還包含執行您的軟體所需的所有依賴項,包括底層作業系統。這意味著在容器內執行的所有程序都將容器視為宿主系統,宿主系統對容器內執行的程序是不可見的,並且從宿主系統的角度來看,容器只是它管理的另一個程序。當然,虛擬機器做的事情幾乎一樣,這就提出了一個合理的問題:為什麼要使用容器技術而不是虛擬機器?答案在於速度和規模。

容器只需執行支援獨立主機所需的軟體,而無需模擬硬體的開銷。虛擬機器必須包含完整的作業系統並模仿底層硬體。後者是一個非常重量級的解決方案,它也會產生更大的檔案。因為從主機系統的角度來看,容器只是另一個正在執行的程序,所以它們可以在幾秒鐘內而不是幾分鐘內啟動。當您的應用程式需要快速擴容時,容器每次都會在資源和速度上擊敗虛擬機器。而且容器也更容易收縮。

從功能的角度來看,可伸縮性超出了本文的範圍,因此本實驗不會演示該特性,但是為了理解為什麼容器技術代表了軟體打包和部署方面的重大進步,理解該原理是很必要的。

注意:雖然可以執行不包含完整作業系統的容器,但很少這樣做,因為可用的最小映象可能會引發其它問題。

PART 03

如何發現和儲存映象

與所有其他型別的軟體打包技術一樣,容器也需要一個可以共享、發現和重用的地方。這些被稱為映象登錄檔,類似於 Java Maven 和 Python 輪子儲存庫或 npm 登錄檔。

以下是網際網路上可用的不同映象登錄檔的例子:

  • Docker Hub: 最初的Docker登錄檔,託管了許多在世界各地專案中廣泛使用的Docker官方映象,併為個人提供託管自己映象的機會。如adoptopenjdk就是在Docker Hub上託管映象的組織之一;可以點選openjdk11檢視其儲存倉庫以獲取該組織專案映象和標籤示例。

  • Red Hat Image Registry: 紅帽的官方映象登錄檔,為那些有效紅帽訂閱者提供映象。

  • Quay: Red Hat 的公共映象登錄檔託管了許多 Red Hat 的公共可用映象,併為個人提供了託管自己的映象的機會。

PART 04

使用映象和容器

有兩個實用程式用於管理映象和容器:Docker和Podman。它們可用於 Windows、Linux 和 Mac 工作站。從開發人員的角度來看,它們在執行命令時是完全等價的。它們可以被認為是彼此的別名。您甚至可以在許多系統上安裝一個包,它會自動將 Docker 更改為 Podman 別名。本文件中無論在哪裡提到 Podman,都可以安全地替換 Docker,而不會改變結果。

您會立即注意到這些實用程式與 Git 非常相似,因為它們執行標籤、推送和拉取操作。您將經常使用或引用此功能。然而,它們不應與 Git 混淆,因為 Git 也管理版本控制,而映象是不可變的,它們的管理實用程式和登錄檔沒有變更管理的概念。如果您將兩個具有相同名稱和標籤的映象推送到同一個儲存庫,則第二個映象將覆蓋第一個映象,而無法檢視或理解發生了什麼變化。

1、子命令

下面是你會經常使用或引用的Podman和Docker子命令的例子:

  • build: 構建映象

    • 例子: podman build -t org/some-image-repo -f Dockerfile

  • image: 本地映象管理

    • 例子: podman image rm -a 刪除所有本地映象

  • images: 列出本地儲存的映象

  • tag : 為映象打標籤

  • container: 容器管理

    • 例子: podman container rm -a 刪除所有已停止的容器

  • run: 建立並啟動一個容器

    • 還有 stop and restart

  • pull/push: 從/向登錄檔上的儲存庫拉/推和映象

2、Dockerfiles

Dockerfile 是定義映象的原始檔,並使用build子命令進行處理。該檔案將定義一個父映象或基礎映象,複製或安裝您希望在映象中執行的任何額外軟體,定義在構建或執行軟體過程中使用到的任何額外元資料,並有可能指定一個命令,該命令在映象例項化為容器後會立即執行。下面的實驗對 Dockerfile 的結構以及其中使用的一些更常用的命令進行了更詳細的描述。

3、Docker 和 Podman 的根本區別

Docker是類unix系統中的守護程序,是Windows系統中的服務。這意味著它一直在後臺執行,並且具有root或管理員許可權。Podman是二進位制的。這意味著它只能按需執行,並且可以作為非特權使用者執行。

這使得Podman對系統資源更安全、更高效。根據定義,以 root 許可權執行任何東西都不太安全。在雲端使用映象時,託管容器的雲可以更安全地管理映象和容器。

4、Skopeo 和 Buildah

和 Docker 是一個單一的實用程式不同,Podman 在 GitHub 上有兩個由 Containers 組織維護的相關實用程式:Skopeo和Buildah。兩者都提供 Podman 和 Docker 不具備的功能,並且都是與Podman一起安裝在Red Hat系列Linux發行版上的容器工具包組的一部分。

在大多數情況下,映象構建可以通過 Docker 和 Podman 執行,但 Buildah 存在以防需要更復雜的映象構建。這些更復雜的構建的細節遠遠超出了本文的範圍,你很少會遇到需要它,但為了完整起見,我在這裡提到了這個實用程式。

Skopeo 提供了 Docker 沒有的兩個實用功能:將映象從一個登錄檔複製到另一個登錄檔的能力以及從遠端登錄檔中刪除映象的能力。同樣,此功能超出了本次討論的範圍,但該功能最終可能對您有用,尤其是在您需要編寫一些 DevOps 指令碼時。

PART 05

Dockerfiles 實驗

以下是一個非常短的實驗(大約 10 分鐘),它將教您如何使用 Dockerfiles 構建映象並將這些映象作為容器執行。它還將演示如何將容器配置外部化,以實現容器開發的全部優勢和“一次構建,多次部署”。

1、安裝

以下實驗室是在本地執行 Fedora 並在已安裝 Podman 和 Git的 Red Hat 沙盒環境 中建立和測試的。我相信在Red Hat沙箱環境中執行它會讓您從這個實驗中獲得最大的收穫,但是在本地執行它也是完全可以接受的。

您還可以在自己的工作站上安裝 Docker 或 Podman 並在本地工作。提醒一下,如果您安裝了 Docker,podman 和 docker對於這個實驗來說是完全可以互換的。

構建映象的步驟:

從 GitHub 克隆 Git 儲存庫:

$ git clone https://github.com/hippyod/hello-world-container-lab

左右滑動檢視完整程式碼

編輯 Dockerfile:

$ cd hello-world-container-lab


$ vim Dockerfile


1 FROM Docker.io/adoptopenjdk/openjdk11:x86_64-ubi-minimal-jre-11.0.14.1_1


2


3 USER root


4


5 ARG ARG_MESSAGE_WELCOME='Hello, World'


6 ENV MESSAGE_WELCOME=${ARG_MESSAGE_WELCOME}


7


8 ARG JAR_FILE=target/*.jar


9 COPY ${JAR_FILE} app.jar


10


11 USER 1001


12


13 ENTRYPOINT ["java", "-jar", "/app.jar"]

左右滑動檢視完整程式碼

這個Dockerfile有以下特性:

  • FROM 語句(第 1 行)定義了構建這個新映象的基礎(或父)映象。

  • USER 語句(第 3 行和第 11 行)定義了在構建期間和執行時正在執行的使用者。起初,root 在構建過程中執行。在更復雜的 Dockerfile 中,我需要 root 才能安裝任何額外的軟體、更改檔案許可權等等,以完成新映象。在 Dockerfile 結束時,我切換到 UID 為 1001 的使用者,這樣,每當映象例項化為容器並執行時,使用者將不是 root,因此更安全。使用 UID 而不是使用者名稱,以便主機可以識別哪個使用者正在容器中執行,以防主機加強了安全措施,阻止容器用root使用者執行。

  • ARG 語句(第 5 行和第 8 行)定義了只能在構建過程中使用的變數。

  • ENV 語句(第 6 行)定義了一個環境變數和值,可以在構建過程中使用,但也可以在映象作為容器執行時使用。請注意它是如何通過引用前面 ARG 語句定義的變數來獲取其值的。

  • COPY 語句(第 9 行)將 Spring Boot Maven 構建建立的 JAR 檔案複製到映象中。為了方便在未安裝 Java 或 Maven 的 Red Hat 沙箱中執行的使用者,我預先構建了 JAR 檔案並將其推送到 hello-world-container-lab 儲存庫。在本實驗室中無需進行 Maven 構建。(注意:還有一個add命令可以替代 COPY。由於該add命令可能具有不可預知的行為,因此最好使用 COPY。)

  • 最後,ENTRYPOINT 語句定義了容器啟動時應該在容器中執行的命令和引數。如果此映象成為後續映象定義的基礎映象並且定義了新的 ENTRYPOINT,它將覆蓋此映象。(注意:還有一個cmd命令可以代替 ENTRYPOINT。兩者之間的區別在此本文中無關緊要,超出了本文的範圍。)

鍵入:q並按 Enter 退出 Dockerfile 並返回到 shell。

構建映象程式碼

$ podman build --squash -t test/hello-world -f Dockerfile

左右滑動檢視完整程式碼

你應該看到:

STEP 1: FROM docker.io/adoptopenjdk/openjdk11:x86_64-ubi-minimal-jre-11.0.14.1_1


Getting image source signatures


Copying blob d46336f50433 done


Copying blob be961ec68663 done


...


STEP 7/8: USER 1001


STEP 8/8: ENTRYPOINT ["java", "-jar", "/app.jar"]


COMMIT test/hello-world


...


Successfully tagged localhost/test/hello-world:latest


5482c3b153c44ea8502552c6bd7ca285a69070d037156b6627f53293d6b05fd7

左右滑動檢視完整程式碼

除了構建映象之外,這些命令還提供了以下說明:

--squash標誌將通過確保在映象構建完成時僅向基礎映象新增一層來減小映象大小。多餘的層會使最後生成的映象變得更大。FROM、RUN 和 COPY/ADD 語句都會新增一層,最佳實踐是在可能的情況下連線這些語句,例如:

RUN dnf -y --refresh update && \


dnf install -y --nodocs podman skopeo buildah && \


dnf clean all

左右滑動檢視完整程式碼

上面的 RUN 語句不僅會執行每個語句才僅建立一個層,而且如果其中任何一個失敗,也會使構建失敗。

-t flag 用於命名映象。因為我沒有顯式地定義名稱的標籤(如test/hello-world:1.0),所以預設將映象標記為latest。我也沒有定義登錄檔(如quay.io/test/hello-world),所以預設的登錄檔將是 localhost。

-f 標誌用於明確宣告要構建的 Dockerfile。

當執行構建時,Podman 將跟蹤“blob”的下載。這些是您的映象將建立的映象層。它們最初是從遠端登錄檔中提取的,它們將被快取在本地以加速未來的構建。

Copying blob d46336f50433 done


Copying blob be961ec68663 done


...


Copying blob 744c86b54390 skipped: already exists


Copying blob 1323ffbff4dd skipped: already exists

左右滑動檢視完整程式碼

當構建完成時,列出映象以確認它已成功構建:

$ podman images

左右滑動檢視完整程式碼

你應該看到:

REPOSITORY                                        TAG                                                      IMAGE ID      CREATED               SIZE


localhost/test/hello-world latest 140c09fc9d1d 7 seconds ago 454 MB


docker.io/adoptopenjdk/openjdk11 x86_64-ubi-minimal-jre-11.0.14.1_1 5b0423ba7bec 22 hours ago 445 MB

左右滑動檢視完整程式碼

2、執行容器

執行映象:

$ podman run test/hello-world

左右滑動檢視完整程式碼

你應該看到:

.   ____          _            __ _ _


/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \


( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \


\\/ ___)| |_)| | | | | || (_| | ) ) ) )


' |____| .__|_| |_|_| |_\__, | / / / /


=========|_|==============|___/=/_/_/_/


:: Spring Boot :: (v2.5.4)






...


GREETING: Hello, world


GREETING: Hello, world

左右滑動檢視完整程式碼

輸出將繼續每三秒列印"Hello, world",直到退出:

crtl-c

證明Java只安裝在容器中:

$ java -version

在容器內執行的 Spring Boot 應用程式需要 Java 才能執行,這也是我選擇基礎映象的原因。如果您在Red Hat 沙盒環境中執行這個實驗,這也證明 Java 僅安裝在容器中,而不是主機上:

-bash: java: command not found...

左右滑動檢視完整程式碼

3、外部化配置

映象現在已構建,但是當我希望將映象部署到每個環境中的“Hello, world”訊息都不同時會發生什麼?例如,我可能想要更改它,因為環境是針對不同的開發階段或不同的語言環境。如果我更改 Dockerfile 中的值,我需要構建一個新映象才能看到訊息,這破壞了容器最基本的好處之一——“一次構建,多次部署”。那麼如何使我的映象真正可移植,以便可以將其部署在我需要的任何地方呢?答案在於外部化配置。

使用新的外部歡迎訊息執行映象:

$ podman run -e 'MESSAGE_WELCOME=Hello, world DIT' test/hello-world

你應該看到:

Output:


. ____ _ __ _ _


/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \


( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \


\\/ ___)| |_)| | | | | || (_| | ) ) ) )


' |____| .__|_| |_|_| |_\__, | / / / /


=========|_|==============|___/=/_/_/_/


:: Spring Boot :: (v2.5.4)


...


GREETING: Hello, world DIT


GREETING: Hello, world DIT

左右滑動檢視完整程式碼

通過使用crtl-c停止,並調整訊息後重新執行:

$ podman run -e 'MESSAGE_WELCOME=Hola Mundo' test/hello-world


. ____ _ __ _ _


/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \


( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \


\\/ ___)| |_)| | | | | || (_| | ) ) ) )


' |____| .__|_| |_|_| |_\__, | / / / /


=========|_|==============|___/=/_/_/_/


:: Spring Boot :: (v2.5.4)


...


GREETING: Hola Mundo


GREETING: Hola Mundo

左右滑動檢視完整程式碼

-e標誌定義了在啟動時注入容器的環境變數和值。如您所見,即使該變數已內建到原始映象中(Dockerfile 中的語句ENV MESSAGE_WELCOME=${ARG_MESSAGE_WELCOME}),它也會被覆蓋。現在,您已經外部化了需要根據部署位置進行更改的資料(例如,在DIT環境中或針對說西班牙語的使用者),從而使映象具有可移植性。

使用檔案中定義的新訊息執行映象:

$ echo 'Hello, world from a file' > greetings.txt


$ podman run -v "$(pwd):/mnt/data:Z" \


-e 'MESSAGE_FILE=/mnt/data/greetings.txt' test/hello-world

左右滑動檢視完整程式碼

執行這個例子,您應該看到:

.   ____          _            __ _ _


/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \


( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \


\\/ ___)| |_)| | | | | || (_| | ) ) ) )


' |____| .__|_| |_|_| |_\__, | / / / /


=========|_|==============|___/=/_/_/_/


:: Spring Boot :: (v2.5.4)


...


GREETING: Hello, world from a file


GREETING: Hello, world from a file

左右滑動檢視完整程式碼

重複,直到按ctrl -c停止

在這種情況下,-e標誌定義了/mnt/data/greetings.txt 的檔案路徑,該路徑通過-v 標誌從主機的本地檔案系統$(pwd)/greetings.txt(pwd是一個 bash 實用程式,輸出當前目錄的絕對路徑,在您的情況下應該是hello-world-container-lab)掛載到容器中。現在,您已經外部化了需要根據部署位置更改的資料,但這一次資料是在掛載到容器中的外部檔案中定義的。如果環境變數數量不多,那麼直接在命令列設定是可以的,但是當您要設定好幾個環境變數時,使用檔案將環境變數注入容器是更有效的方式。

注意::Z上述卷定義末尾的標誌適用於使用SELinux的系統。SELinux 管理許多 Linux 發行版上的安全性,並且該標誌允許容器訪問目錄。如果沒有這個標誌,SELinux 會阻止讀取檔案,並且容器中會丟擲異常。刪除:Z標記後再次嘗試執行上面的命令以檢視演示。

實驗到此結束。

PART 06

容器開發:外部化配置

“一次構建,多次部署”之所以有效,是因為在不同環境中執行的不可變容器不必擔心支援特定軟體專案所需的硬體或軟體的差異。這一原則使軟體開發、除錯、部署和持續維護變得更快、更容易。它也不是完美的,必須在編寫程式碼的方式上進行一些小的更改,才能使容器真正可移植。

為容器化編寫軟體時,最重要的設計原則是決定要外部化什麼。這些決定最終使您的映象可移植,因此它們可以完全實現“一次構建,多次部署”的範例。儘管這看起來很複雜,但在決定配置資料是否應該注入到執行的容器中時,有一些容易記住的因素需要考慮:

  • 資料環境是否特定的? 這包括任何需要根據容器執行位置配置的資料,無論環境是生產環境、非生產環境還是開發環境。此類資料包括國際化配置、資料儲存資訊以及您希望應用程式在其下執行的特定測試配置檔案。

  • 資料釋出是否獨立? 這種型別的資料可以執行從功能標誌到國際化檔案再到日誌級別的所有資料——基本上,您可能想要或需要在版本之間更改的任何資料,而無需構建和新部署。

  • 資料是祕密嗎? 憑證不應被硬編碼或儲存在映象中。憑證通常需要在與釋出時間表不匹配的時間表上重新整理,並且將機密嵌入儲存在映象登錄檔中的映象中存在安全風險。

最佳實踐是選擇應該將配置資料外部化的位置(即在環境變數或檔案中),並且只外部化那些滿足上述條件的部分。如果它不符合上述標準,最好將其保留為不可變映象的一部分。遵循這些準則將使您的映象真正可移植,並使您的外部配置保持合理的大小和可管理性。

PART 07

總結    

本文為剛接觸映象和容器的軟體開發人員介紹了四個關鍵概念和思路:

  • 映象是不可變的二進位制檔案:映象是打包軟體以便以後重用或部署的一種方法。

  • 容器是獨立的程序:當它們被建立時,容器是一個映象的執行時例項。當容器啟動時,它們成為主機上記憶體中的程序,這比虛擬機器更輕、更快。在大多數情況下,開發人員只需要瞭解後者,但瞭解前者是有幫助的。

  • “一次構建,多次部署”:這個原則使容器技術如此有用。映象和容器提供了部署的一致性和獨立於主機的獨立性,允許您跨許多不同的環境進行部署。由於這一原則,容器也很容易伸縮。

  • 將配置外部化:如果您的映象具有特定於環境、獨立於釋出或機密的配置資料,請考慮將該資料置於映象和容器之外。您可以通過注入環境變數或將外部檔案掛載到容器中來將此資料注入正在執行的映象中。

原文 連結:

https://opensource.com/article/22/5/guide-containers-images

譯者介紹

崔瑩峰,51CTO社群編輯,一名70後程序員,擁有10多年工作經驗,長期從事 Java 開發,架構設計,容器化等相關工作。精通Java,熟練使用Maven、Jenkins等Devops相關工具鏈,擅長容器化方案規劃、設計和落地。

直播預告

一次瞭解DevOps與MLOps的異同之處與應用場景、企業如何利用DevOps & MLOps提高效率。特邀極狐GitLab架構師——劉巍鋒、第四正規化開源專案 OpenMLDB 研發負責人——盧冕,為大家詳細介紹MLOps在極狐GitLab中的應用探索,以及第四正規化OpenMLDB提供的生產級特徵平臺,如何在生產環境上線機器學習應用的解決方案。

6月5日14:00,不見不散

點選 視訊號 卡片, 預約 直播

點選此處“ 閱讀全文 ”檢視更多內容