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

image

緣起

Nebula Graph 最先的自動化測試是使用搭建在 Azure 上的 Jenkins,配合着 GitHub 的 Webhook 實現的,在用戶提交 Pull Request 時,加個 ready-for-testing 的 label 再評論一句 Jenkins go 就能夠自動的運行相應的 UT 測試,效果以下:git

image

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

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

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

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

鑑於以前 GitLab CI 的使用經驗,體會到若是能跟 GitHub 深度集成那固然是首選。所謂「深度」表示能夠共享 GitHub 的整個開源的生態以及完美的 API 調用(後話)。恰巧 2019,GitHub Action 2.0 橫空出世,Nebula Graph 便勇敢的入了坑。數據庫

這裏簡單概述一下咱們在使用 GitHub Action 時體會到的優勢:ubuntu

  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 的需求:首要目標比較明確就是自動化測試。c#

做爲數據庫產品,測試怎麼強調也不爲過。Nebula Graph 的測試主要分單元測試和集成測試。用 GitHub Action 其實主要瞄準的是單元測試,而後再給集成測試作些準備,好比 docker 鏡像構建和安裝程序打包。順帶再解決一下 PM 小姐姐的發佈需求,就整個構建起來了初版的 CI/CD 流程。centos

PR 測試

Nebula Graph 做爲託管在 GitHub 上的開源項目,首先要解決的測試問題就是當貢獻者提交了 PR 請求後,如何才能快速地進行變動驗證?主要有如下幾個方面。api

  1. 符不符合編碼規範;
  2. 能不能在不一樣系統上都編譯經過;
  3. 單測有沒有失敗;
  4. 代碼覆蓋率有沒有降低等。

只有上述的要求所有知足而且有至少兩位 reviewer 的贊成,變動才能進入主幹分支。緩存

藉助於 cpplint 或者 clang-format 等開源工具能夠比較簡單地實現要求 1,若是此要求未經過驗證,後面的步驟就自動跳過,再也不繼續執行。

對於要求 2,咱們但願能同時在目前支持的幾個系統上運行 Nebula 源碼的編譯驗證。那麼像以前在物理機上直接構建的方式就再也不可取,畢竟一臺物理機的價格已經高昂,況且一臺還不足夠。爲了保證編譯環境的一致性,還要儘量的減小機器的性能損失,最終採用了 docker 的容器化構建方式。再借助 Action 的 matrix 運行策略和對 docker 的支持,還算順利地將整個流程走通。

image.png

運行的大概流程如上圖所示,在 vesoft-inc/nebula-dev-docker 項目中維護 nebula 編譯環境的 docker 鏡像,當編譯器或者 thirdparty 依賴升級變動時,自動觸發 docker hub 的 Build 任務(見下圖)。當新的 Pull Request 提交之後,Action 便會被觸發開始拉取最新的編譯環境鏡像,執行編譯。

image

針對 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 中執行完成後的效果以下所示:

image

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 的安裝包以下圖所示。

image

上述完整的 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 }}

上述完整的 workflow 文件見 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 也是一個特定的標籤。

image

安全

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。經過配置不一樣系統和編譯器的組合,咱們能夠方便地設置在每一個系統下使用 gccclang 來同時編譯 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 目前單元測試的用例採用靜態連接,編譯後體積較大,超出其可用的配額,遂使用本地緩存的策略。

ccache

ccache 是個編譯器的緩存工具,能夠有效地加速編譯的過程,同時支持 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 腳原本完成任務內容,在任務中調用該腳本。

目前針對儘可能多使用小任務的組合仍是使用大任務的方式,社區也沒有定論。不太小任務組合的方式能夠方便地定位任務失敗位置以及肯定每步的執行時間。

image

  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 研發工程師,對數據庫查詢引擎有濃厚的興趣,但願本次的經驗分享能給你們帶來幫助,若有不當之處也但願能幫忙糾正,謝謝~
相關文章
相關標籤/搜索