iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0

映像建好了、掃過了、簽了章。接下來要讓它真的跑起來——而這一步最容易出現一種
特別難查的失敗:pipeline 全綠,部署的還是舊版本。

更新前:image: ...web@sha256:0527f0c9...
更新後:image: ...web@sha256:0527f0c9...      ← 一模一樣
=== Image tag 已是最新,跳過 commit ===        ← 這句話是假的

沒有任何一個地方是紅的。

它跳過 commit 的真正理由是 git diff 是空的——manifest 裡還是舊 digest。
每一步機制都誠實,說謊的是那行訊息。

讀完你能做到:把新 digest 寫進 GitOps 倉庫、等部署真的完成,
並且讓綠燈的意思從「我 push 了一個 commit」變成「那個 commit 已經在叢集上跑」。

前提

① 一條 CI 管道,已經產出映像並吐得出 IMAGE_DIGEST
② 一個 Git 服務(本文用 Gitea),可以再開一個倉庫和一個使用者
③ 叢集裡可以裝 ArgoCD(本文用 OpenShift GitOps operator)
④ 節點的 memory requests 還有 1.8 GiB 可以給 ArgoCD —— 見 §11,這不是玩笑

本文的管道長這樣,要在最後面接兩個節點:

git-clone ──┬─► 三道掃描 ─► npm-install ─┬─► nx-build ──┐
            └─► generate-sbom            ├─► eslint ────┼─► build-push ─┐
                                         └─► unit-test ─┘               │
        ┌───────────────────────────────────────────────────────────────┘
        └─► trivy-scan ─► cosign-sign ─► update-gitops ─► wait-for-sync

圖裡省略了 workspace-probeupload-sbomtypecheck,以及 finally
cleanup-workspace(§8.1 會用到)。完整的 pipeline 是 18 個 task。

環境:OpenShift 4.21.14、OpenShift GitOps v1.21.3、Kubernetes v1.34.6、Gitea 1.27.0、yq v4。


1. 三分鐘版

① 先建「另一端」:GitOps 倉庫 + ArgoCD + 部署 namespace   → 大部分工作在這
② update-gitops 用 yq 改 YAML,寫之前先確認路徑讀得到值    → yq 也會靜默失敗
③ wait-for-sync 等三個條件,`Healthy` 要放最後             → 少一個會拿到假通過
④ 接進 DAG,確認建的/簽的/跑的是同一個 digest

四個會咬人的地方:

sedyq 都會靜默失敗 路徑/樣式錯就 exit 0,但 image 沒換(§6、§6.4)
「等部署完」是三個條件 少一個會拿到假通過(§8);順序的理由見 §8.2
裝 ArgoCD 可能讓節點掛掉 requests 是宣告,不是用量(§11)
一個標籤換到整組管理權 那是 GitOps 模型的取捨(§10)

Part 1 — 快樂路徑

2. 第 1 步:建另一端

Task 本身是這裡面最簡單的部分。真正的工作在這裡:

Git 服務   倉庫 demo-gitops           apps/web/{deployment,service,route}.yaml
           使用者 ci-gitops-bot       只有 demo-gitops 的寫入權

叢集       ArgoCD                     openshift-gitops namespace
           Application web            auto-sync + prune + selfHeal
           Namespace demo             argocd.argoproj.io/managed-by 標籤
                                      + registry 的 imagePullSecrets

2.1 憑證要分離

讀應用程式的倉庫和寫 GitOps 倉庫是兩種風險等級,不共用憑證。

而且不能只是「多開一把 token」——Gitea 的 access token 掛在使用者身上,
授權單位是權限類別,沒有指定 repo 的欄位。問這台的 swagger:

CreateAccessTokenOption   properties: name, scopes    ← 沒有 repo 欄位
scopes 的 example         read:repository / write:misc / read:user …
建 token 的端點            /users/{username}/tokens

真正的分離需要獨立使用者加 collaborator:

demo-gitops       collaborator permission  →  200
frontend-nx-mono  collaborator 查詢        →  404   ← 完全沒有存取權

2.2 部署目標不要用 CI 那個 namespace

理由見 §10:讓 ArgoCD 管一個 namespace,等於給它那裡的整組管理權。
CI 的 namespace 已經有 pipeline SA 的大權限,不要再疊一層。

3. 第 2 步:update-gitops

三個 step:

steps:
  - name: clone            # alpine/git
  - name: update-image     # mikefarah/yq
  - name: commit-and-push  # alpine/git

拆三個是被映像逼的——alpine/git 裡沒有 yq。拆完剛好也比較乾淨,但那是順帶的結果(而且拆開會撞到 §7 那個坑)。

核心那一步:

FILE=$(workspaces.gitops-clone.path)/gitops/$(params.GITOPS_PATH)
EXPR='.spec.template.spec.containers[] | select(.name == "web") | .image'
WANT='$(params.IMAGE_NAME)$(params.IMAGE_REF)'

# ① 寫之前先讀 —— 這一行才是關鍵,理由見 §6.4
BEFORE=$(yq e "$EXPR" "$FILE")
[ -n "$BEFORE" ] && [ "$BEFORE" != "null" ] || {
  echo "FATAL: 路徑讀不到值,寫入位置可能是錯的"; exit 1; }
echo "更新前:$BEFORE"

yq e -i "($EXPR) = \"$WANT\"" "$FILE"

# ② 寫之後讀回來比對
GOT=$(yq e "$EXPR" "$FILE")
echo "更新後:$GOT"
[ "$GOT" = "$WANT" ] || { echo "FATAL: 寫入後的值不符"; exit 1; }

⚠️ ① 不能省,而且它一定要在寫入之前。 路徑寫錯的話 yq 會自己把那條路徑建出來、
回傳 0——寫完之後再讀,讀到的是它剛建出來的東西,① 就跟 ② 一樣沒用了。
只有在寫入之前,錯的路徑才讀得到空值。② 本身擋不住路徑錯誤,理由見 §6.4。

判斷式要兩半,因為 yq 讀不到東西有兩種輸出(實測 v4.53.6,兩種都 rc=0):

路徑不存在(.containers[0].imagee)        輸出 null   ← 靠 != "null"
select 選不到(.name == "webb")           輸出空字串  ← 靠 -n

用名字選而不是 containers[0],是為了讓「指錯容器」也退回 ① 擋得住的那一族——
理由和實測見 §6.5。本文的 Deployment 只有一個容器(name: web),
[0] 現在也會對;差別在有人加了 sidecar 之後。

路徑存進 EXPR、三處共用同一個變數,是為了讓 ① ② 和寫入不可能各自打錯字。

3.1 IMAGE_REF 前面那個 @

- name: IMAGE_NAME
  value: <registry-route>/docker-hosted/web
- name: IMAGE_REF
  value: "@$(tasks.build-push.results.IMAGE_DIGEST)"

拼出來是 name@sha256:...,不是 name:tagtag 是可變的,digest 不是。

而且三個環節用的位址前綴可能不同:

build-push     推 route 位址   ...apps-crc.testing/docker-hosted/web:<sha>
cosign-sign    簽 svc 位址     ...svc.cluster.local:8081/docker-hosted/web@sha256:...
update-gitops  寫 route 位址   ...apps-crc.testing/docker-hosted/web@sha256:...

manifest 一定要寫 kubelet 解析得到的那個位址(叢集內的 service 位址通常不行)。
三者的 digest 相同,指的是同一份位元組。

4. 第 3 步:wait-for-sync

# 1. ArgoCD 有沒有看到「這一次」的 commit
oc wait --for=jsonpath='{.status.sync.revision}'=$(params.REVISION) "$APP" -n "$NS" --timeout=8m

# 2. 有沒有把它套用上去
oc wait --for=jsonpath='{.status.sync.status}'=Synced "$APP" -n "$NS" --timeout=8m

# 3. 套用的結果健不健康
oc wait --for=jsonpath='{.status.health.status}'=Healthy "$APP" -n "$NS" --timeout=8m

⚠️ 少任何一段都會拿到某種假的通過——這是實測的,見 §8。
順序也有講究,但那一段是推理不是實測,見 §8.2。

REVISION 從上一個 task 的 result 拿:

# update-gitops
results:
  - name: GITOPS_COMMIT

# wait-for-sync
params:
  - name: REVISION
    value: $(tasks.update-gitops.results.GITOPS_COMMIT)

沒有這個 result 的話,你只能等「某個 commit」被同步,不能等「這一次的 commit」。

4.1 timeout 給餘裕,它是保險不是預期值

本文設 8 分鐘,實際跑 89 秒。理由見 §8.3——ArgoCD 的發現延遲比設定值難預測。

三段各 8 分鐘,最壞情況是 24 分鐘。實務上不會發生:真的卡住的話多半是第一段就卡死,
但如果你的 Pipeline 有整體 timeout,要記得它得涵蓋得住這個上界。

5. 驗收

=== 更新前:...web@sha256:00aa5863...
=== 更新後:...web@sha256:1d200ae7...
驗證通過

=== 這次要 commit 的 diff ===
-          image: ...web@sha256:00aa5863...
+          image: ...web@sha256:1d200ae7...
=== 已推送:b59803e ===

=== 等 revision 同步到 b59803e7... ===   condition met
=== 等 sync.status 變 Synced ===         condition met
=== 等 health 變 Healthy ===             condition met

Service/web     Synced  Healthy
Deployment/web  Synced  Healthy
Route/web       Synced  Healthy

Synced + Healthy 已經足以推出 rollout 跑完了(這裡靠一個前提:這段期間
沒有別人 push,見 §8.2)。想直接看的話補一行——
問 kubelet 實際解析到什麼,而不是問 spec 要求什麼:

oc get pods -n demo -l app=web   -o jsonpath='{.items[*].status.containerStatuses[*].imageID}'

它要跟 build-push 產出的 digest 逐字相同。用 imageID 不用 spec…image
因為前者是跑起來之後的結果,後者只是要求。

建的、簽的、部署的、實際在跑的,是同一個 digest。全程沒有人按任何按鈕。

本文各節的 digest 值不一樣(00aa / 1d20 / …),因為它們來自不同次執行。
要對照的是「同一段輸出裡前後一不一致」,不是跨節比對。

代價:

task 耗時
wait-for-sync 89s ← 整條最慢
npm-install 59s
整條 5m26s

wait-for-sync 成為最慢的 task,是選擇「綠燈要有意義」的直接代價。

沒有它   綠燈 = 我 push 了一個 commit
有了它   綠燈 = 那個 commit 已經跑在叢集上

第二行成立的前提是「這段期間沒有別人 push」,見 §8.2。


Part 2 — 細節探討

6. sed 改 YAML 的失敗是綠色的

先看「不要這樣做」的版本:單一容器,clone + sed + commit 一氣呵成。

sed -i "s|image: $(params.IMAGE_NAME):.*|image: $(params.IMAGE_NAME):$(params.IMAGE_TAG)|" "$FILE"

跑出來:

更新前:image: ...web@sha256:0527f0c96f41...
更新後:image: ...web@sha256:0527f0c96f41...
=== Image tag 已是最新,跳過 commit ===

6.1 為什麼一個字都沒改到

sed 的 pattern    image: <name>:.*
manifest 實際是   image: <name>@sha256:...
                              ^ 是 @ 不是冒號

sed 沒有壞掉,它做了你叫它做的事。改用 digest 釘死之後那個 pattern 就再也對不上了
——而沒有任何機制會告訴你。

sed 回傳 0
  → git diff --cached --quiet 成立
    → Task 印出「已是最新」
      → exit 0

機制每一步都誠實,訊息在說謊。
sed 回傳 0 為真,git diff --cached 是空的也為真;但由它們推出的
「Image tag 已是最新」為假——manifest 用 @sha256: 釘死,那裡沒有 tag。

6.2 下游發生了什麼

build-push   建了新映像   sha256:0ac07986...
cosign-sign  簽了它       Pushing signature...
                    ↓
GitOps 倉庫  HEAD 沒動
ArgoCD       Synced / Healthy,一動也不動,而且它是對的
pod          還在跑舊的 sha256:0527f0c9...

ArgoCD 顯示 Synced 完全正確——它忠實反映了 Git 的內容,Git 就是沒變。

整條 17 個 TaskRun 全綠。

6.3 這類失敗的共同形狀

退出碼 0,但是……
  沒設 coverage 門檻      → 測試「通過」但什麼都沒驗
  solution-style tsconfig → 型別檢查跑了 0 個檔案
  HTTP 200                → 但那是預渲染的靜態檔
  sed exit 0              → image 沒換

⚠️ 最後這個特別難察覺:連紅燈都沒有。前幾種看 pipeline 就知道;
這一種要等有人問「怎麼還是舊版本」。

6.4 換成 yq 不會讓這個形狀消失

實測 yq v4.53.6。為了看清楚,例子用最單純的索引寫法,
不是 §3 那個 select 版——這一節的結論跟用哪種路徑無關:

$ yq e -i '.spec.template.spec.containers[0].imagee = "NEW"' d.yaml   # 鍵名打錯
  exit=0   image 現在=OLD

$ yq e -i '.spec.nonexistent.deep.image = "NEW"' d.yaml               # 路徑不存在
  exit=0   image 現在=OLD

路徑寫錯的時候,yq 會把那條路徑整個建出來,然後回傳 0。 原本的 image 一個字都沒動。

而且 §3 那個「讀回來比對」擋不住它——因為寫和讀用的是同一個路徑字串:

$ GOT=$(yq e '.spec.template.spec.containers[0].imagee' d.yaml)
  GOT=NEW  WANT=NEW  →  驗證通過(但 image 根本沒改)

所以擋得住的是 §3 那個 ①:寫之前先確認路徑讀得到值。
讀不到就是路徑錯了,而那正是 yq 會靜默建立新節點的情況。

6.5 這就是 §3 不用 containers[0] 的原因

① 擋的是「路徑不存在」,擋不住「路徑存在但指錯地方」。
把 §3 的 EXPR 換成 containers[0].image,拿一份有 sidecar 的 manifest 去跑:

containers:
  - name: istio-proxy      ← containers[0]
    image: proxy:OLD
  - name: web
    image: web:OLD
①② 都通過,rc=0
→ istio-proxy 被換成了新 digest,web 那格原封不動

用名字選就不會——為什麼見 §6.6。
而且名字打錯的時候,失敗形狀會退回 ① 擋得住的那一族:

.containers[0].imagee    → ① 讀到 null     → exit 1(沒有 ① 的話,yq 會建出一個新節點)
select(.name=="webb")    → ① 讀到空字串   → exit 1(沒有 ① 的話,檔案一個字都不會動)

寫入形式兩種都可以,實測 v4.53.6 結果相同:
(… | select(…) | .image) = v(… | select(…)).image = v

6.6 那 yq 到底好在哪

理由是能指定身分,而不是位置或長相。

sed   我要改「長得像這樣的那一行」
yq    我要改「名字叫 web 的那個容器的 image」

§6.5 那個 sidecar 就是差別所在。containers[0]/image:/ 都是在描述位置或長相,
兩個都會指到 istio-proxyselect(.name == "web") 描述的是身分,
前面插幾個 sidecar 都指得對。這個條件 sed 表達不出來——它一次只看一行,
不知道那一行屬於哪個容器。

另外兩個比較小但實在的:不會誤中另一行剛好含相同文字的內容、
不受縮排和引號風格影響。

至於「yq 失敗會留下痕跡」,那件事在本文的設定裡不會發生——① 在寫入之前就 exit 1
檔案根本不會被動到。它只在沒有 ①、而且用位置索引的時候才是個優點:

路徑/樣式錯,而且沒有 ①
sed 檔案完全沒動,git diff 是空的
yq 多出一個新節點,git diff 看得到

換掉 sed 是有理由的,但理由不是「換了就不會靜默失敗」。
兩個都會。差別在你能不能把「要改哪一個」講清楚。

7. 為什麼拆三個 step 會撞到 UID

換成 yq 的第一版直接紅燈:

step-clone            exit=0
step-update-image     exit=1
  Error: failed copying from /tmp/temp3354747747 to
    .../deployment.yaml: permission denied
step-commit-and-push  Skipping step because a previous step failed

7.1 「同一個 pod = 同一個 UID」這個假設

那個假設只在會重新指派 UID 的 SCC(例如 restricted-v2)下成立——
那種 SCC 會給整個 pod 一個 UID,所有 step 都用它。

Tekton 用的 SCC 常常是 RunAsAny

scc = pipelines-scc                    ← RunAsAny
pod securityContext: 只有 fsGroup,沒有 runAsUser
每個 step container: runAsUser 都是空的

所以每個 step 各自跑自己映像裡的 USER

alpine/git      USER 未設定 → root/0    clone 出來的檔案 owner = root
mikefarah/yq    USER = yq               寫不進去

7.2 修法

git clone -q "$URL" "$DIR"
chmod -R a+rwX "$DIR"     # ← 這一行

7.3 這個失敗值得注意,但它證明的不是 yq 比較好

permission denied  →  紅燈,訊息指名道姓:哪個檔案、什麼原因

紅燈是好事,但它的成因是拆成兩個映像跑,跟 yq 懂不懂 YAML 無關。
同一個拆法配 sed 也會撞到同一個權限錯誤。

換掉 sed 的實際理由見 §6.6。

8. 「等部署完」是三個條件

少一個會怎樣?本文剛好跑出兩種完全不同的症狀:

條件 timeout 結果 wait 耗時
v1 revision → Healthy 5m ❌ timeout 300s
v2 revision → Healthy 8m ⚠️ 假的通過 304s
v3 revision → Synced → Healthy 8m 89s

v1 和 v2 的條件一樣,差別只有 timeout。同一組不足的條件,
一次表現成「等不到被砍」,一次表現成「等到了、然後放行一個還沒部署完的狀態」。

⚠️ 所以不能從單一症狀反推條件對不對。

8.1 v2:兩個條件都成立,東西還沒部署

兩個 oc wait 都印 condition met。然後最終狀態是:

sync     = OutOfSync          ← 還沒套用完
revision = 74c26a25...        ← 但 revision 已經對了
health   = Healthy            ← 舊版留下來的

sync.revision 是「ArgoCD 觀察到的 Git revision」,它在比對階段就會更新,
不代表已經 apply 完;而那時的 health 是上一個版本留下來的。

這裡是 revision 領先了 sync:觀察到的 revision 一路往前,只是套用比觀察慢。
§8.2 要用的前提是這個值不會往回走,這個例子沒有違反它。

兩個條件各自都成立,合起來卻是假的通過。

⚠️ 那一次 pipeline 結束時,部署其實是對的——因為後面還有 cleanup-workspace
ArgoCD 在那段時間裡把事情做完了。

一次綠燈不足以證明 wait 的條件是對的。 要看的是
「條件成立的那一刻叢集是什麼狀態」,不是「pipeline 結束時是什麼狀態」。

8.2 順序:這一段是推理,不是實測

上面那張表只量了「少一個條件」。順序本文只跑過一種,以下是理由。

三個 oc wait連續的阻塞式等待,所以:

只有最後一個條件,保證在整段 wait 退出的那一刻仍然成立。
前兩個都只證明「在它自己退出的那個時間點曾經成立」。

前兩個等完之後 ArgoCD 還會繼續動,成立過的條件隨時可能又不成立——
§8.1 那個 revision 對了但 sync 還是 OutOfSync,就是這件事的實例。

這裡還靠一個前提:這條管道跑的期間,沒有別人往同一個倉庫 push。
有了它,第二段的 Synced 一旦達成,就是針對「這一次的 commit」達成的,
而且不會退回更舊的 revision;進到第三段時叢集已經套用了新版,
此後量到的 Healthy 才是新版的 Healthy

沒有這個前提,Healthy 放最後也只保證「當下同步的那個版本是健康的」,
接不回「這一次的 commit」——§5 的驗收和全篇的結論都掛在這一句上。

所以最後一個位置,要留給你真正想在退出那一刻保證的條件:

revision   ArgoCD 有沒有看到「這一次」的 commit
Synced     有沒有把它套用上去          ← v1/v2 漏的就是這段
Healthy    套用的結果健不健康          ← 放最後,因為只有它退出時仍成立

Healthy 放最前面的話,ArgoCD 停在舊版時它立刻就成立(舊版是健康的),
而整段 wait 退出時被保證的變成 revisionSynced——那兩個都不保證新版跑得起來。

8.3 別相信輪詢設定

v1 敗在 timeout,不是敗在條件。

wait 總耗時(含 sync + health) timeout.reconciliation
v1 ≥300s ← 被 timeout 截斷的下界,不是量測值 預設
v2 304s 已經改成 30s
v3 89s 30s(ArgoCD pod 重建後)
事後單獨量的探針(只量發現延遲) 52s 30s

⚠️ v2 的設定已經寫進 argocd-cm 了,但它跟 v1 一樣慢——要等 ArgoCD 的 pod
重建之後才變快。改完設定要確認它真的生效,不要看到 ConfigMap 有值就當作好了。

文件的預設是 timeout.reconciliation: 120s 加最多 60 秒 jitter,發現延遲的上界
是 180 秒。表裡的 304s 是總耗時,兩個量不能直接比,要先拆掉 sync + health:
v3 總耗時 89s、探針單獨量發現延遲 52s,相減約 37s;304 - 37 ≈ 267s,超過上界
一大截。這是跨執行相減出來的推估,不是量測——52s 那一次的 sync + health 沒有量,
只能假設它和 v3 那一次差不多。v1 的數字是被截斷的下界,撐不起任何比較。

120s 是版本相關的。舊版文件寫的是 180s,這個預設中途改過;本文這台跑的是
Argo CD v3.4.5。而且沒設的時候 argocd-cm 裡不會有這個 key,讀 ConfigMap
看不出預設是多少——要引用就把版本一起寫上。

要真正拿掉這個延遲,用 Git webhook 打 <argocd>/api/webhook,push 完立刻觸發。
本文沒做——那需要在 Git 服務和 ArgoCD 之間共用一把密鑰。

9. yq 會重寫整份文件

yq 不是改那一行,是把整份 YAML 解析成資料結構再序列化回去。
所以它有可能動到你沒要改的地方(引號風格、縮排、空行)——
upstream 的 issue #465 到現在還是 open,
報的是 -i 會移除空行、改動註解前的空白(2020 年的回報,當時還是 v3 的 yq w -i)。

實測的 diff:

@@ -30,7 +30,7 @@ spec:
           # tag 是可變的,digest 不是。
-          image: ...web@sha256:0527f0c9...
+          image: ...web@sha256:3da59c62...
           ports:

只有一行。註解、縮排、空行全部保留。

⚠️ 但這只是這一份 manifest 的結果。 issue 還開著,
而你的檔案有沒有 anchor、多文件、特殊引號,結果可能不一樣。

改完看一次 git diff。如果雜訊不只一行,那就是每次 CI 都會產生一個帶噪音的 commit
——而 review 的人很快會學會忽略它。

10. 一個標籤換到 namespace-admin

要讓 ArgoCD 管一個 namespace,只需要一個標籤:

metadata:
  labels:
    argocd.argoproj.io/managed-by: openshift-gitops

operator 立刻自動建兩個 RoleBinding:

Role/argocd-argocd-server
  apiGroups[*]  resources[*]  verbs[get, patch, delete]

Role/argocd-argocd-application-controller
  apiGroups[*]  resources[*]  verbs[get, list, watch]
  + rolebindings / roles         全動詞
  + serviceaccounts: impersonate

對那個 namespace 內任意資源可 patch/delete、可以自己建 Role、可以 impersonate SA。

(它們是 namespaced Role,所以規則裡那些 cluster-scoped 資源實際上是無效的。)

10.1 這是模型的取捨

要讓一個控制器持續調和「任意 manifest」,你沒辦法預先知道它需要哪些動詞——
使用者下週可能在 repo 裡放一個 CronJob、一個 NetworkPolicy、一個 PVC。

該做的是知道它,並且把 namespace 的邊界劃清楚。
這也是 §2.2 說「部署目標不要用 CI 那個 namespace」的理由。

11. ArgoCD 可能讓節點掛掉

裝完之後節點的 memory requests 多了 1792Mi(10181 → 11973Mi,65% → 77%)。
然後跑 pipeline:

NodeNotReady  →  registry 回 503  →  buildah 拉不到基底映像 exit 125

11.1 requests 是宣告,不是用量

沒有 metrics API 也量得到:

oc exec <pod> -- cat /sys/fs/cgroup/memory.current
元件 實際用量 requests
application-controller 386 MiB 1024Mi 638Mi
repo-server 38 MiB 256Mi 218Mi
server 46 MiB 128Mi 82Mi
redis 13 MiB 128Mi 115Mi
小計(可從 ArgoCD CR 調的四個) 483 MiB 1536Mi ~1 GiB
cluster 32 MiB 128Mi 96Mi
gitops-plugin 33 MiB 128Mi 95Mi
合計 548 MiB 1792Mi ~1.2 GiB

⚠️ 前四個是 §11.2 要調的對象,後兩個 Operator 自己管、本文沒動。
openshift-gitops-operator-controller-manager 沒有宣告 memory request,所以是 0。

那些百分比的分母是節點的 allocatable(這台約 15497Mi),不是 crc config 給的
16384 MiB——中間差的是韌體與 kube/system-reserved。要對帳請用 Mi 那兩欄。

requests 影響的是排程。 宣告得比實際用量寬三倍,等於憑空佔掉排程空間——
而那正是讓節點被排到掛掉的東西。

11.2 兩個措施

四個元件照實測調   1536Mi → 1056Mi   (768/128/96/64,合計約 2 倍餘裕)
含 Operator 那兩個 1792Mi → 1312Mi   (少 480Mi)
節點記憶體         16384 → 17408 MiB  (allocatable 15497 → 16521Mi)
                        ↓
節點 memory requests   11973/15497 = 77%   →   11493/16521 = 70%

只調 requests 是 74%,只加記憶體是 72%,兩個都做是 70%。本文只觀察到 77% 那次掛了,
74% 和 72% 都沒有實測過——臨界值在哪沒有量出來,所以兩個措施都做了。

⚠️ 別把 requests 縮到貼著實際用量。它是排程與驅逐的依據,
太緊的話節點吃緊時會先被踢掉。合計 2 倍是本文選的折衷值——逐元件是 2.0 到 4.9 倍,
用量小的 redis 和 repo-server 沒有再往下壓。你的工作負載可能要別的數字。

12. RBAC 的 resourceNames:答案綁版本

wait-for-sync 只需要讀一個 Application。能不能用 resourceNames 鎖死?

官方文件:

If you restrict list or watch by resourceName, clients must include a
metadata.name field selector in their list or watch request
(that matches the specified resourceName) in order to be authorized.

這段是 KEP-4601 帶進來的——list / watch / deletecollection 會把 field/label
selector 納入授權判斷。它 1.31 進 alpha(AuthorizeWithSelectors 這個 gate 預設
關著)、1.32 beta、1.34 GA。本文實測的 1.34.6 正好是 GA 那一版,1.31 到 1.33
之間要看 gate 有沒有開。

12.1 oc auth can-i list 會騙你

建一個只有 resourceNames: ["web"] 的 Role,用 --as 實測(v1.34.6):

① oc get application web                                  → Synced Healthy   ✅
② oc get applications(不帶 selector)                     → Forbidden        ❌
③ oc get applications --field-selector=metadata.name=web   → Synced Healthy   ✅
④ oc wait --for=jsonpath=... application.argoproj.io/web   → exit 0           ✅

② 就是 oc auth can-i list 會給你的答案。而它證明的只是
「這個探測沒帶 field selector」。

④ 才是實務上要知道的:oc wait 對具名物件會帶 selector,鎖得住。

所以 Role 可以收到:

rules:
  - apiGroups: ["argoproj.io"]
    resources: ["applications"]
    verbs: ["get", "list", "watch"]
    resourceNames: ["web"]

收緊之後 oc wait 照樣 exit 0,但盲目 oc get applications 被擋掉。

「最小權限」能做到多細,取決於動詞、也取決於客戶端送什麼。
而這個答案從 1.31 到 1.34 一路在變——引用這類規則時把版本一起寫上。

13. 這篇沒涵蓋的

  • Git webhook 觸發 ArgoCD:見 §8.3。本文靠輪詢,代價是 89 秒。
  • 多環境/多 Application:本文只有一個 Application、一個 namespace。
  • rollback 流程:GitOps 的 rollback 是 git revert,本文沒有做過一次。
  • ArgoCD 的 HA:單副本,節點餘裕見 §11。

14. 參考文件

Argo CD

yq

Kubernetes

Tekton


上一篇
Day 27:cosign 金鑰產生——私鑰不落地,密碼也不落地
下一篇
Day 29:在 OpenShift 上用 Kyverno 擋掉沒有簽章的映像
系列文
防範軟體供應鏈攻擊:從零打造具備硬性阻擋能力的雲原生 CI/CD 流水線30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言