CircleCI Workflow: 讓 build 時間減少 50% 的設定心得

HzChris
財報狗技術部落格
7 min readJun 25, 2018
photo by deathless from Pexels

財報狗用 CircleCI 有一段時間了,一開始用的時候還是 1.0,2.0 還在 beta 階段。我們的專案在 1.0 上面一直運作良好,所以也沒有想說要換,直到 CircleCI 發佈 2018/8/31 之後將不再支援 1.0,我們才開始考慮要遷移專案 XD,順便趁着這次的遷移導入的 2.0 的新功能: workflow

Workflow 簡介

CircleCI 在 2.0 中推出了 workflow 這個新功能,官網的介紹如下:

A workflow is a set of rules for defining a collection of jobs and their run order. Workflows support complex job orchestration using a simple set of configuration keys to help you resolve failures sooner.

簡單來說,workflow 提供了一種做法,讓你可以把 CI 的每個步驟拆分成一個個獨立的 job,job 之間遵照一定的執行順序或是並行,萬一發生失敗, 只要針對失敗的 job 去做處理就好了,大大提升反應速度。

Workflow 解決了什麼問題?

  1. CircleCI 1.0 中,CI 的每一步流程都是循序的,像是必須要執行完 bundle install 才可以繼續 yarn install,但是這兩個 job 彼此間並沒有相依性,因此在 workflow 中,可以設計讓沒有相依的 job 並行節省時間;開的 container 越多,可以並行的 job 也就越多。
  2. 用循序的方式執行 job,只要其中一個 job 失敗,就需要整個 CI 重來;而 workflow 中的每一個 job 彼此是獨立的,可以只針對失敗的 job 重跑即可。

Workflow 設定

在使用 Workflow 之前,需要先想好專案的 CI 流程可以獨立成哪幾個步驟;以財報狗專案來 說,可以分成以下幾個:

  • checkout_code: 將程式碼從遠端 pull 下來,在 CI 環境中做好路徑設定
  • bundle_dependencies: 安裝 rails application 相依性套件
  • yarn_dependencies: 安裝前端相依性套件
  • run_unit_test: 執行單元測試
  • run_integration_test: 執行整合測試
  • deploy: 將程式佈署到 production

以循序的方式去跑上面這些步驟,大約需要花 16 分鐘

每次都循序跑實在有點沒效率,以上面的流程來說, bundle_dependenciesyarn_dependenciescheckout_code 結束之後就可以同時執行; run_unit_test 以及 run_integration_test 也一樣,只要相依性套件裝完,單元測試跟整合測試是可以一起跑的,這也就是 workflow 想解決的問題之一;在 CricleCI 中只要將 job 定義好 ,就可以很輕易的設定 workflow:

workflows:
version: 2
build_and_test: # Workflow 的名稱
jobs:
- checkout_code
- bundle_dependencies:
requires: # 相依於哪些 jobs
- checkout_code
- yarn_dependencies:
requires:
- checkout_code
- run_unit_tests:
requires:
- bundle_dependencies
- run_integration_tests:
requires:
- yarn_dependencies
- bundle_dependencies
- deploy:
requires:
- run_unit_tests
- run_integration_tests

設定好之後,CircleCI 就會自動以 workflow 的方式執行:

紅色框框圈起來的是並行執行的部分,可以看到比較花時間的 run_unit_test以及 run_integration_test 分別需要跑 4:58 以及 6:19,而因爲並行跑的關係,只要 6:19 就跑完了,整個 CI 的時間也足足從 16 分鐘縮短到 8 分多鐘,速度變快超有感的阿!

重跑 workflow 失敗的 jobs

剛剛提到 workflow 的好處除了並行縮短執行時間外,還有一點就是可以只重跑失敗的 job,而不需要重跑整個 CI。

爲什麼這很重要呢?在跑 CI 的時候,偶爾會遇到一些 random fails,像是網路突然不穩,或是套件安裝因爲不明原因失敗等等,像下面這樣:

仔細檢查會發現是 deploy 階段在 ssh 連到 server 的過程中 timeout 了,這可能是 server 那邊設定出了點問題,通常這種情況只要進到 server 改好設定,重跑一次 CI 就可以解決,不過如果只是 deploy 階段出錯,重跑還需要從設定環境、裝套件、跑完測試後才可以 deploy,實在太沒效率了,所以在 workflow 中,很貼心的讓你可以選擇只跑失敗的 jobs,進一步的節省時間。

善用 cache

在 workflow 中,每個 job 都是運作在一個獨立的 container 當中,爲了確保每個 job 可以獨立運行,所以可能會需要前一次 CI 或是前一個 job 產生的一些資料,這時候 cache 就派上用場了。

舉例來說,像是在 bundle_dependencies 階段會安裝所需要的套件,但是我們並不想要每次跑 CI 的時候都重裝全部的套件,只想要裝之前沒裝過的;又或者是在 run_unit_test階段時,會需要用到在 bundle_dependencies階段裝好的整包套件資料,那這時候我們就可以在 bundle_dependencies階段,把 bundle install 所產生的資料儲存成 cache,等到下一個 job 或是下一次 CI要使用的時候,再用 cache key 把存的資料取出來用。

bundle install 爲例,裝好的 bundle 可以用 save_cache 儲存起來:

- save_cache:
key: v1-gem-{{ checksum "Gemfile.lock" }}
paths:
- vendor/bundle

如果之後的 job 或是 build 需要用到這包裝好的 bundle ,只需要 restore_cache 即可:

restore_cache:
keys:
- v1-gem-{{ checksum "Gemfile.lock" }}
- v1-gem-

這邊 CircleCI 給了一個彈性,可以設定多組 key, 萬一使用第一個 key (v1-gem-{{checksum “Gemfile.lock"}}) 找不到符合的結果,就會使用下一個順位,也就是 v1-gem- 這個 key,去比對最近產生,且前綴符合 v1-gem- 的快取資料。

不要設計太小的 job

由於每個 job 在運作之前仍然需要準備好所需要的環境與資料才能夠運行,即使有做好 cache,但是在取得 cache 的過程中,只要檔案稍大,仍然需要花費 10~20 秒的時間,如果是一個 30 秒內就可以執行完的 command,不太建議拉出來成爲一個獨立的 job,反而會拖慢整個 CI 的速度。

最後…

還是對 workflow 的設定有些不瞭解的話,可以到我的 Gist 看詳細的設定 ,有問題或是有什麼 CI 設定上的建議,也歡迎留言討論 😃。

友情支持拍 1 下手👏;如果文章對你有幫助,可以拍更多(最多 50 下)👏👏👏👏👏👏👏👏👏👏。這可以讓我知道未來要先寫哪一方面的文章。

--

--