配置測試容器以與 Podman 協同工作
1. 概述
近年來, Testcontainers已成為 Java 整合測試的首選庫,它使我們能夠在測試生命週期中啟動真實的容器。這些容器代表外部依賴項,例如資料庫、訊息代理程式和其他元件。官方庫頁面將其描述為「一個開源程式庫,用於提供在 Docker 容器中運行的一次性、輕量級實例」。
同時,許多團隊正在探索或遷移到其他容器解決方案。 Podman 就是其中之一,甚至可以說是最常用的替代方案。原因可能是授權協議的變更,或是人們對無根引擎的偏好。因此,一個自然而然的問題出現了:在 Java 專案中,我們能否使用 Podman 取代 Docker 來運行測試容器?
在本教程中,我們將逐步介紹如何讓 Testcontainers 與 Podman 一起工作,概述官方支援的功能,展示所需的配置,並重點介紹可能遇到的問題。
2. 為什麼考慮使用 Podman 而不是 Docker?
如前所述,Docker 過去曾對其授權協議進行過一些更改,但以下還有更多原因:
- 在 Linux 系統上,Podman 提供了一個無需守護程式、無需 root 權限的容器引擎,在某些環境下可能更簡單、更安全。
- 在 CI/CD 環境(尤其是在 Linux 系統上)中,運行 Podman 可以與現有基礎設施保持一致,並減少對 Docker Engine 的依賴。
- 使用 Apple Silicon Mac 的團隊可能更喜歡 Podman 的 podman 機器模式,而不是 Docker Desktop。
也就是說,對於基於 Testcontainers 的測試,我們需要一個與 Docker API 相容的容器運行時,因為維護者表示,容器運行時在主要開發工作流程中並沒有進行積極的測試。
3. 設定 Podman 來測試容器
接下來,我們將逐步介紹如何設定 Podman,以便 Testcontainers 可以將其用作容器執行時間。本文介紹的配置僅在 Linux 和 Mac 系統上測試過。雖然 WSL2 Linux 發行版也可能適用於 Windows 系統,但尚未經過測試。
3.1 安裝 Podman
首先,讓我們按照官方頁面上的說明在本地環境中安裝 Podman。這將使我們能夠運行一個使用 Podman 容器引擎的容器。
3.2. 啟用 Podman 套接字
Testcontainers 需要透過實作 Docker API 的套接字與容器引擎通訊。
在 Linux 系統上,我們可以像這樣為使用者啟用套接字:
systemctl --user enable --now podman.socket
然後,我們可以確認該套接字處於活動狀態:
ls -la /run/user/$UID/podman/podman.sock
在 Mac 上,此套接字位於 Podman 虛擬機器內部。我們可以使用以下命令檢查其路徑:
podman machine start
podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}'
最後一條指令會列印出 Mac 主機上 Podman Docker API 套接字的完整路徑。我們將在下一節中使用該路徑來配置DOCKER_HOST 。
3.3 設定環境變數
接下來,我們需要指示 Testcontainers 如何連接到 Podman 套接字。這可以透過設定DOCKER_HOST環境變數來實現。
在Linux系統上:
export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/podman/podman.sock
export TESTCONTAINERS_RYUK_DISABLED=true
在 macOS 系統上:
export DOCKER_HOST=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')
export TESTCONTAINERS_RYUK_DISABLED=true
預設情況下,Docker 配置使用 Testcontainers 的輔助容器「Ryuk」來自動清理資源(容器和網路)。但使用 Podman 時,此功能可能並非總是有效(尤其是在無根模式或 macOS 虛擬機器場景下)。解決方法是使用TESTCONTAINERS_RYUK_DISABLED=true將其關閉。
4. 已知的限制和注意事項
需要強調的是,在使用 Podman 執行 Testcontainers 時,仍然可能存在一些問題:
| 特徵缺口 | 官方聲明稱,Testcontainers 團隊並未積極測試其他容器運行時,因此「並非所有功能都可用」。 |
| Ryuk頻道問題 | 如前所述,透過 Ryuk 進行容器清理可能會失敗,或者需要特權模式或停用特權模式。這意味著如果不進行清理,可能會殘留容器、網路和磁碟區。 |
| 套接字/權限問題 | 在無根模式下,Podman 監聽 unix:///run/user/$UID/podman/podman.sock。我們的測試環境必須擁有存取該套接字的權限;在 macOS 上,虛擬機器和主機之間的映射關係可能會導致路徑不符。 |
| 主機網路和映射怪癖 | 由於 Podman 使用不同的預設網路模式(例如,有時使用 podman 網路而不是 Docker 的橋接模式),因此某些進階網路配置(自訂網路、別名、主機連接埠綁定)可能無法與 Docker 完全相同地運作。 |
| 圖片擷取/簡稱查找 | 在某些發行版中,如果未設定 unqualified-search-registries,Podman 在使用短鏡像名稱(例如 busybox)時可能會提示輸入鏡像倉庫位址。如果 Testcontainers 拉取鏡像時遇到此提示,測試可能會掛起。 |
| CI/代理環境變異性 | 如果我們建立的代理程式各不相同(有些使用 Docker,有些使用 Podman),則可能會引入細微差別。我們必須對兩種方案都進行測試。 |
| 與第三方擴充功能的相容性 | 如果您所依賴的框架(例如 Quarkus Dev Services)假定使用 Docker,則其對 Podman 的支援可能不夠成熟。 |
簡而言之,是的,我們可以將 Testcontainers 與 Podman 結合使用來進行 Java 測試,只要我們配置好套接字和環境變量,並考慮清理策略即可。所以它並非即插即用。我們需要儘早測試配置路徑(尤其是在 Mac 或 ARM 架構上),並監控是否有殘留容器、套接字權限問題或功能不符等情況。
5. 測試設置
環境配置完成後,我們就可以像使用 Docker 一樣編寫整合測試了。例如:
@Test
void whenSettingValue_thenCanGetItBack() {
try (RedisContainer redis = new RedisContainer("redis:7-alpine").withExposedPorts(6379)) {
redis.start();
String host = redis.getHost();
int port = redis.getFirstMappedPort();
try (Jedis jedis = new Jedis(host, port)) {
jedis.set("greeting", "hello");
String value = jedis.get("greeting");
Assertions.assertEquals("hello", value);
}
}
}
這是一個簡單的整合測試,使用 Redis 容器,測試內容僅包括設定一個值和使用 Jedis API 來取得該值。雖然測試很簡單,但足以驗證我們的 Podman 配置。
接下來,我們再來使用另一個非常常見的資料庫,它經常用於 Java 專案。
@Test
void whenQueryingDatabase_thenReturnsOne() throws Exception {
try (MySQLContainer mysql = new MySQLContainer("mysql:8.4")) {
mysql.start();
try (Connection conn = DriverManager
.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword());
Statement st = conn.createStatement()) {
st.execute("CREATE TABLE t(id INT PRIMARY KEY)");
st.execute("INSERT INTO t VALUES (1)");
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM t");
rs.next();
Assertions.assertEquals(1, rs.getInt(1));
}
}
}
最後,讓我們使用一個設定稍微複雜一些但仍然使用單一容器的訊息代理程式。
@Test
void whenProducingMessage_thenConsumerReceivesIt() {
DockerImageName image = DockerImageName.parse("confluentinc/cp-kafka:7.6.1");
try (KafkaContainer kafka = new KafkaContainer(image)) {
kafka.start();
String bootstrap = kafka.getBootstrapServers();
String topic = "hello";
Properties prodProps = new Properties();
prodProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap);
prodProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
prodProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
try (KafkaProducer<String, String> producer = new KafkaProducer<>(prodProps)) {
producer.send(new ProducerRecord<>(topic, "key", "hello")).get();
} catch (Exception e) {
throw new RuntimeException(e);
}
Properties consProps = new Properties();
consProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap);
consProps.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group");
consProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
consProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consProps)) {
consumer.subscribe(Collections.singletonList(topic));
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(10));
ConsumerRecord<String, String> first = records.iterator().next();
Assertions.assertEquals("hello", first.value());
}
}
}
透過這些整合測試,我們可以確認容器的兼容性,驗證我們的 Podman 設定是否正常運作,並使我們能夠使用新的執行時間引擎。
6. 結論
在本文中,我們提供了一條在 Java 專案中採用 Podman 和 Testcontainers 的清晰路徑,並幫助我們避免許多開發人員遇到的常見陷阱。
將 Java 測試容器的容器執行時間從 Docker 切換到 Podman 並非只是替換別名那麼簡單。它需要了解套接字配置、清理機制、權限以及底層差異。但只要配置正確,我們就可以確保整合測試能夠可靠地運行,而無需考慮容器引擎。