話說A2A這個案子使用Podman Quadlet的起源,是客戶IT窗口力排眾議要求使用這個大家都很陌生的技術,如同iPad初推之際,消費者無法定位它是iPhone增大版還是MacBook縮小版。IT直接用AI產出大禮包丟給廠商研究,但在對Podman Qaudlet毫無基礎情況反而墊高了學習門檻,就算透過AI也不知該怎麼問起,這也是我參賽的初衷。
大禮包的README.md如下,不必細看直接瀏覽而過:
基於 Podman + Quadlet 的前後端分離微服務容器化部署方案,採用統一 SSL 終止點架構。
┌─────────────────────────────┐
外部 HTTPS :443 → │ SSL Termination Proxy │
│ (OpenResty) │
│ - 統一 SSL 終止 │
│ - 統一 JWT 驗證 │
│ - 路由分發 │
└──────────┬──────────────────┘
│ (內部 HTTP)
┌──────────────┼──────────────┐
↓ ↓ ↓
Frontend BFF (直接訪問)
HTTP :80 HTTP :8080 Backend APIs
↓
═════════════════════
internal-net (HTTP)
═════════════════════
↓
┌─────────────┼─────────────┐
↓ ↓ ↓
API-User API-Order API-Product
:8080 :8080 :8080
| 服務 | 容器內部 | 主機端口 | 說明 |
|---|---|---|---|
| SSL Proxy | 80, 443 | 80, 443 | 統一 HTTPS 入口 |
| Cockpit | 9090 | 9090 | Web 管理介面(Host 服務) |
| Frontend | 80 | - (內部) | 透過 SSL Proxy |
| BFF | 8080 | - (內部) | 透過 SSL Proxy |
| API-User | 8080 | - | 完全隔離(internal-net) |
| API-Order | 8080 | - | 完全隔離(internal-net) |
| API-Product | 8080 | - | 完全隔離(internal-net) |
| 服務 | 主機端口 | 綁定 | 用途 |
|---|---|---|---|
| SSL Proxy | 80, 443 | 所有介面 | 對外服務 |
| API-User | 8101 | 127.0.0.1 | localhost Debug |
| API-Order | 8102 | 127.0.0.1 | localhost Debug |
| API-Product | 8103 | 127.0.0.1 | localhost Debug |
Debug 端口特性:
127.0.0.1,外部無法訪問完整端口設計說明: 詳見 docs/ARCHITECTURE.md 的「端口規劃」章節
在執行部署腳本前,需要安裝必要的系統套件:
# 更新系統
sudo dnf update -y
# 安裝 Podman 和容器相關工具
sudo dnf install -y podman podman-plugins
# 安裝腳本執行所需的工具
sudo dnf install -y \
openssl \
jq \
curl \
git \
systemd
# 驗證安裝
podman --version # 應該是 4.4+
systemctl --version # 應該是 250+
jq --version
openssl version
各套件用途說明:
| 套件 | 用途 | 使用的腳本 |
|---|---|---|
| podman | 容器運行時 | 所有服務部署 |
| podman-plugins | Podman 網路插件 | internal-net 網路設定 |
| openssl | SSL 憑證產生、JWT Secret 產生 | generate-certs.sh, manage-partner-secrets.sh |
| jq | JSON 處理工具 | generate-jwt.sh |
| curl | HTTP 請求工具 | test-connectivity.sh |
| systemd | 服務管理(通常已安裝) | Quadlet 服務管理 |
可選套件(開發除錯用):
# 安裝額外的除錯工具(可選)
sudo dnf install -y \
bind-utils \
net-tools \
tcpdump \
strace
啟用 Podman Socket(Quadlet 必需):
# 啟用 user-level Podman socket
systemctl --user enable --now podman.socket
# 確認 socket 狀態
systemctl --user status podman.socket
# 啟用開機自動啟動(讓使用者服務在登入前啟動)
sudo loginctl enable-linger $(whoami)
檢查 SELinux 狀態(可選):
# 檢查 SELinux 模式
getenforce
# 如果在 Enforcing 模式下遇到權限問題,可以臨時設為 Permissive
# 警告:生產環境請正確配置 SELinux policy,不要禁用
sudo setenforce 0 # 臨時設為 Permissive
# 永久設定(修改配置後需重啟)
# sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
依賴套件已安裝(參考上述步驟 0),確認版本符合:
./scripts/generate-certs.sh
開發環境(推薦開始):
./scripts/setup.sh dev
生產環境:
./scripts/setup.sh prod
./scripts/start-all.sh
# 檢查狀態
./scripts/status.sh
# 執行連通性測試
./scripts/test-connectivity.sh
若已安裝 Cockpit,以 appuser 登入 https://<hostname>:9090 即可使用 Web 管理介面。
詳見 Cockpit 監控管理指南。
# 前端(不需驗證)
curl -k https://localhost/
# Web/App API(由 BFF 處理 Session 認證)
# 先登入取得 Session
curl -k -X POST https://localhost/api/login \
-d '{"username":"user","password":"pass"}' \
-c cookies.txt
# 使用 Session 訪問 API
curl -k https://localhost/api/orders -b cookies.txt
# Partner API(SSL Proxy 驗證 JWT Token)
PARTNER_TOKEN=$(./scripts/generate-jwt.sh partner-company-a)
curl -k -H "Authorization: Bearer $PARTNER_TOKEN" \
https://localhost/partner/api/order/
podman-microservices/
├── 📄 專案文件
│ └── README.md # 快速開始指南(本文件)
│
├── 📁 quadlet/ # Systemd Quadlet 服務定義
│ ├── internal-net.network # 內部隔離網路定義
│ ├── ssl-proxy.container # SSL Termination Proxy(singleton)
│ ├── frontend@.container # Frontend 服務(template unit,多副本)
│ ├── bff@.container # BFF Gateway 服務(template unit)
│ ├── api-user@.container # User API(template unit)
│ ├── api-order@.container # Order API(template unit)
│ ├── api-product@.container # Product API(template unit)
│ └── *@.container.d/ # 環境變數覆蓋(開發模式)
│
├── 📁 configs/ # 配置檔
│ ├── ha.conf # 高可用配置(各服務副本數)
│ ├── images.env # 容器鏡像定義
│ ├── ssl-proxy/ # SSL Proxy 配置
│ │ ├── nginx.conf # 主配置(OpenResty + Lua)
│ │ ├── conf.d/
│ │ │ ├── upstream.conf # Backend 服務定義
│ │ │ └── routes.conf # 路由 + JWT 驗證(Web/App + Partner)
│ │ └── lua/ # Lua 模組
│ │ ├── partner-auth.lua # JWT 驗證邏輯
│ │ └── partners-loader.lua # Partner 設定載入
│ ├── bff/ # BFF 佔位配置
│ │ └── nginx.conf # 監聽 8080 的佔位 Nginx(快速測試用)
│ └── frontend/ # Frontend 配置
│ └── nginx.conf # Frontend Nginx 配置
│
├── 📁 dockerfiles/ # Dockerfile(SUSE BCI)
│ ├── api-user/Dockerfile # User API (OpenJDK 17)
│ ├── api-order/Dockerfile # Order API (OpenJDK 17)
│ ├── api-product/Dockerfile # Product API (OpenJDK 17)
│ ├── bff/Dockerfile # BFF Gateway (Node.js 20)
│ └── frontend/Dockerfile # Frontend (Nginx 1.21)
│
├── 📁 scripts/ # 管理腳本
│ ├── setup.sh # 初始化部署(dev/prod)
│ ├── start-all.sh # 啟動所有服務
│ ├── stop-all.sh # 停止所有服務
│ ├── status.sh # 查看服務狀態
│ ├── restart-service.sh # 重啟服務(支援 rolling restart)
│ ├── scale-service.sh # 動態調整服務副本數
│ ├── logs.sh # 查看服務日誌(支援多副本)
│ ├── test-connectivity.sh # 連通性測試
│ ├── generate-certs.sh # 產生自簽 SSL 憑證
│ └── generate-jwt.sh # 產生 JWT Token(Partner 可用)
│
├── 📁 examples/ # 範例代碼
│ └── partner-clients/ # Partner API 客戶端範例
│ ├── README.md # Partner 整合說明(Shell/Node.js/Python)
│ ├── nodejs-client.js # Node.js 客戶端範例
│ └── python-client.py # Python 客戶端範例
│
├── 📁 cockpit/ # Cockpit 自訂插件
│ └── microservices-monitor/ # 微服務監控儀表板
│ ├── manifest.json # 插件 metadata
│ └── index.html # 監控 UI(單頁應用)
│
└── 📁 docs/ # 詳細文件
├── ARCHITECTURE.md # 架構詳解(含端口規劃)
├── DEPLOYMENT.md # 部署指南
├── PARTNER-INTEGRATION.md # Partner API 整合指南(含安全架構)
├── ENDPOINT-PERMISSIONS.md # API 端點權限對照表
├── COCKPIT-MONITORING.md # Cockpit 監控管理指南
├── DEBUG.md # 故障排除
└── podman_rootless_min_offline_repo_rhel97_v3.md # 離線部署指南(Air-Gapped)
三個命名空間對應三類消費者,認證責任各自獨立:
| Prefix | 消費者 | 認證責任 |
|---|---|---|
/ |
瀏覽器(SPA) | 無 |
/api/ |
Web / Mobile App | BFF(Session) |
/partner/api/{service}/ |
Partner B2B | SSL Proxy(JWT) |
| 請求路徑 | 代理目標 | JWT 驗證 |
|---|---|---|
/ |
frontend:80 |
無 |
/api/* |
bff:8080 |
無(BFF 自行驗 Session) |
/partner/api/user/* |
api-user:8080 |
有 |
/partner/api/order/* |
api-order:8080 |
有 |
/partner/api/product/* |
api-product:8080 |
有 |
JWT 驗證只作用於 /partner/api/*,/ 與 /api/* 不受影響。
/ 經 SSL Proxy 轉發到 frontend:80 後,frontend nginx 再處理:
| Location | 行為 |
|---|---|
= /health |
健康檢查,200 OK |
~* \.(js|css|png|…|woff2)$ |
靜態資源,Cache-Control: public, immutable,expires 1y |
/(fallback) |
try_files → /index.html,SPA 路由支援 |
| 詳細設計見 docs/ARCHITECTURE.md § Endpoint 命名與路由設計。 |
本專案採用雙軌驗證機制,針對不同類型的客戶端使用不同的認證方式:
路徑: /api/*
流程:
Client → SSL Proxy (不驗證) → BFF (Session 驗證) → Backend APIs
說明:
範例:
# 先登入取得 Session
curl -k -X POST https://localhost/api/login \
-d '{"username":"user","password":"pass"}' \
-c cookies.txt
# 使用 Session 訪問 API
curl -k https://localhost/api/orders -b cookies.txt
路徑: /partner/api/*
流程:
Partner → SSL Proxy (JWT 驗證) → Backend APIs(直接)
說明:
範例:
# 產生 Partner JWT Token
TOKEN=$(./scripts/generate-jwt.sh partner-company-a)
# 使用 Token 訪問 Partner API
curl -k -H "Authorization: Bearer $TOKEN" \
https://localhost/partner/api/order/
專案包含使用 SUSE BCI 的 Dockerfile 範例:
# 建置所有後端與前端服務
for service in api-user api-order api-product bff frontend; do
cd dockerfiles/$service
podman build -t localhost/$service:latest .
cd ../..
done
注意:
ssl-proxy直接使用 upstream OpenResty image(docker.io/openresty/openresty:alpine),不需要自訂 build。Quadlet 啟動時會自動 pull;離線環境請參考下方「映像管理」章節預先匯入。
BFF 佔位測試:若暫時以 nginx:alpine 作為 BFF 佔位 image,需掛載 configs/bff/nginx.conf 以確保監聽正確的 8080 port:
# quadlet/bff.container 加入:
# Volume=/opt/app/configs/bff/nginx.conf:/etc/nginx/nginx.conf:ro
# 查看所有服務狀態(含各副本)
./scripts/status.sh
# Rolling restart(逐一重啟副本,零中斷)
./scripts/restart-service.sh api-order
# 重啟指定副本
./scripts/restart-service.sh api-order 1
# 動態調整副本數
./scripts/scale-service.sh api-user 3
# 查看日誌(所有副本)
./scripts/logs.sh api-user
# 查看特定副本日誌
./scripts/logs.sh api-user 1
# 重新產生憑證
./scripts/generate-certs.sh
# 重啟 SSL Proxy
./scripts/restart-service.sh ssl-proxy
開發模式:
# 直接測試 Backend API
curl http://localhost:8101/health # API-User
curl http://localhost:8102/health # API-Order
curl http://localhost:8103/health # API-Product
生產模式:
# 透過 SSL Proxy
curl -k https://localhost/api/users
# 或進入特定副本容器
podman exec -it api-order-1 curl http://localhost:8080/health
prod 模式開發環境:使用固定測試 Secret(由 setup.sh dev 自動配置)
生產環境:使用 Podman Secrets
# 創建 Partner Secrets(每個 Partner 獨立)
./scripts/manage-partner-secrets.sh create a
./scripts/manage-partner-secrets.sh create b
./scripts/manage-partner-secrets.sh create c
# 查看現有 Secrets
./scripts/manage-partner-secrets.sh list
# 輪換 Secret(安全更新)
./scripts/manage-partner-secrets.sh rotate a
詳細說明:參考 docs/PARTNER-INTEGRATION.md
# 執行完整測試
./scripts/test-connectivity.sh
測試項目:
# 檢查服務狀態
systemctl --user status ssl-proxy
# 查看日誌
./scripts/logs.sh ssl-proxy
# 檢查鏡像
podman images | grep ssl-proxy
# 檢查 JWT Secret 是否一致
# 產生 Token 和驗證 Token 必須使用相同的 Partner Secret
# 開發環境:查看環境變數
podman inspect ssl-proxy | grep JWT_SECRET_PARTNER
# 生產環境:檢查 Podman Secrets
./scripts/manage-partner-secrets.sh list
./scripts/manage-partner-secrets.sh show a
# 重新產生測試 Token
TOKEN=$(./scripts/generate-jwt.sh partner-company-a)
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null # 解碼查看 payload
完整 Debug 指南:docs/DEBUG.md
dockerfiles/api-user → dockerfiles/api-newservice
quadlet/api-user@.container → quadlet/api-newservice@.container
configs/ha.conf 加入 API_NEWSERVICE_REPLICAS=2
configs/ssl-proxy/conf.d/upstream.conf 加入新的 upstream 區塊適合完全隔離、無網路連線的生產環境(金融、政府、高安全性場域)。
目標:在 RHEL 9.7 Air-Gapped 環境部署 Rootless Podman
策略:最小 RPM 集合 + 本機 file:// repo(不建整包 BaseOS/AppStream)
核心套件(共 60-120 個 RPM,含依賴):
| 分類 | 關鍵套件 |
|---|---|
| 核心 | podman, podman-plugins |
| Rootless 必要 | shadow-utils, slirp4netns, fuse-overlayfs |
| Runtime | crun, conmon, netavark, aardvark-dns |
| Firewall | iptables-nft, nftables, conntrack-tools |
| SELinux | container-selinux |
# 1. 下載 RPM(含完整依賴樹)
mkdir -p /tmp/podman-offline-repo
sudo dnf download --resolve --alldeps --destdir /tmp/podman-offline-repo \
podman podman-plugins shadow-utils slirp4netns fuse-overlayfs \
containernetworking-plugins crun conmon netavark aardvark-dns \
container-selinux iptables-nft iptables-libs nftables \
libnftnl libnetfilter_conntrack conntrack-tools iproute procps-ng
# 2. 產生 Repo metadata
createrepo_c /tmp/podman-offline-repo
# 3. 打包
cd /tmp
tar czf podman-rootless-offline-repo-rocky97.tgz podman-offline-repo
sha256sum podman-rootless-offline-repo-rocky97.tgz > podman-rootless-offline-repo-rocky97.tgz.sha256
# 1. 驗證檔案完整性
sha256sum -c podman-rootless-offline-repo-rocky97.tgz.sha256
# 2. 解壓並掛載 Repo
sudo mkdir -p /opt/offline-repos
sudo tar xzf podman-rootless-offline-repo-rocky97.tgz -C /opt/offline-repos
sudo mv /opt/offline-repos/podman-offline-repo /opt/offline-repos/podman
# 3. 建立 Repo 設定
sudo tee /etc/yum.repos.d/podman-offline.repo > /dev/null <<'EOF'
[podman-offline]
name=Podman Rootless Offline Repo
baseurl=file:///opt/offline-repos/podman
enabled=1
gpgcheck=0
metadata_expire=never
EOF
# 4. 安裝
sudo dnf makecache --disablerepo="*" --enablerepo="podman-offline"
sudo dnf install -y --disablerepo="*" --enablerepo="podman-offline" \
podman podman-plugins shadow-utils slirp4netns fuse-overlayfs \
containernetworking-plugins crun conmon netavark aardvark-dns \
container-selinux iptables-nft nftables iproute procps-ng
# 1. 啟用 User Namespace
sudo sysctl -w user.max_user_namespaces=15000
echo "user.max_user_namespaces=15000" | sudo tee /etc/sysctl.d/99-userns.conf
# 2. 建立使用者並設定 subuid/subgid
sudo useradd -m appuser
echo "appuser:100000:65536" | sudo tee -a /etc/subuid
echo "appuser:100000:65536" | sudo tee -a /etc/subgid
# 3. 啟用 Linger(容器登出後持續運行)
sudo loginctl enable-linger appuser
# 4. 驗證
su - appuser
podman info | grep -E 'rootless|networkBackend|graphDriverName'
# 預期: rootless=true, networkBackend=netavark, graphDriverName=overlay
| 項目 | 檢查指令 | 預期結果 |
|---|---|---|
| User Namespace | sysctl user.max_user_namespaces |
≥ 15000 |
| UID/GID Mapping | which newuidmap newgidmap |
兩個路徑都存在 |
| Storage Driver | podman info --format '{{.Store.GraphDriverName}}' |
overlay |
| Network Backend | podman network ls |
至少有預設網路 |
| 症狀 | 原因 | 解決方式 |
|---|---|---|
cannot find newuidmap |
shadow-utils 缺失 |
補裝 RPM |
cannot setup slirp4netns |
slirp4netns 缺失 |
補裝 RPM |
graphDriverName: vfs |
fuse-overlayfs 缺失 |
補裝後 podman system reset |
iptables: command not found |
iptables-nft 缺失 |
補裝 RPM |
| 容器登出後消失 | 未啟用 linger | loginctl enable-linger |
離線環境無法 podman pull,需在連網機預先匯出所有 images 後搬移至離線機。
需要匯出的 images:
| Image | 用途 |
|---|---|
docker.io/openresty/openresty:alpine |
ssl-proxy(upstream,無需 build) |
docker.io/library/nginx:alpine |
frontend、bff 佔位 |
localhost/api-user:latest |
API-User 服務 |
localhost/api-order:latest |
API-Order 服務 |
localhost/api-product:latest |
API-Product 服務 |
# 連網機:pull 並匯出所有 images
mkdir -p /tmp/podman-images
podman pull docker.io/openresty/openresty:alpine
podman save docker.io/openresty/openresty:alpine -o /tmp/podman-images/openresty-alpine.tar
podman pull docker.io/library/nginx:alpine
podman save docker.io/library/nginx:alpine -o /tmp/podman-images/nginx-alpine.tar
for svc in api-user api-order api-product; do
podman save localhost/${svc}:latest -o /tmp/podman-images/${svc}-latest.tar
done
# 打包
tar czf podman-images.tgz /tmp/podman-images/
# 離線機:解壓並匯入
tar xzf podman-images.tgz
for tar_file in podman-images/*.tar; do
podman load -i "${tar_file}"
done
完整的匯出/匯入 SOP(含校驗碼驗證):參考離線部署指南 §6。
詳細操作手冊、troubleshooting、維運建議:
MIT License
最後更新:2026-03-10
以上AI產出的大禮包考量算很完整,如果第一天PO文就貼出README.md的內容,不熟Podman Quadlet的人會不知所云。到今天第27天,這大禮包有涵蓋前26天大部份的內容,但規模大到很多未必用到的雞肋,而且角色失焦,既是架構設計又是IT維運,還涉及網管、版控等議題。
對初次接觸Podman Qaudlet的人來說,最好不要一開始就看到這個,會抓不到頭緒。可以酌情專案規模及複雜度對大禮包的內容做修改及取捨,或是重建個適合專案規模的小禮包。就目前A2A專案規模,取捨的依據如下: