某天早上看到這篇文章:Debugging image mode with Red Hat OpenShift 4.20: A practical guide ,一開始我以為是 OpenShift 除了 debug pod 之後有新的除錯方法,後來發現這個 Image Mode 蠻有意思的,可以取代 Machine Config 的方式針對 OpenShift 的 worker node 預先除錯,縮短 troubleshooting 時間。
實驗目標: 在不驚動任何實體 Worker 節點的狀況下,利用 MachineOSConfig(Image Mode On-Cluster Layering)安全地 Dry-Run 編譯出一個內建 tmux 與 htop 的客製化 RHEL CoreOS 9 作業系統映像檔。
注意事項
Caution
本文為了驗證單一 Technology Preview 功能而啟用 TechPreviewNoUpgrade,其影響不是只有功能層級,是整個 cluster 的生命週期:此設定不可逆,且後續無法進行 minor version upgrade。 X: 4.20 -> 4.21 O: 4.20.23 -> 4.20.24
Image Mode 是什麼? 我們可以理解成客製化的 RHCOS 作業系統如下:
把 OpenShift 節點底層的 RHCOS 作業系統,當作 container image 一樣進行客製化、版本化與部署。
以往要做線上的 troubleshooting 都是開 debug pod 臨時針對節點操作。Image Mode 讓某些節點可以永久具備額外套件或功能,例如:
安裝特定 RPM 套件,例如 rsyslog、chrony、iputils
安裝 Security Agent、Inventory Agent、Forensic tool
套用 Red Hat 提供的 hotfix
加入第三方 driver 或特殊工具
讓指定 MCP 的所有節點都以相同 OS image 開機
OpenShift Image Mode 是基於該版本的 RHCOS 建立一個 custom layered image,再由 Machine Config Operator(MCO) 將它部署到指定的 MachineConfigPool(MCP)節點。總而言之是為了解決手動修改造成的追蹤不易等潛在問題。
第一階段:初始化 為了 100% 確保叢集生產環境的安全,本次實驗只驗證 build 階段是否成功。所以建立一個帶有 0 台實體機器 的自訂池子,主要是因為 MachineOSConfig 一定要指定 machineConfigPool。
建立幽靈 node pool 因為我今天只是要測試 dry-run 的過程,並不是實際要替換掉現有的 worker node,所以 建立自訂的幽靈機器池 ghost-pool(機器數量為 0),並準備送入第一版 test-image-mode.yaml。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfigPool metadata: name: ghost-pool spec: machineConfigSelector: matchLabels: machineconfiguration.openshift.io/role: worker nodeSelector: matchLabels: node-role.kubernetes.io/ghost: ""
這邊是我準備的 test-image-mode.yaml,這個在 UI Console 上面沒有陳列,必須要輸入 https://<OpenShift-FQDN>/k8s/cluster/machineconfiguration.openshift.io~v1~MachineOSConfig 才看得到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 apiVersion: machineconfiguration.openshift.io/v1 kind: MachineOSConfig metadata: name: ghost-pool spec: machineConfigPool: name: ghost-pool imageBuilder: imageBuilderType: Job renderedImagePushSecret: name: pull-secret renderedImagePushSpec: image-registry.openshift-image-registry.svc:5000/openshift/custom-rhcos:poc containerFile: - containerfileArch: NoArch content: |- FROM configs RUN dnf install -y tmux htop RUN echo "Welcome to OpenShift Image Mode!" > /etc/motd
第二階段:開始除錯 初次送出配置後,後台 Build Pod 彈出 Init:Error 狀態。
排除排他性防呆鎖 MCO 具備「一池一任務」的 Registry 車道防撞保護。換新配方前必須手動清除舊的 Build 紀錄:
1 oc delete machineosbuild --all
DNS 撞牆:no such host 任務在 VFS 磁碟複製中跑了十幾分鐘,卻敗在最後的 buildah push 階段。
日誌顯示沙盒隔離網路找不到 image-registry.openshift-image-registry.svc,加上 .cluster.local 全網域後依然被 CoreDNS 拒絕。
1 oc get image.config.openshift.io/cluster -o jsonpath='{.status.internalRegistryHostname}'
指令回傳完全的空白 ,看起來內建倉庫被拔除了
1 oc get configs.imageregistry.operator.openshift.io/cluster -o jsonpath='{.spec.managementState}'
結果: Removed
預設沒有內建 registry storage,自然沒有內部的 Image registry,得改外部的 registry。
第三階段:轉存 Docker Hub,成功 既然內網無路可走,轉向外網借道 Docker Hub。
建立外部專用通關金鑰 1 2 3 4 5 oc create secret docker-registry dockerhub-secret \ --docker-server=https://index.docker.io/v1/ \ --docker-username=【你的DockerHub帳號】 \ --docker-password=【你的Token或密碼】 \ -n openshift-machine-config-operator
最終 YAML 排除不在紅帽官方源的 htop(EPEL 源),改用官方 AppStream 的 git 與 jq:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: machineconfiguration.openshift.io/v1 kind: MachineOSConfig metadata: name: ghost-pool spec: machineConfigPool: name: ghost-pool imageBuilder: imageBuilderType: Job renderedImagePushSecret: name: dockerhub-secret renderedImagePushSpec: docker.io/【DockerHub帳號】/custom-rhcos:poc containerFile: - containerfileArch: NoArch content: |- FROM configs RUN dnf install -y git jq RUN echo "Welcome to OpenShift Image Mode!" > /etc/motd
成功 build OS image 超渡舊紀錄後強行覆蓋套用,工廠順利完成 DNF 安裝並將客製化 OS 映像檔推播至 Docker Hub:
1 2 NAME PREPARED BUILDING SUCCEEDED INTERRUPTED FAILED ghost-pool-0d82e40212fe20bf27b9ae9858bcc55a False False True False False
成功版本的 oc log 結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 oc logs -n openshift-machine-config-operator pod/build-ghost-pool-0d82e40212fe20bf27b9ae9858bcc55a-6vjfg -c image-build --tail=100 Copying blob sha256:dc2cddc2c1f6a6c958458497675150ab90733cb228cd302d9484a9432eb2a823 time="2026-05-28T09:01:33Z" level=debug msg="Checking if we can reuse blob sha256:dc2cddc2c1f6a6c958458497675150ab90733cb228cd302d9484a9432eb2a823: general substitution = true, compression for MIME type \"application/vnd.oci.image.layer.v1.tar\" = true" time="2026-05-28T09:01:33Z" level=debug msg="Skipping blob sha256:dc2cddc2c1f6a6c958458497675150ab90733cb228cd302d9484a9432eb2a823 (already present):" Copying blob sha256:fff55d5b118e69c52d431b5d5ad8e6cd04552c9c6f31ed5f2cab0bf4abd077af time="2026-05-28T09:01:33Z" level=debug msg="Checking if we can reuse blob sha256:fff55d5b118e69c52d431b5d5ad8e6cd04552c9c6f31ed5f2cab0bf4abd077af: general substitution = true, compression for MIME type \"application/vnd.oci.image.layer.v1.tar\" = true" time="2026-05-28T09:01:33Z" level=debug msg="Skipping blob sha256:fff55d5b118e69c52d431b5d5ad8e6cd04552c9c6f31ed5f2cab0bf4abd077af (already present):" Copying blob sha256:848b9270a4cb614899eaa2e381bc5b7d21279c79477c2112b70adcbb7172bc14 time="2026-05-28T09:01:33Z" level=debug msg="Checking if we can reuse blob sha256:848b9270a4cb614899eaa2e381bc5b7d21279c79477c2112b70adcbb7172bc14: general substitution = true, compression for MIME type \"application/vnd.oci.image.layer.v1.tar\" = true" time="2026-05-28T09:01:33Z" level=debug msg="Skipping blob sha256:848b9270a4cb614899eaa2e381bc5b7d21279c79477c2112b70adcbb7172bc14 (already present):" Copying blob sha256:12787d84fa137cd5649a9005efe98ec9d05ea46245fdc50aecb7dd007f2035b1 time="2026-05-28T09:01:33Z" level=debug msg="Checking if we can reuse blob sha256:12787d84fa137cd5649a9005efe98ec9d05ea46245fdc50aecb7dd007f2035b1: general substitution = true, compression for MIME type \"application/vnd.oci.image.layer.v1.tar\" = true" time="2026-05-28T09:01:33Z" level=debug msg="Skipping blob sha256:12787d84fa137cd5649a9005efe98ec9d05ea46245fdc50aecb7dd007f2035b1 (already present):" Copying blob sha256:215bb759d988f713b8650f2ec84636d3689223af3570fb2e1e70027f8a430a25 time="2026-05-28T09:01:33Z" level=debug msg="Checking if we can reuse blob sha256:215bb759d988f713b8650f2ec84636d3689223af3570fb2e1e70027f8a430a25: general substitution = true, compression for MIME type \"application/vnd.oci.image.layer.v1.tar\" = true" time="2026-05-28T09:01:33Z" level=debug msg="Skipping blob sha256:215bb759d988f713b8650f2ec84636d3689223af3570fb2e1e70027f8a430a25 (already present):" Copying blob sha256:a543989d318c57b45f9d2c052fa4851f7bae1d331c1786d286f7295fc7c8e2c3 time="2026-05-28T09:01:33Z" level=debug msg="Checking if we can reuse blob sha256:a543989d318c57b45f9d2c052fa4851f7bae1d331c1786d286f7295fc7c8e2c3: general substitution = true, compression for MIME type \"application/vnd.oci.image.layer.v1.tar\" = true" time="2026-05-28T09:01:33Z" level=debug msg="Skipping blob sha256:a543989d318c57b45f9d2c052fa4851f7bae1d331c1786d286f7295fc7c8e2c3 (already present):" Copying blob sha256:9c6b1561152d40674986482ba90352bb321fba1fd4612c8cb26e8a65c5892068 time="2026-05-28T09:01:33Z" level=debug msg="Checking if we can reuse blob sha256:9c6b1561152d40674986482ba90352bb321fba1fd4612c8cb26e8a65c5892068: general substitution = true, compression for MIME type \"application/vnd.oci.image.layer.v1.tar\" = true" time="2026-05-28T09:01:33Z" level=debug msg="Skipping blob sha256:9c6b1561152d40674986482ba90352bb321fba1fd4612c8cb26e8a65c5892068 (already present):" Copying blob sha256:29341f18c3d200d1ba3a8bdedfd3790c2ee3f4ffeb0d4c1bcf88813efaa6a96a time="2026-05-28T09:01:33Z" level=debug msg="Checking if we can reuse blob sha256:29341f18c3d200d1ba3a8bdedfd3790c2ee3f4ffeb0d4c1bcf88813efaa6a96a: general substitution = true, compression for MIME type \"application/vnd.oci.image.layer.v1.tar\" = true" time="2026-05-28T09:01:33Z" level=debug msg="reading layer \"sha256:29341f18c3d200d1ba3a8bdedfd3790c2ee3f4ffeb0d4c1bcf88813efaa6a96a\"" time="2026-05-28T09:01:33Z" level=debug msg="No compression detected" time="2026-05-28T09:01:33Z" level=debug msg="Using original blob without modification" time="2026-05-28T09:01:38Z" level=debug msg="Start untar layer" time="2026-05-28T09:01:39Z" level=debug msg="Untar time: 1.269012695s" time="2026-05-28T09:01:39Z" level=debug msg="finished reading layer \"sha256:29341f18c3d200d1ba3a8bdedfd3790c2ee3f4ffeb0d4c1bcf88813efaa6a96a\"" Copying config sha256:3f370770df4d9446e741f3e5695fce92dbd4c57702b09f0a1dd18f42ab54c7e0 time="2026-05-28T09:01:39Z" level=debug msg="No compression detected" time="2026-05-28T09:01:39Z" level=debug msg="Compression change for blob sha256:3f370770df4d9446e741f3e5695fce92dbd4c57702b09f0a1dd18f42ab54c7e0 (\"application/vnd.oci.image.config.v1+json\") not supported" time="2026-05-28T09:01:39Z" level=debug msg="Using original blob without modification" Writing manifest to image destination time="2026-05-28T09:01:39Z" level=debug msg="setting image creation date to 2026-05-28 09:01:30.422776125 +0000 UTC" time="2026-05-28T09:01:39Z" level=debug msg="created new image ID \"3f370770df4d9446e741f3e5695fce92dbd4c57702b09f0a1dd18f42ab54c7e0\" with metadata \"{}\"" time="2026-05-28T09:01:39Z" level=debug msg="added name \"docker.io/hazel910159/custom-rhcos:ghost-pool-0d82e40212fe20bf27b9ae9858bcc55a\" to image \"3f370770df4d9446e741f3e5695fce92dbd4c57702b09f0a1dd18f42ab54c7e0\"" --> 3f370770df4d time="2026-05-28T09:01:39Z" level=debug msg="parsed reference into \"[vfs@/home/build/.local/share/containers/storage+/var/tmp/storage-run-1000/containers]docker.io/hazel910159/custom-rhcos:ghost-pool-0d82e40212fe20bf27b9ae9858bcc55a\"" Successfully tagged docker.io/hazel910159/custom-rhcos:ghost-pool-0d82e40212fe20bf27b9ae9858bcc55a 3f370770df4d9446e741f3e5695fce92dbd4c57702b09f0a1dd18f42ab54c7e0 time="2026-05-28T09:01:43Z" level=debug msg="printing final image id \"3f370770df4d9446e741f3e5695fce92dbd4c57702b09f0a1dd18f42ab54c7e0\"" time="2026-05-28T09:01:43Z" level=debug msg="shutting down the store" + buildah push --storage-driver vfs --authfile=/tmp/final-image-push-creds/config.json --digestfile=/tmp/done/digestfile --cert-dir /var/run/secrets/kubernetes.io/serviceaccount docker.io/hazel910159/custom-rhcos:ghost-pool-0d82e40212fe20bf27b9ae9858bcc55a Getting image source signatures Copying blob sha256:3a1265f127cd4df9ca7e05bf29ad06af47b49cff0215defce94c32eceee772bc Copying blob sha256:c0a1ed51dc9cede536c633b9c1a37ed2e985a6b80c213d252b6b7820aae6a144 ... ... ... Copying blob sha256:29341f18c3d200d1ba3a8bdedfd3790c2ee3f4ffeb0d4c1bcf88813efaa6a96a
結果
項目
說明
交付產物
docker.io/【你的帳號】/custom-rhcos:poc,完整可開機 RHEL CoreOS 9 映像檔
安全驗證
全程在ghost-pool(0 台機器)中完成,零節點受到影響
解鎖技能
OpenShift 4.20 OS-as-Code(作業系統即程式碼)運維工法
Image Mode 主要限制 這邊整理了一下 官方文件陳列的一些限制 ,使用上需要多加注意。
限制
說明
On-cluster 不支援 multi-architecture compute machines
混合架構的 compute nodes 不能使用 on-cluster image mode。
每個 MCP 只能對應一個 MachineOSConfig
同一個 MachineConfigPool 不支援多個MachineOSConfig。
擴充使用 layered image 的 MachineSet 時,新 Node 會 reboot 兩次
第一次以 base image 建立,第二次套用 custom layered image。
使用 custom layered image 的 Node 不支援 Node Disruption Policy
無法透過該政策避免相關 node disruption。
套用 image 時會造成 Node reboot
MCO 會 drain、cordon 並重新啟動相關 nodes。
Registry 需要額外空間與清理管理
Build 出來的 custom layered images 會持續佔用 registry 空間。
使用 custom layered image 後,需要自行維護更新
Node pool 不會再由 OpenShift 自動更新 OS image;升級後需重新基於新 RHCOS image build。
Custom layered image 不宜落後 cluster 版本太久
若 node image 與 cluster version 差距過大,可能出現非預期行為。
必須使用 image digest,不可只用 tag
建立 custom layered image 時,base image 必須引用 OpenShift image digest。
部署前應先在非 production 環境測試
尤其是 hotfix、agent 或額外 RPM,套用後的相容性與問題由使用者承擔。
實際部署情境 整個部署階段可以分成三階段:
image build 是否成功(ghost node = 0)
image 部署以及啟動是否成功(ghost node = 1)
替換線上 node pool 節點
Reference