iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0

話說A2A這個案子使用Podman Quadlet的起源,是客戶IT窗口力排眾議要求使用這個大家都很陌生的技術,如同iPad初推之際,消費者無法定位它是iPhone增大版還是MacBook縮小版。IT直接用AI產出大禮包丟給廠商研究,但在對Podman Qaudlet毫無基礎情況反而墊高了學習門檻,就算透過AI也不知該怎麼問起,這也是我參賽的初衷。
大禮包的README.md如下,不必細看直接瀏覽而過:


Podman 微服務架構範例

基於 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 終止:憑證只需掛載一處
  • 分層認證機制:BFF 處理 Session,SSL Proxy 驗證 Partner JWT
  • 完全網路隔離:Backend APIs 在 internal-net
  • 內部 HTTP 通訊:簡化配置,提升效能
  • 獨立更新部署:每個服務獨立容器
  • 多副本負載均衡:Template Unit + OpenResty upstream
  • systemd 依賴管理:Quadlet 自動處理啟動順序
  • Web 監控管理:Cockpit + 自訂微服務監控插件
  • 混合 Debug 模式:開發環境可開啟 localhost 訪問

端口規劃

設計原則

  1. 容器內部端口統一化 - 所有 Backend APIs 內部使用 8080
  2. 主機端口分層編號 - 清晰識別服務類型
  3. 生產完全隔離 - Backend APIs 不暴露到主機
  4. 開發綁定 localhost - Debug 端口只能本機訪問

生產環境

服務 容器內部 主機端口 說明
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)

開發環境(Debug 模式)

服務 主機端口 綁定 用途
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,外部無法訪問
  • 編號規則:81 + 服務編號(01, 02, 03)
  • 生產環境自動禁用

完整端口設計說明: 詳見 docs/ARCHITECTURE.md 的「端口規劃」章節

快速開始

0. 安裝依賴(RHEL 9 / Rocky Linux 9 / AlmaLinux 9)

在執行部署腳本前,需要安裝必要的系統套件:

# 更新系統
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

1. 前置需求

依賴套件已安裝(參考上述步驟 0),確認版本符合:

  • ✅ RHEL 9 / Rocky Linux 9 / AlmaLinux 9
  • ✅ Podman 4.4+
  • ✅ systemd 250+
  • ✅ openssl, jq, curl 已安裝

2. 產生自簽憑證

./scripts/generate-certs.sh

3. 選擇環境模式

開發環境(推薦開始):

./scripts/setup.sh dev

生產環境:

./scripts/setup.sh prod

4. 啟動服務

./scripts/start-all.sh

5. 驗證部署

# 檢查狀態
./scripts/status.sh

# 執行連通性測試
./scripts/test-connectivity.sh

6. Cockpit Web 監控(可選)

若已安裝 Cockpit,以 appuser 登入 https://<hostname>:9090 即可使用 Web 管理介面。
詳見 Cockpit 監控管理指南

7. 測試訪問

# 前端(不需驗證)
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)

Endpoint 命名與路由

URL Namespace

三個命名空間對應三類消費者,認證責任各自獨立:

Prefix 消費者 認證責任
/ 瀏覽器(SPA)
/api/ Web / Mobile App BFF(Session)
/partner/api/{service}/ Partner B2B SSL Proxy(JWT)

SSL Proxy 路由表

請求路徑 代理目標 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/* 不受影響。

Frontend 內部路由(第二跳)

/ 經 SSL Proxy 轉發到 frontend:80 後,frontend nginx 再處理:

Location 行為
= /health 健康檢查,200 OK
~* \.(js|css|png|…|woff2)$ 靜態資源,Cache-Control: public, immutableexpires 1y
/(fallback) try_files/index.html,SPA 路由支援
詳細設計見 docs/ARCHITECTURE.md § Endpoint 命名與路由設計

Token 驗證機制

本專案採用雙軌驗證機制,針對不同類型的客戶端使用不同的認證方式:

1. Web/Mobile App → Session 驗證(BFF 層)

路徑: /api/*

流程:

Client → SSL Proxy (不驗證) → BFF (Session 驗證) → Backend APIs

說明:

  • SSL Proxy 只做路由轉發,不驗證
  • 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

2. Partner API → JWT Token 驗證(SSL Proxy 層)

路徑: /partner/api/*

流程:

Partner → SSL Proxy (JWT 驗證) → Backend APIs(直接)

說明:

  • SSL Proxy 使用 OpenResty + Lua 進行 JWT 驗證
  • 適合固定合作夥伴的 B2B API
  • 邏輯簡單、效能優先

範例:

# 產生 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

Debug 操作

開發模式:

# 直接測試 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 模式
  • [ ] 創建 Partner Podman Secrets(每個 Partner 獨立 Secret)
  • [ ] 使用正式 SSL 憑證(非自簽)
  • [ ] 設定 firewall 規則
  • [ ] 配置日誌輪轉
  • [ ] 設定監控告警

Partner JWT Secrets 管理

開發環境:使用固定測試 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

測試項目:

  • ✅ SSL Proxy 可訪問
  • ✅ Web/App API 路由(BFF Session 認證)
  • ✅ Partner JWT Token 驗證(SSL Proxy 層)
  • ✅ Backend APIs 完全隔離(生產模式)
  • ✅ 容器間通訊正常

故障排除

服務無法啟動

# 檢查服務狀態
systemctl --user status ssl-proxy

# 查看日誌
./scripts/logs.sh ssl-proxy

# 檢查鏡像
podman images | grep ssl-proxy

JWT 驗證失敗

# 檢查 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

擴展指南

新增 Backend API

  1. 複製 Dockerfile:dockerfiles/api-userdockerfiles/api-newservice
  2. 複製 Quadlet 配置:quadlet/api-user@.containerquadlet/api-newservice@.container
  3. 修改配置中的服務名稱(ContainerName、Label 等)
  4. configs/ha.conf 加入 API_NEWSERVICE_REPLICAS=2
  5. configs/ssl-proxy/conf.d/upstream.conf 加入新的 upstream 區塊
  6. 更新 BFF 配置以包含新服務
  7. 啟動新服務

離線環境部署(Air-Gapped)

適用場景

適合完全隔離、無網路連線的生產環境(金融、政府、高安全性場域)。

快速摘要

目標:在 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:連網機(Rocky Linux 9.7)

# 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

階段 2:離線機(RHEL 9.7)

# 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

階段 3:Rootless 系統設定

# 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、維運建議:


文件

版本資訊

  • Podman: 4.4+
  • RHEL: 9.0+
  • systemd: 250+
  • OpenResty: 1.21+

授權

MIT License


最後更新:2026-03-10


以上AI產出的大禮包考量算很完整,如果第一天PO文就貼出README.md的內容,不熟Podman Quadlet的人會不知所云。到今天第27天,這大禮包有涵蓋前26天大部份的內容,但規模大到很多未必用到的雞肋,而且角色失焦,既是架構設計又是IT維運,還涉及網管、版控等議題。

對初次接觸Podman Qaudlet的人來說,最好不要一開始就看到這個,會抓不到頭緒。可以酌情專案規模及複雜度對大禮包的內容做修改及取捨,或是重建個適合專案規模的小禮包。就目前A2A專案規模,取捨的依據如下:

  • 版控控制粒度:
    • 可由Git版控:Quadlet檔(如.container等)、Dockerfile
    • image版控由podman管理,比如保留近三版的image
  • script維運:專案初期我反對將容器包版、過版、trouble shooting等寫成方便的script給客戶IT或廠商用,主要鐵打的PM流水的PG,接手的人用script維運可能知其然不知其所以然,過程需求若有異動或調整,沒有相關背景知識,不會知道script是否需要跟著調整。

上一篇
.artifact的飯粒
下一篇
小禮包
系列文
Podman Quadlet-容器服務化(在單體系統到微服務之間)28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言