標籤: Git

  • Jujutsu vs Git:從底層原理到 AI Agent Team 時代的版本控制選擇

    重點摘要

    • Git 的本質是一個 content-addressed key-value 資料庫,所有的 branch、rebase、reflog 都是建構在這個底層上的抽象
    • Jujutsu(jj)不是 Git 的替代品,而是把 Git 的 content-addressed 哲學「再往上推一層」 — 從檔案快照擴展到操作快照
    • Git 的核心限制不是指令不爽,而是它要求人類的同步注意力 — merge 衝突會 block 整個世界等你處理
    • jj 允許異步注意力 — 衝突變成 commit 的一種狀態,rebase 永遠不會停,你可以明天再一次處理完
    • Agent Team 有三種任務類型,jj 的價值差很多:Review(只讀)完全不需要 jj;Debug(試驗假設)是 jj 最被低估的甜蜜點;Dev(平行開發)在合併階段需要 jj
    • 最優的使用模式是 git 為主、jj 只在 Agent Team 合併階段出場 — 由 orchestrator agent 承擔 jj 學習成本,人類使用者和 subagent 全程保持 git 心智
    • 決策準則:默認 Git,三個訊號任一觸發時考慮 jj — 平行 Agent 撞車、review round 批次修正、Agent 實驗成本太高

    最近 GitHub 上 jj-vcs/jj(Jujutsu)越來越熱,社群討論開始出現「Git 要被取代了嗎」的聲音。我花了幾個小時認真研究這個工具,過程中意外地被迫重新理解 Git 的底層設計 — 因為只有懂 Git 真正是什麼,才看得懂 jj 想解決什麼。這篇文章就是這趟理解的完整紀錄,從 Git 底層原理開始,一路推到 AI Agent Team 時代的務實使用模式。

    結論先說:Jujutsu 和 Git 不是互斥關係。jj 的儲存後端其實就是 Git,你可以隨時切回純 Git 指令。真正該問的不是「要不要換」,而是「你的工作流是否已經撞到 Git 的設計假設」。而這個答案,必須先從 Git 的底層長什麼樣講起。

    先搞懂一件事:Git 其實是一個資料庫

    很多人用 Git 很多年,但沒意識到一件事:Git 本身就是一個資料庫,而且是一個設計極其精巧的資料庫。整個 .git/ 目錄就是一個 key-value store,只是它用檔案系統當儲存引擎而已。

    你現在去任何 git repo 跑 ls .git/objects/,會看到一堆兩位數的資料夾:00/ 01/ ... ff/,每個資料夾裡面有一堆 hash 開頭的檔案。這就是 Git 的資料庫。它叫 Object Database,key 是 SHA-1 hash,value 是壓縮過的內容。所有其他概念 — commit、branch、tag、stash、reflog — 全部都是建構在這個 key-value store 之上的抽象。

    四種物件,解構整個 Git

    • Blob:儲存單一檔案的內容,不包含檔名。SHA1(content) = hash,內容相同 hash 必然相同。同樣內容只存一份(自動去重)。
    • Tree:儲存一個資料夾的結構,內容是一張表 — 列出這個資料夾有哪些檔案(指向 blob)和哪些子資料夾(指向另一個 tree)。Tree 可以遞迴指向其他 tree。
    • Commit:儲存一個時間點的快照,內容只有幾行 — 指向根目錄的 tree hash、前一個 commit 的 hash、作者時間、commit message。Commit 本身不含任何檔案內容,它只是一個指標鏈的進入點。
    • Tag:annotated tag 的容器,對理解本質不重要,跳過。

    一個 commit 怎麼「記錄整個資料夾」

    你 commit 時,Git 為每個檔案內容計算 hash 建 blob,為每個資料夾建 tree(一張表,列出那個資料夾裡所有 blob 和 sub-tree 的 hash + 檔名),最後建 commit,只包含 root tree hash + parent hash + 作者 + 訊息。結果是一個指標鏈:commit → tree → sub-tree → blob每個 commit 都完整地、不可變地描述了那一刻整個資料夾的狀態,不是 diff,是完整快照。

    但每次存完整快照不會爆嗎?

    這就是 Git 設計最美的地方。答案是:不會爆,因為 content-addressed 帶來自動去重。你 commit 了 1000 次,但 99% 的檔案沒動,那 99% 的檔案永遠只有一個 blob,因為內容一樣 hash 一樣,儲存只有一份。改一個檔案的成本 = 1 個新 blob + 2-3 個新 tree + 1 個新 commit,其他全部重用。實際效果:Linux kernel 有 100 萬+ commits,整個 .git/ 也就 5 GB 左右。

    Git 從來不儲存 diffgit diff 是每次問的時候走訪兩個 commit 的 tree DAG、現場跑 Myers 演算法算出來的。這個設計的好處是:讀任何一個歷史版本都是 O(1),不用還原一串 diff 才能看到那個版本的樣子。這一切都是「content-addressed immutable store + pointer DAG」這一個設計決策的延伸。Linus 2005 年花兩週寫出 Git 核心,就是這個設計。後來所有東西都是在這個底層上加糖。

    Jujutsu 的核心創新:把 Git 的哲學再推一層

    現在你懂了 Git 的底層,就能看懂 jj 在做什麼。jj 沒有推翻 Git 的設計,它做的事情是:把 Git 的 content-addressed DAG 從「檔案狀態」擴展到「操作狀態」 — 同樣的哲學,應用在不同的層級。

    雙層身份:commit hash + change-id

    Git 只有一個 ID — commit hash,由內容決定。這導致 rebase 後 hash 全變,你找不到「原本那個 commit」。jj 對每個 commit 同時維護兩個 ID:commit hash(跟 Git 一樣)+ change-id(隨機產生的 128-bit ID,只在 commit 誕生時生成一次,rebase/amend/split/squash 都不變)。這個設計的意義是:身份認同不綁在 hash 上,綁在 change-id 上。你可以隨便改歷史,change-id 永遠追蹤得到「同一個概念上的改動」。這就是為什麼 jj 可以把「歷史重寫」當日常操作,而 Git 必須當儀式感操作。

    Op Log:第二層 content-addressed DAG

    jj 的另一個核心是 operation log。每個 jj 操作都產生一個 operation 物件,包含 operation id(hash)、parent operation、執行的指令、操作前後 repo 整體狀態的快照。這些 operation 本身也組成一個 DAG,content-addressed 儲存。注意這裡的模式跟 Git 完全一樣:不可變物件 + 指標 DAG + 內容定址。jj 只是把 Git 用來管檔案狀態的那套機制,再蓋一層管操作狀態。

    結果是 jj undo 變成一步 pointer 移動 — 直接把 repo view 指回前一個 operation 的「操作前快照」就好,不用反向計算任何東西。而且 undo 本身也是一個 operation,可以被 undo,可以無限回退。

    指令層面的六個差異

    面向 Git Jujutsu (jj)
    Staging Area working dir → index → commit(三層) working dir 就是 commit(兩層,無 git add)
    Branch 概念 一等公民,必須 checkout/rebase/merge 可選的 bookmark,每個 commit 有穩定 change-id
    Undo 機制 reflog(補救機制,90 天 TTL) jj undo(第一等操作,可無限回退)
    Conflict 處理 停止世界,當場解完才能繼續 衝突直接存進 commit,可延後解
    Stash 必要指令(stash push/pop/apply) 不存在,jj new 直接開新 commit
    歷史重寫 rebase -i 是儀式感操作 jj split / jj squash 是日常

    最重要的差異:同步注意力 vs 異步注意力

    到這裡為止,都還是「指令差異」和「設計哲學」的層面。但 jj 真正的價值,不在指令爽不爽,而在它如何消耗人類的注意力。這個差異是質變,不是量變。

    Git 是同步注意力模型 — merge/rebase 遇到衝突會停止世界,直到人類出現、看懂、解決、繼續。它要求你在「不可預期的時刻」立刻在場。這個模型在單人或 2-3 人團隊下完全沒問題,因為衝突頻率低、你本來就在鍵盤前。

    jj 是異步注意力模型 — rebase 遇到衝突不停下來,衝突被存進 commit 變成一種有效狀態,rebase 繼續往下跑。你可以明天處理、或一次批次處理所有衝突、或開另一個 Agent 專門解衝突。jj 允許人類「異步在場」 — 系統自己跑,有問題幫你記下來,你有空再來看。

    Git 的設計誕生於 2005 年,當時的假設是「careful human curator」 — 每個 commit 都是人類慎重思考後的決定,每個 merge 都有人類在場。但 Claude Code、Cursor、Copilot Agent 這類 AI 工具改變了「誰在產生 commit」這件事。Agent 產出的是高頻、探索性、非線性、大量並行的中間態。你不可能一邊看 3 個 Agent 平行工作,一邊在它們合併時即時解衝突,一邊決定每個中間態要不要保留。人類同步注意力的頻寬根本跟不上 Agent 的產出速度。

    jj 在這裡的價值不是「比 Git 更爽」,而是它讓 Git 做不到的事變可能 — 讓 Agent Team 真的平行跑到底,明天早上來一次處理所有衝突。這不是量變(爽度提升),是質變(能做的事情不同)。

    Workspace 隔離:平行的真正解法

    要澄清一個常見誤解:jj 本身不能解決「兩個 Agent 同時寫同一個檔案」這種檔案系統 race condition。檔案系統的併發是 OS 管的,任何 VCS 都介入不了。平行 Agent 的正確架構是每個 Agent 給一間房 — Git 用 git worktree add,jj 用 jj workspace add。每個 workspace 是獨立的 working directory,三個 Agent 在不同資料夾各寫各的,完全不會互相污染。

    Git 和 jj 在 workspace 隔離這一步的行為相同。差異出現在下一步 — 把三個 Agent 的輸出合併回 main 時。Git 會序列化 blocking(解一個、繼續、解下一個、繼續),你的注意力被三次阻塞切碎;jj 則是三個動作一氣呵成,最後一次性 batch 處理所有衝突。Workspace 隔離是必要條件,jj 的優勢出現在合併階段的併發模型

    Agent Team 的三種任務類型:jj 的價值差很多

    這是這篇文章最重要的一個區分,也是大部分「jj 推坑文」忽略的。你叫 Agent Team 做不同類型的事,jj 的價值完全不同 — 甚至有些情境 jj 根本沒有用。決定性的問題是:這些 agent 需要寫檔案才能完成任務嗎?

    類型 1:Review(只讀) — 完全不需要 jj

    場景:叫 Agent Team 做 code review、security audit、dead code 掃描、架構審閱。這些 agent 只讀檔案,產出報告,不寫任何東西

    這種場景 jj 沒有價值:沒有 commit、沒有 merge、沒有 undo 需求。三個 agent 同時讀同一個檔案完全沒問題,OS 的 file handle 對讀不是互斥資源。純 git 就好,甚至不需要 workspace 隔離。

    常見誤區:很多人把「開 Agent Team」跟「需要 jj」畫等號,導致在純 review 場景也導入 jj 的學習成本。這是沒必要的。

    類型 2:Debug(試驗假設) — jj 最被低估的甜蜜點

    場景:叫 Agent Team debug 複雜 bug。Debug 的本質是「形成假設 → 驗證 → 推翻 → 再假設」的循環,而驗證通常需要動手改 code — 加 log、改條件、插 assert、暫時跳過某段邏輯。

    Git 下的 debug 痛點:加了 10 個 print() 驗證完要全部刪掉,漏刪一個就污染 production;改了某個函式測試假設,後來發現方向錯,手動改回去容易漏;想試兩個假設 A 和 B 哪個對,兩個互相覆蓋,不能同時存在。

    這是 jj 最強的場景,**比一般開發還強**。原因很微妙:development 的產出是「最終正確的 code」,中間過程 git 可以容忍(最終 squash 成 clean commit 就好)。但 debug 的產出是「對問題的理解」,這個理解存在於中間過程本身,不存在於最終狀態。

    Git 丟掉中間過程 = 丟掉 debug 的精華。jj 保留所有中間過程 — 每個假設是一個 commit、jj undo 可以乾淨撤銷、多個假設可以平行存在於 op log,等於保留 debug 的完整價值。debug agent 可以試 5 個假設、留下完整推理紀錄、報告說「我試了 A/B/C/D,D 可行因為…」,你事後可以 jj op restore 跳回任何一個假設狀態看 agent 那時候看到什麼。

    結論:如果你常叫 AI debug 複雜問題,jj 的價值比你想的大很多。這是 op log 從「檔案歷史」升級為「推理紀錄」的唯一場景。

    類型 3:Dev(平行開發) — jj 在合併階段是必要品

    場景:叫 Agent Team 平行開發多個模組。多個 agent 會產出多個改動分支,最後要合併回 main。這是大家熟悉的場景 — 前面講的「同步 vs 異步注意力」、「workspace 隔離」、「conflict as commit」全部都在這個場景發揮。

    但注意:jj 在這個場景的價值集中在合併階段,不是 agent 工作期間。agent 自己工作時,Git 和 jj 沒差。差異出現在「把 N 個平行輸出 reconcile 回主線」這一步。沒有 jj 的非阻塞合併,你的 Agent Team 上限會被 git 的序列化 blocking 硬生生壓到小規模 — 即使記憶體還有餘裕。

    三種使用模式:該怎麼在日常工作裡擺 jj

    理解完上面的分析,最後的問題是:你該怎麼把 jj 放進你的日常工作?有三種模式,各自適合不同情境。

    模式 1:純 git(現狀)

    適合:單人或「1 RD + 1 Reviewer」工作流、穩定專案、強烈依賴 IDE Git 整合、團隊只認 git。這個模式沒什麼好討論,就是你現在的樣子,對大多數人完全夠用。

    模式 2:jj 為主,git 只是後端

    安裝 jj colocated mode,日常指令都打 jj,幾乎不打 git。這個模式的問題是:邊界仍然是 git 世界。IDE Git panel 顯示的是 jj 上次匯出的狀態,你在本地 jj rebase 一下,push 後 GitHub 還是看到 commit hash 全變,change-id 根本沒傳出去。你本地很爽,對外還是 git 的世界。

    這個模式只適合「單人、沒有協作壓力、不在意 IDE 整合退化」的使用者。對多數人代價太大。

    模式 3:git 為主,jj 只在 Agent Team 合併階段出場

    這是我研究完之後推薦的模式,也是最被低估的一個。玩法是:

    • 你日常全程用 git — 習慣、IDE、CI、PR 流程完全不變
    • Subagent 也用 git — 他們在各自 workspace 裡用 git commit,根本不知道 jj 存在
    • Orchestrator agent(Claude Code)在合併階段用 jj — 只需要學 5 個指令:workspace addgit importrebaselogworkspace forget
    • 一次性設定:jj git init --colocate,之後你完全忘記它存在

    這個模式最精妙的地方:學習成本完全落在 orchestrator agent 身上,不是你身上。你的認知負擔幾乎為零,只需要記住一個 trigger:「要開 Agent Team 時,叫 Claude」。其他時間 jj 完全不存在。

    失敗零成本:任何一個環節出問題,你都可以回退到純 git。subagent 的 commit 是正常 git commit,無論是否用 jj 都能 git cherry-pick;合併失敗就 git merge 手動解;jj 整個壞掉就刪掉 .jj/,git repo 毫髮無傷。沒有 lock-in,沒有 point of no return

    Review Round 批次修正:寫之前切 vs 寫之後切

    另一個 jj 爆發的場景是 review round 工作流。很多團隊的 QA/review 會產出幾十個 issue,你拿到一份清單要一次修完。典型的 commit message 長這樣:fix(fee): R5-12/13/03/04/10/16/17 remaining module items — 塞了 7 個獨立的 issue 修正在一個 commit。

    為什麼塞一起?因為 Git 下的三個選擇都不理想:每個 issue 一個 commit 要 git add -p 切七次太累;全部一個 commit 未來想單獨 backport 做不到;開 7 個 branch 管理成本爆炸。結果大多數人選「全部一個 commit」,歷史變成粗顆粒。

    jj 的解法:打開 editor,一次改完 7 個 issue,不用管 commit 切分。改完後 jj split,互動畫面讓你把每個 hunk 分配到哪個 commit,七次後自動產生 7 個獨立 commit。Git 的 add -p 是「寫之前」切,jj 的 split 是「寫之後」切。這讓你把「commit 切分」這個決策延後到資訊最充足的時候(全部改完後),而不是在你還在思考邏輯的時候被迫處理。

    決策準則:你該用哪個模式?

    純 git 就夠的訊號

    • 單人或「1 RD + 1 Reviewer」工作流 — 沒有平行檔案衝突
    • Agent Team 只做 review / audit(agent 不寫檔案) — jj 沒有價值
    • 穩定專案、強依賴 IDE Git 整合
    • 老 Git 肌肉記憶還在運作,且團隊只認 git

    考慮模式 3(git + jj for Agent Team merge)的訊號

    1. Agent Team 在共享檔案上撞車 — 你為了避免 merge 衝突開始序列化 Agent,生產力被 VCS 綁住
    2. Review round 產生批次修正 — 你寫過大量 fix: XXX-01/02/03/04/05 這種塞一堆 issue 的 commit,事後想拆卻沒工具
    3. AI 常常需要做 debug 試驗 — 你希望 AI 大膽試很多假設,但現在因為回退成本太高,限制了 agent 的自由度

    三個訊號都沒中?純 git 就夠了,別為了新鮮感切工具。

    我的具體建議:安全試用路徑

    1. 選一個個人實驗性專案,執行 jj git init --colocate(一次性,30 秒)
    2. 繼續用 git 做 99% 的事 — 你的肌肉記憶完全不變
    3. 下次真的要開 Agent Team 時,讓 orchestrator agent 用 jj 的 workspace + 非阻塞 rebase 處理合併階段
    4. 如果是 debug 場景,給 agent 「大膽試驗」的權限,利用 op log 保留推理紀錄
    5. 一個月後評估:有沒有真的遇到 jj 幫上忙的場景?沒有就卸載,git repo 完全不受影響

    跨專案的架構原則

    這次研究讓我提煉出一個跨專案適用的原則,寫進我的領域腦系統:

    VCS 的選型要跟「人類注意力的可用模式」匹配,不是跟「指令爽不爽」匹配。Git 假設人類可以同步在場,jj 允許人類異步在場。AI Agent 工作流打破了 Git 的第一個假設,但是否影響到你,取決於你的團隊形狀和工作型態,不取決於你對 AI 的熱情。

    更深一層的 insight:Git 的偉大不在於它的指令設計,而在於 content-addressed DAG 這個設計模式。這個模式你會在 IPFS、Nix、Docker layers、區塊鏈到處看到 — 懂 Git 底層等於懂了這一整類系統的通用結構。jj 的聰明,是看到這個模式可以往上再推一層,應用到操作本身,而不只是檔案狀態。

    AI 時代最大的認知陷阱是「新工具一定比舊工具好」,但真實世界是新舊工具的取捨點會隨場景變化。把場景描述清楚,比把工具吹捧清楚重要得多。下次你看到任何新的版本控制、儲存系統、分散式工具時,問自己這三個問題就好:它的不可變單位是什麼?怎麼定址?DAG 結構長怎樣?答案對上 content-addressed DAG 模式,你就已經懂 80% 了

  • 舊系統不死,AI 讓它進化:不重寫也能持續成長

    重點摘要

    • 舊系統不是問題,缺乏 AI 輔助才是問題——AI 讓「不重寫、持續演進」成為可行選項
    • 歷史證明:超過 80% 的「重寫計畫」以失敗或兩個系統都要維護收場
    • AI 最確定的價值是讀懂舊代碼、補文件、補測試,讓團隊敢繼續開發
    • 「AI 能不能讓大規模翻新變安全」——這是尚待驗證的命題,不宜過度樂觀

    你的公司有一套跑了十年的系統。它能動,它撐起了整個業務,但沒有人敢碰它。文件不齊、邏輯散落在各處、原始開發者早就離職了。每次有人提議「重寫」,討論就會陷入沉默——因為大家心裡都知道,這條路走過很多次,沒幾次是成功的。

    AI 的出現,讓這個困境有了新的解法。但不是你想像中的那種解法。

    為什麼重寫這麼難?歷史給了殘酷的答案

    軟體界有個著名的現象叫做 Second System Effect(第二系統效應),由《人月神話》作者 Fred Brooks 提出:工程師在重寫時,往往會把所有「本來想做卻沒做」的功能都塞進去,結果新系統比舊系統更複雜、更難維護。

    但更根本的問題是:舊系統裡有隱藏的業務邏輯,它們沒有寫在文件裡,只存在於代碼的行為中——某個奇怪的判斷式、某個例外處理、某個只在特定情境才觸發的路徑。這些邏輯,是十年來無數個「為什麼這樣做?」的答案。

    重寫計畫的典型失敗模式

    • 兩個系統並行期拉太長:新舊系統同時維護,工程師精力分散,bug 在兩邊都出現
    • 上線那天發現漏了邏輯:舊系統某個角落的行為,新系統根本沒有對應
    • 商業壓力中斷重寫:計畫進行到一半,業務需求改變,只能把新舊系統黏在一起
    • 重寫後反而更慢:新架構雖然「漂亮」,但少了十年累積的效能優化細節

    這不是悲觀,這是現實。重寫不是不可能,但它的成功率遠低於大多數人的預期。在 AI 出現之前,面對舊系統,企業只有兩條路:硬撐,或者賭一把。

    AI 帶來了第三條路:輔助成長

    AI 最確定的價值,不是幫你重寫系統,而是讓「不重寫、持續演進」這件事變得可持續。

    過去,舊系統的最大問題不是代碼本身,而是「沒有人理解它」。原始開發者離職了,知識沒有傳承;文件過時了,沒有人有時間更新;要改一個功能,必須花三天理解上下文,才敢動一行代碼。

    AI 改變了這個方程式。

    AI 能為舊系統做什麼?

    挑戰 過去的困境 AI 輔助後
    理解舊代碼 要花幾天甚至幾週閱讀 AI 幾分鐘內產出架構圖和流程說明
    補充文件 沒時間寫、寫了也快過時 AI 根據現有代碼生成,每次修改後更新
    補充測試 舊系統通常 0 測試,改動沒有安全網 AI 針對現有行為補寫測試,改動有保護
    修復 bug 怕改了 A 壞了 B,只敢最小化改動 AI 追蹤影響範圍,降低連帶破壞風險
    新人上手 要跟著老人學幾個月才敢動 AI 隨時解釋任何一段代碼的邏輯和背景
    新增功能 不知道該插在哪裡,怕破壞現有邏輯 AI 建議最小侵入式的擴充點

    實際做法:AI 如何輔助舊系統成長

    第一步:讓 AI 讀懂系統,產出活的文件

    不要急著改代碼。第一件事是讓 AI 理解現有系統,然後把理解結果固化成文件。

    # 讓 AI 讀整個 codebase,產出架構說明
    # 在 Claude Code 中,直接描述你的需求:
    
    「請閱讀這個專案的所有代碼,並產出:
    1. 系統架構圖(模組與模組之間的關係)
    2. 核心業務流程說明(從使用者角度描述主要流程)
    3. 高風險區域列表(邏輯最複雜、最不敢動的地方)
    4. 技術債清單(有哪些地方明顯需要改善)」

    這份文件不是給外部人看的,是給你的團隊每天使用的工作手冊。它會隨著系統改動而更新——這一點很重要,因為過去文件之所以沒用,是因為沒有人有時間維護它。AI 讓維護文件的成本降低了 90%。

    第二步:為現有行為補測試,建立安全網

    舊系統的問題不是「代碼爛」,而是「沒有測試保護」。任何一個修改都是在沒有安全網的情況下走鋼絲。

    AI 可以閱讀現有代碼,理解它的行為,然後為這些行為寫測試——即使這些行為從來沒有文件。

    # 範例:讓 AI 為現有函式補測試
    「這個函式 calculateDiscount() 已經跑了八年,
    請分析它的所有分支條件,為每個分支寫一個測試案例,
    包括正常情況、邊界值和異常情況。
    不要改動現有邏輯,只補充測試。」

    有了測試,團隊才敢改動。改動有安全網,系統才能持續演進而不是不斷累積技術債。

    第三步:最小侵入式地新增功能

    新增功能不等於重構整個模組。AI 擅長找到「最小侵入式的擴充點」——在不動現有邏輯的前提下,把新功能插進去。

    這個原則來自 Open/Closed Principle(開放封閉原則):對擴充開放,對修改封閉。即使舊系統沒有遵循這個原則,AI 也可以建議如何在外圍包一層,讓新功能不影響舊邏輯。

    第四步:漸進式現代化,而非大爆炸式重寫

    如果真的有部分需要改善,AI 輔助的方式是:一次只動一個模組,改完之後讓它穩定跑一段時間,確認沒有問題再動下一個。

    這不是「重寫」,這是「漸進式現代化」。兩者的關鍵差異:

    重寫 漸進式現代化
    範圍 全部 一次一個模組
    風險 集中在上線日 分散,每步都可以回滾
    業務中斷 長期並行維護兩個系統 系統持續運作,局部更新
    AI 的角色 「幫我重新實作這一切」 「幫我安全地改善這一塊」

    那麼,「重寫」這條路呢?

    這是一個需要誠實面對的問題。

    過去的答案很清楚:重寫計畫成功率低,風險高,通常不是好選擇。大多數成功的案例,仔細看都是「漸進式替換」而不是「一次性重寫」。

    AI 會改變這個答案嗎?

    這是一個尚待驗證的命題。AI 確實讓理解舊系統更容易,讓知識遷移成本降低,理論上應該讓重寫的準備工作做得更完整。但「做得更完整的準備」不等於「執行時不會出問題」。隱藏的業務邏輯、時序問題、效能細節——這些在代碼裡只有在跑了幾百萬筆資料之後才會浮現。

    更誠實的說法是:

    • AI 讓「輔助成長」這條路變得可行——這是現在就可以驗證的事
    • AI 讓「重寫」變得更安全——這是有可能的,但還需要更多實際案例來驗證
    • AI 能取代「漸進式替換」的謹慎原則——不太可能,這個原則的價值在於限制風險暴露,而不是技術能力

    所以,如果有人告訴你「有了 AI,重寫就不危險了」——保持懷疑。如果有人告訴你「AI 讓你不需要擔心舊系統的技術債了」——同樣保持懷疑。

    如何判斷你的系統需要什麼?

    面對舊系統,用這個框架來判斷方向:

    優先考慮 AI 輔助成長,如果:

    • 系統仍然在提供商業價值,只是難以維護
    • 核心業務邏輯複雜,沒有人完整理解
    • 團隊規模小,無法支撐兩個系統並行
    • 業務需求變化頻繁,不能停下來等重寫完成

    可以考慮漸進式替換(不是重寫),如果:

    • 某個模組已經明顯成為瓶頸,且邊界清晰
    • 有足夠的測試保護現有行為
    • 可以部署影子流量,讓新舊模組並行驗證
    • 有明確的回滾機制

    謹慎考慮大規模重寫,只有當:

    • 現有系統在技術上已經無法繼續擴充(不只是難,而是真的不可能)
    • 有足夠的資源支撐至少 18 個月的並行期
    • 有完整的行為規格文件(或 AI 幫你產出的等效文件)
    • 組織願意接受在過渡期期間功能停滯

    結語:舊系統不是問題,缺乏支援才是

    回到最初的問題:那套跑了十年的系統,它的問題不是年齡,而是孤立。沒有人理解它,沒有測試保護它,沒有文件說明它,改動它需要承擔巨大的個人風險。

    AI 能做的,是讓這個系統不再孤立。它可以成為每個工程師的「老前輩」——隨時解釋任何一段邏輯,隨時分析改動的影響,隨時生成測試保護現有行為。

    這不是重寫的故事,這是陪伴成長的故事。

    至於重寫——如果未來真的需要,AI 會讓你準備得更充分。但那是另一個故事,而且它的結局還沒有寫完。

  • 10 年舊系統如何安全導入 AI 開發:Strangler Fig 遷移方法論

    重點摘要

    • 10 年的舊系統能跑就是最有價值的資產,不要試圖先修好再遷移
    • 核心方法論:Strangler Fig 模式 — 新軌道在旁邊長起來,舊系統自然退場
    • AI 第一件事不是寫 code,而是讀懂 10 年的系統邏輯,再動手
    • 四個階段:快照現況 → 建平行新軌 → 一次搬一個服務 → 封存舊系統

    你的系統跑了 10 年。它很髒、沒有文件、CI/CD 靠手動、密碼可能在 .env 裡或者在某個工程師的腦袋裡。但它能跑,而且在服務真實的用戶。

    現在你想引入 AI 輔助開發,想現代化整個工作流。問題來了:要從哪裡開始? 要先把舊的修乾淨,還是直接用新方法?

    錯誤的答案是:「先把舊的修好。」正確的答案是:不要動正在跑的東西,在旁邊建一條新軌道。

    為什麼「先修好再用新方法」行不通?

    這個直覺很自然,但在實際工程上幾乎都會失敗,原因有三:

    1. 無法停止開發等你修 — 業務不會暫停,新需求還是會進來,你邊修邊開發,舊問題永遠追不完
    2. 「修好」的定義會不斷移動 — 一開始說只要加 .gitignore,結果發現歷史有密碼,要 filter-repo,然後發現測試覆蓋率是零…沒有終點
    3. 你在修一個不完全理解的系統 — 10 年的系統有太多隱性知識,修的過程中很容易把「能跑的」改成「不能跑的」

    工程界有一個著名的模式專門解決這個問題,叫做 Strangler Fig(絞殺榕)模式

    Strangler Fig 模式:不砍舊樹,讓新藤蔓長過去

    絞殺榕是一種熱帶植物。它的種子落在老樹上,慢慢向下長出根,包住舊樹,最後舊樹自然退場,絞殺榕站立在原位。整個過程中,舊樹從未停止「提供支撐」,直到新系統完全就緒。

    應用到 DevOps 遷移:

    ❌ 錯誤思維:
    舊系統(停機)→ 修好 → 接新流程 → 恢復服務
    
    ✅ 正確思維(Strangler Fig):
    舊系統(持續運行,不動)
        ↓
    新軌道在旁邊建立(不影響舊系統)
        ↓
    一次搬一個服務,驗證後切換流量
        ↓
    所有服務搬完,舊系統自然退場

    關鍵洞察:能跑的系統是你最有價值的資產,不是問題的來源。遷移的目標是「讓它繼續跑,同時讓新系統在旁邊成長」,不是「讓它停下來修好再說」。

    四個階段的完整遷移方法論

    階段一:快照現況(不動任何東西)

    第一步不是改 code,不是設定 CI/CD,而是把「現在是怎麼跑起來的」完整記錄下來。這份快照是整個遷移過程的地基。

    為什麼要快照?因為在 10 年的系統裡,repo 裡的 .env 可能是舊的,文件可能是錯的,只有正在跑的進程才是真相:

    # 從正在跑的容器抽出真實的環境變數
    docker inspect <container_name> \
      --format='{{range .Config.Env}}{{println .}}{{end}}' \
      > /tmp/real-env-snapshot.txt
    
    # 或直接讀進程的環境變數
    cat /proc/$(pgrep java)/environ | tr '\0' '\n' | grep -E "DB_|API_|SECRET_"
    
    # K8s 環境
    kubectl get pods -n production -o name | while read pod; do
      echo "=== $pod ==="
      kubectl exec $pod -n production -- env 2>/dev/null
    done > /tmp/real-k8s-env-snapshot.txt

    同時盤點服務清單和依賴關係:

    # 有哪些服務在跑
    docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
    
    # 服務之間怎麼通訊
    docker network inspect bridge
    
    # 對外開放哪些 port
    ss -tlnp | grep LISTEN

    這個階段的產出是一份真實架構圖和一份真實密碼清單(妥善保管,不進任何 repo)。

    階段二:AI 讀懂你的系統

    這是大多數人忽略的步驟,也是決定 AI 協作能否成功的關鍵。

    AI 第一件事不是寫 code。

    在 AI 動手之前,它需要讀你 10 年的系統。這個過程大概需要幾天,但會產出你可能從未有過的東西:

    AI 讀完後的產出 為什麼重要
    系統架構圖 你們可能自己也沒有,新人上手和遷移規劃的基礎
    模組依賴關係 知道改哪個地方會影響哪些服務
    高風險區域標記 「這段 code 10 個人改過,有 3 個已知 bug 的修復」
    技術債清單(按影響排序) 知道先解決什麼,不是看到髒的就改
    新人上手文件 從 code 反推出來,不需要老工程師口傳

    這個階段同樣不動任何 code。AI 只是閱讀和理解。等到真正開始寫新功能時,AI 已經知道你的系統慣例是什麼、有哪些地雷不能踩。

    階段三:建立平行的新軌道

    新建一個乾淨的 repo,在旁邊建立完整的現代化工作流,舊 repo 繼續照舊運作

    舊 GitLab repo(繼續跑,不動)
         │
         │  正在服務用戶的系統
         │
    新 repo(乾淨起點)
         │
         ├─ 正確的 .gitignore(.env 全部排除)
         ├─ CI/CD pipeline(gitleaks + build + sign)
         ├─ K8s Secrets(從快照搬進來)
         └─ Branch Protection Rules

    把真實密碼從快照搬到 K8s Secrets(不是從舊 repo 搬,是從正在跑的進程抽出來):

    # 從快照建立 K8s Secrets
    kubectl create secret generic app-prod-creds \
      --from-literal=DB_PASSWORD="$(grep DB_PASSWORD /tmp/real-env-snapshot.txt | cut -d= -f2)" \
      --from-literal=SHOPEE_KEY="$(grep SHOPEE_KEY /tmp/real-env-snapshot.txt | cut -d= -f2)" \
      -n production
    
    # 建立完成後,安全刪除快照
    shred -u /tmp/real-env-snapshot.txt

    階段四:一次搬一個服務

    這是遷移的主體。原則是:每次只搬一個服務,驗證通過才搬下一個

    服務搬移的優先順序建議:

    優先順序 選擇原則 理由
    第一批 流量最小的非核心服務 風險最低,可以放心試錯
    第二批 獨立性高、依賴少的服務 不會牽一髮動全身
    最後 核心業務邏輯(訂單、付款) 等前幾批證明新流程可靠後再動

    每個服務的搬移步驟:

    1. AI 在新 repo 重寫該服務的乾淨版本(理解舊 code 後重寫,不是 copy paste)
    2. Jenkins 構建新鏡像,部署到 Staging
    3. 用影子流量驗證新舊行為一致(新舊同時收請求,比對回應)
    4. 確認無誤,切換這個服務的流量到新系統
    5. 觀察 24-48 小時
    6. 舊服務下線

    影子流量驗證(用 nginx 實現,不影響用戶):

    # nginx 配置:新舊同時收請求,比對行為
    location /api/orders {
        mirror /mirror-new;       # 複製請求到新系統
        proxy_pass http://old-api; # 用戶實際收到舊系統的回應
    }
    
    location = /mirror-new {
        internal;
        proxy_pass http://new-api;  # 新系統的回應只寫 log,不回傳用戶
    }

    AI 在整個過程中的實際角色

    理解了遷移方法論後,AI 在這個過程中的職責非常清楚:

    AI 能做(且做得很好):
      ✅ 讀 10 年的舊 code,產出架構文件
      ✅ 識別技術債,按優先順序排列
      ✅ 用「符合你們系統慣例」的方式寫新功能
      ✅ 在 feature branch 開發,開 MR,等你 review
      ✅ 補測試(補現有 code 的測試覆蓋率)
      ✅ Debug(分析 log,找根因)
      ✅ 在新服務和舊系統的接縫處寫 adapter
    
    AI 不能做(也不應該做):
      ❌ 直接 push 到 main 或 production branch
      ❌ 決定什麼時候切換流量
      ❌ 接觸任何密碼或 production secrets
      ❌ 在沒有充分理解舊系統的情況下動手

    一個典型的 AI 協作對話長這樣:

    你:「我要加一個批次出貨的功能」
    
    AI:「讓我先讀一下現有的出貨相關 code...
         我看到 OrderService.java 的 shipOrder()
         和 ShipmentRepository,還有一個舊的
         BatchJob 在 legacy/ 目錄。
    
         注意:legacy/BatchJob 有個已知的
         transaction 問題(commit a3f2c1b 的修復紀錄),
         新功能要避開。
    
         建議的實作方向是...你確認嗎?」
    
    你:「方向對,去做吧」
    
    AI:「好,我開 feature/batch-shipment,
         預計改動:
         - 新增 BatchShipmentService(核心邏輯)
         - 修改 OrderController(加 API endpoint)
         - 不動 legacy/ 目錄
         
         完成後開 MR 給你 review」

    AI 因為讀過 10 年的系統,不會做出「不符合你們慣例」的 code,也不會踩進已知的地雷。

    最常遇到的三個現實問題

    問題一:「不知道哪個 .env 是現在真正在用的」

    10 年的系統通常有多個 .env 版本,有的是舊的,有的是工程師自己改過的。以正在跑的進程為準,不要相信文件

    # 找到 Java 進程真正使用的環境變數
    cat /proc/$(pgrep java)/environ | tr '\0' '\n' | grep -E "DB_|API_|SECRET_"
    
    # 不要用
    cat .env  # 這可能是 6 個月前的版本

    問題二:「團隊還在開發,不能凍結」

    不需要凍結。舊 repo 繼續用,新 repo 並行開發。重要的 bugfix 透過 cherry-pick 同步:

    舊 repo: feature → dev → main(繼續照舊)
                  │
                  └─ cherry-pick 重要修復
                          │
    新 repo:              └─ feature → dev → main(新流程)

    問題三:「歷史 commit 有密碼怎麼辦」

    分兩步處理:

    1. 立即輪換所有洩露的密碼 — 因為 GitHub/GitLab 可能已有快取,這一步不能等
    2. 舊 repo 等到遷移完成後再 archive — 不需要現在重寫歷史,新 repo 一開始就是乾淨的

    注意:很多人以為 git rm --cached .env 就安全了,但舊 commit 裡的內容仍然可以被 git show <old-commit>:.env 讀出。唯一的技術修復是 git filter-repo,但遷移方法論讓你可以跳過這一步——因為所有新開發都在新的乾淨 repo 上進行。

    時間軸規劃

    時間 工作 風險
    第 1 週 快照現況、輪換外部 API Key、AI 開始讀系統 零(不動任何 code)
    第 2-4 週 建新 repo、CI/CD pipeline、K8s Secrets、第一個服務搬過去 低(影子流量驗證)
    第 1-3 個月 逐服務遷移,每個驗證 24-48 小時後才繼續 中(每次只影響一個服務)
    遷移完成後 舊 repo archive、完整安全強化(RBAC、Audit Log、鏡像簽名) 低(新系統已穩定)

    決策樹:你現在該從哪裡開始

    你的系統現在能跑嗎?
      │
      ├─ 能跑 → 用 Strangler Fig 模式(本文的方法)
      │           │
      │           ├─ 步驟 1:快照現況(本週就做)
      │           ├─ 步驟 2:AI 讀系統(同步進行)
      │           ├─ 步驟 3:建新軌道(第 2-4 週)
      │           └─ 步驟 4:逐服務遷移(之後)
      │
      └─ 不能跑 → 先讓它跑起來,再回到這裡

    這套方法論的本質

    Strangler Fig 模式應用在 AI 輔助開發遷移上,核心洞察只有一個:

    「10 年的技術債是過去的決策的結果,你無法在不破壞現有價值的情況下一次消除它。但你可以選擇:從今天開始,所有新的工作都用正確的方式做。」

    舊系統是你團隊的集體記憶,AI 有能力閱讀並理解這些記憶,然後用現代化的方式繼續往前走。不需要推倒重建,也不需要凍結開發去修舊債——只需要一條平行的新軌道,和耐心地一次移動一個服務。

    想了解新軌道的 CI/CD 具體設計,可以參考上一篇文章:AI 輔助開發 CI/CD 工作流:Jenkins、K8s、ISO 27001 完整設計

  • AI 輔助開發 CI/CD 工作流:Jenkins、K8s、ISO 27001 完整設計

    重點摘要

    • AI 只負責寫 code、提 PR,不碰版本決策和 Production 部署,人類保留最終控制權
    • 透過 Git tag 觸發 Jenkins,Staging 全自動部署、Production 手動 helm 執行,兩階段驗證才上線
    • 敏感資訊三層隔離:.gitignore → K8s Secrets → etcd 加密,密碼永遠不進 repo
    • 補齊 RBAC、Audit Log、鏡像簽名、Secrets Scan 四大安全缺口,達到 ISO 27001 合規

    AI 輔助開發越來越普遍,但大多數團隊面臨同一個問題:AI 寫的 code 要怎麼安全地上線? 誰決定部署時機?密碼怎麼管?如果 AI 出錯了,有什麼防護網?

    本篇文章完整說明 ONEEC OMS 系統實際採用的 AI 協作工作流設計,包含完整的 User Story、Jenkins Pipeline 架構、三環境部署策略,以及通過安全審查後補齊的 RBAC、Audit Log、鏡像簽名等安全強化配置。

    核心設計理念:人類掌控節奏,AI 加速執行

    這套工作流的核心原則只有一句話:AI 是高效能的執行者,不是決策者。具體體現在以下四點:

    • AI 負責:寫 code、建 Dockerfile、提 PR、提供 Jenkins script 和 Helm chart
    • 用戶負責:code review、創建 git tag(決定版本和部署時機)、手動 helm 部署到 Production
    • 運維負責:管理 K8s Secrets、設定 Jenkins credentials、維護集群
    • 敏感資訊:密碼、API Key、SSL 憑證永遠不進入 Git repo

    完整 User Story:從需求到上線的 10 個步驟

    以下用一個真實場景說明整個流程:場景:優化訂單 API 的查詢效能

    Step 1:AI 開發(feature branch)

    AI 從 dev 分支切出 feature branch,完成開發後推送 PR:

    # AI 執行
    git checkout dev && git pull origin dev
    git checkout -b feature/order-api-optimize
    
    # 編寫程式碼...
    
    # 本地驗證
    docker-compose up -d
    curl http://localhost:8080/api/orders?status=pending
    # ✅ 回傳正確,效能提升 30%
    
    # 提交並推送
    git add . && git commit -m "feat(order-api): optimize query performance"
    git push origin feature/order-api-optimize
    # 建立 PR → dev

    Step 2:用戶 Code Review & Merge

    用戶在 GitHub UI 審查 PR:確認邏輯正確、有測試覆蓋、無敏感資訊後 approve 並 merge 到 dev。此時沒有任何自動化觸發,代碼靜靜等待部署決策。

    Step 3:用戶創建 Staging Tag → Jenkins 自動觸發

    用戶決定要部署到測試環境時,創建一個 staging-v* tag:

    # 用戶執行
    git tag staging-v1.0.1
    git push origin staging-v1.0.1
    
    # GitHub Webhook → Jenkins 自動執行:
    # ├─ Secrets 掃描(gitleaks)
    # ├─ docker build(所有 pods)
    # ├─ cosign 簽名鏡像
    # ├─ docker push to registry
    # ├─ helm deploy to Staging K8s(使用 values-staging.yaml)
    # └─ 通知用戶:Staging v1.0.1 is live

    Step 4:用戶在 Staging 驗證

    kubectl get pods -n staging
    curl https://staging-api.example.com/api/orders?status=pending
    # ✅ 功能正常,效能優化生效
    # ✅ 錯誤率 0%
    # ✅ 回應時間 < 100ms

    Step 5:用戶創建 Production Tag → Jenkins 構建正式鏡像

    # 用戶執行(確認 Staging 無誤後)
    git tag v1.0.1
    git push origin v1.0.1
    
    # Jenkins 執行:
    # ├─ Secrets 掃描
    # ├─ docker build(所有 pods,tag 改為 v1.0.1)
    # ├─ cosign 簽名鏡像
    # ├─ docker push to registry
    # ├─ 生成 Helm values(不含敏感資訊)
    # └─ 通知用戶:Images ready, run helm command

    Step 6:用戶手動部署到 Production

    Production 部署是整個流程中唯一純手動的步驟,這是刻意設計的——確保每一次正式上線都有人類判斷:

    # 用戶在本機執行
    helm upgrade --install order-api \
      /path/to/your/prod-configs/order-api/values-prod.yaml \
      --set image.tag=v1.0.1 \
      -n production
    
    # K8s 自動從 Secrets 注入密碼、API Key
    # Kyverno 自動驗證鏡像簽名(未簽名直接拒絕)
    # Deployment 完成 ✅

    Step 7:監控確認上線成功

    kubectl get pods -n production
    curl https://api.example.com/api/orders?status=pending
    # ✅ 正式環境驗證通過,上線成功

    三個部署環境的定義與分工

    環境 用途 部署方式 配置來源 觸發者
    Dev 本地開發驗證 docker-compose up .env.dev AI(開發時)
    Staging 測試環境(K8s) Jenkins 自動部署 values-staging.yaml(在 repo) 用戶(tag 觸發)
    Production 正式環境(K8s) 手動 helm 部署 values-prod.yaml(用戶維護)+ K8s Secrets 用戶(手動執行)

    Jenkins Pipeline 完整架構

    Jenkins Pipeline 由 GitHub Webhook(tag push)觸發,整個流程分為 6 個 Stage:

    Stage 0:Secrets 掃描(安全門控)

    這是整個 Pipeline 的第一道防線,也是最重要的安全門控。使用 gitleaks 掃描 repo 中是否含有密碼、API Key 等敏感資訊,發現即中止構建並通知安全告警

    stage('Secrets Scan') {
        steps {
            sh '''
                gitleaks detect \
                  --source . \
                  --config .gitleaks.toml \
                  --exit-code 1 \
                  --report-format json \
                  --report-path gitleaks-report.json
            '''
        }
        post {
            failure {
                sh 'sh scripts/notify-security-alert.sh ${TAG_NAME} gitleaks-report.json'
                error('❌ Secrets 掃描發現敏感資訊,構建中止!')
            }
        }
    }

    Stage 1:Tag 偵測(決定部署目標)

    根據 tag 名稱判斷本次構建的部署目標:

    stage('Detect Tag') {
        steps {
            script {
                if (env.TAG_NAME =~ /^staging-v.*/) {
                    env.DEPLOYMENT_ENV = 'staging'
                } else if (env.TAG_NAME =~ /^v.*/) {
                    env.DEPLOYMENT_ENV = 'production'
                } else {
                    error("❌ 未知 tag 格式: ${env.TAG_NAME}")
                }
            }
        }
    }

    Stage 2:Build Images

    構建所有 Pod 的 Docker 鏡像。鏡像本身不含任何配置、密碼、API Key,這是配置與代碼分離的核心原則:

    #!/bin/bash
    # scripts/build-docker.sh
    TAG=$1
    
    docker build -t registry.example.com/order-api:${TAG} ./simpleec-api
    docker build -t registry.example.com/user-app:${TAG} ./user-app
    docker build -t registry.example.com/channel-job:${TAG} ./simpleec-channel-job
    # ... 所有 pods

    Stage 3:Sign Images(供應鏈安全)

    使用 cosign 為每個鏡像簽名,確保 Production 只能部署來自 Jenkins 的受信任鏡像:

    stage('Sign Images') {
        steps {
            withCredentials([file(credentialsId: 'cosign-private-key', variable: 'COSIGN_KEY')]) {
                sh 'sh scripts/sign-docker.sh ${TAG_NAME} ${COSIGN_KEY}'
            }
        }
    }
    
    # scripts/sign-docker.sh
    for IMAGE in "${IMAGES[@]}"; do
        cosign sign --key "${COSIGN_KEY}" \
          --tlog-upload=false \
          "${IMAGE}"
    done

    Stage 4:Push Images

    推送到 Docker Registry。Registry 啟用 Immutable Tags,同一個 tag 無法被覆蓋,確保版本不可篡改:

    stage('Push Images') {
        steps {
            withCredentials([usernamePassword(
                credentialsId: 'docker-registry-creds',
                usernameVariable: 'REGISTRY_USER',
                passwordVariable: 'REGISTRY_PASS'
            )]) {
                sh 'sh scripts/push-docker.sh ${TAG_NAME}'
            }
        }
    }

    Stage 5a(Staging):自動部署到 Staging K8s

    stage('Deploy to Staging') {
        when { expression { env.DEPLOYMENT_ENV == 'staging' } }
        steps {
            withCredentials([file(credentialsId: 'kubeconfig-staging', variable: 'KUBECONFIG')]) {
                sh '''
                    helm upgrade --install order-api ./k8s/helm/order-api \
                      --values ./k8s/helm/order-api/values-staging.yaml \
                      --set image.tag=${TAG_NAME} \
                      -n staging
                '''
            }
        }
    }

    Stage 5b(Production):生成 Helm Values,通知用戶手動部署

    對於 Production tag,Jenkins 不自動部署,而是生成配置檔並通知用戶手動執行:

    stage('Generate Helm Values') {
        when { expression { env.DEPLOYMENT_ENV == 'production' } }
        steps {
            sh 'sh scripts/generate-helm-values.sh ${TAG_NAME}'
            // 生成 values-v${TAG_NAME}.yaml(不含敏感資訊)
            // 通知用戶:Images ready, run helm command
        }
    }

    Helm 配置隔離:敏感資訊三層防護

    配置分為三層,層層隔離:

    第一層:values-staging.yaml(在 repo,測試配置)

    # 主機名用占位符,從 Jenkins 環境變數注入,不硬編碼內網地址
    env:
      DATABASE_HOST: "${POSTGRES_STAGING_HOST}"
      DATABASE_NAME: simpleec_test
      REDIS_HOST: "${REDIS_STAGING_HOST}"
      API_LOG_LEVEL: DEBUG

    第二層:values-prod.yaml(用戶本機維護,不進 repo)

    # 用戶的私密文件,只在本機
    env:
      DATABASE_HOST: postgres-prod.example.com
      API_LOG_LEVEL: WARN
      # ⚠️ 資料庫密碼不在這裡!從 K8s Secrets 注入
    
    envFrom:
      - secretRef:
          name: database-prod-creds  # K8s Secret(運維管理)
      - secretRef:
          name: api-keys-prod        # K8s Secret(運維管理)

    第三層:K8s Secrets + etcd 加密

    # 運維在 Production K8s 上創建
    kubectl create secret generic database-prod-creds \
      --from-literal=username=prod_user \
      --from-literal=password=<secure-password> \
      -n production
    
    # K8s 預設 Secrets 以 base64 存在 etcd(並非加密!)
    # 必須啟用 encryption at rest
    # /etc/kubernetes/encryption-config.yaml
    apiVersion: apiserver.config.k8s.io/v1
    kind: EncryptionConfiguration
    resources:
      - resources: ["secrets"]
        providers:
          - aescbc:
              keys:
                - name: key1
                  secret: <base64-encoded-32-byte-key>

    安全強化:補齊四大缺口

    原始設計經過安全審查後,發現四個必須在投產前補足的缺口:

    缺口一:K8s RBAC 未定義

    三個角色各有最小權限(文件放在 k8s/rbac/):

    角色 允許操作 明確禁止
    Jenkins SA(staging) update/patch Deployments, get Pods 讀取任何 Secrets
    用戶(production) helm 部署相關資源 讀取業務 Secrets(DB 密碼、API Key)
    運維(production) Secrets 完整管理權
    # 驗證 Jenkins SA 無法讀取 Secrets(應輸出 no)
    kubectl auth can-i get secrets \
      --as=system:serviceaccount:staging:jenkins-deployer \
      -n staging

    缺口二:K8s Audit Log 未配置

    ISO 27001 A.12.4.1 要求所有敏感操作都要有日誌。以下 Audit Policy 至少記錄 Secrets 訪問和 Deployment 變更:

    # /etc/kubernetes/audit-policy.yaml
    apiVersion: audit.k8s.io/v1
    kind: Policy
    rules:
      - level: Metadata
        resources:
          - group: ""
            resources: ["secrets"]  # 所有 Secrets 訪問都記錄
    
      - level: Request
        verbs: ["create", "update", "delete", "patch"]
        resources:
          - group: "apps"
            resources: ["deployments"]
    
      - level: None
        users: ["system:kube-proxy"]
        verbs: ["watch", "list"]

    缺口三:鏡像簽名驗證(Kyverno 準入控制)

    確保集群只能部署來自 Jenkins 簽名的鏡像,防止鏡像替換攻擊:

    apiVersion: kyverno.io/v1
    kind: ClusterPolicy
    metadata:
      name: verify-image-signatures
    spec:
      validationFailureAction: Enforce  # 未簽名鏡像直接拒絕
      rules:
        - name: check-image-signature
          match:
            any:
              - resources:
                  kinds: ["Pod"]
                  namespaces: ["staging", "production"]
          verifyImages:
            - imageReferences:
                - "registry.example.com/*"
              attestors:
                - count: 1
                  entries:
                    - keys:
                        publicKeys: |-
                          -----BEGIN PUBLIC KEY-----
                          # cosign.pub 內容
                          -----END PUBLIC KEY-----

    缺口四:GitHub Branch Protection 口頭約定 → 技術強制

    分支 Required Reviews CI 必須通過 Push 限制
    main 2 人 approve ✅ jenkins-build + secrets-scan 僅 team-lead
    staging 1 人 approve ✅ jenkins-build + secrets-scan 僅 team-lead
    dev 1 人 approve 必須透過 PR(AI 不能直接 push)

    Git 分支策略與 Tag 命名規範

    整個工作流的分支拓撲如下:

    main                     # Production 對應,受嚴格保護
     └─ tag: v1.0.0, v1.0.1  # 觸發 Jenkins 構建 Production 鏡像
    
    staging                  # 測試環境,中度保護
     └─ tag: staging-v1.0.0  # 觸發 Jenkins 自動部署到 Staging K8s
    
    dev                      # 開發積累,AI 透過 PR 提交
     └─ 來源:feature/* 合入
    
    feature/*                # AI 的工作分支(每個功能一個)
     ├─ feature/user-auth
     ├─ feature/order-api
     └─ feature/channel-job-momo

    敏感資訊完整隔離架構

    存放位置 可以存什麼 絕對不能存什麼 管理者
    Git Repository 代碼、Dockerfile、values-staging.yaml、Helm chart 模板 密碼、API Key、SSL 憑證、values-prod.yaml AI + 用戶
    Docker Registry 不含配置的乾淨鏡像(cosign 簽名) 任何敏感資訊 Jenkins(push)
    K8s Secrets(etcd 加密) database-prod-creds、api-keys-prod、SSL 憑證 運維
    Jenkins Credentials GitHub token、Registry credentials、cosign key、kubeconfig 運維

    回滾策略

    Staging 環境回滾

    # 快速回滾到上一個版本
    helm rollback order-api 0 -n staging
    
    # 或指定版本
    helm upgrade order-api ./k8s/helm/order-api \
      --values ./k8s/helm/order-api/values-staging.yaml \
      --set image.tag=staging-v1.0.0 \
      -n staging

    Production 環境回滾

    # 查看部署歷史
    helm history order-api -n production
    
    # 回滾到上一個版本
    helm rollback order-api 0 -n production
    
    # 所有 tag 在 Git 可追溯
    git log --oneline --all | grep "v1.0"

    投產前安全檢查清單

    在正式上線前,以下所有項目必須確認通過:

    代碼倉庫安全

    • ✅ .gitignore 包含 .env, .env.dev, **/values-prod.yaml
    • ✅ repo 根目錄存在 .gitleaks.toml 配置文件
    • ✅ pre-commit hook 已安裝
    • ✅ git log –all — ‘*.env’ 確認歷史中無敏感文件

    Jenkins Pipeline

    • ✅ 第一個 Stage 為 Secrets Scan(gitleaks)
    • ✅ Sign Images Stage 已配置(cosign)
    • ✅ Push Images 使用 Jenkins Credentials(非明文)
    • ✅ GitHub Webhook Secret 已配置(Jenkins + GitHub 雙端)

    K8s 訪問控制

    • ✅ k8s/rbac/ 三個 RBAC 文件已 apply
    • ✅ Jenkins SA 驗證:kubectl auth can-i get secrets … → no
    • ✅ Kyverno 已安裝,鏡像簽名驗證策略已 apply
    • ✅ etcd encryption at rest 已啟用(運維確認)

    審計和監控

    • ✅ K8s Audit Log 已配置(audit-policy.yaml)
    • ✅ Audit Log 保留策略 ≥ 90 天
    • ✅ 告警規則已配置(部署失敗、Secrets 掃描失敗)

    總結:這套工作流解決了什麼問題?

    AI 輔助開發的核心挑戰不是技術,而是信任邊界:誰能做什麼?誰為每個決定負責?這套工作流的答案很清楚:

    • AI 的邊界:寫 code、提 PR、建 Docker image — 技術執行層
    • 用戶的邊界:review 代碼、創建 tag、手動部署 Production — 決策層
    • 運維的邊界:管理 Secrets、維護集群、配置 credentials — 基礎設施層
    • 自動化的邊界:Jenkins 在 tag 觸發後執行既定腳本 — 不越界,不決策

    這種分層設計讓 AI 協作既高效又安全,每一個部署都有完整的審計軌跡,每一個敏感操作都需要人類授權。

  • git flow說明

    git flow

    網路版本

    我方版本

    gitGraph
           commit id: "main"
           branch develop
           commit id: "init"
           branch feature1
           commit id: "feature1"
           checkout develop
           branch feature2
           commit id: "feature2"
           checkout feature2
           branch mergeFeature
           merge  feature1
           commit id: "merge12ToDev"
           commit tag:"dev-0.0.1"
           branch ga
           commit id: "toPro"
           commit tag:"ga-0.0.1"
           checkout main
           merge  ga
           commit id: "mergeAllFeature"
           checkout develop
           merge  main
           commit id: "202301XX"
           checkout develop
           branch feature3
           commit id: "feature3"
           checkout develop
           branch feature4
           commit id: "feature4"
           checkout mergeFeature
           merge  feature3
           merge  feature4
           commit id: "merge34ToDev"
    
    

    主要的幾個分支

    main(有時候是master)

    基本上每一次上板後都要merge回這一個分支,此分支為主要基底,方向只有ga可以改回來他,平常不可改動他,任何情境下都不可以

    (閱讀全文…)

  • 分散式資料庫訊息

    重要觀念

    分散式資料庫鎖

    https://www.gushiciku.cn/pl/gkKm/zh-tw

    (閱讀全文…)

  • 各家API的比較

    目標們:
    * 91APP
    * cyberbiz
    * EasyStore
    * friday
    * iopenmall
    * MOMO
    * MO+
    * PChome
    * shopify
    * Shopline
    * Yahoo購物中心
    * 樂天
    * 東森
    * 博客來
    * 蝦皮
    * Coupang
    * 露天

    (閱讀全文…)

  • 推薦的V1新算法

    定義參數

    可超接的量 =(設定可超接==null)? 0: (可超接的量有值)?可超接值: 近兩天的訂單中此料號的量總和

    (閱讀全文…)

  • 成本預估故事

    Cost

    所有服務

    服務 VM VCPU Memory size(GB) Hard disk size(GB)
    K8s(Run 69 Pods) 3 16 128 200
    Solr 2 16 16 200
    PostgreSQL 2 16 32 600
    Kafka 3 4 4 80
    ZooKeeper(zk01) 1 16 8 20
    ZooKeeper(zk02,zk03) 2 4 4 20
    Infinispan 2 2 4 20
    HAProxy 2 4 4 20
    Nginx 2 2 4 50
    GitLab 1 8 8 100
    Jenkins 1 2 4 50
    Harbor Registry (IMG Hub) 1 2 2 100
    Elasticsearch 1 8 8 750
    Logstash 1 4 4 20
    Kibana 1 4 8 100
    DNS 1 2 2 16
    MAIL Server 1 4 4 20
    Object Storage (Ceph) 3 4 4 150

    故事

    2021

    5月 我加入精誠,非Oneec身分,但是閒暇時會與Ethan進行相關的討論,並且不時會看SHOPEE跟東森的API文件思考架構
    8月 infra加入精誠,非Oneec身分,但是Ethan已經準備好了技術選型並且請這位Infra整理機器,清理空間
    9月 PM加入精誠 ,Oneec身分,Ethan請她進行思考
    10月 最強的全端RD入場,Oneec身分,Ethan請他跟Infra準備K8S環境底下的高可用環境程式
    12月 還在討論Topic,12月中 全端RD回報,準備好了,開工

    (閱讀全文…)

  • kafka 綜合筆記

    名詞介紹

    1. Producer:訊息生產者
    2. Broker:傳遞訊息的中介者
    3. Consumer:訊息消費者
    4. Topic:訊息的主題
    5. Partition:主題內的分區
    6. ComsumerGroup:消費者群組

    Producer

    只要是發送訊息出去的都是這一個腳色,定位上是往kafka push queue的就是。

    (閱讀全文…)