再會了 Jenkins 與 Docker
我承認我標題想了一段時間,絕對沒有想騙點擊的意思,這篇文章主要記錄漸強實驗室 MAAC 的前端部署流程的遷移過程以及過程中遇到的問題跟解法,希望看到最後「喔我真的是被騙進來看」的感覺可以少一點。
這篇文章會涵蓋以下幾個技術主題:
- Jenkins
- Docker
- GitHub Actions
- Firebase Hosting
文章大綱:
- 遇到的問題
- 目標
- 計畫擬定
- 結果
- 注意事項(坑?)
- 後記
- 參考資料
遇到的問題
MAAC 是一個 SaaS (software as a service),我們前後端分離,前端部分採用 React 與 TypeScript,構建檔案使用 create-react-app (CRA) 提供的構建方式,部署其實不麻煩,理論上只要將構建出來的 HTML, CSS, JS 檔案放到伺服器上即可。
一個 React App 部署的流程大約是這樣:
安裝 Node modules -> 執行構建 -> 上傳產出檔案至伺服器
在這個流程之下,我們之前用的工具鏈與流程:
我們的原始碼放在 GitHub 上面,已經設定 merge, tag 等事件會觸發 Jenkins
- Jenkins 被 GitHub event 觸發開始構建與部署流程
- Jenkins 將環境變數提供給 Docker 構建流程(docker build 指令)
- Docker 構建 image,這個過程會安裝 Node modules 以及執行 CRA 構建,最後的 image 會包含 CRA 產出檔案與 Nginx
- Jenkins 將上一步生成的 Docker image 上傳到 Google Container Registry (GCR)
- Jenkins 呼叫 Google Kubernetes Engine (GKE) 拉取並使用最新的 Docker image,此時在 GKE 中的伺服器將會更新,使用者將可以看到新的內容
這邊我們遇到的問題有以下兩個:
- 整個構建與部署過程非常耗時
在 Node modules 越來越大的情況下,即使有 Docker image cache 快取 Node modules,整個部署流程常常耗時超過 25 分鐘,需要全新安裝 Node modules 時,耗時甚至會超過 45 分鐘!
我們追查過,docker build 常常卡在 Removing intermediate container 很長一段時間;上傳到 GCR 時速度也時快時慢。這些拖慢速度的情況我們目前沒有足夠精力去查出原因並修正。 - 設定檔案複雜且不好維護
依照這個流程,我們需要設定 Jenkinsfile, Dockerfile 還有 GKE 的設定檔。想像一個情況:當要新增環境變數時,並須透過 Jenkins 傳給 Docker,Docker 再傳給 CRA,需要調整多支檔案。
目標
針對遇到的問題,目標很明確:
- 部署過程時間縮短,至少縮減到 10 分鐘上下
- 調整工具鏈以減少設定檔
計畫擬定
我們其實在其他小型專案已經使用 Firebase Hosting 做為伺服器,這似乎是一個取代 GKE 的替代方案,能將 MAAC 前端部署到 Firebase Hosting 的話,我們就再也不需要 Docker 跟 GKE 了。
那麼,是不是連 Jenkins 也可以移除呢?我們研究了 GitHub Actions,發現其實很多資源在談論使用 GitHub Actions 做 CI/CD,在 GitHub Actions 的 workflow 中,也可以輕易透過 Firebase CLI 部署到 Firebase Hosting,或是可以使用社群開發的 Action — FirebaseExtended/action-hosting-deploy。
到這邊調整方向已經大致浮現 — 從 Jenkins + Docker 遷移到 GitHub Actions + Firebase Hosting。我們決定先從 staging 開始測試,藉此來評估可行性以及實際省下的時間。初步的測試發現可以在 10 分鐘左右完成「安裝 Node modules、構建以及部署到 Firebase Hosting」。
於是我們將這個計畫的遷移步驟制定如下:
- 先保留 GKE 上的伺服器,域名依舊指向此
- 先到 Firebase 主控台把新網站的 Hosting 開好
- 調整 GitHub Actions workflow,設定在合併進主線時,用正式環境的環境變數構建,最後將構建產出物部署到新網站的 Hosting
- 經過幾次的部署,期間我們直接到 Firebase 上面確認功能以及記錄整個構建與部署花費的時間
- 經過確認部署的 Hosting 內容都正常無誤後,選定時間將域名指向到這個 新網站 Hosting
- 移除 GKE 上面的伺服器,移除 Jenkinsfile 跟任何 Docker 相關的檔案
結果
經過幾次透過 GitHub Actions 的部署,我們發現總花費時間平均大約 13 分多(已算入 GitHub Actions cache 節省時間)。
成功使用 GitHub Actions 取代 Jenkins 後,我們也成功移除了 Jenkinsfile 跟 Dockerfile。
注意事項(坑?)
整個遷移過程從開始到結束大約花了 2 個月,期間遇到了幾個問題拖慢了我們的進度。
第一個問題是 Firebase Hosting 的客製域名特性。Firebase Hosting 開好之後,可以直接用 Firebase 提供的 yourAppName.web.app 或是 yourAppName.firebaseapp.com 域名,但是我們已經有在線的服務,必需要將原本的域名指定到這個新的 Hosting。發現首次設定客製域名時,Firebase 會特別為這個域名生成新的 TLS 憑證,在等待憑證部署到全球的 DNS 的過程中,使用瀏覽器瀏覽這個域名會顯示「不安全」警示,簡言之就是網站會停擺一段時間。我們在一些小專案中發現這個情況,所以在 DNS 重新指向時,特地先公告並挑了半夜時段做這件事。
第二個問題是快取問題,我們遇到了構建完部署到 Firebase 後程式內環境變數依然是舊的的問題。這個問題發生的原因是我們使用 webpack 的 hard-source-webpack-plugin 加上 GitHub Actions cache 快取整個 node_modules 資料夾。hard-source-webpack-plugin 它把被注入到程式碼中的環境變數快取起來且存在 node_modules 資料夾中,讓下一次 webpack 的編譯可以使用快取而加速,因為環境變數是被注入的,沒辦法監控特定檔案變動而去無效這個快取,構建過程中會拿到舊的內容。最後經過評估,GitHub Actions cache 可以替我們省下 3 分鐘左右,但是 hard-source-webpack-plugin 在構建時省不到 30 秒,我們決定關掉 hard-source-webpack-plugin 來解決這個問題。
第三個問題是我們調整了 branch model。我們新增了 rc 分支,從 rc 分支開 PR 到主線時會部署到另一個環境,因為新增了分支以及觸發條件,額外花了一些時間調整 GitHub Actions workflow。
後記
這次的調整我們成功達成目標,在過程中也做了不少研究,例如:GitHub Deployments, Firebase Hosting cache headers, GitHub Actions events。另外也發現原本的工具鏈帶來的潛在問題,會使用 hard-source-webpack-plugin 的主因是因為我們程式碼龐大造成 CRA 的初始啟動變得超慢,我們額外用了 CRACO + esbuild 以及 hard-source-webpack-plugin 來最佳化編譯速度,結果反而造成上面提到的第二個問題。
這次的遷移起因於 Jenkins + Docker 流程太慢,我們也成功改進它,下次可能就會針對 CRA 開刀了。