
柏修斯接到的任務,是要帶回梅杜莎的首級,梅杜莎是一個蛇妖,任何人只要正眼看見她,就會立刻變成石頭,而梅杜莎的兩個姊姊還是不死之身。
所以他沒有急著挑戰梅杜莎,而是在雅典娜與荷米斯的引導下,先找到格賴埃三姊妹,再從她們口中問出寧芙所在的位置。
寧芙交給他一些裝備,能在空中移動的飛鞋、裝得下梅杜莎首級的袋子、戴上後不會被看見的黑帝斯頭盔。
荷米斯給了他一把能夠砍下梅杜莎首級的彎刀,雅典娜則在關鍵時刻,用青銅盾映出梅杜莎的倒影,引導他的手避開那道會把人變成石頭的目光。
準備完成後,柏修斯才飛到蛇髮女妖居住的地方,他看著盾牌裡的倒影砍下梅杜莎的首級,立刻把它收進袋子,再靠頭盔躲過另外兩名女妖的追趕,最後活著完成任務回去。
柏修斯並不是出發之前就把全希臘最流行的神器裝滿背包,而是經過指引,慢慢替下一個已知的風險準備好對應的裝備,少了其中任何一件,都不一定有辦法完成任務,更別說安全回來。
我不是非常專業的 DevOps 講師,所以今天不會深入講解工具,也不會有實作,而是以自己身處 Legacy System 團隊的經驗,分享我們怎麼從一路流血流汗,到讓人流連忘返 XD
以前對 DevOps 的認知很天真,以為寫 pipeline 自動化、容器技術、單元測試之類的,工具跟技術堆疊愈多,好像就離現代化團隊愈近,後來才發現如果一口氣就在 Legacy System 上做技術堆疊轉型,反而會造成很多反效果。
有多可怕?這得從開始導入測試說起了 TT
以前我們公司的開發模式,是非常傳統的資料驅動開發,最早的 Legacy Code 可以追溯到 VB6 時期的 ERP 桌面應用程式,這幾十年一路轉型到現在使用 .NET + Angular 做前後端分離,不過這樣的開發方式已經烙印十幾年了,大家也都習慣了,畢竟運作這麼多年,哪會有什麼問題?幹嘛有事沒事要導入什麼測試?
記得大概是我大二的時候吧我不會透露年齡的,有一堂必修課叫做「軟體工程」,老師在課堂上講到了設計模式跟 TDD 之類的東西,平常上課愛睡覺的我,整個超有興趣的:
「怎麼可能程式還可以這樣寫?」
這真的很有趣,可以把程式寫得出神入化,那麼地...「優雅」!從此刻開始我就徹底愛上軟工了,也開始拜讀眾大神的書籍,也就是這個時期為起點,慢慢從練習 TDD 開始向外延伸,不過喜歡歸喜歡,真的決定把測試帶進公司,還經歷過一場災難。
當時團隊想做技術轉型,所以請了一位專職 Playwright 工程師替系統做 E2E 測試,團隊一開始的想法聽起來蠻合理的,E2E 測試最貼近使用者操作,畫面怎麼點、資料怎麼送、最後又怎麼寫進資料庫,這種測試當然最有價值吧?連我也在想跟書上講的怎麼不一樣?是不是理論歸理論,現實歸現實。
後來回頭看,我才比較理解為什麼測試金字塔一直強調,不要把所有信心都壓在昂貴又慢的 E2E 上,開發變化的速度,很快就超過測試的速度了,只要我們功能一改,就是亮一片紅燈,整個腳本都要跟著調整,而每次要拿寫完的腳本來測我們的 code 又跑超久,久了心裡難免會想:
「我到底是在寫功能,還是在配合測試?」
當然另一邊也不好受,測試人員才剛把腳本修好,下一版功能又變了,開發覺得測試效率有夠差,測試覺得開發一直製造無法穩定驗證的變動,原本是為了提升品質才開始的轉型,最後卻變成兩邊看到紅燈就先覺得是對方問題,搞得整個團隊的心情都超差,速度跟品質都比以前還要爛,每次開會我們都會被老闆臭罵一頓,最後他也因為撐不下去就旅行去了,團隊暫時不再有請人來寫測試的念頭。
被折磨了一段時間後,我的救世主情節發作了 XD
我想,既然測試金字塔是對的,那我們是不是缺了更靠近程式碼、回饋更快的測試?加上我平常也會在 Kata 練習 TDD,於是便想直接把 TDD 和單元測試帶進團隊,還能幫公司省錢不用再請專職測試,一開始我真的有一種「這條路是對的!讓我來拯救這個團隊吧」的氣勢,後來才慢慢理解,難的不是技術,是人。
剛開始導入大家都很支持,我也從中做為指導,實際執行後,只能說是哀嚎遍野,各種負面情緒緊接著出現:
「我有沒有寫關你什麼事!」
「老闆有規定嗎?」
「我覺得我這禮拜又要被罵了...」
「你現在不要跟我講這些,你不懂!」

圖片節錄自網路
其實剛開始心裡還蠻挫折的,不過我個性就是比較強勢機車,加上運氣很好有主管支持,團隊成員也不是容易放棄的類型,所以最後還是慢慢透過帶頭效應讓大家去習慣這件事情,到了現在不管是什麼樣層級的測試,都變成是一種大家有共識的交付責任,而不是靠某個專職去做,開發的心情跟節奏也比過去還要來得好很多,隨著 Legacy System 慢慢開始有了測試做保護後,我們才有辦法繼續往後做延伸。
這次經驗也讓我第一次真正感受到,再好的初衷都可能招來挨罵,除非你像我一樣偏執又不要臉 XD
Day09 有提到我們原本都是怎麼做部署的,有多痛苦今天就不再多說了,我們來聊聊更細節的部分,我是怎麼把 CI/CD 引入團隊的。
一開始是因為專案真的太大了,每次要部署都要進 Server 自己手動處理,不過這也還好,反正大家一直都是這樣撐過來的啊!直到有一次,團隊成員部署時不小心把正式環境的設定檔蓋掉,服務當場中斷,客服那邊馬上接到大量客訴,整間公司突然都知道工程師正在部署了 XD
我們得先確認哪份才是正確設定、目前 Server 上是什麼、剛才蓋過哪些檔案,雖然可能這樣的狀況發生過很多次,而每次問題都會解決,但是!我那該死的救世主情結又出現了。
於是我便開始找看看有什麼 CI/CD 工具,繞了一圈後,接觸到了也許比較符合團隊的 Jenkins。

圖片節錄自網路
Jenkins 是一套可以自己架設的 Automation Server,透過 Pipeline 把 Build、Test、產生 Artifact 與 Deploy 排成固定階段,Pipeline 還能寫進 Jenkinsfile 跟著程式碼一起進版控,相較於其他選項,Jenkins 最大的特色是對執行環境有極高的控制權與整合彈性,公司既有的 Server、內網環境甚至各種新舊工具,都能直接整進來,真的就跟它的 icon 一樣,跟管家一樣什麼都做得到!
基於這些優點,我當下決定,就是它了!
我們公司的規模不大,所以基本上自由度算是蠻高的,加上老闆本身也是一個技術狂魔,在這裡只要我們願意,沒有什麼是不能嘗試的,所以我就借用公司的某台 Server 來架設 Jenkins,先把原本人工做的部署流程自動化,從此之後除了特殊狀況,不然基本上根本不用再進 IIS Server 手動複製檔案,實在是有種重獲新生的感覺 XD
很快地,團隊也開始享受到 Jenkins 所帶來的各種福利,一開始 CI 會先執行各種測試,並且部署上去,簡直香到不行,更爽的是直接開發完 git push,就會觸發 Jenkins,部署迭代的速度變快了,但問題也接踵而來。
以前開發分支、Hotfix 分支和不同時期留下的版本一多,我們就常常得先確認某個修正到底在哪一條分支,合併回來了沒,現在準備部署的又是哪一版,之前提到替特定客戶另外開 IIS 站台、換一組臨時網址,確實能先把其他客戶隔開,但只要臨時版本繼續活著,Git、Server 與資料庫的狀態就會一起分岔,後面還得有人記得怎麼合併、何時關站,以及哪一份設定才能算數。
隨著我們已經堆疊了許多測試以及也有了 Jenkins 這一大利器後,我們覺得應該沒有什麼是沒辦法解決的了吧?我想說既然最後所有修改都還是要回到同一條主線,那我們是不是可以不要讓分支活那麼久?只要不要讓客戶停機就好了嘛!
經過我們討論過後,決定了,就這麼做!反正之後 Push 後都會先部署到 Staging,通過驗證與 Gate 後才會 Swap 到 Production,我們以為這就是所謂的 Trunk-Based Development(主幹式開發),當下確實非常開心!客戶呢?好像不這麼覺得...
久而久之,我們的認知就開始出現問題了,剛開始確認我們的測試都會通過 CI ,最後 CD 到 Staging 環境之後還會再去手動測測看,但是!就是因為太舒適了,迭代太快了,我們遺忘了初衷是什麼?除了改善手動部署成本之外,還有更重要的「品質」。
主幹式開發加上自動化後,我們把變更送出去的速度變快了,但原本藏在流程裡的品質問題,也用同樣的速度被送了出去,在相同時間內 Bug 的數量竟然是之前的好幾倍....以前人工部署還會讓人停下來多看兩眼,現在只要一 Push,錯誤版本也能用完全相同的效率到正式環境。
我們似乎把測試綠燈這件事情當作是系統品質的保證了,所以就理所當然覺得 CI 過了,系統應該就不會有問題才對,所以連最後到 Staging 親自使用的這件事情也不做了,以前在沒有 Jenkins 的時候明明就會做的啊!為什麼有了工具幫我們省下時間後,這些事情反而不做了呢?
首當其衝的是我,因為是我導入這些東西的,團隊開始在想:
是變方便了沒錯,但是好像問題沒有解決
其實我想了一下,覺得其實工具並沒有問題啊!有問題的是我們吧?
所以我約大家開個會,想要大家一起來解決這些問題,在我們一致認同過後,決定把工具跟測試都先拋在一旁,我們先從開發為出發點做討論。
討論到一半大家才發現,這些問題根本不是導入 Jenkins 或 Trunk-Based Development 以後才出現的,它們只是把我們原本就沒有建立好的觀念放大。

圖片擷取自網路
順著每一次出包的過程做討論,我們最後整理出幾個最明顯的問題:
整理完上述問題後,我們沒有馬上就解決,而是一步一步改掉工作的方式,試錯、被罵、再調整,最後才長成一套流程。
現在我們團隊有一個最基本的共識:
進入主幹的每一個 Commit,都要是可以部署的程度。
這不代表每次 Commit 都必須立刻發布給客戶,而是任何一個 Commit 通過 Build 與測試後,Jenkins 都能產生一份可識別的 Artifact,我們也會替 Artifact 記錄 Jenkins fingerprint,往後要找某個檔案由哪次 Build 產生、又被哪些流程使用時,至少不用靠壓縮檔名稱通靈。
而因為 commit 變小了,所以我們做了一件很大膽的事情,要一條線就一條到底,我們甚至把做到一半的功能,也跟著小批次進入主幹與部署流程。
「蛤!?那使用者不就會看到半套功能嗎?」
既然本來功能就要去控權限了,那為什麼我們不現在就控要等到全部做完再控呢?所以我們透過權限機制去控制,把還沒完成或還沒決定公開的功能隱藏起來,或是由後台控制哪些客戶可以優先使用,這真的花了不少心血呢 :)
一開始我們常常哪個 service 忘記控,哪個設定忘記補上,但是!因爲團隊慢慢地習慣讓每個 Commit 都小到可部署的程度,導致我們修正的速度真的很快,到了現在我們基本上已經烙印在團隊基因裡了。
有了更小的可部署粒度,我們才敢進一步做「認知交換」,假設這次 Sprint 有三個新功能,團隊剛好也是三個人,以前通常是一個人將一個 Item 前後端全部自幹,最後每個功能都只有一顆腦袋知道,現在我們不讓這三個人各自完成一個 Item,而是把所有 Item 的所有 Task 打散,大家交換著做,甚至是用抽的 XD
每天站立會議也不只是輪流報告做到哪裡,我們會交換完成的 Task 的脈絡還有在寫的過程看到另一個人寫的哪裡好像怪怪的。
聽起來真是夠殘忍的,一開始我們也真的不習慣,但是不習慣都是習慣來的,到了現在大家還活得好好的,遇到問題時對焦的速度,體感上真的改善很多。
這種做法要能運作,Task 就不能只寫一句「完成 XX 功能」,切得太大或驗收條件不清楚,接下 Task 的人就要花很多時間去想,所以我們會在開發前更積極地對齊需求,開發期間如果工程師做到一半覺得怪怪的,對 Domain 不太熟悉,也會直接打電話跟客戶聊天(我是講真的,但不會亂承諾什麼啦!),而需要複雜度較高的 Task 就會做 Pair,Review 也交換著做,甚至一起做。
這樣做有一點必須要做到,就是:
對自己要有責任,對彼此都要殘忍
我們覺得這樣的方式,讓某個功能不再只屬於某個人,程式碼品質確實也有所提升,需求比起以前來說也更清晰,這不是我自己講的喔,是大家一致認可的。
需求有對齊、Task 也切小後,我們還是得把沿途的證據留下來,不然半年後看到一段 Code,照樣不知道幹嘛要這樣寫。
客戶提出問題後,我們會先把客服紀錄或討論結果整理成 Work Item,寫下情境與驗收條件,再拆成足以讓不同人交換開發的小 Task,member 接手後,Commit 和 PR 都會連接到這張 Work Item,PR 裡則留下改了什麼、怎麼驗證。
接著換 Reviewer 進行 Code Review,除了看程式碼,也順便確認這次修改是否真的對應 Work Item 裡的驗收條件,如果需求本身還有歧義,就在合併前把問題拉回團隊討論,而不是等功能進到 Staging 才發現大家理解的不一樣,通過 Review、Merge 進主幹之後,由 Jenkins 觸發 CI,從這個版本建置並產生 Artifact,再把這份 Artifact 部署到 Staging,負責驗收的人再照著 Work Item 裡寫下的條件操作一遍。
正式環境最後發布的是同一份 Artifact,不是到了 Production 才重新 Build 一次,所以在 Staging 確認的東西跟上線的是同一包,不用擔心「Staging 過了、Production 卻長不一樣」的烏龍。
如果 Staging 驗收沒有通過,就把結果記錄回 Work Item,再用新的 Commit 與 PR 修正,因為原本的 PR 已經 Merge,它代表的是「當時那一次變更」,所以不需要回頭修改歷史,新的問題就讓新的變更留下新的紀錄。

以前這些關聯都四散各地,現在透過 Azure DevOps 與 Jenkins 把連結保存下來,至於每個詳細內容,還是得由團隊寫清楚。
這樣做的價值,是讓需求、討論、程式碼與部署版本終於能沿著同一條鏈路往回追查,半年後看到一段奇怪的 Code,不必再靠考古或通靈,可以一路從 Commit 找回 PR、Work Item,甚至當初的客服紀錄,Review 時如果發現驗收條件根本沒寫清楚,也能順著同一條路找到需求來源。
眾多的 Legacy System 其實都是啞巴,他生病了還是怎樣,都是客戶先來跟我們講我們才知道,我們想要盡量做到:
讓客戶不知道系統有問題
也就是在客戶發現之前,我們就盡早知道。
Observability 可觀測性:指的是我們能不能從系統釋放出的訊號理解它內部正在發生什麼,在 Legacy System 不可能突然把 log 跟告警都補齊,所以我們是先這樣做的:
有問題發生時,我們至少能更快縮小範圍,沿著 ID 找到同一次操作留下的 Log,而這些有標籤特徵的 log,要再回推到需求單本身就不是問題了。
到了後來,我們也在 Grafana 上建立很多儀表板,成了我們的戰情室,甚至是業務的攻略地圖 XD
回頭看,我們並不是一開始就決定做這麼多事情,每一次往前,都是因為原本的做法開始露出下一個問題,為了能解決這些問題,而去找更好或更有效率的方式,直到現在我們還是一直持續在做這件事情。
這條路走得不快,甚至常常是出包以後才知道少了哪件裝備,要說轉型容不容易,當然是不容易,隊友真的很重要,對軟體工程影響最大的 X 因素,絕對不是什麼多新的技術,而是「人」。
到了現在,我們的工作流不能說是最好、最舒服,但至少很有趣,也真的能學到不少東西,基本上從需求開始就一起對焦,開發一起,到了部署、維護和 Hotfix 也一起扛,從「這不是我負責的」慢慢變成「這東西是我們一起部署的」,這不是 DevOps,什麼才是 XD
柏修斯沒有因為拿到一把彎刀,就直接衝去找梅杜莎,Legacy System 的演化也是,這條路不會一夜之間變漂亮,但每走一步,我們就少依賴一點記憶、多留下一點證據,也替下一步準備好可以站穩的位置,尤其現在因為 AI 迭代速度又翻了好幾倍。
明天我們繼續看:不砍掉重練,怎麼讓新系統從一小條路開始接手?