天天看點

用 GitHub Action 建構一套 CI/CD 系統

用 GitHub Action 建構一套 CI/CD 系統

緣起

Nebula Graph 最早的自動化測試是使用搭建在 Azure 上的

Jenkins

,配合着 GitHub 的 Webhook 實作的,在使用者送出 Pull Request 時,加個

ready-for-testing

的 label 再評論一句

Jenkins go

 就可以自動的運作相應的 UT 測試,效果如下:

用 GitHub Action 建構一套 CI/CD 系統

因為是租用的 Azure 的雲主機,加上 nebula 的編譯要求的機器配置較高,而且任務的觸發主要集中在白天。是以上述的方案成本效益較低,從去年團隊就在考慮尋找替代的方案,準備下線 Azure 上的測試機,并且還要能提供多環境的測試方案。

調研了一圈現有的産品主要有:

  1. TravisCI
  2. CircleCI
  3. Azure Pipeline
  4. Jenkins on k8s(自建)

雖然上面的産品對開源項目有些限制,但整體都還算比較友好。

鑒于之前 GitLab CI 的使用經驗,體會到如果能跟 GitHub 深度內建那當然是首選。所謂“深度”表示可以共享 GitHub 的整個開源的生态以及完美的 API 調用(後話)。恰巧 2019,GitHub Action 2.0 橫空出世,Nebula Graph 便勇敢的入了坑。

這裡簡單概述一下我們在使用 GitHub Action 時體會到的優點:

  1. 免費。開源項目可以免費使用 Action 的所有功能,而且機器 配置較高
  2. 開源生态好。在整個 CI 的流程裡,可以直接使用 GitHub 上的所有開源的 Action,哪怕就是沒有滿足需求的 Action,自己上手寫也不是很麻煩,而且還支援 docker 定制,用 bash 就可以完成一個專屬的 Action。
  3. 支援多種系統。Windows、macOS 和 Linux 都可以一鍵使用,跨平台簡單友善。
  4. 可跟 GitHub 的 API 互動。通過

    GITHUB_TOKEN

    可以直接通路 GitHub API V3 ,想上傳檔案,檢查 PR 狀态,使用 curl 指令即可完成。
  5. 自托管。隻要提供 workflow 的描述檔案,将其放置到

    .github/workflows/

     目錄下,每次送出便會自動觸發執行新的 action run。
  6. Workflow 描述檔案改為 YAML 格式。目前的描述方式要比 Action 1.0 中的 workflow 檔案更加簡潔易讀。

下面在講實踐之前還是要先講講 Nebula Graph 的需求:首要目标比較明确就是自動化測試。

作為資料庫産品,測試怎麼強調也不為過。Nebula Graph 的測試主要分單元測試和內建測試。用 GitHub Action 其實主要瞄準的是單元測試,然後再給內建測試做些準備,比如 docker 鏡像建構和安裝程式打包。順帶再解決一下 PM 小姐姐的釋出需求,就整個建構起來了第一版的 CI/CD 流程。

PR 測試

Nebula Graph 作為托管在 GitHub 上的開源項目,首先要解決的測試問題就是當貢獻者送出了 PR 請求後,如何才能快速地進行變更驗證?主要有以下幾個方面。

  1. 符不符合編碼規範;
  2. 能不能在不同系統上都編譯通過;
  3. 單測有沒有失敗;
  4. 代碼覆寫率有沒有下降等。

隻有上述的要求全部滿足并且有至少兩位 reviewer 的同意,變更才能進入主幹分支。

借助于 cpplint 或者 clang-format 等開源工具可以比較簡單地實作要求 1,如果此要求未通過驗證,後面的步驟就自動跳過,不再繼續執行。

對于要求 2,我們希望能同時在目前支援的幾個系統上運作 Nebula 源碼的編譯驗證。那麼像之前在實體機上直接建構的方式就不再可取,畢竟一台實體機的價格已經高昂,何況一台還不足夠。為了保證編譯環境的一緻性,還要盡可能的減少機器的性能損失,最終采用了 docker 的容器化建構方式。再借助 Action 的

matrix 運作政策

和對

docker 的支援

,還算順利地将整個流程走通。

用 GitHub Action 建構一套 CI/CD 系統

運作的大概流程如上圖所示,在

vesoft-inc/nebula-dev-docker

 項目中維護 nebula 編譯環境的 docker 鏡像,當編譯器或者 thirdparty 依賴更新變更時,自動觸發 docker hub 的 Build 任務(見下圖)。當新的 Pull Request 送出以後,Action 便會被觸發開始拉取最新的編譯環境鏡像,執行編譯。

用 GitHub Action 建構一套 CI/CD 系統

針對 PR 的 workflow 完整描述見檔案 

pull_request.yaml

。同時,考慮到并不是每個人送出的 PR 都需要立即運作 CI 測試,且自建的機器資源有限,對 CI 的觸發做了如下限制:

  1. 隻有 lint 校驗通過的 PR 才會将後續的 job 下發到自建的 runner,lint 的任務比較輕量,可以使用 GitHub Action 托管的機器來執行,無需占用線下的資源。
  2. 隻有添加了

    ready-for-testing

      label 的 PR 才會觸發 action 的執行,而 label 的添加有權限的控制。進一步優化 runner 被随意觸發的情況。對 label 的限制如下所示:
jobs:
  lint:
    name: cpplint
    if: contains(join(toJson(github.event.pull_request.labels.*.name)), 'ready-for-testing')           

在 PR 中執行完成後的效果如下所示:

用 GitHub Action 建構一套 CI/CD 系統

Code Coverage 的說明見博文:

圖資料庫 Nebula Graph 的代碼變更測試覆寫率實踐

Nightly 建構

在 Nebula Graph 的內建測試架構中,希望能夠在每天晚上對 codebase 中的代碼全量跑一遍所有的測試用例。同時有些新的特性,有時也希望能快速地打包交給使用者體驗使用。這就需要 CI 系統能在每天給出當日代碼的 docker 鏡像和 rpm/deb 安裝包。

GitHub Action 被觸發的事件類型除了 pull_request,還可以執行

schedule

類型。schedule 類型的事件可以像 crontab 一樣,讓使用者指定任何重複任務的觸發時間,比如每天淩晨兩點執行任務如下所示:

on:
  schedule:
    - cron: '0 18 * * *'           

因為 GitHub 采用的是 UTC 時間,是以東八區的淩晨 2 點,就對應到 UTC 的前日 18 時。

docker

每日建構的 docker 鏡像需要 push 到 docker hub 上,并打上 nightly 的标簽,內建測試的 k8s 叢集,将 image 的拉取政策設定為 Always,每日觸發便能滾動更新到當日最新進行測試。因為當日的問題目前都會盡量當日解決,便沒有再給 nightly 的鏡像再額外打一個日期的 tag。對應的 action 部分如下所示:

- name: Build image
        env:
          IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/nebula-${{ matrix.service }}:nightly
        run: |
          docker build -t ${IMAGE_NAME} -f docker/Dockerfile.${{ matrix.service }} .
          docker push ${IMAGE_NAME}
        shell: bash           

package

GitHub Action 提供了

artifacts

的功能,可以讓使用者持久化 workflow 運作過程中的資料,這些資料可以保留 90 天。對于 nightly 版本安裝包的存儲而言,已經綽綽有餘。利用官方提供的

actions/upload-artifact@v1

  action,可以友善的将指定目錄下的檔案上傳到 artifacts。最後 nightly 版本的 nebula 的安裝包如下圖所示。

用 GitHub Action 建構一套 CI/CD 系統

上述完整的 workflow 檔案見

package.yaml

分支釋出

為了更好地維護每個釋出的版本和進行 bugfix,Nebula Graph 采用分支釋出的方式。即每次釋出之前進行 code freeze,并建立新的 release 分支,在 release 分支上隻接受 bugfix,而不進行 feature 的開發。bugfix 還是會在開發分支上送出,最後 cherrypick 到 release 分支。

在每次 release 時,除了 source 外,我們希望能把安裝包也追加到 assets 中友善使用者直接下載下傳。如果每次都手工上傳,既容易出錯,也非常耗時。這就比較适合 Action 來自動化這塊的工作,而且,打包和上傳都走 GitHub 内部網絡,速度更快。

在安裝包編譯好後,通過 curl 指令直接調用 GitHub 的 API,就能上傳到 assets 中,

具體腳本

内容如下所示:

curl --silent \
     --request POST \
     --url "$upload_url?name=$filename" \
     --header "authorization: Bearer $github_token" \
     --header "content-type: $content_type" \
     --data-binary @"$filepath"           

同時,為了安全起見,在每次的安裝包釋出時,希望可以計算安裝包的 checksum 值,并将其一同上傳到 assets 中,以便使用者下載下傳後進行完整性校驗。具體步驟如下所示:

jobs:
  package:
    name: package and upload release assets
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os:
          - ubuntu1604
          - ubuntu1804
          - centos6
          - centos7
    container:
      image: vesoft/nebula-dev:${{ matrix.os }}
    steps:
      - uses: actions/checkout@v1
      - name: package
        run: ./package/package.sh
      - name: vars
        id: vars
        env:
          CPACK_OUTPUT_DIR: build/cpack_output
          SHA_EXT: sha256sum.txt
        run: |
          tag=$(echo ${{ github.ref }} | rev | cut -d/ -f1 | rev)
          cd $CPACK_OUTPUT_DIR
          filename=$(find . -type f \( -iname \*.deb -o -iname \*.rpm \) -exec basename {} \;)
          sha256sum $filename > $filename.$SHA_EXT
          echo "::set-output name=tag::$tag"
          echo "::set-output name=filepath::$CPACK_OUTPUT_DIR/$filename"
          echo "::set-output name=shafilepath::$CPACK_OUTPUT_DIR/$filename.$SHA_EXT"
        shell: bash
      - name: upload release asset
        run: |
          ./ci/scripts/upload-github-release-asset.sh github_token=${{ secrets.GITHUB_TOKEN }} repo=${{ github.repository }} tag=${{ steps.vars.outputs.tag }} filepath=${{ steps.vars.outputs.filepath }}
          ./ci/scripts/upload-github-release-asset.sh github_token=${{ secrets.GITHUB_TOKEN }} repo=${{ github.repository }} tag=${{ steps.vars.outputs.tag }} filepath=${{ steps.vars.outputs.shafilepath }}           
release.yaml

指令

GitHub Action 為 workflow 提供了一些

友善在 shell 中進行調用,來更精細地控制和調試每個步驟的執行。常用的指令如下:

set-output

有時在 job 的 steps 之間需要傳遞一些結果,這時就可以通過

echo "::set-output name=output_name::output_value"

 的指令形式将想要輸出的

output_value

 值設定到

output_name

 變量中。

在接下來的 step 中,可以通過

${{ steps.step_id.outputs.output_name }}

 的方式引用上述的輸出值。

上節中上傳 asset 的 job 中就使用了上述的方式來傳遞檔案名稱。一個步驟可以通過多次執行上述指令來設定多個輸出。

set-env

set-output

 一樣,可以為後續的步驟設定環境變量。文法:

echo "::set-env name={name}::{value}"

 。

add-path

将某路徑加入到

PATH

 變量中,為後續步驟使用。文法:

echo "::add-path::{path}"

Self-Hosted Runner

除了 GitHub 官方托管的 runner 之外,Action 還允許使用線下自己的機器作為 Runner 來跑 Action 的 job。在機器上安裝好 Action Runner 之後,按照

教程

,将其注冊到項目後,在 workflow 檔案中通過配置

runs-on: self-hosted

 即可使用。

self-hosted 的機器可以打上不同的 label,這樣便可以通過

不同的标簽

來将任務分發到特定的機器上。比如線下的機器安裝有不同的作業系統,那麼 job 就可以根據

runs-on

 的 label

在特定的機器

上運作。

self-hosted

 也是一個特定的标簽。

用 GitHub Action 建構一套 CI/CD 系統

安全

GitHub 官方是不推薦開源項目使用 Self-Hosted 的 runner 的,原因是任何人都可以通過送出 PR 的方式,讓 runner 的機器運作危險的代碼對其所在的環境進行攻擊。

但是 Nebula Graph 的編譯需要的存儲空間較大,且 GitHub 隻能提供 2 核的環境來編譯,不得已還是選擇了自建 Runner。考慮到安全的因素,進行了如下方面的安全加強:

虛拟機部署

所有注冊到 GitHub Action 的 runner 都是采用虛拟機部署,跟主控端做好第一層的隔離,也更友善對每個虛拟機做資源配置設定。一台高配置的主控端可以配置設定多個虛拟機讓 runner 來并行地跑所有收到的任務。

如果虛拟機出了問題,可以友善地進行環境複原的操作。

網絡隔離

将所有 runner 所在的虛拟機隔離在辦公網絡之外,使其無法直接通路公司内部資源。即便有人通過 PR 送出了惡意代碼,也讓其無法通路公司内部網絡,造成進一步的攻擊。

Action 選擇

盡量選擇大廠和官方釋出的 action,如果是使用個人開發者的作品,最好能檢視一下其具體實作代碼,免得出現網上爆出來的

洩漏隐私密鑰

等事情發生。

比如 GitHub 官方維護的 action 清單:

https://github.com/actions

私鑰校驗

GitHub Action 會自動校驗 PR 中是否使用了一些私鑰,除卻

GITHUB_TOKEN

 之外的其他私鑰(通過

${{ secrets.MY_TOKENS }}

 形式引用)均是不可以在 PR 事件觸發的相關任務中使用,以防使用者通過 PR 的方式私自列印輸出竊取密鑰。

環境搭建與清理

對于自建的 runner,在不同任務(job)之間做檔案共享是友善的,但是最後不要忘記每次在整個 action 執行結束後,清理産生的中間檔案,不然這些檔案有可能會影響接下來的任務執行和不斷地占用磁盤空間。

- name: Cleanup
        if: always()
        run: rm -rf build           

将 step 的運作條件設定為

always()

 確定每次任務都要執行該步驟,即便中途出錯。

基于 Docker 的 Matrix 并行建構

因為 Nebula Graph 需要在不同的系統上做編譯驗證,在建構方式上采用了容器的方案,原因是建構時不同環境的隔離簡單友善,GitHub Action 可以原生支援基于 docker 的任務。

Action 支援 matrix 政策運作任務的方式,類似于 TravisCI 的

build matrix

。通過配置不同系統和編譯器的組合,我們可以友善地設定在每個系統下使用

gcc

clang

來同時編譯 nebula 的源碼,如下所示:

jobs:
  build:
    name: build
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        os:
          - centos6
          - centos7
          - ubuntu1604
          - ubuntu1804
        compiler:
          - gcc-9.2
          - clang-9
        exclude:
          - os: centos7
            compiler: clang-9           

上述的 strategy 會生成 8 個并行的任務(4 os x 2 compiler),每個任務都是(os, compiler)的一個組合。這種類似矩陣的表達方式,可以極大的減少不同緯度上的任務組合的定義。

如果想排除 matrix 中的某個組合,隻要将組合的值配置到

exclude

選項下面即可。如果想在任務中通路 matrix 中的值,也隻要通過類似

${{ matrix.os }}

擷取上下文變量值的方式拿到。這些方式讓你定制自己的任務時都變得十分友善。

運作時容器

我們可以為每個任務指定運作時的一個容器環境,這樣該任務下的所有步驟(steps)都會在容器的内部環境中執行。相較于在每個步驟中都套用 docker 指令要簡潔明了。

container:
      image: vesoft/nebula-dev:${{ matrix.os }}
      env:
        CCACHE_DIR: /tmp/ccache/${{ matrix.os }}-${{ matrix.compiler }}           

對容器的配置,像在 docker compose 中配置 service 一樣,可以指定 image/env/ports/volumes/options 等等

參數

。在 self-hosted 的 runner 中,可以友善地将主控端上的目錄挂載到容器中做檔案的共享。

正是基于 Action 上面的容器特性,才友善的在 docker 内做後續編譯的緩存加速。

編譯加速

Nebula Graph 的源碼采用 C++ 編寫,整個建構過程時間較長,如果每次 CI 都完全地重新開始,會浪費許多計算資源。因為每台 runner 跑的(容器)任務不定,需要對每個源檔案及對應的編譯過程進行精準判别才能确認該源檔案是否真的被修改。目前使用最新版本的

ccache

來完成緩存的任務。

雖然 GitHub Action 本身提供

cache 的功能

,由于 Nebula Graph 目前單元測試的用例采用靜态連結,編譯後體積較大,超出其可用的配額,遂使用本地緩存的政策。

是個編譯器的緩存工具,可以有效地加速編譯的過程,同時支援 gcc/clang 等編譯器。Nebula Graph 使用 C++ 14 标準,低版本的 ccache 在相容性上有問題,是以在所有的

vesoft/nebula-dev

鏡像

中都采用手動編譯的方式安裝。

Nebula Graph 在 cmake 的配置中自動識别是否安裝了 ccache,并決定是否對其打開啟用。是以隻要在容器環境中對 ccache 做些配置即可,比如在

ccache.conf

中配置其最大緩存容量為 1 G,超出後自動替換較舊緩存。

max_size = 1.0G           

ccache.conf 配置檔案最好放置在緩存目錄下,這樣 ccache 可友善讀取其中内容。

tmpfs

tmpfs 是位于記憶體或者 swap 分區的臨時檔案系統,可以有效地緩解磁盤 IO 帶來的延遲,因為 self-hosted 的主機記憶體足夠,是以将 ccache 的目錄挂載類型改為 tmpfs,來減少 ccache 讀寫時間。在 docker 中使用 tmpfs 的挂載類型可以參考

相應文檔

。相應的配置參數如下:

env:
      CCACHE_DIR: /tmp/ccache/${{ matrix.os }}-${{ matrix.compiler }}
    options: --mount type=tmpfs,destination=/tmp/ccache,tmpfs-size=1073741824 -v /tmp/ccache/${{ matrix.os }}-${{ matrix.compiler }}:/tmp/ccache/${{ matrix.os }}-${{ matrix.compiler }}            

将所有 ccache 産生的緩存檔案,放置到挂載為 tmpfs 類型的目錄下。

并行編譯

make 本身即支援多個源檔案的并行編譯,在編譯時配置

-j $(nproc)

便可同時啟動與核數相同的任務數。在 action 的 steps 中配置如下:

- name: Make
        run: cmake --build build/ -j $(nproc)           

說了那麼多的優點,那有沒有不足呢?使用下來主要體會到如下幾點:

  1. 隻支援較新版本的系統。很多 Action 是基于較新的 Nodejs 版本開發,沒法友善地在類似 CentOS 6 等老版本 docker 容器中直接使用。否則會報 Nodejs 依賴的庫檔案找不到,進而無法正常啟動 action 的執行。因為 Nebula Graph 希望可以支援 CentOS 6,是以在該系統下的任務不得不需要特殊處理。
  2. 不能友善地進行本地驗證。雖然社群有個開源項目 act ,但使用下來還是有諸多限制,有時不得不通過在自己倉庫中反複送出驗證才能確定 action 的修改正确。
  3. 目前還缺少比較好的指導規範,當定制的任務較多時,總有種在 YAML 配置中寫程式的感受。目前的做法主要有以下三種:
    1. 根據任務拆配置設定置檔案。
    2. 定制專屬 action,通過 GitHub 的 SDK 來實作想要的功能。
    3. 編寫大的 shell 腳本來完成任務内容,在任務中調用該腳本。

目前針對盡量多使用小任務的組合還是使用大任務的方式,社群也沒有定論。不過小任務組合的方式可以友善地定位任務失敗位置以及确定每步的執行時間。

用 GitHub Action 建構一套 CI/CD 系統
  1. Action 的一些曆史記錄目前無法清理,如果中途更改了 workflows 的名字,那麼老的 check runs 記錄還是會一直保留在 Action 頁面,影響使用體驗。
  2. 目前還缺少像 GitLab CI 中手動觸發 job/task 運作的功能。無法運作中間進行人工幹預。
  3. action 的開發也在不停的疊代中,有時需要維護一下新版的更新,比如: checkout@v2

不過總體來說,GitHub Action 是一個相對優秀的 CI/CD 系統,畢竟站在 GitLab CI/Travis CI 等前人肩膀上的産品,還是有很多經驗可以借鑒使用。

後續

定制 Action

前段時間

docker 釋出了自己的第一款 Action

,簡化使用者與 docker 相關的任務。後續,針對 Nebula Graph 的一些 CI/CD 的複雜需求,我們亦會定制一些專屬的 action 來給 nebula 的所有 repo 使用。通用的就會建立獨立的 repo,釋出到 action 市場裡,比如追加 assets 到 release 功能。專屬的就可以放置 repo 的

.github/actions

 目錄下。

這樣就可以簡化 workflows 中的 YAML 配置,隻要 use 某個定制 action 即可。靈活性和拓展性都更優。

跟釘釘/slack 等 IM 內建

通過 GitHub 的 SDK 可以開發複雜的 action 應用,再結合

釘釘

/slack 等 bot 的定制,可以實作許多自動化的有意思的小應用。比如,當一個 PR 被 2 個以上的 reviewer approve 并且所有的 check runs 都通過,那麼就可以向釘釘群裡發消息并 @ 一些人讓其去 merge 該 PR。免去了每次都去 PR list 裡面 check 每個 PR 狀态的辛苦。

當然圍繞 GitHub 的周邊通過一些 bot 還可以迸發許多有意思的玩法。

One More Thing...

圖資料庫 Nebula Graph 1.0 GA 快要釋出啦。歡迎大家來圍觀。

本文中如有任何錯誤或疏漏歡迎去 GitHub:

https://github.com/vesoft-inc/nebula

issue 區向我們提 issue 或者前往官方論壇:

https://discuss.nebula-graph.com.cn/

建議回報

分類下提建議 👏;加入 Nebula Graph 交流群,請聯系 Nebula Graph 官方小助手微信号:

NebulaGraphbot
作者有話說:Hi,我是 Yee,是 圖資料 Nebula Graph 研發工程師,對資料庫查詢引擎有濃厚的興趣,希望本次的經驗分享能給大家帶來幫助,如有不當之處也希望能幫忙糾正,謝謝~