寫在前邊
這一篇文章是基於 Gitea+Drone CI+Vault 打造屬於自己的CI/CD工作流系列文章第二篇,讓我們一起來完成 drone
與 vault
的搭配使用,這篇主要講 vault
的部署和使用,以及怎麼通過 drone
來使用 vault
上一篇文章(一) Drone CI For Github —— 打造自己的CI/CD工作流我們一起了解了,Drone
的部署和使用,一起感受了 Drone
的簡單強大的功能帶來的方便和快捷。在實際的應用中,我們會有很多的敏感資料,Drone
自身的 Secret
是以倉庫為單位進行管理,而且也沒有嚴格獨立的許可權控制,其實是不太方便的。
帶著這樣的疑惑,我再次向@Dee luo老哥尋求幫助,老哥掏了掏肚子上的口袋,取出一本武林祕籍,曰:“小夥子,我這裡有一個色情網站記錄在上面,你看看”。我開啟只看到 www.vaultproject.io 一行小字赫然記錄在內,我迫不及待的開啟,美滋滋的看了起來。
此處省略一萬個字...
另外強烈推薦YUHAO的部落格 私密資訊管理利器 HashiCorp Vault系列文章
好了,現在我已經知道該做什麼了。
說幹就幹,開始搞事。
瞭解Vault
Vault是一個管理Secrets並保護敏感資料的工具,來自HashiCorp,如果你對這個名字有點陌生,那麼你一定知道Vagrant
Vault是一種安全訪問
Secret
的工具。Secret
就是您要嚴格控制訪問的任何內容,例如API金鑰,密碼或證照。Vault為任何機密提供統一的介面,同時提供嚴格的訪問控制並記錄詳細的審計日誌。現代系統需要訪問大量
Secret
:資料庫憑證,外部服務的API金鑰,面向服務的體系結構通訊的憑證等。瞭解誰正在訪問哪些祕密已經非常困難且特定於平臺。如果沒有自定義解決方案,幾乎不可能新增金鑰滾動,安全儲存和詳細的審計日誌。這是Vault介入的地方。Vault的主要功能包括:
- 安全祕密儲存:任意金鑰/值祕密可以儲存在Vault中。Vault會在將這些機密寫入持久儲存之前加密這些機密,因此獲取對原始儲存的訪問許可權不足以訪問您的機密。Vault可以寫入磁碟,Consul等。
- 動態祕密:Vault可以按需為某些系統生成機密,例如AWS或SQL資料庫。例如,當應用程式需要訪問S3儲存桶時,它會要求Vault提供憑據,Vault將根據需要生成具有有效許可權的AWS金鑰對。建立這些動態機密後,Vault也會在租約到期後自動撤消它們。
- 資料加密:Vault可以加密和解密資料而無需儲存資料。這允許安全團隊定義加密引數,並允許開發人員將加密資料儲存在SQL等位置,而無需設計自己的加密方法。
- 租賃和續訂:Vault中的所有機密都有與之相關的租約。在租約結束時,Vault將自動撤銷該祕密。客戶可以通過內建續訂API續訂租約。
- 撤銷:Vault內建了對祕密撤銷的支援。保險櫃不僅可以撤銷單個祕密,還可以撤銷祕密樹,例如特定使用者讀取的所有祕密,或特定型別的所有祕密。撤銷有助於關鍵滾動以及在入侵情況下鎖定系統。
上邊都是廢話,這裡說一下我自己的體驗。
首先我沒有體驗的很深,我現在只是體驗了 key/value
結構和 database
,然後看了一下 ACL Policies
管理許可權。
- 許可權控制很嚴格,機制也很靈活和安全
- UI美觀,操作簡單,不過UI功能也簡單
- CLI 操作容易理解,文件齊全。
- 安全確實很放心,token定時重新整理,可以通過token來獲取資料庫的配置,UI的啟用需要輸入key,容器重啟也需要重新輸入key來啟用。
部署 vault
這裡並不是單獨部署vault,而是參考上一篇文章來結合vault使用,在上一篇文章的
docker-compose.yml
基礎上,加入vault
的容器,併為vault
提供web
服務.
為了區分,以下程式碼塊中,用
$
開頭表示宿主機命令,用/ #
開頭表示容器中的命令
編寫模板檔案
...
vault:
image: vault:latest
container_name: vault
restart: always
networks:
- dronenet
volumes:
- ./vault/file:/vault/file
- ./vault/config:/vault/config
- ./vault/logs:/vault/logs
cap_add:
- IPC_LOCK
environment:
- VAULT_ADDR=http://127.0.0.1:8200
command: vault server -config=/vault/config/local.json #這句非常重要,一定要替換原有的Dockerfile中的CMD,不然會自動初始化,生成的資料都在docker logs中,不說你肯定找不到。dog.jpg,所以我選擇手動初始化
...
複製程式碼
配置 vault
事先準備好一個資料庫和對該資料庫具有訪問許可權的資料庫賬號
參考
database
: vaultname
: vaultpassword
: vault123456樣例中的配置請事先閱讀文件知曉含義
參考Vault Configuration
Vault
的配置檔案是HCL或者json
,這裡我使用json
HCL to json
請參考HCL
- 初始化
vault
$ mkdir -p vault/config
$ mkdir -p vault/file
$ mkdir -p vault/logs
$ vim vault/config/local.json
{
"ui": true,
"storage": {
"mysql": {
"address": "mysql:3306",
"username": "vault",
"password": "vault123456"
}
},
"listener": {
"tcp": {
"address": "0.0.0.0:8200",
"tls_disable": 1
}
},
"backend": {
"file": {
"path": "/vault/file"
}
},
"log_level": "Debug", #除錯階段建議開啟Debug
"default_lease_ttl": "168h",
"max_lease_ttl": "720h"
}
$ docker-compose up -d
$ docker-compose exec vault ash
/ # vault operator init
Unseal Key 1: cz/cSHqUep5IBQjtDWBgFnN+G02hLFh8s/19rPxKjxCe #記下來
Unseal Key 2: o+BRjfy64sUKLTWKV0jV+JjvKZWd0R3ibBR7IUbCn8sB #記下來
Unseal Key 3: xiJF+XI8gF1PWGMvOYhy0go16x2VgZdAVKw/xBIVGeo7 #記下來
Unseal Key 4: xd/H1hBdPGwm2qchkShgzGbVtWWHeeCv8S1RyYg34yKi #記下來
Unseal Key 5: 4OOHfxxwuX7Hz40E/bHJLbkwLLWeZkWnz7/pmdtgm7mn #記下來
Initial Root Token: s.RIeC53WBWizfl0OXVbDYuxbh #記下來
Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.
Vault does not store the generated master key. Without at least 3 key to
reconstruct the master key, Vault will remain permanently sealed!
It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.
複製程式碼
初始化動作為我們生成了 5 個 Unseal key,此外還有預設的 Root Token。所以應該馬上把這些資訊記錄到安全的地方,因為以後你是沒有辦法再看到它們的。
- 編輯nginx,為vault提供web服務
編輯完記得重啟nginx服務
server {
listen 80;
server_name vault.yiranzai.top;
location / {
proxy_pass http://vault:8200;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
複製程式碼
- 訪問
vault.yiranzai.top
Vault 對於資料保護是非常重視的。伺服器啟動後,並不能夠馬上訪問其資料,而必須經過一個解封(Unseal)的動作。
依次輸入上邊得到的五個
Unseal Key
中的三個來解封服務,然後使用Root Token
登入,登入後如圖,右側會有教程,先跟著做一邊熟悉熟悉。
- 建立一個
Secrets Engines
這個東西,怎麼說呢,不太好理解。字面意思就是
祕密引擎
,是儲存,生成或加密資料的一個元件,可以理解為一個資料集,也可以理解為一種資料渠道。只有Secrets Engines
存在,資料才能被指定方式儲存。
- Generic
- KV (這裡只示範這種)
- PKI Certificates
- SSH
- Transit
- TOTP
- Cloud
- Active Directory
- AliCloud
- AWS
- Azure
- Google Cloud
- Google Cloud KMS
- Infra
- Consul
- Databases (支援多種驅動)
- Nomad
- RabbitMQ
- 建立一個
ACL Policy
建立一個
ACL Policy
,配置為僅對剛剛建立的Secrets Engine
——dronetest
有可讀和列表許可權。ACL Policy
的配置格式為HCL,HCL 是 HashiCorp 創造的、專門用於配置檔案的語言格式)
# Allow tokens to look up their own properties
path "auth/token/lookup-self" {
capabilities = ["read"]
}
# Allow tokens to renew themselves
path "auth/token/renew-self" {
capabilities = ["update"]
}
# Allow tokens to revoke themselves
path "auth/token/revoke-self" {
capabilities = ["update"]
}
# Allow a token to look up its own capabilities on a path
path "sys/capabilities-self" {
capabilities = ["update"]
}
path "dronetest" {
capabilities = ["read", "list"]
}
path "dronetest/*" {
capabilities = ["read", "list"]
}
複製程式碼
至此,我們的
Vault
就配置好了,接下來讓我們建立secret
建立 Secret
上面我們簡單配置了一下
Vault
,現在讓我們建立Secret
首先我們明白的是
dronetest(Policy)
對dronetest(Secrets Engine)
只有可讀和列表的許可權。
- 登入
回到
vault
容器中,使用Root token
登入
/ # vault login s.RIeC53WBWizfl0OXVbDYuxbh
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token s.RIeC53WBWizfl0OXVbDYuxbh
token_accessor 5JoYSxx4CqR10mmeG7HusChz
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
複製程式碼
- 建立一個用於驗證的
token
這個token應當只對
dronetest(Secrets Engine)
可讀,所以我們為他分配一個dronetest(Policy)
/ # vault token create -policy=dronetest -no-default-policy
Key Value
--- -----
token s.mx4KQycrFAfiaHIuPnNLhFCy #記下來非常重要
token_accessor 8rzot4pDJvfX0JTi2ImYLvg4
token_duration 168h
token_renewable true
token_policies ["dronetest"]
identity_policies []
policies ["dronetest"]
複製程式碼
- 寫入
Secret
如圖所示,這裡我們為
web
建立了兩對KV
接下來嘗試用命令建立
/ # vault kv put dronetest/test v=k
Key Value
--- -----
created_time 2019-02-26T03:53:02.240676864Z
deletion_time n/a
destroyed false
version 1
/ # vault kv get dronetest/test
====== Metadata ======
Key Value
--- -----
created_time 2019-02-26T03:53:02.240676864Z
deletion_time n/a
destroyed false
version 1
== Data ==
Key Value
--- -----
v k
/ # vault kv put dronetest/test a=b
Key Value
--- -----
created_time 2019-02-26T03:53:31.073689586Z
deletion_time n/a
destroyed false
version 2
/ # vault kv get dronetest/test
====== Metadata ======
Key Value
--- -----
created_time 2019-02-26T03:53:31.073689586Z
deletion_time n/a
destroyed false
version 2
== Data ==
Key Value
--- -----
a b
/ # curl \
> --header "X-Vault-Token: s.RIeC53WBWizfl0OXVbDYuxbh" \
> http://127.0.0.1:8200/v1/dronetest/data/test
{"request_id":"f5ce7d8f-cc60-ec79-20a5-5875c5e4362c","lease_id":"","renewable":false,"lease_duration":0,"data":{"data":{"a":"b"},"metadata":{"created_time":"2019-02-26T03:53:31.073689586Z","deletion_time":"","destroyed":false,"version":2}},"wrap_info":null,"warnings":null,"auth":null}
複製程式碼
可以看出後者覆蓋了前者,注意
version
增加了1
,vault
為每個path
每次寫入的資料都定義為了一個新的版本,這樣就不難理解,這裡為什麼是後者覆蓋了前者。
在 Drone
中使用 Vault
以上我們瞭解了
Vault
簡單的部署和使用,現在我們一起了解怎麼在Drone
中使用Vault
編輯Docker-compose.yml
結合上一篇文章(一) Drone CI For Github —— 打造自己的CI/CD工作流,我們將
Vault
加入進來然後我們還需要
drone-vault
這個外掛來實現Drone
和Vault
之間的通訊中轉參考
- Drone使用Vault儲存Secret (仔細看,文件是有缺失的)
- Drone-vault原始碼分析 (之所以要分析,是因為官方文件中沒有說明怎麼和vault通訊,觀看原始碼一目瞭然)
version: "3.7"
services:
nginx:
image: nginx:alpine
container_name: dronetest_nginx
ports:
- "80:80"
restart: always
networks:
- dronenet
mysql:
image: mysql:5.7
restart: always
container_name: dronetest_mysql
environment:
- MYSQL_ROOT_PASSWORD=root_password
- MYSQL_DATABASE=drone
- MYSQL_USER=drone
- MYSQL_PASSWORD=drone_password
networks:
- dronenet
volumes:
- /path/to/conf/my.cnf:/etc/mysql/my.cnf:rw
- /path/to/data:/var/lib/mysql/:rw
- /path/to/logs:/var/log/mysql/:rw
vault:
image: vault:latest
container_name: vault
restart: always
networks:
- dronenet
volumes:
- ./vault/file:/vault/file
- ./vault/config:/vault/config
- ./vault/logs:/vault/logs
cap_add:
- IPC_LOCK
environment:
- VAULT_ADDR=http://127.0.0.1:8200
command: vault server -config=/vault/config/local.json #這句非常重要,一定要替換原有的Dockerfile中的CMD,不然會自動初始化,生成的資料都在docker logs中,不說你肯定找不到。dog.jpg,所以我選擇手動初始化
drone-server:
image: drone/drone:1.0.0-rc.5 #不要用latest,latest並非穩定版本
container_name: dronetest_server
networks:
- dronenet
volumes:
- ${DRONE_DATA}:/var/lib/drone/:rw
- /var/run/docker.sock:/var/run/docker.sock:rw
restart: always
environment:
- DRONE_DEBUG=true
- DRONE_DATABASE_DATASOURCE=drone:drone_password@tcp(dronetest_mysql:3306)/drone?parseTime=true #mysql配置,要與上邊mysql容器中的配置一致
- DRONE_DATABASE_DRIVER=mysql
- DRONE_GITHUB_SERVER=https://github.com
- DRONE_GITHUB_CLIENT_ID=${Your-Github-Client-Id} #Github Client ID
- DRONE_GITHUB_CLIENT_SECRET=${Your-Github-Client-Secret} #Github Client Secret
- DRONE_RUNNER_CAPACITY=2
- DRONE_RPC_SECRET=YOU_KEY_ALQU2M0KdptXUdTPKcEw #RPC祕鑰
- DRONE_SERVER_PROTO=http #這個配置決定了你啟用時倉庫中的webhook地址的proto
- DRONE_SERVER_HOST=dronetest.yiranzai.top
- DRONE_USER_CREATE=username:yiranzai,admin:true #管理員賬號,一般是你github使用者名稱
drone-vault:
image: drone/vault
container_name: dronetest_vault
restart: always
networks:
- dronenet
environment:
- SECRET_KEY=7890bcce69bb685a9a424767fe9d1be1 #和drone-agent通訊的加密
- DEBUG=true
- VAULT_ADDR=http://vault:8200
- VAULT_TOKEN_RENEWAL=84h
- VAULT_TOKEN_TTL=168h
- VAULT_TOKEN=s.mx4KQycrFAfiaHIuPnNLhFCy #這裡不要用root token,用上邊生成的只讀token
drone-agent:
image: drone/agent:1.0.0-rc.5
container_name: dronetest_agent
restart: always
networks:
- dronenet
depends_on:
- drone-server
volumes:
- /var/run/docker.sock:/var/run/docker.sock:rw
environment:
- DRONE_SECRET_SECRET=7890bcce69bb685a9a424767fe9d1be1
- DRONE_SECRET_ENDPOINT=http://dronetest_vault:3000
- DRONE_RPC_SERVER=http://dronetest_server
- DRONE_RPC_SECRET=YOU_KEY_ALQU2M0KdptXUdTPKcEw
- DRONE_DEBUG=true
- DRONE_LOGS_DEBUG=true
- DRONE_LOGS_PRETTY=true
- DRONE_LOGS_NOCOLOR=false
networks:
dronenet:
複製程式碼
改寫倉庫中的 .drone.yml
這裡的
.drone.yml
與上一篇文章中的類似,沒有太大改動。參考
- 為
dronetest
中的web
建立一個新版本
私鑰中的換行符需要替換成\n貼上進來
{
"port":"22",
"name":"yiranzai",
"host":"dronetest.yiranzai.top",
"deploy_path":"/path/to/web",
"rsa":"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAu1uWokQH6tuExm1RcFTJj2F6b8TXUkAzpAywVFQuyIepQwVG\nfl+Y1iD8YrqB31ZDjJk3DbC6DYGsecJILpBKDJ6T1M4UPKF6m0DH+O4bb33rhBy/\nY/zeN2jOygKTfTWpxrs13ZLvsJzNsH7rs6P+K3c+2heAhrYlyzeXTO/VbqCAsjMp\nsDMRjCDEp7jxNeEOdGM/4gIxpatDkVSIbgCj1jkYJJ3C0ipS3KvI2A1Lic8Vjr4V\nxtRK82r4aa1qeWunUFJWaO1O/V11l63lixfrr12QDgRyAkC/GCVOso6+NRP+DfGj\nhGQ+XziBZRdaEkQwv4kI717l4ToFDvUj1G8K1QIDAQABAoIBADx0q/sIT0DYkpXb\nMPUw7W7361We6gBAXiPMTvot0pGeXeYSCiBpL/g40vwBvB47TzM8olcKDzqRAgKO\nn/IZXzNn2qLN4emT482p34b1O/AWtNFy7h4Le1SBernQULT1mQtIgt/rBMB2FzZ7\n//p4q0x3ZXKkRi+i/NTayi/pzW6DTxYreIek+FSIPPJ3Kc1tsooD8greiEYo3wef\nbzV17YIvEry3RRRKYcR/tjS/oWOKdG1YzxsiPVALrZtgHS3KcDCYRltQoALGfMjl\nL4iicfTe2Jlr36CvdH0yqFOsRPH0eh4dC1sFKBzHFqcccnNfnhOGgwCbj/zDEuX+\nDrUUnqECgYEA3NqvHSAg3ULF0Y+emRXDDLbLmBDsIX+7ygQeYcwC1EVVf7F2z+Tw\ntYTjTNu4HzXAHVQ3WcB4K0fXEAJuK0LtORpHPPxfMse6vURSOTubBqhp6+H33TTq\nJa0ph1vLvGp2G1sJHQ+QrwZw+SyJ2LtiSYlMTIf617DDYAc+iWoBc10CgYEA2XZ8\nfRy20YMb01znorEG7WQ/vErtasAJbN122M/e6a1FPMmDdiTuYvA4Q0xylDXBgwZ/\nXxBFc1fs6QI+Vks3J6lzAgJ+XbX3gtoPxTFRJu4yZjCmhxLOpr1zzR8eWWrJ3aA3\nIS2UmQhr8mGPPnQbc7OeLTUv+7ZokDElZmMpxekCgYB1ZPi4Lp/JfPjRz3mp3dt0\nIqZOCpC1rcAQPeg4a80FMGWmHprdHwCkPCLmc3SHIncgH+sMjjZSKzmyFNiivkyC\nkelUDYI813XnTS23pmtdOqAy3kmPD3V2eXkd0D6XxK3LEzTg8akin/XlPTt4rQIt\nvIGGHLHN/jOcE722JVboMQKBgQCFOyqaHHWFdyYdINZpvrvXxYum+ODsfitIH4co\n3nJcCGRbEbsRLx8+Tp6p3LR2SVj3xYVT4MwsFrp3J4C1re8R1ac4m/1/u3ShHqh6\nz/RAPb3zDGt6ZfNmBVk3WstlTR/e3QV+xkY8XASGw27XfJs1D37hI6z6Mo3tiC61\nxBdbwQKBgHMDXneApZOsByrm0fvrOdIeW247kkmO6jLrI6QI/mR70gjODKXApLbU\nAbyglSi/i6Ewp+Au3+2mMvHFGc8iRkh0pwEo+xMKqPUAZEnmCbo3mKSjnR/vE3Pa\nO1frNKNLFff6kMh0ufbO3YIixYHCNJO6j/k1GmkkECSTMMka1tig\n-----END RSA PRIVATE KEY-----"
}
複製程式碼
- 編輯
.drone.yml
---
kind: pipeline
name: drone
workspace:
base: /app
path: git/drone
steps:
- name: build
image: node:alpine
volumes:
- name: webroot
path: /wwwroot
commands:
- /bin/sh bash.sh
environment:
host:
from_secret: host
port:
from_secret: port
abc: abctest
- name: deploy
image: appleboy/drone-scp
when:
status:
- success
settings:
host:
from_secret: host
port:
from_secret: port
key:
from_secret: rsa
username:
from_secret: username
target:
from_secret: deploy_path
source: ./*
volumes:
- name: webroot
host:
path: /opt
- name: cache
host:
path: /tmp/cache
trigger:
branch:
- master
event:
- push
---
kind: secret
external_data:
host:
path: dronetest/data/web
name: host
username:
path: dronetest/data/web
name: name
port:
path: dronetest/data/web
name: port
deploy_path:
path: dronetest/data/web
name: deploy_path
rsa:
path: dronetest/data/web
name: rsa
複製程式碼
- 提交修改
git add .;git commit -m 'init test 4';
git push origin master
複製程式碼
- 檢視
Drone
的ACTIVITY FEED
如果成功就會如下圖所示
-
去伺服器檢查一下
看到
bash.sh
和.drone.yml
都被上傳到這裡(只是測試,不是真的讓你這麼幹)
$ pwd
/home/www
$ ll -a
total 40
drwx------ 4 www www 4096 Feb 20 04:23 .
drwxr-xr-x. 4 root root 4096 Feb 20 03:55 ..
-rw------- 1 www www 61 Feb 19 03:00 .bash_history
-rw-r--r-- 1 www www 18 Oct 30 13:07 .bash_logout
-rw-r--r-- 1 www www 193 Oct 30 13:07 .bash_profile
-rw-r--r-- 1 www www 231 Oct 30 13:07 .bashrc
-rw-r--r-- 1 www www 35 Feb 20 04:23 bash.sh
-rw-r--r-- 1 www www 812 Feb 20 04:23 .drone.yml
drwxr-xr-x 8 www www 4096 Feb 20 04:23 .git
drwxr-xr-x 2 www www 4096 Feb 19 02:40 .ssh
複製程式碼
總結和推薦
寫到這裡,就算是結束了。說實話,有了上一篇文章的的經驗和踩坑,我已經對於 Drone
的文件相當失望了,沒想到後邊有更深的坑在等著我,當我瞭解到真相以後,氣得差點砸鍵盤。
本次總結
- Vault 的文件其實還算清晰,在實際操作之間,建議至少把KV相關的部分熟讀,而且第一次啟用之後的手冊一定要照著實踐一遍。(如果你的英文不錯,你可以看
vault
官方提供的高階教程) drone-vault
的官方樣例中沒有說明怎麼與vault
通訊,在我第一遍的操作中,我沒有細想這個問題,跳過了,結果不用多說,自然是失敗。後面發現問題一臉矇蔽,這玩意他不說我怎麼找呢?還是@Dee luo老哥提醒我,我才想起來去看看原始碼,在原始碼中找到了相關引數。(強烈吐槽!!!倉庫還是隻讀的!!!)drone-vault
是用GO語言開發的,drone-vault
使用的http
包,不支援沒寫協議的url
,例如vault:8200
不能被識別,要寫成http(s)://vault:8200
vault
中的Secret
儲存路徑是{Engine Name}/data/{Secret Name}
,這一點要牢記
如果你對 vault
想有更深層次的瞭解,可以看下YUHAO的部落格 私密資訊管理利器 HashiCorp Vault系列文章
系列文章
- 基於 Gitea+Drone CI+Vault 打造屬於自己的CI/CD工作流
- (一) Drone CI For Github —— 打造自己的CI/CD工作流
- (二) Drone CI使用Vault作為憑據儲存 —— 打造自己的CI/CD工作流
- (三) 輕量化自建 Drone CI For Gitea —— 打造自己的CI/CD工作流
- 番外:基於Gitea打造一個屬於你自己的程式碼託管平臺
好了,祝大家擼碼愉快,沉迷於BUG不能自拔。
不要砸鍵盤!