容器技術的興起,讓應用程式佈署更加彈性便捷。然而,資料的持久化和分享是容器化應用中不可迴避的議題。Podman 作為新一代容器引擎,提供強大的儲存管理功能,讓開發者能輕鬆應對這些挑戰。預設情況下,許多資料函式庫映象,例如 MongoDB,會自動建立 Volume 來儲存資料。這確保了資料函式庫在容器重啟後資料依然存在。Podman 也允許我們透過 --image-volume 選項,將這些預設 Volume 掛載到 tmpfs 或完全忽略,提供更高的彈性。更進一步地,Podman 支援跨容器 Volume 掛載,讓不同容器之間可以分享資料。這對於需要存取共同資料集的應用程式來說非常實用。例如,可以將一個容器的資料函式庫 Volume 掛載到另一個應用程式容器中,實作資料分享。然而,在 SELinux 啟用的系統中,必須注意檔案的安全性上下文。直接掛載主機目錄到容器可能會因為 SELinux 策略限制而導致存取錯誤。Podman 提供了 :z:Z 字尾來簡化 SELinux 標籤管理,讓開發者可以輕鬆設定容器對掛載目錄的存取許可權,確保資料安全。

玄貓解密:容器資料持久化的兩種武器 - Bind Mounts 與 Volumes

在容器化的世界裡,資料的持久化一直是個重要的議題。身為玄貓,我經常被問到:「容器不是應該設計成無狀態的嗎?那資料到底該怎麼辦?」沒錯,容器本身是設計成可以隨時啟動、停止的,但資料總是要有個地方存放。今天,玄貓就來跟大家聊聊兩種常見的容器資料持久化方案:Bind Mounts 和 Volumes。

Bind Mounts:Linux 原生神兵

Bind Mounts 其實是 Linux 系統裡的一個老牌功能。簡單來說,它就像是檔案系統的「任意門」,可以把 host 機器上的某個目錄,直接「掛載」到容器裡面去。

還記得玄貓剛開始接觸 Docker 的時候,就被 Bind Mounts 的簡單粗暴給震懾了。直接指定 host 上的目錄,容器裡面的程式就能讀寫,方便得不得了。

舉個例子,假設我們想把 host 機器上的 /opt/mydata 目錄,掛載到容器的 /data 目錄,可以這樣做:

podman run -v /opt/mydata:/data my_image

這行指令下去,容器裡面的程式,讀寫 /data 目錄,就等於直接讀寫 host 機器上的 /opt/mydata 目錄。

Bind Mounts 的優點很明顯:

  • 簡單易用:設定簡單,容易理解。
  • 效能較好:直接讀寫 host 檔案系統,效能損耗較小。

但 Bind Mounts 也有一些限制:

  • host 依賴性:容器的行為會受到 host 檔案系統的影響,例如許可權設定等等。
  • 移植性較差:換一台 host 機器,目錄結構可能不一樣,容器就不能用了。

Volumes:容器引擎的貼心管家

Volumes 是由容器引擎(例如 Podman 或 Docker)所管理的資料儲存區。它可以想像成是一個「虛擬磁碟」,容器可以把資料寫到這個磁碟裡面,而不用管 host 機器上的檔案系統。

玄貓在設計分散式系統時,經常使用 Volumes 來儲存容器的狀態資料。因為它夠獨立、夠乾淨,不會受到 host 環境的幹擾。

要建立一個 Volume,可以使用 podman volume create 指令:

podman volume create my_volume

然後,在啟動容器時,把這個 Volume 掛載到容器的某個目錄:

podman run -v my_volume:/data my_image

這樣,容器裡面的程式,讀寫 /data 目錄,就會把資料寫到 my_volume 這個 Volume 裡面。

Volumes 的優點是:

  • 獨立性高:不依賴 host 檔案系統,容器可以在不同的 host 機器上執行。
  • 管理方便:容器引擎會負責 Volume 的建立、刪除和備份。

但 Volumes 也有一些缺點:

  • 效能較差:資料需要經過容器引擎的處理,效能損耗較大。
  • 設定較複雜:需要先建立 Volume,再掛載到容器,設定步驟較多。

玄貓的經驗之談:如何選擇?

Bind Mounts 和 Volumes 各有優缺點,該如何選擇呢?玄貓的建議是:

  • 開發階段:如果需要頻繁修改程式碼,並且希望直接在 host 機器上看到修改結果,可以使用 Bind Mounts。
  • 佈署階段:如果需要確保容器的獨立性和移植性,可以使用 Volumes。

當然,這不是絕對的。在實際應用中,還是要根據具體的需求來選擇。

程式碼範例

以下是一個使用 Volume 的簡單範例。這個範例會建立一個 Nginx 容器,並且把 Volume 掛載到 Nginx 的網頁根目錄:

# 建立 Volume
podman volume create nginx_data

# 啟動 Nginx 容器,並且掛載 Volume
podman run -d -p 80:80 -v nginx_data:/usr/share/nginx/html nginx

這樣,我們就可以把網頁檔案放到 nginx_data 這個 Volume 裡面,Nginx 容器就會自動載入這些檔案。

程式碼解密

  1. podman volume create nginx_data:這行指令會建立一個名為 nginx_data 的 Volume。
  2. podman run -d -p 80:80 -v nginx_data:/usr/share/nginx/html nginx:這行指令會啟動一個 Nginx 容器,並且把 nginx_data 這個 Volume 掛載到容器的 /usr/share/nginx/html 目錄。-d 引數表示在背景執行容器,-p 80:80 引數表示把 host 機器上的 80 埠,對應到容器的 80 埠。

容器資料儲存實戰:深入理解與應用 Volume 技術

在容器化應用中,資料的持久化是一個核心議題。Volume 提供了一種有效的方式,讓容器在生命週期結束後,資料依然能夠儲存下來。本文將探討 Volume 的各種使用方式,從基本概念到進階應用,助你掌握容器資料管理的精髓。

Volume 的基本使用:自動產生與指定名稱

在使用 Podman 執行容器時,可以透過 -v 引數來掛載 Volume。如果沒有指定 Volume 的名稱,Podman 會自動產生一個 UID 作為 Volume 的名稱。例如:

$ podman run -d -p 8080:80 --name nginx_volume2 -v nginx_vol:/usr/share/nginx/html docker.io/library/nginx

在這個範例中,Podman 會建立一個名為 nginx_vol 的 Volume,並將其掛載到 Nginx 容器的 /usr/share/nginx/html 目錄。使用指定名稱的 Volume,可以避免 Podman 自動產生 UID,方便管理。

Volume 的儲存位置:Rootless 與 Rootful 容器的差異

Volume 的預設儲存位置取決於容器的型別:

  • Rootless 容器: Volume 儲存在 <USER_HOME>/.local/share/containers/storage/volumes 目錄下。
  • Rootful 容器: Volume 儲存在 /var/lib/containers/storage/volumes 目錄下。

這些 Volume 在容器銷毀後依然存在,可以被其他容器重複使用。玄貓建議定期清理不再使用的 Volume,以釋放磁碟空間。

Volume 的管理:移除與清理

可以使用 podman volume rm 命令來手動移除 Volume:

$ podman volume rm nginx_vol

若要清理所有未使用的 Volume,可以使用 podman volume prune 命令。對於 Rootful 容器,需要使用 sudo 字首:

$ podman volume prune
$ sudo podman volume prune

玄貓小提示: 定期監控主機上的 Volume,避免它們佔用過多磁碟空間。

預先建立與填充 Volume

除了讓 Podman 自動建立 Volume,也可以使用 podman volume create 命令預先建立 Volume,並填充資料。以下範例建立一個名為 custom_nginx 的 Volume,並在其中新增一個 index.html 檔案:

$ podman volume create custom_nginx
$ echo "Hello World!" >> $(podman volume inspect custom_nginx --format "{{ .Mountpoint }}")/index.html

接著,可以執行一個 Nginx 容器,並掛載這個預先填充的 Volume:

$ podman run -d -p 8080:80 --name nginx_volume3 -v custom_nginx:/usr/share/nginx/html docker.io/library/nginx

現在,透過瀏覽器存取 localhost:8080,應該可以看到 “Hello World!"。

使用 --mount 選項掛載 Volume

除了 -v 選項,Podman 還提供了 --mount 選項來掛載 Volume。以下範例使用 --mount 選項執行 Nginx 容器:

$ podman run -d -p 8080:80 --name nginx_volume4 --mount type=volume,src=custom_nginx,dst=/usr/share/nginx/html docker.io/library/nginx

--mount 選項的語法更清晰,可以明確指定掛載的型別。

Volume Driver:擴充套件 Volume 的功能

預設情況下,Podman 使用 local volume driver 來管理 Volume。Local volume driver 將 Volume 儲存在主機的檔案系統中。此外,還可以組態其他的 Volume driver,例如 NFS。

使用 NFS Volume

Local volume driver 也可以用來掛載 NFS 分享。以下範例建立一個根據 NFS 分享的 Volume,並將其掛載到 MongoDB 容器的 /data/db 目錄:

$ sudo podman volume create --driver local --opt type=nfs --opt o=addr=nfs-host.example.com,rw,context="system_u:object_r:container_file_t:s0" --opt device=:/opt/nfs-export nfs-volume
$ sudo podman run -d -v nfs-volume:/data/db docker.io/library/mongo

玄貓提醒: 在執行此範例之前,需要先組態 NFS 伺服器,確保主機能夠存取 NFS 分享。

在 Dockerfile 中定義 Volume

可以在 Dockerfile 中使用 VOLUME 指令來預先定義 Volume。以下是一個簡單的 Dockerfile 範例:

FROM docker.io/library/nginx:latest
VOLUME /usr/share/nginx/html

這個 Dockerfile 在 docker.io/library/nginx 映像檔的基礎上,新增了一個 VOLUME 指令,指定 /usr/share/nginx/html 目錄應該被掛載為 Volume。

當使用這個 Dockerfile 建立映像檔並執行容器時,Podman 會自動建立一個匿名 Volume,並將其掛載到容器的 /usr/share/nginx/html 目錄。

容器儲存掛載:突破資料存取的界限

在容器的世界中,資料的永續性和分享性至關重要。Podman 提供了多種方式來管理容器的儲存,讓我們可以靈活地在容器之間分享資料,或將主機儲存掛載到容器中。

容器內建 Volume 的奧秘

當我們啟動一個容器時,如果沒有明確指定 Volume,Podman 會自動為容器建立和掛載 Volume。這種自動 Volume 定義在需要持久化資料的容器中非常常見,例如資料函式庫。

docker.io/library/mongo 映象為例,它預設會建立兩個 Volume,分別用於 /data/configdb/data/db。PostgreSQL、MariaDB 和 MySQL 等常見資料函式庫也有類別似的行為。

啟動容器時,我們可以定義如何掛載這些預定義的匿名 Volume。預設情況下,Podman 會建立新的 Volume 並將其掛載到容器中。但我們也可以使用 --image-volume 選項來使用 tmpfs 或完全忽略掛載。

例如,以下命令會啟動一個 MongoDB 容器,並將其預設 Volume 掛載為 tmpfs

$ podman run -d --image-volume tmpfs docker.io/library/mongo

tmpfs 是一種根據記憶體的檔案系統,速度快但資料不會持久儲存。

跨容器 Volume 掛載:資料分享的橋樑

Volume 最棒的優點之一是其靈活性。例如,一個容器可以掛載另一個正在執行的容器的 Volume,以分享相同的資料。要實作這個目標,我們可以使用 --volumes-from 選項。

以下範例啟動一個 MongoDB 容器,然後將其 Volume 跨掛載到一個 Fedora 容器:

$ podman run -d --name mongodb01 docker.io/library/mongo
$ podman run -it --volumes-from=mongodb01 docker.io/library/fedora

第二個容器會開啟一個互動式的 root shell,我們可以用它來檢查檔案系統內容:

[root@c10420016687 /]# ls -al /data
total 20
drwxr-xr-t. 4 root root 4096 Oct 17 15:36 .
dr-xr-xr-x. 19 root root 4096 Oct 17 15:36 ..
drwxr-xr-x. 2 999 999 4096 Sep 20 22:20 configdb
drwxr-xr-x. 4 999 999 4096 Oct 17 15:36 db

如預期,我們可以在 Fedora 容器中找到掛載的 MongoDB Volume。即使我們停止甚至移除第一個 mongodb01 容器,這些 Volume 仍然保持活動狀態,並掛載在 Fedora 容器中。

SELinux 的考量:安全第一

到目前為止,我們已經看到了基本的使用案例,沒有對容器或掛載的資源進行特定的隔離。如果主機啟用了 SELinux 並處於強制模式,則必須考慮一些額外的因素。

SELinux 會遞迴地將標籤應用於檔案和目錄,以定義它們的上下文。這些標籤通常儲存為擴充的檔案系統屬性。SELinux 使用上下文來管理策略,並定義哪些程式可以存取特定的資源。

可以使用 ls 命令來檢視資源的型別上下文:

$ ls -alZ /etc/passwd
-rw-r--r--. 1 root root system_u:object_r:passwd_file_t:s0 2965
Jul 28 21:00 /etc/passwd

在上面的範例中,passwd_file_t 標籤定義了 /etc/passwd 檔案的型別上下文。根據型別上下文,當 SELinux 在強制模式下執行時,程式可以或不能存取檔案。

程式也有它們的型別上下文 – 容器以 container_t 標籤執行,並且具有對標記為 container_file_t 型別上下文的檔案和目錄的讀/寫存取許可權,以及對標記為 container_share_t 的資源的讀/執行存取許可權。

預設情況下,可以存取的其他主機目錄包括 /etc(唯讀)和 /usr(讀/執行)。此外,/var/lib/containers/overlay/ 下的資源標記為 container_share_t

如果我們嘗試掛載一個未正確標記的目錄會發生什麼?

Podman 仍然會執行容器,而不會抱怨標籤錯誤,但是從容器內執行的程式(標記為 container_t 上下文型別)將無法存取掛載的目錄或檔案。以下範例嘗試為 Nginx 容器掛載一個自定義的 document root,而不考慮標籤約束:

$ mkdir ~/custom_docroot
$ echo "Hello World!" > ~/custom_docroot/index.html
$ podman run -d \
--name custom_nginx \
-p 8080:80 \
-v ~/custom_docroot:/usr/share/nginx/html \
docker.io/library/nginx

表面上,一切都很好 – 容器已正確啟動,並且其中的程式正在執行,但是如果我們嘗試連線到 Nginx 伺服器,我們會看到以下錯誤:

$ curl localhost:8080
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.21.3</center>
</body>
</html>

403 Forbidden 表示 Nginx 程式無法存取 index.html 頁面。

要解決這個錯誤,我們有兩個選擇 – 將 SELinux 置於寬容模式,或重新標記掛載的資源。透過將 SELinux 置於寬容模式,它會繼續追蹤違規行為,但不會阻止它們。無論如何,這不是一個好的做法,只應在我們無法正確排除存取問題並需要將 SELinux 排除在外的情況下使用。以下命令將 SELinux 設定為寬容模式:

$ sudo setenforce 0

重要提示

寬容模式不等於完全停用 SELinux。在這種模式下工作時,SELinux 仍然會記錄 AVC 拒絕,但不會阻止它們。系統管理員可以立即在寬容和強制模式之間切換,而無需重新啟動。另一方面,停用則意味著完整的系統重新啟動。

第二個首選選項是簡單地重新標記我們需要掛載的資源。為了實作這個結果,我們可以使用 SELinux 命令列工具。作為一個捷徑,Podman 提供了一種更簡單的方法 – 應用於 Volume 掛載引數的 :z:Z 字尾。這兩個字尾之間的差異很微妙:

  • :z 字尾告訴 Podman 重新標記掛載的資源,以便所有容器都能夠讀寫它。它適用於 Volume 和繫結掛載。
  • :Z 字尾告訴 Podman 重新標記掛載的資源,以便只有目前的容器才能獨佔地讀寫它。這也適用於 Volume 和繫結掛載。

為了測試差異,讓我們嘗試使用 :z 字尾再次執行容器,看看會發生什麼:

$ podman run -d \
--name custom_nginx \
-p 8080:80 \
-v ~/custom_docroot:/usr/share/nginx/html:z \
docker.io/library/nginx

現在,HTTP 呼叫傳回了預期的結果,因為程式能夠存取 index.html 檔案,而沒有被 SELinux 阻止:

$ curl localhost:8080
Hello World!

讓我們看看自動應用於掛載目錄的 SELinux 檔案上下文:

$ ls -alZ ~/custom_docroot
total 20
drwxrwxr-x. 2 packt packt system_u:object_r:container_file_t:s0 4096 Oct 16 15:53 .
drwxrwxr-x. 74 packt packt unconfined_u:object_r:user_home_dir_t:s0 12288 Oct 16 16:32 ..
-rw-rw-r--. 1 packt packt system_u:object_r:container_file_t:s0 13 Oct 16 15:53 index.html

內容解密

  • podman run -d: 在背景執行容器。
  • --image-volume tmpfs: 將映象中定義的 Volume 掛載為 tmpfs,資料儲存在記憶體中。
  • --volumes-from=mongodb01: 掛載名為 mongodb01 的容器中的所有 Volume。
  • ls -alZ /etc/passwd: 顯示 /etc/passwd 檔案的詳細資訊,包括 SELinux 上下文。
  • setenforce 0: 將 SELinux 設定為寬容模式。
  • -v ~/custom_docroot:/usr/share/nginx/html:z: 將主機目錄 ~/custom_docroot 掛載到容器的 /usr/share/nginx/html,並使用 :z 選項重新標記該目錄,允許所有容器讀寫。

透過這些方法,我們可以靈活地管理容器的儲存,並確保資料的永續性和安全性。玄貓(BlackCat)建議在實際應用中,根據需求選擇最適合的儲存方案。