?背景介紹
Zadig 是目前很火的云原生持續(xù)交付平臺(tái),具備靈活易用的高并發(fā)工作流、面向開發(fā)者的云原生環(huán)境、高效協(xié)同的測(cè)試管理、強(qiáng)大免運(yùn)維的模板庫(kù)、客觀精確的效能洞察以 及云原生 IDE 插件等重要特性,為工程師提供統(tǒng)一的協(xié)作平面,可以滿足大部分的企業(yè)交付場(chǎng)景。
但是,大家有沒有遇到過以下情況:
- 當(dāng)你在”帶薪拉屎“的時(shí)候,叫你發(fā)流水線
- 當(dāng)你在”聆聽會(huì)議精神“的時(shí)候,叫你發(fā)流水線
- 當(dāng)你身邊只有手機(jī)的時(shí)候,叫你發(fā)流水線
- ......
總之,隨時(shí)隨地都可能叫你發(fā)流水線,對(duì)于這種無(wú)聊而又頻繁的操作,有沒有更好的解決辦法呢?
Zadig 在1.15.0版本的時(shí)候,已經(jīng)很友好的支持手機(jī)端了,按理說(shuō)應(yīng)該能滿足平時(shí)的工作需求。但是,作為一個(gè)愛折騰的運(yùn)維,并不滿足于此,我希望能夠通過機(jī)器人的方式來(lái)完成某些運(yùn)維工作,比如合并分支、發(fā)流水線、執(zhí)行腳本等,這樣做主要有以下兩個(gè)好處:
- 移動(dòng)化:隨時(shí)隨地能夠通過移動(dòng) APP 和機(jī)器人溝通,讓機(jī)器人完成本來(lái)在命令行,或者是 web 端才能完成的任務(wù)。
- 共享化:機(jī)器人所在群里的成員都能看到群聊信息,能夠收到任務(wù)的處理結(jié)果,極大的提高了信息溝通的效率。
這其實(shí)就是 ChatOps 的實(shí)現(xiàn),但是這只是初級(jí)階段——也就是字符串匹配的方式進(jìn)行操作,但是隨著人工智能、機(jī)器學(xué)習(xí)等技術(shù)不斷成熟,ChatOps 的交付性體驗(yàn)會(huì)越來(lái)越好。
當(dāng)然,我還停留在初級(jí)階段,本文也是帶大家通過釘釘機(jī)器人的方式發(fā)布 Zadig 流水線。
架構(gòu)解析
ChatOps 的核心在于把 WEB 端或者命令行下的人工操作,轉(zhuǎn)換能通過聊天工具機(jī)器人來(lái)完成,所以整體的架構(gòu)并不會(huì)很復(fù)雜,如下:

整體流程如下:
- 技術(shù)人員在聊天群里@機(jī)器人,發(fā)送需要執(zhí)行的指令
- 機(jī)器人接收到指令,對(duì)指令進(jìn)行判斷
- 根據(jù)指令執(zhí)行相關(guān)操作,并將結(jié)果反饋到聊天群
要想接入到 ChatOps,需要服務(wù)有對(duì)應(yīng)的開放 API。所幸,Zadig 提供了一些 API【1】,可以到文檔中進(jìn)行查看學(xué)習(xí)。
開發(fā)階段
為了不重復(fù)造輪子,我使用的是 Github 上一個(gè) ChatOps bot 框架
【2】,該框架已經(jīng)實(shí)現(xiàn)了命令行、微信網(wǎng)頁(yè)版、企業(yè)微信、釘釘?shù)攘奶鞕C(jī)器人,我們只需要在此基礎(chǔ)上實(shí)現(xiàn)具體的業(yè)務(wù)即可。
封裝 Zadig 請(qǐng)求
要實(shí)現(xiàn)對(duì) Zadig 進(jìn)行 API 操作,就需要我們封裝 HTTP 請(qǐng)求,為了便于操作,我將 Zadig 的一些 API 封裝了一個(gè) SDK【3】,該 SDK 簡(jiǎn)單實(shí)現(xiàn)了 Zadig 開發(fā) API 的功能(沒仔細(xì)調(diào)試,也許有 Bug),如下:

現(xiàn)在我們只需要在項(xiàng)目中實(shí)現(xiàn)自己需要的功能即可。
首先,需要?jiǎng)?chuàng)建 Zadig 請(qǐng)求,創(chuàng)建一個(gè)zadig/zadig.go文件,實(shí)現(xiàn) Zadig 初始化,代碼如下:
package zadig
import (
"errors"
"log"
"github.com/joker-bai/go-zadig"
)
var MyZadig myZadig
type myZadig struct {
client *zadig.Client
}
var (
token = "x.x.x"
baseURL = "http://xxx/"
)
func Setup() *myZadig {
client, err := zadig.NewClient(token, zadig.WithBaseURL(baseURL))
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
return &myZadig{client: client}
}
!! PS:token、url 這些配置其實(shí)是可以放到配置文件中,這里為了便于演示,就放在代碼文件中了。
其中:
- token 是用戶認(rèn)證使用,在 WEB 端右上角用戶->賬號(hào)設(shè)置中獲取
- baseURL 是 zadig 的地址
然后再在該文件中實(shí)現(xiàn)CreateWorkflowTask方法,該方法用于執(zhí)行工作流,如下:
package zadig
var (
callbackURL="xxx"
)
......
func (m *myZadig) CreateWorkflowTask(workfolwName, envName, serviceName, serviceType, repoName, branch string) error {
_, _, err := m.client.Workflow.CreateWorkflowTask(&zadig.CreateWorkflowTaskOptions{
WorkflowName: workfolwName,
EnvName: envName,
Targets: []zadig.TargetArgs{
{
Name: serviceName,
ServiceType: serviceType,
Build: zadig.BuildArgs{
Repos: []zadig.Repository{
{
RepoName: repoName,
Branch: branch,
},
},
},
},
},
Callback: zadig.Callback{
CallbackUrl: callbackURL,
},
})
if err != nil {
return errors.New("執(zhí)行工作流失敗")
}
return nil
}
該方法接收?qǐng)?zhí)行工作流所需的參數(shù),然后調(diào)用 SDK 完成執(zhí)行。由于我這里都是構(gòu)建部署?的方式,所以只寫了Targets實(shí)現(xiàn)。
注冊(cè) Zadig 插件
上面簡(jiǎn)單的把 Zadig 執(zhí)行工作流的請(qǐng)求封裝了,接下來(lái)就注冊(cè) Zadig 插件了。
rboot 項(xiàng)目【2】采用插件的方式注冊(cè)新的指令,系統(tǒng)會(huì)自動(dòng)把這些指令加載到應(yīng)用中,并且可以通過使用help命令查看運(yùn)行規(guī)則。
在robot/plugins?中創(chuàng)建zadig/zadig.go文件,用來(lái)注冊(cè) zadig 執(zhí)行流水線指令,內(nèi)容如下:
package zadig
import (
"fmt"
"regexp"
"strings"
"github.com/sirupsen/logrus"
"devops-chatops/rboot"
"devops-chatops/zadig"
)
var zadigInfo = map[string]map[string]string{
"dev": {
"branch": "dev",
"workflow": "devops-dev",
"serviceType": "helm",
"env": "dev",
},
"test": {
"branch": "test",
"workflow": "devops-qa",
"serviceType": "helm",
"env": "qa",
},
"uat": {
"branch": "uat",
"workflow": "devops-uat",
"serviceType": "helm",
"env": "uat",
},
"prod": {
"branch": "master",
"workflow": "devops-prod",
"serviceType": "helm",
"env": "prod",
},
"yamldev": {
"branch": "master",
"workflow": "chatops-dev",
"serviceType": "k8s",
"env": "dev",
},
}
func init() {
// 注冊(cè)腳本
rboot.RegisterPlugin(`pipeline`, rboot.Plugin{
// 腳本處理函數(shù)
Action: func(bot *rboot.Robot, incoming *rboot.Message) []*rboot.Message {
// 去除空格
content := strings.Replace(incoming.Content, " ", "", -1)
res := createWorkflowTask(content)
return rboot.NewMessages(res)
},
Ruleset: map[string]string{`pipeline`: `執(zhí)行[\w ]+環(huán)境[\w- ]+流水線`}, // 腳本規(guī)則集
Usage: map[string]string{
"pipeline": "執(zhí)行dev環(huán)境devops-chatops流水線",
},
Description: `example '執(zhí)行dev環(huán)境devops-chatops流水線'`,
})
}
func createWorkflowTask(content string) string {
res := regexp.MustCompile(`[\w-/]+流水線`)
s := res.FindStringSubmatch(content)
sp := strings.Split(s[0], "流水線")
pipelineName := sp[0]
br := regexp.MustCompile(`[\w]+環(huán)境`)
tb := br.FindStringSubmatch(content)
env := strings.Split(tb[0], "環(huán)境")[0]
workflow := zadigInfo[env]["workflow"]
repoName := pipelineName
branch := zadigInfo[env]["branch"]
envName := zadigInfo[env]["env"]
serviceName := pipelineName
serviceType := zadigInfo[env]["serviceType"]
logrus.Debugf("工作流:%v 流水線: %v 環(huán)境: %v 服務(wù):%v 服務(wù)類型:%v 分支:%v\n",
workflow,
pipelineName,
envName,
serviceName,
serviceType,
branch,
)
zd := zadig.Setup()
err := zd.CreateWorkflowTask(workflow, envName, serviceName, serviceType, repoName, branch)
if err != nil {
return fmt.Sprintf("執(zhí)行%s環(huán)境的流水線%s失敗", env, pipelineName)
}
return fmt.Sprintf("執(zhí)行%s環(huán)境的流水線%s成功", env, pipelineName)
}
其中:
- init 方法就是插件注冊(cè)的實(shí)現(xiàn)
Action 腳本的處理函數(shù)
Ruleset 是指令規(guī)則
Usage 使用方式
Description 描述信息
- createWorkflowTask 執(zhí)行工作流方法,主要用來(lái)獲取指令的關(guān)鍵詞,然后調(diào)用 zadig.CreateWorkflowTask 執(zhí)行工作流
- zadigInfo 用來(lái)定義 zadig 的環(huán)境信息
workflow 是工作流名稱
branch 是分支名
serviceType 是服務(wù)類型,有 k8s 和 helm 服務(wù)
env 部署環(huán)境信息
上面的匹配規(guī)則、環(huán)境信息等比較簡(jiǎn)單粗暴,最好是把這些數(shù)據(jù)存到數(shù)據(jù)庫(kù)里,我這里為了不引入額外的組件就直接放代碼中了。
業(yè)務(wù)代碼開發(fā)完,我們需要把 zadig 插件引入,在 robot/plugins/plugins.go 中 import 即可,如下:
package plugins
import (
_ "github.com/ghaoo/rboot/robot/plugins/hello"
_ "github.com/ghaoo/rboot/robot/plugins/ping"
_ "github.com/ghaoo/rboot/robot/plugins/vote"
_ "devops-chatops/robot/plugins/zadig"
)
至此,執(zhí)行流水線業(yè)務(wù)開發(fā)完成。
部署 ChatOps
開發(fā)完成就要部署,部署要分幾個(gè)階段:
- 創(chuàng)建聊天機(jī)器人
- 部署應(yīng)用
創(chuàng)建聊天機(jī)器人
該聊天機(jī)器人不是釘釘?shù)钠胀ㄗ远x機(jī)器人,而是需要在釘釘開發(fā)者后臺(tái)【4】創(chuàng)建機(jī)器人,具體操作見文檔【5】,這里不再贅述。
創(chuàng)建到內(nèi)部機(jī)器人過后,就會(huì)在釘釘上生成一個(gè)測(cè)試群并創(chuàng)建了一個(gè)機(jī)器人,如下:

該機(jī)器人和普通機(jī)器人的不同之處在于多了一個(gè) POST 地址,該地址是我們創(chuàng)建機(jī)器人的時(shí)候配置的,也是應(yīng)用的訪問地址。
隨著機(jī)器人的不斷開發(fā),關(guān)鍵詞會(huì)越來(lái)越多,所以我這里選擇的是加簽校驗(yàn)。
部署應(yīng)用
(1)修改配置文件,為了簡(jiǎn)單,我直接將配置文件放到代碼倉(cāng)庫(kù),推到鏡像中。在代碼根目錄下創(chuàng)建.env 文件,內(nèi)容如下:
# 機(jī)器人名稱
ROBOT_NAME=DEVOPS-CHATOPS
# 聊天轉(zhuǎn)接器名稱
ROBOT_ADAPTER="dingtalk"
# 緩存器名稱
ROBOT_BRAIN=bolt
# 消息秘鑰
ROBOT_SECRET=
# 是否開啟DEBUG
DEBUG=false
# 緩存位置
DATA_PATH=.data
# bolt數(shù)據(jù)保存地址
BOLT_DB_FILE=db/rboot.db
# web 服務(wù)監(jiān)聽端口
WEB_SERVER_PORT=9000
# 是否啟用TSL
WEB_SERVER_TLS=false
# CA證書位置
WEB_SERVER_CERT=
# CA秘鑰位置
WEB_SERVER_CERT_KEY=
# 釘釘機(jī)器人秘鑰
DING_ROBOT_SECRET="xxxx"
# 釘釘webhook機(jī)器人access_token
DING_ROBOT_HOOK_ACCESS_TOKEN="xxxx"
# 釘釘webhook機(jī)器人秘鑰
DING_ROBOT_HOOK_SECRET="xxxx"
配置轉(zhuǎn)接器名稱以及釘釘機(jī)器人相關(guān)信息。
(2)添加 Dockerfile,用于制作應(yīng)用鏡像,如下:
FROM golang:1.19.1 AS build-env
ENV GOPROXY https://goproxy.cn
ADD . /go/src/app
WORKDIR /go/src/app
RUN go mod tidy
RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server
FROM alpine
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \
apk add -U tzdata
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=build-env /go/src/app/app-server /app/app-server
COPY --from=build-env /go/src/app/.env /app/.env
WORKDIR /app
EXPOSE 9000
CMD [ "./app-server" ]
(3)添加應(yīng)用 K8S YAML 配置清單,主要有 deployment、service、ingress 資源,如下:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: devops-chatops
name: devops-chatops
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: devops-chatops
app.kubernetes.io/name: devops-chatops
template:
metadata:
labels:
app.kubernetes.io/instance: devops-chatops
app.kubernetes.io/name: devops-chatops
spec:
containers:
- image: registry.cn-hangzhou.aliyuncs.com/coolops/devops-chatops:1c5e4c9274959c8efcecfb286103b052abb44d27
imagePullPolicy: IfNotPresent
name: devops-chatops
ports:
- containerPort: 9000
name: http
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/instance: devops-chatops
app.kubernetes.io/name: devops-chatops
name: devops-chatops
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app.kubernetes.io/instance: devops-chatops
app.kubernetes.io/name: devops-chatops
sessionAffinity: None
type: ClusterIP
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: devops-chatops
spec:
rules:
- host: chatops.jokerbai.com
http:
paths:
- backend:
serviceName: devops-chatops
servicePort: 80
path: /
(4)在 Zadig 上部署應(yīng)用 由于我們這里使用的 YAML 類應(yīng)用,所以先在 Zadig 上創(chuàng)建一個(gè) YAML 類項(xiàng)目,如下:

然后在項(xiàng)目中創(chuàng)建添加服務(wù),我們選擇從代碼倉(cāng)庫(kù)中同步,如下:

接下來(lái)我們需要給該應(yīng)用增加構(gòu)建操作,配置如下:

接著我們把服務(wù)添加到環(huán)境即可。
現(xiàn)在就可以執(zhí)行工作流發(fā)布任務(wù)了,如下:

測(cè)試機(jī)器人
現(xiàn)在我們可以在群里進(jìn)行測(cè)試了,先測(cè)試簡(jiǎn)單的help?,看能不能輸出我們想要的幫助信息,如下:

我們發(fā)現(xiàn)可以得到我們想要的信息。
接下來(lái)測(cè)試發(fā)布 Zadig 流水線,如下:

可以看到給我們反饋的是流水線創(chuàng)建成功,那到底有沒有成功呢?
我們到 Zadig WEB 端查看如下:

我們可以看到有一個(gè)由 openAPI 觸發(fā)的流水線正在運(yùn)行,這表示流水線已經(jīng)觸發(fā)成功。
為了得到工作流執(zhí)行的最終結(jié)果,我們可以在 Zadig 上為工作流添加 IM 通知,同樣可以使用該機(jī)器人,這樣就形成閉環(huán)了。
最后
到此,我們把 Zadig 和 ChatOps(聊天機(jī)器人)結(jié)合就算完成了,當(dāng)然,這種機(jī)器人需要我們根據(jù)規(guī)則來(lái)玩,如果你輸?shù)闹噶詈鸵?guī)則不匹配,就沒法進(jìn)行下一步了。
在整個(gè)過程中,還是發(fā)現(xiàn)一些問題:
- 使用 openAPI 觸發(fā) Helm 項(xiàng)目目前存在問題,無(wú)法正常獲取到服務(wù),導(dǎo)致流水線無(wú)法進(jìn)行
- 使用 openAPI 觸發(fā)的工作流不會(huì)進(jìn)行 IM 通知
聊天機(jī)器人,可以接入很多能力,如果某種操作比較頻繁且無(wú)趣,可以考慮做成各種自動(dòng)化,chatops 就是其中的選擇之一。
文檔
【1】Zadig 開放 API https://docs.koderover.com/zadig/v1.15.0/api/usage/
【2】ChatOps 框架 https://github.com/ghaoo/rboot.git
【3】Zadig SDK https://github.com/joker-bai/go-zadig.git
【4】釘釘開發(fā)者后臺(tái)? https://open-dev.dingtalk.com
【5】釘釘內(nèi)部機(jī)器人文檔 https://open.dingtalk.com/document/robots/enterprise-created-chatbot