或許你該懂一下 CI/CD?用 CD 部署到 Render.com

CI/CD

前言

身為一名工程師來講,其實稍微懂一點 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
2
# 註解
name: 'Ray'

key: value

1
name: 'Ray'
1
2
3
{
"name": "Ray"
}

多行文字

第一種寫法(保留換行):

1
2
3
4
name: |
Ray
IsRayNotArray
Hello
1
2
3
{
"name": "Ray\nIsRayNotArray\nHello"
}

第二種寫法(去除換行):

1
2
3
4
name: >
Ray
IsRayNotArray
Hello
1
2
3
{
"name": "Ray IsRayNotArray Hello"
}

陣列

第一種寫法:

1
2
3
4
name:
- Ray
- IsRayNotArray
- Hello
1
2
3
{
"name": ["Ray", "IsRayNotArray", "Hello"]
}

第二種寫法:

1
name: [Ray, IsRayNotArray, Hello]
1
2
3
{
"name": ["Ray", "IsRayNotArray", "Hello"]
}

變數

1
name: {{ secrets.GITHUB_TOKEN }} # 自動取得 GitHub 的 token 並注入
1
2
3
{
"name": "1234567890"
}

物件

1
2
3
4
name:
firstName: Ray
lastName: IsRayNotArray
age: 18
1
2
3
4
5
6
7
{
"name": {
"firstName": "Ray",
"lastName": "IsRayNotArray",
"age": 18
}
}

陣列物件:

1
2
3
4
5
6
7
name:
- firstName: Ray
lastName: IsRayNotArray
age: 18
- firstName: Ray
lastName: IsRayNotArray
age: 18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": [
{
"firstName": "Ray",
"lastName": "IsRayNotArray",
"age": 18
},
{
"firstName": "Ray",
"lastName": "IsRayNotArray",
"age": 18
}
]
}

以上差不多就是 .yml 的基本概念。

GitHub Actions 基礎

前面認識了基本的 YAML 之後就可以準備來玩 GitHub Actions 囉!

那麼 GitHub Actions 會有介面可以直接設定嗎?不,沒有,主要你必須使用 .yml 設定,這個檔案又稱為 workflow,而 workflow 會放在 .github/workflows 資料夾下,在官方文件上有提供一張圖

workflow

在這張圖我們可以看到以下東西

  • Event
  • Runner 1
    • Job 1
      • Step 1: Run action
      • Step 2: Run action
      • Step 3: Run action
      • Step 4: Run action
  • Runner 2
    • Job 2
      • Step 1: Run action
      • Step 2: Run action
      • Step 3: Run action

基本上所有的 workflow 檔案必定會有 Event(事件),而 Event 會觸發 Runner,而 Runner 會執行 Job,而 Job 會依照 Step 順序執行。

在這邊官方有提供一個範例,我們先來看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
name: learn-github-actions
run-name: ${{ github.actor }} is learning GitHub Actions
on: [push]
jobs:
check-bats-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
- run: npm install -g bats
- run: bats -v

不用太緊張,這邊我會逐行去解釋。

1
2
name: learn-github-actions
run-name: ${{ github.actor }} is learning 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
2
3
4
5
6
7
8
9
10
jobs:
check-bats-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
- run: npm install -g bats
- run: bats -v

最後就是 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

所以其實這一段的白話文意思是這樣的

「我要使用 actions 組織下的 checkout 儲存庫,並且版本是 v3 版本」

checkout 這個儲存庫用途是幹嘛的呢?簡單來講就是讓你的 Workflow 可以取得你的程式碼,而 setup-node 則是讓你可以設定 Node.js 的版本,這邊的 v3 則是版本號。

相信應該滿多人對於 uses: actions/checkout@v3actions/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 batsbats -v 的意思,這邊的 - run: 代表著執行指令。

最後來看一下 GitHub Actions 文件所提供的可視化圖

GitHub Action

相信你應該有一些概念了,那麼以上就是官方 GitHub Actions 的範例解釋哩~

GitHub Actions 實戰

前面學了很多基本觀念,如 YAML 語法、GitHub Actions 基礎,所以接下來就準備來寫一下自己的 GitHub Actions 囉~

首先一開始我們先使用 Express 建立一個初始化專案,這邊我會使用我先前準備好的空白專案

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
2
3
name: Main Pull Requests Check
on:
pull_request:

那麼 pull_request 你會發現我是用物件形式撰寫,代表著它底下其實有超多屬性可以使用,詳細你可以觀看「pull_request」這邊的官方文件,所以這邊我只會示範一個最常用的屬性,也就是 branches,意旨當有人發起 Pull Requests 時,我們會監聽哪些分支,這邊我們會監聽 main 分支,所以我們就會這樣寫

1
2
3
4
5
name: Main Pull Requests Check
on:
pull_request:
branches:
- main

代表著只要有人發起 Pull Requests 到 main 分支時就會觸發這個 workflow。

如果你覺得這樣寫很難懂,你也可以改成陣列的寫法

1
2
3
4
name: Main Pull Requests Check
on:
pull_request:
branches: [main]

當然也有萬用的寫法,就是 *,代表著監聽特定名稱的分支,例如 Git Flow 的 feature/*release/*hotfix/* 等等

1
2
3
4
name: Main Pull Requests Check
on:
pull_request:
branches: [feature/*]

但這邊我們只需要監聽 main 分支就好。

接著呢?接著我們已經撰寫了 Event 事件後就是要寫 Job 了,這邊我們會寫一個 Job,而這個 Job 會有三個步驟,分別是剛剛所提到的 ESLint 掃描、測試、打包,那麼如果要能夠掃描 ESLint 跟測試的話,專案就必須要有相對應的套件,所以我們先來安裝一下

1
npm i -D eslint jest

接下來你可以替這個專案初始化 ESLint 跟撰寫一點基本的測試檔案,我這邊就不額外說明與介紹這一塊了,只是由於我們需要讓 GitHub Actions 可以執行 ESLint 跟測試,所以我們就需要在 package.json 裡面加入一些指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"name": "2022-express-example",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
// 加入這兩行
"lint": "eslint . --ext .js",
"test": "jest"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"morgan": "~1.9.1"
},
"devDependencies": {
"eslint": "^8.41.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.27.5",
"jest": "^29.5.0"
}
}

這邊我們加入了 linttest 兩個指令,分別是執行 ESLint 跟測試,以上準備好之後,就可以繼續回來寫 Job 了囉。

一開始我們會針對 ESLint 跑一下掃描,所以 Job 就會這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name: Main Pull Requests Check
on:
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run lint

到目前為止來講,你只要 ESLint 有發生錯誤,那麼這個 Job 就會失敗,而且會顯示在 GitHub Actions 上面,如下圖

lint

只有你修正了該 ESLint 錯誤才能夠繼續往下執行

通過

測試跟打包呢?其實測試跟打包都是一樣差不多的行為,只是因為我們是 Express 專案並不需要打包的行為,所以我底下程式碼會忽略掉打包的行為

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name: Main Pull Requests Check
on:
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run test

這邊你會發現一件很有趣的事情,也就是類似的行為 runs-on: ubuntu-latestuses: actions/checkout@v3uses: actions/setup-node@v3 這些行為都是重複的,為什麼呢?因為每個 Job 都是獨立的,所以每個 Job 都必須要有這些行為,這邊你可以想像成每個 Job 都是一個獨立的 Container,所以每個 Job 都必須要有這些行為,這樣才能夠執行哩。

那麼這邊有一件事情很特別,因為 GitHub Actions 在執行 Job 時的順序是不固定的,所以有可能是 ESLint 先跑完又或者是測試先跑完,那麼如果你期望順序是先跑完 ESLint 再跑測試,那麼你就必須要使用 needs 這個屬性,這個屬性可以讓你指定 Job 的執行順序,所以這邊我們就會這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: Main Pull Requests Check
on:
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run lint
test:
needs:
- lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run test

否則的話你應該會在 GitHub Actions 的可視化圖形長這樣子

可視化圖形

當你使用了 needs 這個屬性後,你會發現可視化圖形會變成這樣

可視化圖形

Note
可視化圖形觀看位置位於專案的 Actions 頁面。

到目前為止我們的 PR workflow 就完成了,接下來就是要來寫部署的 workflow 囉~

Deploy

接下來打開 .github/workflows/main-deploy.yml 檔案,這邊我們會監聽 push 事件,所以我們就會在 on 屬性下面加入 push,代表 Pull Requests 合併到主要分支後就會觸發這個 workflow,並且我們只監聽 main 這個分支

1
2
3
4
5
name: Main Deploy
on:
push:
branches:
- main

只需要做到這樣子,你就可以監聽並執行部屬行為了,很簡單吧?

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 上面

Auto Deploy

如果是已經建立好的專案,那麼你可以到專案的設定頁面關閉 Auth Deploy

Auto Deploy

那麼剛好你也會看到 Deploy Hook 的欄位,請把這個網址複製起來,因為我們會使用這個網址來部署,請小心不要把這個網址外流出去,因為這個網址可以讓任何人都可以部署你的專案,所以請小心保管。

接下來我們必須要到個人設定中取得 API Key,請你點一下自己頭像找到「Account Setting」

Account Setting

接著找到 API Keys,並點一下「Create API Key」

Create API Key

名稱基本上隨便你取,我這邊是取名為 Example,按下「Create API Key」之後,你會取得一組 Key,請把那一組 Key 保存下來,因為我們會使用到這一組 Key

Key

接下來讓我們回到 Job 的部分,那麼部署之前你可能會想說,雖然 Pull Requests 已經通過了,但是我還是想要再跑一次 ESLint 跟測試,這樣才能夠確保我們的專案是沒有問題的,所以可能就會這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: Main Deploy
on:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run lint
test:
needs:
- lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run test

(其實就只是把 Pull Requests 的 workflow 複製過來而已)

再來就是要來撰寫部署了,在部署之前我們會先把剛剛複製的 Deploy Hook 跟 API Key 貼到 GitHub 的 Secrets 裡面,位置會在該儲存庫的 Setting 可以找到

Secrets

接著點一下「new repository secret」,然後輸入 RENDER_DEPLOY_HOOKRENDER_TOKEN 並貼入相對應的值,最後點一下「Add secret」就完成了~

都完成以上後回來到 main-deploy.yml 繼續撰寫部署的部分,基本上都跟剛剛差不多,只是我們必須跑完 ESLint 跟測試後才能夠部署,所以我們就會使用 needs 這個屬性,這邊我們就會這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: Main Deploy
on:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run lint
test:
needs:
- lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run test
deploy:
needs:
- lint
- test
runs-on: ubuntu-latest

接下來呢?接下來這邊稍微有一點特別了,如果要部署到 Render.com 的話將會使用到 curl 的方式,因此就會變成以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: Main Deploy
on:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run lint
test:
needs:
- lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- run: npm run test
deploy:
needs:
- lint
- test
runs-on: ubuntu-latest
steps:
- name: Deploy
run: |
curl -X POST -H "Authorization: Bearer ${{ secrets.RENDER_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{}' \
${{ secrets.RENDER_DEPLOY_HOOK }}

接下來你就可以試著發起 PR 並且合併到 main 分支,你會發現 GitHub Actions 會自動執行,並且會自動部署到 Render.com 上面囉~

部署中

部署完成

部署完成

最後這邊我也附上我寫完的範本給予參考哩

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ