前言

你有沒有曾經想過,在部署程式碼之前,希望先檢查一下有沒有錯誤,或者程式碼格式有沒有符合規範?這時候,CI/CD 就可以派上用場。CI(持續整合)可以幫你自動檢查程式碼品質,而 CD(持續交付/部署)則可以自動化部署流程,讓你的程式碼從開發到上線都更順暢。

但是,如果你有很多專案,而且每個專案都需要寫一模一樣或非常相似的 CI/CD 腳本,這就會變成一件既繁瑣又浪費時間的事情。你可能會想:「有沒有辦法只要簡單放入某些內容,就可以套用特定的腳本?」

其實,GitLab CI/CD 就提供了這樣的功能。透過重複利用與共享配置,你可以「讓多個專案共用同一套 CI/CD 流程」,不但節省時間,還能確保所有專案都遵循相同的規範。

例如,你可以在 .gitlab-ci.yml 套用別人是先撰寫好的腳本:

1
2
3
4
include:
- component: $CI_SERVER_FQDN/my-org/security-components/secret-detection@1.0.0
inputs:
stage: build

這樣,你的 stages 裡面就會載入 secret-detection 裡面所包含的所有 stages,可以直接套用別人寫好的流程,再也不用每次都重頭寫一遍!
在後續文章中,我會進一步介紹 GitLab CI/CD Component 的用法以及最佳實踐,讓你更輕鬆地掌握自動化流程的威力!

CI/CD Component 是一種可重複使用的單一 Pipeline 配置單元。你可以利用這些組件來建構大型 Pipeline 的一小部分,甚至可以用它們來組合出完整的 Pipeline 配置。這些組件可以透過輸入參數進行配置,以實現更動態的行為。CI/CD Component 與其他使用 include 關鍵字引入的配置類似,但它具有以下幾個優勢:

  • 可以在 CI/CD Catalog 中列出: 你可以方便地瀏覽和管理已發佈的組件。CI/CD Catalog 類似一個用於與他人存儲和分享 CI/CD Components 的地方。
  • 可以以『特定版本』發佈和使用: 讓你能夠穩定地引用特定版本,避免因更新造成的問題。
  • 同一專案中可以定義多個組件並一同版本管理: 更容易進行維護和升級。

除了自行創建組件外,你也可以在 CI/CD Catalog 中尋找已發佈、且符合你需求的組件,省去重頭開始設計的麻煩。

1 Basic Introduction

這個章節主要會介紹:

  • 建立一個 Component 的架構
  • 如何引用 Component
  • Component 的版本控制

1.1 Directory structure

一個 Component Project 是一個 Gitlab project (就是一個 repository)裡面其中一個 component。所有在這個 project 底下的 components 是一起進行版本控制的,每個專案最多有 30 個 components

那了解之後我們可以來看看如果要建立一個 Component Project 該怎麼做。GitLab Component 有嚴格的目錄結構,必須清晰且具有良好的可維護性。以下是必要的內容和建議的結構配置:

必須包含的檔案

  • README.md: 一個最外層的 README.md 檔案,用來詳細說明儲存庫中所有 Component 的內容和用途。可以提供每個 Component 的簡介及如何使用的說明。
  • templates/: 在專案根目錄下建立 templates/ 資料夾,裡面放置所有 Component 的配置檔案。

templates/ 底下的結構

可以有兩種定義方式

單一檔案形式:每個 Component 各自一個 .yml 檔案,例如:

1
2
templates/
└── secret-detection.yml

子目錄形式:如果某個 Component 包含多個相關檔案,可以使用子目錄,並以 template.yml 作為進入點,子目錄中可以包含多個相關檔案,適合用於複雜的 Component。

  • 可選的 README.md 如果某個 Component 的內容較複雜,可以在其目錄內加上 README.md,用於提供更詳細的說明。
  • 這些子目錄中的 README.md 可以在頂層的 README.md 中加上連結,讓使用者更容易找到相關資訊。
1
2
3
4
5
templates/
└── secret-detection/
└── template.yml
└── scripts.sh
└── README.md

1.2 Use a Component

如果要開始把一個 component 加入到自己的 CI/CD 配置中,可以使用 include 關鍵字,並指定 Component 的路徑,比較需要注意的是 Component 的路徑怎麼組成:

1
2
3
4
include:
- component: <Gitlab-FQDN>/<project-path>/<component-name>@<specific-version>
inputs:
stage: build
  • Gitlab-FQDN
    • 你的 GitLab 伺服器的完整網域名稱,例如 gitlab.com
    • 如果是同一包 component 專案可以使用 $CI_SERVER_FQDN 來代表。
  • project-path
    • Component 專案的路徑,也就是你 component 所放置的 repository name。
    • 如果是同一包 component 專案可以使用 $CI_PROJECT_PATH 來代表。
    • 通常如果沒特別設置會是 username/project-name
  • component-name
    • Component 的名稱,也就是你 component 的檔案名稱。
    • 通常會是 template 底下的 .yml 名稱,或是 folder 名稱。
    • 例如 templates/secret-detection.yml 或是 templates/secret-detection/template.yml 那就填寫 secret-detection
  • specific-version
    • Component 的特定版本,可以是 ~latest 或是 1.0.0 這樣的版本號。
    • 是透過打 tag 來進行版本控制。
    • 又或是使用分支名稱,如 master 來代表最新的版本。

warning:需要注意的是 Pipeline 裡面如果使用 include 載入的 component 都不是獨立處理的,他會把 component 裡面所有的 configuration, stages 等等都合併到原本的 pipeline 裡面,所以要注意 component 裡面的設定是否會影響到原本的 pipeline。像是你的 Pipeline 跟 Component 裡面有同名的 stages,那就會有衝突。

Notice: 如果元件需要使用到像是 Token 或是密碼等敏感數據才能運作,請務必審核 Component 的原始碼,確保不會有任何敏感數據外洩的風險。

1.3 Version Control

建議不要使用 branch 或是 SHA 來引入 component,因為很有可能使用的版本不存在,但是可以用於測試。另外需要注意的是,因為所有在這個 project 底下的 components 是一起進行版本控制的,如果該 project 底下的 component 如果有其他版本的依賴,請務必把該 component 移到專屬的 Component 專案中。

我們剛剛看到使用 include 指定 component 時可以選擇版本,版本可能來自於 tag 或是 branch,但是他們是有優先順序的:

  • SHA
    • 例如 e3262fdd0914fa823210cdb79a8c421e2cef79d8 提供的 SHA 是最高優先順序,如果指定了 SHA,則會忽略其他版本號。
  • tag
    • 例如 1.0.0 如果指定了 tag,則會使用 tag 來引入 component。
    • 請根據Semantic Versioning 2.0.0規則命名 tag。
  • branch
    • 例如 master 如果指定了 branch,則會使用 branch 來引入 component。
    • 如果存在與 tag 相同名稱的分支,則會優先使用 tag。
  • ~latest
    • 會指到最新的tag版本 (Semantic Version),僅當您希望使用絕對最新的版本(可能包含重大更改)才使用。

關於 Semantic Version:

  • 對於 user 而言 最好的方式是可以自動的的使用 minor 或是 patch 版本,這樣可以確保不會有重大的更動,同時確保穩定性及最新的錯誤修復與補丁。
    • 可以使用 major 或是 minor version 但是不使用 patch version。
    • 使用 minor verion 像是 1.1 來引入 component,這樣可以確保不會有重大的更動,它包含了 1.1.01.1.9,但不包含 1.2.0
    • 使用 major version 像是 1 或是 1. 來引入最新版本,這包含 1.0.0 或是 1.9.9,但不包含 2.0.0
  • 對於 component owner 而言 允許使用透過設置 major 版本發佈新功能,同時不會影響到舊有版本的使用者,從而有時間按照自己的節奏更新 Pipeline。

舉例來說,發布的版本順序可以像是這樣:

  1. 1.0.0 發布,major version
  2. 1.1.0 發布,minor version
  3. 2.0.0 發布,major version
  4. 1.1.1 發布,patch version
  5. 1.2.0 發布,minor version
  6. 2.1.0 發布,minor version
  7. 2.0.1 發布,patch version

在上面的範例中:

  • 1 會使用 1.2.0,因為它是最新的 minor version。
  • 1.1 會使用 1.1.1,因為它是最新的 patch version。
  • ~latest 會使用 2.1.0,因為它是最新的 major version。

注意的是,如果你的版本是欲發佈版本,需要特別指定完整的版本名稱,像是 1.1.0-rc1 或是 1.1.0-beta 這樣的版本。

需要注意的是一個 CI/CD Component 具有與其他 component 的依賴性時,需要與其他 component 使用不同的版本管理時,就應該把它移到專屬的 Component 專案中。這樣做的目的是避免版本衝突,並且更方便地維護和更新。

2 Write a Component

接下來這邊會介紹一些高品質的 Component 所需遵循的最佳實踐:

2.1 Manage Dependencies

Component 裡面是可以引用其他 Component 的,但是請務必『仔細選擇依賴』應該遵循以下原則:

  1. 將依賴關係保持在最低限度,少量的重複比依賴好
  2. 盡可能的使用 local 本地依賴,可以確保在多個檔案中使用相同的 Git SHA。
  3. 當某個 component 需要依賴於其他 component 時,請不要使用 ~latest,而是使用特定的版本號,這樣可以確保不會因為其他 component 的更新而導致問題。
  4. 請定期更新 component 的依賴,然後發部具有更新依賴的元件新版本。

2.2 Write a clear README

再來就是寫一個清晰的 README,這樣可以讓使用者更容易了解 component 的用途,以及如何使用。README 應該包含以下內容:

  • 文件應該從這個 Project 的 components 主要提供什麼功能做介紹。
  • 如果一個 Project 包含多個 Components 請使用 Table of Content 協助使用者快速跳轉到特定的元件。
  • 添加 ## Components 標題,並且底下再包含 ### Component A 等子部分。
  • 在每個 component 的說明中應該要包含:
    • 元件的功能說明
    • 至少添加一個 YAML 的範例,展示他如何使用
    • 如果元件需要輸入 inputs 請添加一個『表格』並且描述每一個 input 的『名稱』、『類型』、『預設值』、『描述』。
    • 如果元件有使用仁和『變數』或是『Secret』,也請務必記錄下來。
  • 如果歡迎貢獻,建議使用 ## Contribution 標題,並且提供如何貢獻的方式。

如果元件需要更多說明,請在元件目錄的 Markdown 檔中添加其他文件,並從主 README.md 檔案連結到該文件。例如:

1
2
3
4
5
6
7
8
README.md    # with links to the specific docs.md
templates/
├── component-1/
│ ├── template.yml
│ └── docs.md
└── component-2/
├── template.yml
└── docs.md

以下是一個範例:

更多鏈可以參考:Deploy to AWS with GitLab CI/CD component’s README.md

2.3 Test Component

強烈建議在開發工作流程中測試 CI/CD 元件,這有助於確保行為一致。可以直接在根目錄建立 .gitlab-ci.yml 並且在裡面引入 component 來進行測試。如果必要可以使用 Gitlab API來檢查 component 的行為。

下面是一個範例,每次的推送與提交,管道會測試元件是否已添加到管道中,然後可以檢查通過使用 tag 來創建發佈。

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
include:
# include the component located in the current project from the current SHA
- component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/my-component@$CI_COMMIT_SHA
inputs:
stage: build

stages: [build, test, release]

# Check if `component job of my-component` is added.
# This example job could also test that the included component works as expected.
# You can inspect data generated by the component, use GitLab API endpoints, or third-party tools.
ensure-job-added:
stage: test
image: badouralix/curl-jq
# Replace "component job of my-component" with the job name in your component.
script:
- |
route="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs"
count=`curl --silent "$route" | jq 'map(select(.name | contains("component job of my-component"))) | length'`
if [ "$count" != "1" ]; then
exit 1; else
echo "Component Job present"
fi

# If the pipeline is for a new tag with a semantic version, and all previous jobs succeed,
# create the release.
create-release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
script: echo "Creating release $CI_COMMIT_TAG"
rules:
- if: $CI_COMMIT_TAG
release:
tag_name: $CI_COMMIT_TAG
description: "Release $CI_COMMIT_TAG of components repository $CI_PROJECT_PATH"

2.4 Avoid Hard-code, Global Keyword

Hard-code

  • 如果在 component 裡面使用其他的 component 請使用 $CI_SERVER_FQDN,如果在元件中訪問 GitLab API,請使用 $CI_API_V4_URL,這樣可以確保元件可以在任何 GitLab 實例上運行。
  • 請務必參考 Predefined Variables 來確保你的 component 可以在任何環境中運行。

Global Keyword

  • 有些是 gitlab 的 default global keyword 是不支援的,請不要使用。像是after_script或是image等等。
  • 如果真的想要使用建議可以用 extends 來取代。
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
# Not recommended
default:
image: ruby:3.0

rspec-1:
script: bundle exec rspec dir1/

rspec-2:
script: bundle exec rspec dir2/

# Recommended
.rspec-image:
image: ruby:3.0

rspec-1:
extends:
- .rspec-image
script: bundle exec rspec dir1/

rspec-2:
extends:
- .rspec-image
script: bundle exec rspec dir2/

# Output
rspec-1:
image: ruby:3.0
script: bundle exec rspec dir1/

rspec-2:
image: ruby:3.0
script: bundle exec rspec dir2/

2.5 Replace Hard-code with inputs

避免在 CI/CD 元件中使用 hard-code 的值。hard-code 可能會迫使 component user 需要查看 compoonent 的內部詳細資訊,並調整其 pipeline 以使用元件。通常常見的 hard-code 是 stage 名稱,如果一個 component 的 job 其 stage 名稱是 hard-code 的,那使用這個 component 的 pipeline 就必須定義完全相同的 stage 名稱。

建議使用 inputs 來指定 component user 需要的值。例如在 component 做以下設定:

1
2
3
4
5
6
7
8
9
10
11
12
spec:
inputs:
stage:
default: test
---
unit-test:
stage: $[[ inputs.stage ]] # 讓使用者自己定義 stage 名稱
script: echo unit tests

integration-test:
stage: $[[ inputs.stage ]]
script: echo integration tests

引用方法

1
2
3
4
5
6
stages: [verify, release]

include:
- component: $CI_SERVER_FQDN/myorg/ruby/test@1.0.0
inputs:
stage: verify

也可以使用 inputs 來定義 job name,我們也應該避免在 CI/CD 中對 job name 進行 hard-code,當 component user 可以自定義 job name 時,可以有效的防止與其管道中的現有名稱發生衝突。使用者還可以透過使用不同的 job name 來多次包含具有不同輸入選項的 component。

例如可以使用 inputs 允許 component user 定義特定的 job name 或是 job 的 prefix 前綴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spec:
inputs:
job-prefix:
description: "Define a prefix for the job name"
job-name:
description: "Alternatively, define the job's name"
job-stage:
default: test
---

"$[[ inputs.job-prefix ]]-scan-website":
stage: $[[ inputs.job-stage ]]
script:
- scan-website-1

"$[[ inputs.job-name ]]":
stage: $[[ inputs.job-stage ]]
script:
- scan-website-2

2.6 Replace CI/CD Variables with inputs

在 component 使用 CI/CD 變數時,請評估是否應該改用 inputs 來定義這些變數。

  • 這樣可以讓 component user 輕鬆地設置這些變數,而避免要求使用者定義自定義變數來配置元件
  • 另外,使用 spec.inputs 的好處是,如果缺少必要的 inputs 那 pipeline 會返回錯誤,但是如果 CI/CD variables 沒有定義的話,pipeline 會繼續執行,不會有錯誤訊息。
1
2
3
4
5
6
7
spec:
inputs:
scanner-output:
default: json
---
my-scanner:
script: my-scan --output $[[ inputs.scanner-output ]]

component 使用

1
2
3
4
include:
- component: $CI_SERVER_FQDN/path/to/project/my-scanner@1.0.0
inputs:
scanner-output: yaml

有幾個例外狀況可以使用 CI/CD 變數:

  1. 使用 predefined_variables 這些變數是不需要使用 inputs 來定義的。
  2. 如果需要儲存敏感資訊,並且透過 masked 等方式設定。

3 Security Considerations

在專案中使用 component 之前,您應該仔細查看這些元件。使用 GitLab CI/CD 元件的風險由您自行承擔,GitLab 無法保證第三方元件的安全性。

  1. 審核和審查元件原始程式碼:仔細檢查代碼以確保它沒有惡意內容。
  2. 盡可能地減少對 credentials 或 tokens 的依賴性
    1. 確保 component’s source code 裡面所使用的敏感訊息是在你的期望與授權範圍內。
    2. 使用最小範圍的 token。
    3. 避免使用長期訪問的 token。
  3. 使用固定的版本:盡可能的使用發佈的版本,僅當你信任 component owner 時才使用 release 標籤,避免使用 ~latest
  4. 安全儲存 secrets:不要將 secret 儲存在 CI/CD configuration 檔案裡。
  5. 使用 ephemeral (短暫的) 且 isolated (隔離) 的環境執行 runner
  6. 安全的處理 cache 和 artifacts:除非必要,否則不要將管道中其他 job 的 cache 或是 artifacts 傳遞給 component。
  7. Review component changes:當 component 有更新時,請確保裡面更動的內容。
  8. 仔細檢查 component 使用的 container images:確保裡面沒有任何惡意的內容。

4. Conclusion

要維護安全可信的 CI/CD component 並確保您交付給使用者的管道配置的完整性,請遵循以下最佳實踐:

  • 使用雙重身份驗證 (2FA):確保所有 CI/CD 元件項目維護者和擁有者都啟用了 2FA,
  • 使用 Protected branches 開啟以下設定
    • 使用 Protected branches 來發佈版本,例如 master
    • 可以使用 Wildcards rules 來保護 release branch。
      • *-stable 保護所有以 -stable 結尾的分支。
      • release/* 保護所有以 release/ 開頭的分支。
    • 要求所有人對 Protected branches 使用 Merge Request 來進行更改。並且設定 Allowed to push and merge 為 No one
    • 不允許強制推送。
  • 禁止使用 @latest 在你的 README.md 中,這樣可以確保使用者不會因為不小心使用最新版本而導致問題。
  • 期更新 CI/CD component 的依賴,並且發佈具有更新依賴的元件新版本。

Reference