再會了 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

  1. Jenkins 被 GitHub event 觸發開始構建與部署流程
  2. Jenkins 將環境變數提供給 Docker 構建流程(docker build 指令)
  3. Docker 構建 image,這個過程會安裝 Node modules 以及執行 CRA 構建,最後的 image 會包含 CRA 產出檔案與 Nginx
  4. Jenkins 將上一步生成的 Docker image 上傳到 Google Container Registry (GCR)
  5. 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,需要調整多支檔案。
Jenkins 部署費時紀錄圖
Jenkins 部署費時紀錄

目標

針對遇到的問題,目標很明確:

  • 部署過程時間縮短,至少縮減到 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」。

於是我們將這個計畫的遷移步驟制定如下:

  1. 先保留 GKE 上的伺服器,域名依舊指向此
  2. 先到 Firebase 主控台把新網站的 Hosting 開好
  3. 調整 GitHub Actions workflow,設定在合併進主線時,用正式環境的環境變數構建,最後將構建產出物部署到新網站的 Hosting
  4. 經過幾次的部署,期間我們直接到 Firebase 上面確認功能以及記錄整個構建與部署花費的時間
  5. 經過確認部署的 Hosting 內容都正常無誤後,選定時間將域名指向到這個 新網站 Hosting
  6. 移除 GKE 上面的伺服器,移除 Jenkinsfile 跟任何 Docker 相關的檔案
新舊部署流程比較圖
新舊部署流程比較

結果

經過幾次透過 GitHub Actions 的部署,我們發現總花費時間平均大約 13 分多(已算入 GitHub Actions cache 節省時間)。

GitHub Actions 運行紀錄圖
GitHub Actions 運行紀錄

成功使用 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。

Branch model 跟 CI/CD 間關係圖
Branch model 跟 CI/CD 間關係

後記

這次的調整我們成功達成目標,在過程中也做了不少研究,例如:GitHub Deployments, Firebase Hosting cache headers, GitHub Actions events。另外也發現原本的工具鏈帶來的潛在問題,會使用 hard-source-webpack-plugin 的主因是因為我們程式碼龐大造成 CRA 的初始啟動變得超慢,我們額外用了 CRACO + esbuild 以及 hard-source-webpack-plugin 來最佳化編譯速度,結果反而造成上面提到的第二個問題。
這次的遷移起因於 Jenkins + Docker 流程太慢,我們也成功改進它,下次可能就會針對 CRA 開刀了。

--

--