映像建好了、掃過了、簽了章。接下來要讓它真的跑起來——而這一步最容易出現一種
特別難查的失敗: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-probe、upload-sbom、typecheck,以及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。
① 先建「另一端」:GitOps 倉庫 + ArgoCD + 部署 namespace → 大部分工作在這
② update-gitops 用 yq 改 YAML,寫之前先確認路徑讀得到值 → yq 也會靜默失敗
③ wait-for-sync 等三個條件,`Healthy` 要放最後 → 少一個會拿到假通過
④ 接進 DAG,確認建的/簽的/跑的是同一個 digest
四個會咬人的地方:
sed 和 yq 都會靜默失敗 |
路徑/樣式錯就 exit 0,但 image 沒換(§6、§6.4) |
| 「等部署完」是三個條件 | 少一個會拿到假通過(§8);順序的理由見 §8.2 |
| 裝 ArgoCD 可能讓節點掛掉 | requests 是宣告,不是用量(§11) |
| 一個標籤換到整組管理權 | 那是 GitOps 模型的取捨(§10) |
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
讀應用程式的倉庫和寫 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 ← 完全沒有存取權
理由見 §10:讓 ArgoCD 管一個 namespace,等於給它那裡的整組管理權。
CI 的 namespace 已經有 pipeline SA 的大權限,不要再疊一層。
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、三處共用同一個變數,是為了讓 ① ② 和寫入不可能各自打錯字。
IMAGE_REF 前面那個 @- name: IMAGE_NAME
value: <registry-route>/docker-hosted/web
- name: IMAGE_REF
value: "@$(tasks.build-push.results.IMAGE_DIGEST)"
拼出來是 name@sha256:...,不是 name:tag。tag 是可變的,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 相同,指的是同一份位元組。
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」。
本文設 8 分鐘,實際跑 89 秒。理由見 §8.3——ArgoCD 的發現延遲比設定值難預測。
三段各 8 分鐘,最壞情況是 24 分鐘。實務上不會發生:真的卡住的話多半是第一段就卡死,
但如果你的 Pipeline 有整體 timeout,要記得它得涵蓋得住這個上界。
=== 更新前:...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。
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 ===
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。
build-push 建了新映像 sha256:0ac07986...
cosign-sign 簽了它 Pushing signature...
↓
GitOps 倉庫 HEAD 沒動
ArgoCD Synced / Healthy,一動也不動,而且它是對的
pod 還在跑舊的 sha256:0527f0c9...
ArgoCD 顯示 Synced 完全正確——它忠實反映了 Git 的內容,Git 就是沒變。
整條 17 個 TaskRun 全綠。
退出碼 0,但是……
沒設 coverage 門檻 → 測試「通過」但什麼都沒驗
solution-style tsconfig → 型別檢查跑了 0 個檔案
HTTP 200 → 但那是預渲染的靜態檔
sed exit 0 → image 沒換
⚠️ 最後這個特別難察覺:連紅燈都沒有。前幾種看 pipeline 就知道;
這一種要等有人問「怎麼還是舊版本」。
實測 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 會靜默建立新節點的情況。
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。
理由是能指定身分,而不是位置或長相。
sed 我要改「長得像這樣的那一行」
yq 我要改「名字叫 web 的那個容器的 image」
§6.5 那個 sidecar 就是差別所在。containers[0] 和 /image:/ 都是在描述位置或長相,
兩個都會指到 istio-proxy;select(.name == "web") 描述的是身分,
前面插幾個 sidecar 都指得對。這個條件 sed 表達不出來——它一次只看一行,
不知道那一行屬於哪個容器。
另外兩個比較小但實在的:不會誤中另一行剛好含相同文字的內容、
不受縮排和引號風格影響。
至於「yq 失敗會留下痕跡」,那件事在本文的設定裡不會發生——① 在寫入之前就 exit 1,
檔案根本不會被動到。它只在沒有 ①、而且用位置索引的時候才是個優點:
| 路徑/樣式錯,而且沒有 ① | |
|---|---|
sed |
檔案完全沒動,git diff 是空的 |
yq |
多出一個新節點,git diff 看得到 |
換掉
sed是有理由的,但理由不是「換了就不會靜默失敗」。
兩個都會。差別在你能不能把「要改哪一個」講清楚。
換成 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
那個假設只在會重新指派 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 寫不進去
git clone -q "$URL" "$DIR"
chmod -R a+rwX "$DIR" # ← 這一行
permission denied → 紅燈,訊息指名道姓:哪個檔案、什麼原因
紅燈是好事,但它的成因是拆成兩個映像跑,跟 yq 懂不懂 YAML 無關。
同一個拆法配 sed 也會撞到同一個權限錯誤。
換掉 sed 的實際理由見 §6.6。
少一個會怎樣?本文剛好跑出兩種完全不同的症狀:
| 條件 | timeout | 結果 | wait 耗時 | |
|---|---|---|---|---|
| v1 | revision → Healthy | 5m | ❌ timeout | 300s |
| v2 | revision → Healthy | 8m | ⚠️ 假的通過 | 304s |
| v3 | revision → Synced → Healthy |
8m | ✅ | 89s |
v1 和 v2 的條件一樣,差別只有 timeout。同一組不足的條件,
一次表現成「等不到被砍」,一次表現成「等到了、然後放行一個還沒部署完的狀態」。
⚠️ 所以不能從單一症狀反推條件對不對。
兩個 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 結束時是什麼狀態」。
上面那張表只量了「少一個條件」。順序本文只跑過一種,以下是理由。
三個 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 退出時被保證的變成 revision 或 Synced——那兩個都不保證新版跑得起來。
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 之間共用一把密鑰。
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 的人很快會學會忽略它。
要讓 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 資源實際上是無效的。)
要讓一個控制器持續調和「任意 manifest」,你沒辦法預先知道它需要哪些動詞——
使用者下週可能在 repo 裡放一個 CronJob、一個 NetworkPolicy、一個 PVC。
該做的是知道它,並且把 namespace 的邊界劃清楚。
這也是 §2.2 說「部署目標不要用 CI 那個 namespace」的理由。
裝完之後節點的 memory requests 多了 1792Mi(10181 → 11973Mi,65% → 77%)。
然後跑 pipeline:
NodeNotReady → registry 回 503 → buildah 拉不到基底映像 exit 125
沒有 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 影響的是排程。 宣告得比實際用量寬三倍,等於憑空佔掉排程空間——
而那正是讓節點被排到掛掉的東西。
四個元件照實測調 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 沒有再往下壓。你的工作負載可能要別的數字。
resourceNames:答案綁版本wait-for-sync 只需要讀一個 Application。能不能用 resourceNames 鎖死?
官方文件:
If you restrict
listorwatchbyresourceName, clients must include ametadata.namefield selector in theirlistorwatchrequest
(that matches the specifiedresourceName) 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 有沒有開。
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 一路在變——引用這類規則時把版本一起寫上。
git revert,本文沒有做過一次。prune 與 selfHeal 的語意Healthy 怎麼判定oc explain application.status.sync —— 另外兩個條件(revision / status)上游timeout.reconciliation 預設 120s,加最多 60s jittere -i '.path = "value"'
resourceNames —— §12 引的那段GITOPS_COMMIT