Homemade CI/CD Workflow#
先說為什麼要自幹,我爽,因為其他的都太複雜,我想要乾淨清爽 <— 官腔說法
自幹 CI/CD 有幾個步驟要克服
- build image
- 通知伺服器 image 有更新
- 重開伺服器
第一點也是最重要的,既然都在 GitHub 上代管原始碼了,當然先選 GitHub Action 囉,第二點有兩個選擇一個是由伺服器每隔一段時間去檢查 image 有沒有更新,可以用 這個套件 ↗,但是我不太喜歡這樣,因為我覺得他沒有「持續交付」的感覺。我想到了 webhook,所以我要找一個可以開 webhook 伺服器的 docker image。
技術都選好了,就來慢慢
填坑完成囉!
Build Image#
先假設我們的 Dockerfile 已經寫好了,可以參考 這裡,那麼 build 這個動作就是這樣定義,另外既然都在 GitHub 上 build image 了,就順便用 ghcr ↗ 吧。
- name: build image
run: |
docker build . -t ghcr.io/simbafs/coscup-attendance:latest -t ghcr.io/simbafs/coscup-attendance:${{ steps.vars.outputs.tag }}
yaml裡面有個奇怪的東西 ${{ steps.vars.outputs.tag }}
,這是代表 GitHub Action 某個步驟的產出變數 tag
,這是為了幫 build 出來的 image 加上 tag,所以開支線任務:「找出 tag」
支線任務:「找出 tag」#
tag 不會憑空生出來,所以我們需要一個來源,我選擇 git tag。那麼根據谷歌大神的開示,用 ${GITHUB_REF#refs/*/}
可以找出這次觸發 Action 的 tag(或是 reference),那麼我們就在 build image
前面新增一個步驟,之後就都可以用 ${{ steps.vars.outputs.tag }}
取得 tag
- name: Set env
id: var
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
yamlPush Image#
登入 GHCR#
我們已經決定要推上 GHCR 了,首先一定要先登入才能推送的。
- name: 'Login to GitHub Container Registry'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
yaml注意這裡 ${{ github.actor }}
和 ${{ secrets.GITHUB_TOKEN }}
是不需要設定的,他會自己代入該代的值,不過要去設定裡面設定調整,讓 Action 可以寫入 package,位置在 settings > actions > general > Workflow permissions > read and write permissions
Push Image#
push 就很簡單,一條指令,不過我不確定怎麼把兩個 tag 都推上去,就分成兩條,如果第一條不寫只推 :latest
的話,新的 iamge 推上去舊的就會失去 tag 變成 untagged iamge,比較醜
- name: push image
run: |
docker push ghcr.io/simbafs/coscup-attendance:${{ steps.vars.outputs.tag }}
docker push ghcr.io/simbafs/coscup-attendance:latest
yamlGitHub Action 還沒完喔!
Webhook#
webhook 說穿了就是設定收到 http request 後要做什麼,用 bash 硬幹也不是不行,我找了一個用 go 寫的程式 webhook ↗,定義一個 JSON 檔,除了最基本的觸發外,還可以 設定條件 ↗ ,基本的值要相符、regex, 還有 HMAC 驗證,進階一點含有 and or not 等邏輯可以用。官方推薦了三個 docker image,第一個 ↗ 最多人用,但是他沒有 shell 可以 debug,所以我選擇了 第二個 ↗。
docker-compose.yml#
首先是 docker-compose.yml
version: '3.3'
services:
webhook:
image: roxedus/webhook
container_name: webhook
environment:
- PUID=0
- PGID=0
- TZ=Asiz/Taipei
- EXTRA_PARAM=-hotreload -verbose #optional
volumes:
- ./hooks.json:/config/hooks/hooks.json
- ./script/:/var/webhook/
- /volume1/docker/:/var/webhook/docker/
- /var/run/docker.sock:/var/run/docker.sock
- /usr/local/bin/docker:/usr/local/bin/docker
- /usr/local/bin/docker-compose:/usr/local/bin/docker-compose
ports:
- 5748:9000
yaml這裡 volume 掛了一堆東西,第一個是設定檔,第二個是方便放 script 和 log,第三個是為了到要更新的 docker 專案目錄執行 docker-compose
,後面三個都是為了可以執行 host 上的 docker 指令。
hooks.json#
接著是 hooks.json
[
{
"id": "coscup-attendance",
"execute-command": "/var/webhook/docker/coscup/update.sh",
"command-working-directory": "/var/webhook/docker/coscup/",
"trigger-rule": {
"match": {
"type": "value",
"value": "random key",
"parameter": {
"source": "payload",
"name": "key"
}
}
}
}
]
jsonid
就是 webhook 裡的 id https://localhost:9000/hooks/{id}
,然後是要執行的命令和工作目錄,命令建議寫絕對路徑,不過路徑要是掛載到 docker container 裡面後的路徑,不是在外面的路徑。接著 trigger-rule
描述了要在 payload(就是 http body)中 key
欄位要是 "random key
才會執行命令,詳細設定可以去 這裡 ↗ 看。以這裡的設定為例,要成功執行的話就要用以下方式呼叫才會執行 update.sh
$ curl -XPOST --header 'Content-Type: application/json' -d'{"key": "random key"}' http://localhost:9000/hooks/coscup-attendance
bashupdate.sh#
接著就是當 webhook 執行時要執行的 update.sh,只有三個步驟,down、pull、up
#!/bin/sh
cd /var/webhook/docker/coscup
/usr/local/bin/docker-compose down
/usr/local/bin/docker-compose pull
/usr/local/bin/docker-compose up -d
bashGitHub Action 觸發 webhook#
最後一步,設定讓 Action build 完 image 後就發 request 觸發 webhook
- name: trigger webhook
run: |
curl -XPOST --header 'Content-Type: application/json' -d'{"key": "${{ secrets.WEBHOOK_KEY }}"}' "https://webhook.simbafs.cc/hooks/coscup-attendance"
yaml這裡把 "random key
拉出來放到 secrets 裡面是因為我不希望隨便一個人都能重啟 docker container,雖然不會怎樣但是服務會被中斷,所以才設計這個密碼,至於為什麼不用 hmac 驗證呢?如果是 hooks.json
洩漏,那麼有沒有驗證都沒差了,如果是封包內容被抓到,也是沒差了,因為我的 payload 每次都一樣,如果 webhook 能驗證時間之類的才會有用,所以單純驗 value 就可以了,而且我都是走 https,要洩漏也沒那麼容易。
完整 GitHub Action#
說了這麼多 GitHub Action 都還是零碎的片段,下面就是完整的設定檔,我把 build, push 和 webhook 分成兩個 jobs 是因為我光是測試 webhook 就好幾次,每次重跑 build 真的好久(抹汗。另外希望以後有機會能把 build-and-push
拆分的更細。然後開頭我有設定只有當 git tag 符合 v*.*.*
才會觸發這個 Action,主要是希望 ${{ steps.vars.outputs.tag }}
抓到的都是可以用的版本邊號,不會是沒有上標籤的 main,而且可以控制什麼時候要發新版本。
name: Deploy Images to GHCR
on:
push:
tags:
- 'v*.*.*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@main
- name: 'Login to GitHub Container Registry'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set env
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: echo
run: echo ${{ steps.vars.outputs.tag }}
- name: build image
run: |
docker build . -t ghcr.io/simbafs/coscup-attendance:latest -t ghcr.io/simbafs/coscup-attendance:${{ steps.vars.outputs.tag }}
- name: push image
run: |
docker push ghcr.io/simbafs/coscup-attendance:${{ steps.vars.outputs.tag }}
docker push ghcr.io/simbafs/coscup-attendance:latest
cd:
runs-on: ubuntu-latest
needs: [build-and-push]
steps:
- name: trigger webhook
run: |
curl -XPOST --header 'Content-Type: application/json' -d'{"key": "${{ secrets.WEBHOOK_KEY }}"}' "https://webhook.simbafs.cc/hooks/coscup-attendance"
yaml