在自己的 k3s cluster 中建立 Drone CI



前言

今年因為 docker hub 用起來太不開心、客服有夠雷,
問三個問題只回答一個,後續就開始水,最後就要回不回了。
想說不如把付費版的錢補回 ec2,決定自己建 CI server。

目前 docker hub 提供的功能有兩個:CI 以及 image registry。


CI: Drone CI

查了各式各樣的解決方案,看起來小規模目前很適合用 Drone CI,簡單輕量又好用。

不考慮 GHA (Github Action),因為

  1. 有額度限制,另外也都決定要自己 host CI server 了,運算資源都是自己的,沒有額度問題。
  2. 就算可以 self-hosting GHA runner,以過去開發 pipeline 的經驗,感受不是很順暢。

對我自己的專案來說,我傾向只把 Github 當作 git repo,其他功能都不要用。

如果未來採用 monorepo,那可能還是得使用 GHA,
因為 Drone CI 對於 monorepo 控制的顆粒度比較差。


Image Registry: docker registry

資源有限,我有考慮過幾個方案:

  1. Sonatype Nexus:公司用過這個,整體來說挺不錯。可以同時放 image 跟 python, node 套件。
    但以我自己的專案規模而言,這個方案太重型了。
  2. Harbor:乍看了幾個評價,似乎不是很好用,說不定是過時的評論,但我不想冒險。
  3. Gitea:沒錯 Gitea 有內建 registry。原本是我的首選,後來考慮到 git 的部分有點重疊,
    最後決定延用 Github 儲存程式碼以及處理 Auth。這樣的話 Gitea 只為了 registry 似乎有點浪費。

如果自己開公司全部自己 host 的話,絕對會用 Gitea + Drone CI。

現階段就先拿 registry 擋著用吧。



主機考量

Drone CI 是由 Server 與 Runner 組成 (CI 都差不多是這樣)。

考量到目前的流量與成本,我決定不另外開 ec2 去 host CI server,
直接把他跟 prod 服務放在同一個 k3s cluster 上。

一方面可以加大 CPU 使用效率,等到流量大起來撐不住再考慮移轉;
一方面可以把 build 好的 image 直接在同一個 cluster 上啟用,節省硬碟空間。


架構

既然要裝在 k3s (K8s),用 helm 安裝 server 以及 runner 自然是最方便的選擇。

另外,目前 cluster 中有安裝 cert manager,所以除了原有的 helm 安裝的資源之外,
要自己額外補上 cert 資源,
就不去像一般文件說明使用 NodePort 的方式連接,
而是直接新增 route53 的 A Record,用 https 直接連到建立的 Drone CI Server。

當 CI server 觸發 github 事件之後,
同樣把 build request 發給同一個 cluster 中的 runner,
直接在同一個 cluster 完成 build。

簡單的架構圖像是這樣:

 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
39
40
41
42
  ┌─────────────────────────────┐
  │         k3s cluster         │
  │                             │
  │                             │
  │    ┌───────────────────┐    │
  │    │                   │    │
  │    │    drone server   │    │
  │    │                   │    │
  │    └───┬────────────┬──┘    │
  │        │            │       │
  │        │  request   │       │
  │        │            │       │
  │    ┌───▼────┐  ┌────▼───┐   │
  │    │        │  │        │   │
  │    │ drone  │  │ drone  │   │
  │    │ runner │  │ runner │   │
  │    │        │  │        │   │
  │    └──┬─────┘  └──────┬─┘   │
  │       │               │     │
  │ build │  ┌────────┐   │     │
  │       └► │ images ││ ◄┘     │
  │          └┬───────┘│        │
  │           └────────┘        │
  │              │              │
  │       push   │              │
  │              │              │
  │              │              │
  │    ┌─────────▼─────────┐    │
  │    │                   │    │
  │    │     registry      │    │
  │    │                   │    │
  │    └───────────────────┘    │
  │             ▲               │
  │             │ pull          │
  │          ┌──┘               │
  │     ┌────┴────────┐         │
  │     │             │         │
  │     │ deployments │         │
  │     │             │         │
  │     └─────────────┘         │
  │                             │
  └─────────────────────────────┘

Github?

Drone CI server 要串接 Github:
一方面是要獲得 repo 的 event,用來觸發 CI、test、build,
另一方面也是順便用 Github OAuth 驗證 user。



安裝 Drone CI Server

安裝 server 的部分主要參考 REF [1]


先決定 CI server domain

因為這個 CI server 沒有打算要給所有人使用,
也因為共用 cluster,不想在 aws 額外處理網路白名單,
所以直接用一個亂數當作 host 當作暫時的解決方式:

1
2
openssl rand -hex 16
d485238e29ad124123ed0c2df363515c

所以這個 CI server 的 FQDN 就直接變成:
d485238e29ad124123ed0c2df363515c.mydomain.com

DNS A record

然後在你的 DNS 供應商平台上面新增一個 A record,像是:

d485238e29ad124123ed0c2df363515c.mydomain.com. -->  <your server ip>

Github 註冊 OAuth application

在 github 新增 app

Settings > Developer Settings > Register a new OAuth application

然後把剛剛的 FQDN 填進去,要加上 scheme https://

設定完成之後會有 Client ID,記得先複製下來。

接著也要點選 Generate a new client secret 的按鈕,然後也把結果複製下來。


server 內設定 helm values

進入 server 的環境,在你喜歡的路徑下載 values.yaml

1
2
3
mkdir droneci

curl -s https://raw.githubusercontent.com/drone/charts/refs/heads/master/charts/drone/values.yaml > values.yaml

(這個 values.yaml 取自 REF [2])

然後 vim values.yaml 編輯以下的地方

 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

# ...

ingress:
  enabled: true
  className: "nginx"
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
  hosts:
    - host: d485238e29ad124123ed0c2df363515c.mydomain.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: ci-tls
      hosts:
        - d485238e29ad124123ed0c2df363515c.mydomain.com

# ...

resources:
  requests:
    cpu: 100m
    memory: 128Mi

# ...

env:
  DRONE_SERVER_HOST: "d485238e29ad124123ed0c2df363515c.mydomain.com"
  DRONE_SERVER_PROTO: https
  DRONE_RPC_SECRET: e4eaeb0fb3d1b4d1c78d8c5f85906600eeb0c1e0073aeb84
  DRONE_GITHUB_CLIENT_ID: TrAe4IFcWAhhB5QKMhS4
  DRONE_GITHUB_CLIENT_SECRET: 37154fa1aadcaf5a0f2152d7d07c789d

Note

  1. DRONE_GITHUB_CLIENT_IDDRONE_GITHUB_CLIENT_SECRET
    就是先前 OAuth application 步驟的 Client IDClient Secret

  2. DRONE_RPC_SECRET 可以自己用亂產生,像是前面提到的 openssl rand -hex 16

  3. 因為 cluster 有 cert manager,且我打算新增 cert 資源綁定 tls secret 稱為 ci-tls
    如果有其他名字就自行調整。

  4. 預設的 persistentVolume.size8Gi,可以自己調整;我這邊先用預設。


補上 cert

先驗證 domain 可以解析

在新增 cert 之前,要先確認 DNS 有 A Record,而且解析得到:

1
nslookup d485238e29ad124123ed0c2df363515c.mydomain.com

新增 cert yaml

因為有安裝了 cert manager,所以可以新增 cert 資源,

可以用來自動驗證憑證以及新增 tls secret。

vim cert.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: ci-tls
  namespace: drone
spec:
  dnsNames:
  - d485238e29ad124123ed0c2df363515c.mydomain.com
  issuerRef:
    group: cert-manager.io
    kind: ClusterIssuer
    name: letsencrypt-production
  secretName: ci-tls

然後 apply 它:

1
kubectl apply -f cert.yaml

觀察 cert 結果,等到 READY 變成 True 就可以了:

1
2
3
4
# kubectl get cert -n drone -w

NAME     READY   SECRET   AGE
ci-tls   True    ci-tls   93m

萬事俱備,一氣呵成

完成 values.yaml 的修改後,就可以透過 helm install 命令來安裝了:

1
2
kubectl create ns drone
helm install drone drone/drone --namespace drone -f values.yaml

結果類似這樣:

1
2
3
4
5
6
7
8
9
NAME: drone
LAST DEPLOYED: Sat Sep 21 05:44:03 2024
NAMESPACE: drone
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
  https://d485238e29ad124123ed0c2df363515c.mydomain.com

用瀏覽器打開設定的 URL,就可以進入 server 的網頁了。



安裝 Drone CI Runner

目前在網路上搜尋了一下沒有找到比較完整的文件可以抄 ,所以只好自己準備 helm template。


用 helm 安裝

我直接沿用前面安裝 DroneCI server 的資料夾,在他底下新增一個 runner/
用來渲染 DroneCI Runner 相關的 helm template。

所以目前資料夾長得像這樣:

1
2
3
4
5
6
7
8
9
.
├── cert.yaml
├── runner
│   ├── Chart.yaml
│   ├── templates
│   │   ├── deployment.yaml
│   │   └── rbac.yaml
│   └── values.yaml
└── values.yaml

如果想拆乾淨一點可以自行在另一個資料夾處理。

如果想看完整一點結構,可以用 helm create 一個完整的範例專案。

這邊簡單起見就直接放在一起。

東西不多,主要就三部分:

  1. Chart.yaml
  2. values.yaml
  3. templates/
    1. deployments.yaml
    2. rbac.yaml

可以手動貼上:

Chart.yaml

1
2
3
4
5
6
apiVersion: v2
name: droneci-runner
description: DroneCI Runner
type: application
version: 0.1.0                # 目前借用 helm 渲染用,可以不用維護版本
appVersion: "1.16.0"          # 目前借用 helm 渲染用,可以不用維護版本

values.yaml

1
2
3
4
5
6
 # 這邊沒有要對外,用 Kubernetes 內網的 CI server 的 service 連接即可。
drone_rpc_host: droneci.droneci:8080                                 
drone_rpc_proto: http

# 要跟前一步驟中 server 的 DRONE_RPC_SECRET 相同
drone_rpc_secret: e4eaeb0fb3d1b4d1c78d8c5f85906600eeb0c1e0073aeb84    

templates/deployments.yaml

 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
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
  labels:
    app.kubernetes.io/name: drone
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: drone
  template:
    metadata:
      labels:
        app.kubernetes.io/name: drone
    spec:
      containers:
      - name: runner
        image: drone/drone-runner-kube:latest
        ports:
        - containerPort: 3000
        env:
        - name: DRONE_RPC_HOST
          value: {{ .Values.drone_rpc_host }}
        - name: DRONE_RPC_PROTO
          value: {{ .Values.drone_rpc_proto }}
        - name: DRONE_RPC_SECRET
          value: {{ .Values.drone_rpc_secret }}
        - name: DRONE_NAMESPACE_DEFAULT
          value: {{ .Release.Namespace }}
      serviceAccount: {{ .Release.Name }}

一開始沒有加上 DRONE_NAMESPACE_DEFAULT
跑起來之後一直抱怨 default namespace 不能 create secrets,
後來終於在 REF [3] 找到這個環境變數。


templates/rbac.yaml

 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
39
40
41
42
43
44
45
46
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: {{ .Release.Namespace }}
  name: {{ .Release.Name }}
rules:
- apiGroups:
  - ""
  resources:
  - secrets
  verbs:
  - create
  - delete
- apiGroups:
  - ""
  resources:
  - pods
  - pods/log
  verbs:
  - get
  - create
  - delete
  - list
  - watch
  - update

---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
subjects:
- kind: ServiceAccount
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
roleRef:
  kind: Role
  name: {{ .Release.Name }}
  apiGroup: rbac.authorization.k8s.io

最後直接 helm 安裝:

1
helm upgrade -i droneci-runner -n drone .
  • 我習慣用 upgrade,這樣可以安裝更新都用同一組指令。

新增「暫時測試」的 pipeline yaml

在當前專案裡面新增一個 branch,

然後在最上層目錄新增 .drone.yaml 並放入 pipeline 的 yaml。

pipeline 可以參考 REF [4],以我 Python API 的專案來說會類似這樣:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
kind: pipeline
type: kubernetes
name: default

steps:
  - name: test
    image: python:3.12-slim
    environment:
      API_HOST: localhost
      API_PORT: 8080
      DB_USER: ci
      DB_PASSWORD:
        from_secret: DB_PASSWORD
      JWT_ACCESS_TOKEN_EXPIRE_MINUTES: 30
      JWT_REFRESH_TOKEN_EXPIRE_MINUTES: 1440
      JWT_ALGORITHM: HS256
      JWT_SECRET_KEY: xxx
      JWT_SECRET_REFRESH_KEY: yyy
      HOST_NAME: localhost
    commands:
      - echo "debug ---" && env
      - pip install -r requirements.txt

commit 之後 push。

由於目前沒有限制 branch,所以 push 之後就會觸發執行。

到先前設定的 DRONE_SERVER_HOST 查看,
例如 https://d485238e29ad124123ed0c2df363515c.mydomain.com

點擊歷程查看內容

⬆︎ 前兩次失敗就是因為前面沒有指定到 DRONE_NAMESPACE_DEFAULT 環境變數,
導致權限不正確直接噴錯。

現在可以依照自己需要的 CI 步驟去更新 .drone.yaml 裡面的 pipeline,例如:

  • pytest
  • 驗證環境變數可以連線到測試用 DB

目前這個範例 YAML 會說是「暫時測試」,原因是我們的 registry 還沒有建立,
如果5此時要 build image 會沒辦法指定 image registry。

現階段只能測試 commit、push 後是否有正確觸發 CI;
在建立 registry 之後會再更新 pipeline.yaml,讓 build 的流程一併加入,詳見下一節。



安裝 Registry

原本一度考慮看能不能把 image 推在 local k3s cluster,
後來想想還是獨立開來,因為之後 CI 跟 registry 有可能會在其他台主機上面,
保留處理的彈性。

不過這樣限於硬碟空間大小,要額外做 housekeeping。


製作 registry 的帳號密碼

htpasswd 指令產生 registry 的帳號密碼:

1
2
3
4
5
6
htpasswd -Bbn  demo  bf69d401df35bedad6f7d83f0cac18f0
#              ^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#          username  password

# result
demo:$2y$05$2kkHyxUkq.izxOWbOlqW3ePJkzG/WhuJCbPrdiOixXtIo/QvQjf/2

Note

  1. 密碼可以自行更改
  2. 我是用 openssl rand -hex 16 產生的亂數

如果你的電腦或 server 沒有 htpasswd,可以:

  1. 安裝指令,各平台指令參考 https://command-not-found.com/htpasswd
1
2
3
4
5
# CentOS
yum install httpd-tools

# Ubuntu
apt-get install apache2-utils
  1. 或用 docker 直接執行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
docker run --rm cmd.cat/htpasswd htpasswd -Bbn demo bf69d401df35bedad6f7d83f0cac18f0
# Unable to find image 'cmd.cat/htpasswd:latest' locally
# latest: Pulling from htpasswd
# 7264a8db6415: Pull complete
# 3b63c57d36dd: Pull complete
# dbc368251d6c: Pull complete
# 2a456f825220: Pull complete
# Digest: sha256:05a4ef9d5eb70d2252778d3d66835ece1b62f169c2d5f37b04253ce7bc9ecdd4
# Status: Downloaded newer image for cmd.cat/htpasswd:latest
demo:$2y$05$2id3XHub8tl7HYmx5Eih5OC90RbipWQuGpBRLx3UY8SLio9VeHHnm

把密碼存成 secret

先創建一個 namespace 用來放 registry 相關的資源:

1
kubectl create ns docker-registry

然後 create secret 存放剛剛用 htpasswd 製作的內容:

1
2
3
4
kubectl -n docker-registry create secret generic registry-htpasswd \
    --from-literal=USERNAME=demo \
    --from-literal=PASSWORD=bf69d401df35bedad6f7d83f0cac18f0  \
    --from-literal=HTPASSWD="demo:$2y$05$2id3XHub8tl7HYmx5Eih5OC90RbipWQuGpBRLx3UY8SLio9VeHHnm"

準備 pvc, deployment, svc

pvc.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: docker-registry
  namespace: docker-registry
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi

因為有 storage class (local-path),所以不用處理 pv。


deployment.yaml

 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
39
40
41
42
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: registry
  name: registry
  namespace: docker-registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    metadata:
      labels:
        app: registry
    spec:
      containers:
      - image: registry:2.8.3
        name: registry
        volumeMounts:
        - name: registry-pvc
          mountPath: "/var/lib/registry"
        - name: htpasswd
          mountPath: /etc/auth/htpasswd
        env:
        - name: REGISTRY_AUTH
          value: "htpasswd"
        - name: REGISTRY_AUTH_HTPASSWD_REALM
          value: "Registry Realm"
        - name: REGISTRY_AUTH_HTPASSWD_PATH
          value: "/etc/auth/htpasswd/HTPASSWD"
        - name: REGISTRY_STORAGE_DELETE_ENABLED
          value: "true"

      volumes:
      - name: registry-pvc
        persistentVolumeClaim:
          claimName: docker-registry
      - name: htpasswd
        secret:
          secretName: registry-htpasswd

Note

  1. 先前建立的 secret htpasswd 要設定。
    概念上是先
    volumes from secret (registry-htpasswd)
    –> mount 指定路徑 (/etc/auth/htpasswd)
    –> 環境變數 REGISTRY_AUTH_HTPASSWD_PATH 要指定該路徑
  2. 考量到會需要做 housekeeping,要把 REGISTRY_STORAGE_DELETE_ENABLED 設定為 true

svc.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1
kind: Service
metadata:
  name: registry
  namespace: docker-registry
spec:
  selector:
    app: registry
  ports:
  - port: 5000
    targetPort: 5000

以這個 svc 的設定來說,最後內網連線的 registry 就會是

  • registry.docker-registry.svc:5000

設定 cert, ingress

原本考慮內網 registry 就好,後來發現直接設定的話 deployments 會認不到,出現 ImagePullBackOff

考量到之後還是有可能把 CI server 與 registry 放在另外一組 cluster 上,
所以現在就改成外網的 domain 當作 registry 沒有不好。

現在要補上 certs 以及 ingress。

certs.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: cr-tls
  namespace: docker-registry
spec:
  dnsNames:
  - d485238e29ad124123ed0c2df363515c0-my-cr.mydomain.com
  issuerRef:
    group: cert-manager.io
    kind: ClusterIssuer
    name: letsencrypt-production
  secretName: cr-tls
  usages:
  - digital signature
  - key encipherment

ingress.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: 1500m  # avoid 413 request entity too large
  name: registry
  namespace: docker-registry
spec:
  ingressClassName: nginx
  rules:
  - host: d485238e29ad124123ed0c2df363515c0-my-cr.mydomain.com
    http:
      paths:
      - backend:
          service:
            name: registry
            port:
              number: 5000
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - d485238e29ad124123ed0c2df363515c0-my-cr.mydomain.com
    secretName: cr-tls
  • 可以考慮把 proxy-body-size 改成 0,不檢查大小。
  • 一定要設定 body size,不然 image 隨便推經過 nginx 都會報 413 (request entity too large)



Housekeeping: 清理 Drone CI Server 以及 registry 的資料

如果需要完整稽核紀錄,是我的話會直接付錢採用雲端服務,省事多了。

本篇不屬於這個案例,自建、自用以經濟實惠為主,不用考慮稽核紀錄,
東西都放在同一台 server,資源有限,及時做清理可以減少硬碟的成本。


清理 Drone CI Server 的歷程紀錄

預設 Drone CI 是用 sqlite 的資料庫來做紀錄,

1
2
3
apk add sqlite

sqlite3 /data/database.sqlite

只保留近 20 次的 build

 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
DELETE FROM logs WHERE log_id IN (
  SELECT step_id FROM steps WHERE step_stage_id IN (
    SELECT stage_id FROM stages WHERE stage_build_id IN (
      SELECT build_id
      FROM repos, builds
      WHERE repo_id = build_repo_id
      AND build_number < repo_counter - 20
    )
  )
);

DELETE FROM stages WHERE stage_build_id IN (
  SELECT build_id
  FROM repos, builds
  WHERE repo_id = build_repo_id
  AND build_number < repo_counter - 20
);

DELETE FROM builds WHERE build_id IN (
  SELECT build_id
  FROM repos, builds
  WHERE repo_id = build_repo_id
  AND build_number < repo_counter - 20
);

-- Reclaim space and optimize the database
VACUUM;

套用 Cronjob 幫忙自動觸發:

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
apiVersion: batch/v1
kind: CronJob
metadata:
  name: drone-housekeeping
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: drone-housekeeping
            image: alping/sqlite:latest
            volumeMounts:
            - name: drone-data
              mountPath: /data
            command: ["/bin/sh", "-c"]
            args:
            - |
              sqlite3 /data/database.sqlite <<EOF
              DELETE FROM logs WHERE log_id IN (
                SELECT step_id FROM steps WHERE step_stage_id IN (
                  SELECT stage_id FROM stages WHERE stage_build_id IN (
                    SELECT build_id
                    FROM repos, builds
                    WHERE repo_id = build_repo_id 
                    AND build_number < repo_counter - 20
                  )
                )
              );
              DELETE FROM stages WHERE stage_build_id IN (
                SELECT build_id
                FROM repos, builds
                WHERE repo_id = build_repo_id 
                AND build_number < repo_counter - 20
              );
              DELETE FROM builds WHERE build_id IN (
                SELECT build_id
                FROM repos, builds
                WHERE repo_id = build_repo_id 
                AND build_number < repo_counter - 20
              );

              -- Reclaim space and optimize the database
              VACUUM;
              EOF              
          restartPolicy: OnFailure
          volumes:
          - name: drone-data
            persistentVolumeClaim:
              claimName: storage-volume  # 安裝 Drone CI 時的 PVC

清理 registry 的資料

一般情況直接進入 pod 執行

1
2
3
kubectl -n docker-registry exec -it \
    $(kubectl -n docker-registry get pod -l app=registry -oname) \
    -- registry garbage-collect /etc/docker/registry/config.yml -m

如果想定期執行可以考慮包成 cronjob。


Note

  1. 如果要包成 kubernetes cronjob 資源,就需要把 docker 的 config.yaml 包成 configmap,然後共用;想想覺得太麻煩,就先直接在外面用指令執行。



大功告成:最後更改 Drone CI 的 yaml

現在已經可以完成觸發,只要把流程改成自己的理想的狀態就可以了。

我的目標是:

  1. 只有在 PR 開起來以及後續 synchronize 了才觸發 CI,並且發出通知。
  2. 只有在押上 tag 之後才會 build
  3. build 完成之後通知 keel 觸發 deploy
 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
kind: pipeline
type: kubernetes
name: default

clone:
  depth: 1

trigger:
  event:
    - pull_request
  action:
    - opened
    - synchronized

steps:
  - name: ci
    image: python:3.12-slim
    environment:
      ENV: DEV
      API_HOST: localhost
      API_PORT: 8080
      API_DOCS_URL_PREFIX: "test"
      API_DOCS_ENABLED: 1
      API_SERVER_URL_PREFIX_IN_DOCS: ""
      API_MONGODB_COLLECTIONS_SUFFIX: "_edge"
      MONGODB_USER: ci-user
      MONGODB_PASSWORD:
        from_secret: MONGODB_PASSWORD
      MONGODB_URI:
        from_secret: MONGODB_URI
      MONGODB_DATABASE: testDB
      JWT_ACCESS_TOKEN_EXPIRE_MINUTES: 1
      JWT_REFRESH_TOKEN_EXPIRE_MINUTES: 1440
      JWT_ALGORITHM: HS256
      JWT_SECRET_KEY: "secretkeyforci"
      JWT_SECRET_REFRESH_KEY: "refreshkeyforci"
      HOST_NAME: localhost
    commands:
      - export PATH="$HOME/.local/bin:$PATH"
      - pip install pipx && pipx install poetry && poetry install
      - poetry run pytest || exit 1

  - name: notify
    image: plugins/slack
    settings:
      webhook:
        from_secret: SLACK_WEBHOOK
      message: |
        REPO: `${DRONE_REPO}`
        source -> target: `${DRONE_SOURCE_BRANCH}` -> `${DRONE_TARGET_BRANCH}`
        build link:  ${DRONE_BUILD_LINK}        
    when:
      status:
        - success
        - failure

---
kind: pipeline
type: kubernetes
name: tag-build

clone:
  depth: 1

trigger:
  event:
    - tag

steps:
  - name: build
    image: plugins/docker
    settings:
      username:
        from_secret: REGISTRY_USER
      password:
        from_secret: REGISTRY_PASSWORD
      registry: d485238e29ad124123ed0c2df363515c0-my-cr.mydomain.com
      insecure: true
      repo: d485238e29ad124123ed0c2df363515c0-my-cr.mydomain.com/my-repo
      dockerfile: ./Dockerfile
      tags:
        - ${DRONE_TAG}
        - latest

  - name: deploy
    image: plugins/webhook
    # ref: https://keel.sh/docs/#webhooks
    settings:
      urls: http://keel.kube-system.svc:9300/v1/webhooks/native
      content_type: application/json
      template: |
        {
          "name": "d485238e29ad124123ed0c2df363515c0-my-cr.mydomain.com/my-repo",
          "tag": "${DRONE_TAG}"
        }        

Note

  1. 因為原本就有裝 keel;如果直接觸發 helm 等方式部署也是可以的。



REF

  1. https://dev.to/neelanjan00/how-to-install-drone-ci-in-kubernetes-39e5
  2. https://github.com/drone/charts/blob/master/charts/drone/values.yaml
  3. https://docs.drone.io/runner/vm/configuration/reference/drone-namespace-default/
  4. https://docs.drone.io/pipeline/kubernetes/examples/language/python/#test-multiple-architectures
  5. https://medium.com/swlh/deploy-your-private-docker-registry-as-a-pod-in-kubernetes-f6a489bf0180
  6. https://stackoverflow.com/questions/46545705/how-to-set-retention-policy-in-drone-ci

主題 StackJimmy 設計