或許你該懂一下 CI/CD?用 CD 部署到 Render.com
前言
身為一名工程師來講,其實稍微懂一點 CI/CD 是滿有幫助的,畢竟如果想要做一點小專案之類的,那麼 CI/CD 是可以派上一些用場,就算你只是一名前端工程師,也可以善加利用 CI/CD 來做一些事情。
CI/CD 是什麼?
首先我們要先認識一下什麼是 CI 與 CD,CI 其實是 Continuous integration 的縮寫,而中文是持續整合,當然也有可能看到持續集成,而 CD 則是 Continuous deployment 的縮寫,中文是持續部署。
ok,如果不出意外的話,現在已經出意外了,因為你應該感覺到滿頭問號中,你應該想著…
「到底持續整合跟持續部署是在持續什麼東西呢?」
所以這邊我們先進入一段想像畫面…
當我們想要將專案給部署到正式開發環境時,通常會先輸入一些指令,例如 npm run test
先跑一下測試在執行打包指令 npm run build
,然後再將打包好的檔案給部署到正式環境,這樣的流程就是一般的開發流程。
雖然看是沒有什麼太大問題,但是身為人類的我們,總是會有一些疏忽,例如忘記跑測試、忘記打包等等,尤其是忘記跑測試這件事情是非常嚴重的,輕則上線後立刻修正,重則上線後立刻爆炸 on call 修正。
而這一段就是 CI 要幫我們做的事情,他會自動幫我們持續的去整合這些我們應該本身要做的事情,例如測試、打包等等。
CD 呢?其實 CD 就跟部署有關了,簡單來講 CD 就是幫我們自動部署,這樣的好處就是我們可以省下一些時間,而且也可以避免一些人為的疏忽。
講了那麼多 CI/CD 的基本概念後,接下來就準備要來使用 CI/CD 了,但是我們要用哪一套了?市面上的 CI/CD 工具我大概列下有以下
- Travis CI
- Circle CI
- Jenkins
- GitLab CI
- GitHub Actions
以上是我大概知道的 CI/CD 工具,但這一篇我會以 GitHub Actions 作為教學範例,畢竟 GitHub 是我們常用的平台,而且 GitHub Actions 也是 GitHub 官方提供的,所以我們就以 GitHub Actions 來做教學哩~
YAML 語法
開始玩 GitHub Actions 之前,我們必須先學習一個東西,也就是 .yml 檔,這個檔案是 GitHub Actions 的設定檔,我們可以在這邊設定我們要做什麼事情,例如測試、打包、部署等等之類,另一種檔案格式是 .yaml,其實 .yml 跟 .yaml 是一樣的,只是副檔名不同而已。
那麼 .yml 該怎麼寫呢?基本上 .yml 有以下幾個特性
- 以縮排來區分層級
#
代表註解- 主要是以
key: value
的方式來寫 - 用
-
區分多個值,也可以用[]
來區分多個值,其實-
跟[]
是一樣的,只是[]
可以讓你的 .yml 檔案看起來比較整齊而已,就是類似陣列一種,如果是物件的話也是使用key: value
的方式來寫 |
與>
代表多行文字,但是前者會保留換行,後者則會將換行去除{{ }}
代表變數,可以用來取代一些值,例如{{ secrets.GITHUB_TOKEN }}
,這邊的secrets.GITHUB_TOKEN
是 GitHub Actions 提供的一個變數,可以用來取得 GitHub 的 token,這邊的secrets
是一個物件,而GITHUB_TOKEN
則是物件的 key,所以我們可以這樣取得secrets.GITHUB_TOKEN
的值
本質上來講 .yml 其實是 JSON 的一種格式,所以如果你有 JSON 的基礎,那麼 .yml 應該不難理解,底下我也寫一下 .yml 跟 JSON 的對照表
註解
1 | # 註解 |
key: value
1 | name: 'Ray' |
1 | { |
多行文字
第一種寫法(保留換行):
1 | name: | |
1 | { |
第二種寫法(去除換行):
1 | name: > |
1 | { |
陣列
第一種寫法:
1 | name: |
1 | { |
第二種寫法:
1 | name: [Ray, IsRayNotArray, Hello] |
1 | { |
變數
1 | name: {{ secrets.GITHUB_TOKEN }} # 自動取得 GitHub 的 token 並注入 |
1 | { |
物件
1 | name: |
1 | { |
陣列物件:
1 | name: |
1 | { |
以上差不多就是 .yml 的基本概念。
GitHub Actions 基礎
前面認識了基本的 YAML 之後就可以準備來玩 GitHub Actions 囉!
那麼 GitHub Actions 會有介面可以直接設定嗎?不,沒有,主要你必須使用 .yml 設定,這個檔案又稱為 workflow,而 workflow 會放在 .github/workflows
資料夾下,在官方文件上有提供一張圖
在這張圖我們可以看到以下東西
- Event
- Runner 1
- Job 1
- Step 1: Run action
- Step 2: Run action
- Step 3: Run action
- Step 4: Run action
- Job 1
- Runner 2
- Job 2
- Step 1: Run action
- Step 2: Run action
- Step 3: Run action
- Job 2
基本上所有的 workflow 檔案必定會有 Event(事件),而 Event 會觸發 Runner,而 Runner 會執行 Job,而 Job 會依照 Step 順序執行。
在這邊官方有提供一個範例,我們先來看一下
1 | name: learn-github-actions |
不用太緊張,這邊我會逐行去解釋。
1 | name: learn-github-actions |
首先第一行 name: learn-github-actions
代表著這個 workflow 的名稱,而這會影響到 GitHub Actions 呈現的名稱,接著是 run-name: ${{ github.actor }} is learning GitHub Actions
,這個是 workflow 的描述,而這邊的 github.actor
是一個變數,代表著 GitHub 的使用者名稱,所以這邊的意思就是說「使用者名稱正在學習 GitHub Actions」。
1 | on: [push] |
接著比較核心重點在於 on: [push]
,這一行主要是告知 GitHub Actions 這個 workflow 是要在什麼時候觸發,而這邊的 push
代表著當你將程式碼推到 GitHub 時就會觸發這個 workflow。
1 | jobs: |
最後就是 jobs
,這邊主要是描述了當我們觸發這個事件後要做的哪些任務行為,接下來你會看到 check-bats-version
這個屬性,這個屬性是在描述這個任務的名稱,然後 runs-on: ubuntu-latest
是在描述我們要在什麼樣的環境下執行這個任務,接著 steps
就是這個任務有哪些步驟。
接下來的 - uses:
有一點特別了,相信應該是滿多人會感到混亂的地方,因為後面寫著 actions/checkout@v3
,那麼這是什麼意思呢?而且這又是哪裡來的東西呢?
首先其實 actions/checkout@v3
跟下一行的 actions/setup-node@v3
都是來自 GitHub Actions 的官方庫,在 GitHub 官方有一個組織叫做 Actions
所以其實這一段的白話文意思是這樣的
「我要使用 actions 組織下的 checkout 儲存庫,並且版本是 v3 版本」
那 checkout
這個儲存庫用途是幹嘛的呢?簡單來講就是讓你的 Workflow 可以取得你的程式碼,而 setup-node
則是讓你可以設定 Node.js 的版本,這邊的 v3
則是版本號。
相信應該滿多人對於 uses: actions/checkout@v3
、actions/setup-node@v3
這兩個東西終於有點概念了。
而後面 actions/setup-node@v3
有一個 with
屬性,這個屬性是用來設定一些值,例如設定 node-version: '14'
,這邊的 node-version
就是設定 Node.js 的版本,而 14
則是版本號,當然 setup-node
它不只有這個屬性,只是大多都只需要設定版本就可以了,如果你想了解更多可以點擊 setup-node 這個儲存庫來觀看。
最後就是 - run: npm install -g bats
、- run: bats -v
,這兩個非常簡單,就是你本地電腦執行 npm install -g bats
、bats -v
的意思,這邊的 - run:
代表著執行指令。
最後來看一下 GitHub Actions 文件所提供的可視化圖
相信你應該有一些概念了,那麼以上就是官方 GitHub Actions 的範例解釋哩~
GitHub Actions 實戰
前面學了很多基本觀念,如 YAML 語法、GitHub Actions 基礎,所以接下來就準備來寫一下自己的 GitHub Actions 囉~
首先一開始我們先使用 Express 建立一個初始化專案,這邊我會使用我先前準備好的空白專案
可以先 Fork 並 Clone 下來準備練習。
以上準備好後,就打開這個專案,然後在根目錄下面建立一個 .github/workflows
資料夾,因為 GitHub Actions 的設定檔案必須放在這個資料夾下,接著在這個資料夾下建立一個 main.yml
檔案,這個檔案就是我們的 workflow 檔案,所以目前你的結構應該會長這樣
那麼底下的 .yml 檔案名稱沒有特別限制一定要怎樣,只是為了方便管理,所以我們就以 main.yml
來命名(後續這邊我更改成 main-pr.yml
,所以你也可以直接使用 main-pr.yml
來命名)。
接下來我們要來做一些事情,首先我們會期望原本我們常常在做的幾個行為自動化
- ESLint 掃描
- 測試
- 打包
- 部署
那麼前三者基本上都是在發起 Pull Requests 之後就會自動執行,而部署則是在合併到主要分支後才會自動執行,所以我們就會來撰寫兩個 workflow 檔案,一個是在發起 Pull Requests 後就會自動執行的,另一個則是在合併到主要分支後才會自動執行的。
因此這邊基本上我會建立兩個檔案,分別是
.github/workflows/main-pr.yml
.github/workflows/main-deploy.yml
Pull Requests
首先先打開 .github/workflows/main-pr.yml
檔案,接著我們這邊會需要監聽 pull_request
事件,所以我們就會在 on
屬性下面加入 pull_request
,代表著當有人發起 Pull Requests 時就會觸發這個 workflow
1 | name: Main Pull Requests Check |
那麼 pull_request
你會發現我是用物件形式撰寫,代表著它底下其實有超多屬性可以使用,詳細你可以觀看「pull_request」這邊的官方文件,所以這邊我只會示範一個最常用的屬性,也就是 branches
,意旨當有人發起 Pull Requests 時,我們會監聽哪些分支,這邊我們會監聽 main
分支,所以我們就會這樣寫
1 | name: Main Pull Requests Check |
代表著只要有人發起 Pull Requests 到 main
分支時就會觸發這個 workflow。
如果你覺得這樣寫很難懂,你也可以改成陣列的寫法
1 | name: Main Pull Requests Check |
當然也有萬用的寫法,就是 *
,代表著監聽特定名稱的分支,例如 Git Flow 的 feature/*
、release/*
、hotfix/*
等等
1 | name: Main Pull Requests Check |
但這邊我們只需要監聽 main
分支就好。
接著呢?接著我們已經撰寫了 Event 事件後就是要寫 Job 了,這邊我們會寫一個 Job,而這個 Job 會有三個步驟,分別是剛剛所提到的 ESLint 掃描、測試、打包,那麼如果要能夠掃描 ESLint 跟測試的話,專案就必須要有相對應的套件,所以我們先來安裝一下
1 | npm i -D eslint jest |
接下來你可以替這個專案初始化 ESLint 跟撰寫一點基本的測試檔案,我這邊就不額外說明與介紹這一塊了,只是由於我們需要讓 GitHub Actions 可以執行 ESLint 跟測試,所以我們就需要在 package.json
裡面加入一些指令
1 | { |
這邊我們加入了 lint
跟 test
兩個指令,分別是執行 ESLint 跟測試,以上準備好之後,就可以繼續回來寫 Job 了囉。
一開始我們會針對 ESLint 跑一下掃描,所以 Job 就會這樣寫
1 | name: Main Pull Requests Check |
到目前為止來講,你只要 ESLint 有發生錯誤,那麼這個 Job 就會失敗,而且會顯示在 GitHub Actions 上面,如下圖
只有你修正了該 ESLint 錯誤才能夠繼續往下執行
測試跟打包呢?其實測試跟打包都是一樣差不多的行為,只是因為我們是 Express 專案並不需要打包的行為,所以我底下程式碼會忽略掉打包的行為
1 | name: Main Pull Requests Check |
這邊你會發現一件很有趣的事情,也就是類似的行為 runs-on: ubuntu-latest
、uses: actions/checkout@v3
跟 uses: actions/setup-node@v3
這些行為都是重複的,為什麼呢?因為每個 Job 都是獨立的,所以每個 Job 都必須要有這些行為,這邊你可以想像成每個 Job 都是一個獨立的 Container,所以每個 Job 都必須要有這些行為,這樣才能夠執行哩。
那麼這邊有一件事情很特別,因為 GitHub Actions 在執行 Job 時的順序是不固定的,所以有可能是 ESLint 先跑完又或者是測試先跑完,那麼如果你期望順序是先跑完 ESLint 再跑測試,那麼你就必須要使用 needs
這個屬性,這個屬性可以讓你指定 Job 的執行順序,所以這邊我們就會這樣寫
1 | name: Main Pull Requests Check |
否則的話你應該會在 GitHub Actions 的可視化圖形長這樣子
當你使用了 needs
這個屬性後,你會發現可視化圖形會變成這樣
Note
可視化圖形觀看位置位於專案的 Actions 頁面。
到目前為止我們的 PR workflow 就完成了,接下來就是要來寫部署的 workflow 囉~
Deploy
接下來打開 .github/workflows/main-deploy.yml
檔案,這邊我們會監聽 push
事件,所以我們就會在 on
屬性下面加入 push
,代表 Pull Requests 合併到主要分支後就會觸發這個 workflow,並且我們只監聽 main
這個分支
1 | name: Main Deploy |
只需要做到這樣子,你就可以監聽並執行部屬行為了,很簡單吧?
Deploy to Render.com
那麼我們要部署到哪裡呢?這邊我會示範部署到 Render.com,在 Render.com 的官方文件中,有一個 Deploy Hook 可以使用。
那你可能會想說為什麼還要自己用 CD 部署?不是可以直接自己監聽 Branch 分支,當有新的分支讓 Render.com 自己部署就好了嗎?但是實際上來講,我們有些時候會遇到一些特殊的狀況是必須要先跑完自己的 CI/CD 之後才能部署的,所以這邊才會額外介紹如何用 CI/CD 自己部署到 Render.com,而不是直接讓 Render.com 自己監聽 Branch 分支。
但是是如果要做到這個需求的話,是必須要做一些前置動作的,所以接下來就跟著我一起來設定吧!
首先在你建立好 Render 專案時,你要記得把 Auth Deploy 給關閉,否則你會發現 CI/CD 還沒跑完,專案就已經被部署到 Render 上面
如果是已經建立好的專案,那麼你可以到專案的設定頁面關閉 Auth Deploy
那麼剛好你也會看到 Deploy Hook
的欄位,請把這個網址複製起來,因為我們會使用這個網址來部署,請小心不要把這個網址外流出去,因為這個網址可以讓任何人都可以部署你的專案,所以請小心保管。
接下來我們必須要到個人設定中取得 API Key,請你點一下自己頭像找到「Account Setting」
接著找到 API Keys,並點一下「Create API Key」
名稱基本上隨便你取,我這邊是取名為 Example,按下「Create API Key」之後,你會取得一組 Key,請把那一組 Key 保存下來,因為我們會使用到這一組 Key
接下來讓我們回到 Job 的部分,那麼部署之前你可能會想說,雖然 Pull Requests 已經通過了,但是我還是想要再跑一次 ESLint 跟測試,這樣才能夠確保我們的專案是沒有問題的,所以可能就會這樣寫
1 | name: Main Deploy |
(其實就只是把 Pull Requests 的 workflow 複製過來而已)
再來就是要來撰寫部署了,在部署之前我們會先把剛剛複製的 Deploy Hook 跟 API Key 貼到 GitHub 的 Secrets 裡面,位置會在該儲存庫的 Setting 可以找到
接著點一下「new repository secret」,然後輸入 RENDER_DEPLOY_HOOK
跟 RENDER_TOKEN
並貼入相對應的值,最後點一下「Add secret」就完成了~
都完成以上後回來到 main-deploy.yml 繼續撰寫部署的部分,基本上都跟剛剛差不多,只是我們必須跑完 ESLint 跟測試後才能夠部署,所以我們就會使用 needs
這個屬性,這邊我們就會這樣寫
1 | name: Main Deploy |
接下來呢?接下來這邊稍微有一點特別了,如果要部署到 Render.com 的話將會使用到 curl 的方式,因此就會變成以下
1 | name: Main Deploy |
接下來你就可以試著發起 PR 並且合併到 main
分支,你會發現 GitHub Actions 會自動執行,並且會自動部署到 Render.com 上面囉~
最後這邊我也附上我寫完的範本給予參考哩