我採用 Gitlab Runner 運行在 Container,並使用 Docker 當 Executor,為每個 CI Job 建立一個乾淨的 Container 來執行 Docker 命令的做法。
以下我會展示從 0 開始設置的各個步驟,包含 gitlab-ci.yml 開始創建 & 指令的調整、Docker 版 Gitlab Runner 的 create & register 方式,Docker in Docker 與 Docker Socket 兩種的做法我都會提到,以及過程中的一些 error 問題排解。
前言
在此之前先解釋一下 Docker Socket 與 Docker in Docker 的差別。
Docker in Docker(又名 dind)顧名思義就是在 Docker 容器內再運行一個 Docker 容器來執行 Docker 命令的做法。
而我這裡要採用的做法是 Docker Socket(又名 docker.sock),Docker Socket 跟 Docker in Docker 很像,一樣是在 Container 內運行 Docker,但是 Docker Socket 是與 host 共用 Docker Daemon,內部建置 Container 的方式與層級不同,下面連結文章中會有詳細解釋,有興趣的小夥伴再自己去看吧,我後面會再展示使用上的差別。
關於 Docker in Docker 與 Docker Socket 的優缺點比較眾說紛紜,目前都各有支持者,大家可以參考以下文章,根據自身的情境選擇適合的做法。
Using Docker-in-Docker for your CI or testing environment? Think twice. (jpetazzo.github.io)
Why is Docker-in-Docker considered bad? - DevOps Stack Exchange
最後也會再跟大家提到我個人為什麼選擇使用 Docker Socket 的原因與考量~
示範環境
Linux OS Version:Ubuntu 20.4
Docker Version:20.10.21
Gitlab Version:15.5
Dockerfile TargetFramework:.NET 6 (這裡特別註明我是 .NET Project 是有原因的,後面會再提到)
安裝 Container 版 Gitlab Runner
這裡的 volume 有兩種選擇,local system volume mounts 與 Docker named volumes,其實就只是 volume 的路徑不同而已,你可以視情況擇一即可,不一定要跟我一樣。
run Git Runner in Container
[local system volume mounts 版]
docker run -d --name gitlab-runner --restart always \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest
[Docker named volumes 版]
Create the Docker volume
docker volume create gitlab-runner-config
docker run -d --name gitlab-runner --restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gitlab-runner-config:/etc/gitlab-runner \
gitlab/gitlab-runner:latest
關於 Volume 的說明我覺得這篇挺詳細的
The Complete Guide to Docker Volumes | by Mahbub Zaman | Towards Data Science
也可以看一下官方說明瞭解他們的差異
Manage data in Docker | Docker Documentation
Container 建立完畢
註冊 Gitlab Runner(基礎 Docker 版)
官方教學:Registering runners (deprecated) | GitLab
不要被 (deprecated) 嚇到,由於我的 Gitlab 目前是 15.5
版,所以用以下方式註冊沒有問題
這裡的註冊方式使用 Docker 版
後面如果沒有特別提到的部分代表是以 local system volume mounts 為主示範,如果你是用 Docker named volumes 可以再參考前面的指令自己替換
這裡要注意,如果 run git runner Container 時用哪一個 volume,註冊就必須要用同一種喔~他才有辦法建立註冊的關聯
上述官方教學內有說明去哪裏取得 registration token,我就不再說了,下面的 --registration-token 請自行更換成自己的,不要直接複製我的阿 😓
單行 register 指令
[local system volume mounts 版]
docker run --rm -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register
[Docker named volumes 版]
docker run --rm -it -v gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest register
此指令會後續再一個一個選項問你
maintenance 我沒有要輸入,所以直接按 Enter 就可以了
executor 這裡我選擇 Docker,讓每個 CI Job 都建立一個全新的 Container 來做事
executor 的詳情可參考以下文章
GitLab CI 之 Runner 的 Executor 該如何選擇? | 艦長,你有事嗎? (chengweichen.com)
這裡要注意的是 default Docker image,他是指到時候 gitlab-ci.yml 要預設使用哪個 image 運行 Container 來做 Job,我這裡是選擇預設 docker:20.10.16,你可以視情況,你的 job 比較常用哪個 image 就設置哪個就可以了。image 建議指定版本,以免某天因為版本升級後會有不同的動作而導致異常。
但是這種一個一個選項問你的設置方式,要是途中一旦打錯字就要整個重來,麻煩死了 🙃 ,而且他也不會全部選項都問一遍,如果還要多加些設置,也還要我自己再進去設定檔做手動調整,所以這裡可以改成將所有設置整合成多行的命令,一次執行就好
多行 register 指令
docker run --rm -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register \
--non-interactive \
--executor="docker" \
--docker-image docker:20.10.16 \
--url "https://git.kitty.net/" \
--registration-token "GR4148941iKu8vYTueE4AuzzBi22A" \
--description "kitty_docker_gitrunner" \
--tag-list "kittydemo_docker_runner" \
--run-untagged="true" \
--locked="false";
要小心指令不要有任何多餘的空格,都有可能導致他無法正常註冊
註冊完畢後,就可以在 Gitlab 的 runners 列表中看到了(看你註冊的是哪一種 runner 就去哪裏看)
※ 如果後續還想要改註冊的 info,可以點 🖍 編輯
到 Server 查看 config.toml,可以看到一些 runner 註冊的資訊
這章的註冊指令是基本的 Docker 版,後面還會再提到 dind 版與 docker.sock 版的指令需要再加些什麼,如果你已經確定好要用哪種方式,你也可以直接改參考下面兩章的指令擇一註冊就好了。
如果你原本已經使用基本的 Docker 版指令註冊了,我後面也會再提到執行 Job 時會遇到什麼問題,以及可以如何事後手動修改。
取消註冊 Gitlab Runner
如果你不小心設置有誤或者有其他原因想要重新註冊,可以用以下方式刪掉註冊的關聯重新註冊~
在 Gitlab 頁面中可以直接 Remove runner,但是這只是代表刪除了 Gitlab 上的 runner,並不代表 Server 上的 Gitlab Runner 有被刪除註冊的設置,所以還需要至 Server 上做以下操作
查看 gitlab runner 清單
gitlab-runner list
像上圖的範例可以看到有被註冊了 2 次
刪除註冊 gitlab runner
gitlab-runner verify --delete
此指令會逐一檢查所有 runner,刪除所有未註冊 / 已被刪除的 runner
在查看一次 list 就會發現只剩下一個了
添加 Docker 版 gitlab-ci.yml
搞定好 Git Runner 後,來設置 gitlab-ci.yml 吧~
如果你還沒新增過 ci yaml,可以使用以下方式新增,如果你已經有 yml 了,就直接跳過這章吧
至 Gitlab Project > CI/CD > Editor 頁
切換好你想要添加 yaml 的 Branch,然後點選中間的 Configure pipeline
藍色按鈕
一進來他就會先幫你 create 好一個 Default 的 gitlab-ci.yml,但是這不是我們要的
點選 Browse templates
按鈕
在這裡 Gitlab 貼心的有提供很多版本的 yaml 範例
我們這裡可以參考 Docker 版的 yaml 來改
不過 Pipeline Editor 頁面沒辦法直接套用 template
你可以選擇以下方法:(這 2 種操作出來的內容都是一樣的,擇一即可)
🟣 前往 Browse templates 頁面,把想要的 template 內容複製,貼回 Pipeline Editor 頁
🟣 回 Project 主頁,使用 New file 功能
在下拉視窗中選擇
選擇完後他就會自動幫你添加你選擇的 Template 內容了
補充閱讀:Gitlab CI/CD yaml的撰寫注意事項
Docker 版 gitlab-ci.yml 說明
這裡我們拿這一個官方的 Docker 版 gitlab-ci.yml 範本來說明
lib/gitlab/ci/templates/Docker.gitlab-ci.yml · master · GitLab.org / GitLab FOSS · GitLab
官方這裡的 Template 是 Docker in Docker 版的 yaml
# This file is a template, and might need editing before it works on your project.
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Docker.gitlab-ci.yml
# Build a Docker image with CI/CD and push to the GitLab registry.
# Docker-in-Docker documentation: https://docs.gitlab.com/ee/ci/docker/using_docker_build.html
#
# This template uses one generic job with conditional builds
# for the default branch and all other (MR) branches.
docker-build:
# Use the official docker image.
image: docker:latest
stage: build
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# Default branch leaves tag empty (= latest tag)
# All other branches are tagged with the escaped branch name (commit ref slug)
script:
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
他與一般的 Job 內容相比,多了一些以下設置項目:
image: docker:latest
:指定 executor 要使用哪一個 image 來起 Container 執行 Job,這個不一定要設置,沒有設置的話,就會使用前面 register Git Runner 時設置的 Default Image
services:
:可以在這裡指定其他 image,他會 create 另一個 Container,並且可以在 Job 中與其他 Container 溝通(可以指定多個)。更多使用詳情可參考官方文章 Services | GitLab
- docker:dind
:Docker in Docker 做法的話一定要加這個
docker login
:這是用來登入 docker registry 的指令,如果你們的 registry 不需要登入即可使用,這行就可以不用寫
docker build
:將你的 Project 包成 docker image
docker push
:將你的 docker image 發布至指定的 registry
rules:
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
為此 Job 添加 rule 判定
這裡是判定如果這個 branch 內存在 Dockerfile 檔案,才會執行這個 Job,判定的路徑預設為 Gitlab Project 根目錄
過程中可以看到有些 $CI_COMMIT_BRANCH
等內容 ,這是官方預設的 variables,提供你更方便的存取某些 data,更多 predefined variables 可參考 Predefined variables reference | GitLab
dind 版之疑難排解
因為官方提供的預設 yaml 是 docker in docker 版的,所以我先使用 Docker in Docker 的部屬方式,上面的 yaml 照理來說應該就要可以直接使用了,不過…………
當我第一次執行 Job 時,發現出現以下錯誤 👇
⛔ 問題 1:dial tcp: lookup docker on 168.95.1.1:53: no such host
🟢 問題 1 解決辦法:這是因為 dind 做法必須開啟特權模式,default 的 register Runner 指令預設 privileged = false 才會出此錯誤
進到 Git Runner Container bash 內的 /etc/gitlab-runner 路徑,編輯 config.toml
將 privileged 改為 true,改好儲存,不用重啟 Container,再執行一次 Pipeline 即可排除
接著會遇到第 2 個錯誤 👇
⛔ 問題 2:Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?
🟢 問題 2 解決辦法:這是因為 TLS 的問題,因為預設會啟用 TLS 進行通訊,可以參考官方解答 Use Docker to build Docker images | GitLab 選擇 啟用 TLS 的設置做法 or 禁用 TLS(官方寫得很詳細了,我就不補充了)。
TLS 的補充說明
dind 疑難排解的手動設置項目,可以參考 註冊 Gitlab Runner(dind 版)章節,在註冊時直接指定,就不用再進去手動調整了。 |
註冊 Gitlab Runner(dind 版)
多行註冊的腳本
docker run --rm -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register \
--non-interactive \
--executor="docker" \
--docker-image docker:20.10.16 \
--url "https://git.kitty.net/" \
--registration-token "GR4148941iKu8vYTueE4AuzzBi22A" \
--description "kitty_docker_gitrunner" \
--tag-list "kittydemo_docker_runner" \
--run-untagged="true" \
--docker-privileged \
--locked="false";
與基礎版相比,多了 --docker-privileged
dind 版 yml 優化
做完上述異常排除後,就可以正常的產出 docker image 了,不過由於他每次 Job 都會重新啟動一個全新的 Container,所以導致他執行時間非常非常久,所以在 gitlab-ci.yml 還需要做一些優化~
在執行 docker build 前,預先下載前一個版本的 docker image
docker pull $CI_REGISTRY_IMAGE
在 docker build 命令中加入 --cache-from
docker build --pull --cache-from $CI_REGISTRY_IMAGE -t "$CI_REGISTRY_IMAGE" .
因為 dind 是 create 子容器 & 在子容器中執行 CI Job,所以導致他預設無法使用任何 image layer 的 cache,就必須要 pull 上一個版本的 image ,並指定進 Container 中當 image cache
使用後,可以看到 CI Job 就會判定,有完全相同的 image layer 就直接用,不用重新 create layer,如此一來就可以節省不少 build 的時間
不過這時我卻發現,原本應該要節省不少時間的,卻發現實際總時長根本沒有減少???
並且仔細觀察之後,發現除了 base 階段 & final 階段的 image layer 有 Using cache 以外,其他東西都沒有 cache 到!!
後來我才意識到……因為在 .NET Core 的多階段 Dockerfile 中,只會把 base & pulish & final 階段的東西打包成 image,build 階段完全沒有包進 image 裡面(也不應該包進去,因為太龐大了),也就是說,因為 dind 是使用 image 當作 cache 來源的,所以 image 內沒有的 layer 當然也就無法重複使用
這麼一來,執行時間最久的 restore 等內容就完全無法用到 cache,每次都要重新 build layer ……😭
這裡如果看不懂我指的 .NET Core 多階段 Dockerfile 是什麼的話,可以參考我的上一篇文章
[Docker] .NET Core 的 Dockerfile 指令詳解 | K. C. - 點部落 (dotblogs.com.tw)
改用 docker.sock 版運行 Gitlab CI
因為 dind 版無法完整的使用 .NET 的 cache 減少 build 的時間,所以我必須改用 Docker Socket 的運行方式。改綁定 docker.sock 的話,git runner & yaml 都需要做些更改。
Gitlab Runner register info 調整
進到 Git Runner Container bash 內的 /etc/gitlab-runner
路徑,編輯 config.toml
改綁定 docker.sock,就可以不用再開 privileged mode 了
還有在 volumes 添加 "/var/run/docker.sock:/var/run/docker.sock"
以上設置也可以直接參考 註冊 Gitlab Runner(docker.sock 版)章節,在註冊時直接指定,就不用再進去手動調整了。 |
gitlab-ci.yml 調整
砍掉
services:
- docker:dind
砍掉 --cache-from
綁定 docker.sock 後,他不會再像 dind 是 create 子層級的容器,他會 create 同層級的容器,因此 image layer cache 可以共用,不需要再指定 image 當 cache
預先 pull image 下來用於 cache 的動作也可以砍掉了
docker pull $CI_REGISTRY_IMAGE
執行 Job 後,就可以看到 build 階段的 csproj 那些東西也可以使用 cache 了 🥳
拿來跟 dind 版的 Job 進行比較
(為了可以更清楚展示哪些部分會使用 cache,所以這裡以在不動 code 的情況下,直接拿同個 branch 重複 run Pipeline)
dind 的第一次執行 & 第二次執行,由於能 cache 的內容不多,所以執行時間並沒有減少,每次執行都花費差不多的時間
docker.sock 的第一次執行 & 第二次執行,雖然第一次執行的時間比較久,但是由於執行時間最耗時的 build 階段有 Using cache,所以第二次的執行時間比第一次整整減少了 6 倍~~~
這裡是因為 code 完全沒有任何變更所以才有辦法減少那麼多執行時間,實際上每次的執行效率還是會以你改動的幅度、能使用 cache 的量而有所增減喔!
註冊 Gitlab Runner(docker.sock 版)
docker run --rm -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register \
--non-interactive \
--executor="docker" \
--docker-image docker:20.10.16 \
--url "https://git.kitty.net/" \
--registration-token "GR4148941iKu8vYTueE4AuzzBi22A" \
--description "kitty_docker_gitrunner" \
--tag-list "kittydemo_docker_runner" \
--run-untagged="true" \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock \
--locked="false";
與基礎版相比,多了 --docker-volumes /var/run/docker.sock:/var/run/docker.sock
最終 Gitlab yml
最後附上我的 docker.sock 版最終 yml 給大家參考
default:
before_script:
- echo CI_COMMIT_BRANCH=$CI_COMMIT_BRANCH
- >
if [ "$CI_COMMIT_BRANCH" == 'deploy/docker-poc' ]; then
export ENVIRONMENT="POC"
elif [ "$CI_COMMIT_BRANCH" == 'deploy/docker-sit' ]; then
export ENVIRONMENT="SIT"
elif [ "$CI_COMMIT_BRANCH" == 'deploy/docker-uat' ]; then
export ENVIRONMENT="UAT"
elif [ "$CI_COMMIT_BRANCH" == 'deploy/docker-prod' ]; then
export ENVIRONMENT="PROD"
else
export ENVIRONMENT="POC"
fi
- echo ENVIRONMENT=$ENVIRONMENT
stages:
- build
docker-build:
stage: build
tags:
- kittydemo_docker_runner
variables:
CI_PRIVATE_REGISTRY: 'docker.private.net'
DOCKERFILE_PATH: Demo.Api/Dockerfile
IMAGE_NAME=$CI_PROJECT_NAME:$ENVIRONMENT
script:
- docker build --pull -t "$CI_PRIVATE_REGISTRY/$IMAGE_NAME" -f $DOCKERFILE_PATH .
- docker push "$CI_PRIVATE_REGISTRY/$IMAGE_NAME"
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH
exists:
- '*/Dockerfile''
這篇文章由於是著重在 Docker 執行 CI Job 的部分,所以這裡我只展示 build Job 的內容,其他的 Job 之後有機會的話我再獨立一篇文章說明 ~ 😄