大家好,我是乔克,一个爱折腾的运维工程,一个睡觉都被自己丑醒的云原生爱好者。
作者:乔克
公众号:运维开发故事
博客:www.jokerbai.com
背景介绍
Zadig 是目前很火的云原生持续交付平台,具备灵活易用的高并发工作流、面向开发者的云原生环境、高效协同的测试管理、强大免运维的模板库、客观精确的效能洞察以 及云原生 IDE 插件等重要特性,为工程师提供统一的协作平面,可以满足大部分的企业交付场景。
但是,大家有没有遇到过以下情况:
- 当你在”带薪拉屎“的时候,叫你发流水线
- 当你在”聆听会议精神“的时候,叫你发流水线
- 当你身边只有手机的时候,叫你发流水线
- ......
总之,随时随地都可能叫你发流水线,对于这种无聊而又频繁的操作,有没有更好的解决办法呢?
Zadig 在1.15.0
版本的时候,已经很友好的支持手机端了,按理说应该能满足平时的工作需求。但是,作为一个爱折腾的运维,并不满足于此,我希望能够通过机器人的方式来完成某些运维工作,比如合并分支、发流水线、执行脚本等,这样做主要有以下两个好处:
- 移动化:随时随地能够通过移动 APP 和机器人沟通,让机器人完成本来在命令行,或者是 web 端才能完成的任务。
- 共享化:机器人所在群里的成员都能看到群聊信息,能够收到任务的处理结果,极大的提高了信息沟通的效率。
这其实就是 ChatOps 的实现,但是这只是初级阶段——也就是字符串匹配
的方式进行操作,但是随着人工智能、机器学习等技术不断成熟,ChatOps 的交付性体验会越来越好。
当然,我还停留在初级阶段,本文也是带大家通过钉钉机器人的方式发布 Zadig 流水线。
架构解析
ChatOps 的核心在于把 WEB 端或者命令行下的人工操作,转换能通过聊天工具机器人来完成,所以整体的架构并不会很复杂,如下:
整体流程如下:
- 技术人员在聊天群里@机器人,发送需要执行的指令
- 机器人接收到指令,对指令进行判断
- 根据指令执行相关操作,并将结果反馈到聊天群
要想接入到 ChatOps,需要服务有对应的开放 API。所幸,Zadig 提供了一些 API【1】,可以到文档中进行查看学习。
开发阶段
为了不重复造*,我使用的是 Github 上一个 ChatOps bot 框架【2】,该框架已经实现了命令行
、微信网页版
、企业微信
、钉钉
等聊天机器人,我们只需要在此基础上实现具体的业务即可。
封装 Zadig 请求
要实现对 Zadig 进行 API 操作,就需要我们封装 HTTP 请求,为了便于操作,我将 Zadig 的一些 API 封装了一个 SDK【3】,该 SDK 简单实现了 Zadig 开发 API 的功能(没仔细调试,也许有 Bug),如下:
现在我们只需要在项目中实现自己需要的功能即可。
首先,需要创建 Zadig 请求,创建一个zadig/zadig.go
文件,实现 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 这些配置其实是可以放到配置文件中,这里为了便于演示,就放在代码文件中了。
其中:
- token 是用户认证使用,在 WEB 端右上角用户->账号设置中获取
- baseURL 是 zadig 的地址
然后再在该文件中实现CreateWorkflowTask
方法,该方法用于执行工作流,如下:
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("执行工作流失败")
}
return nil
}
该方法接收执行工作流所需的参数,然后调用 SDK 完成执行。由于我这里都是构建部署
的方式,所以只写了Targets
实现。
注册 Zadig 插件
上面简单的把 Zadig 执行工作流的请求封装了,接下来就注册 Zadig 插件了。
rboot 项目【2】采用插件的方式注册新的指令,系统会自动把这些指令加载到应用中,并且可以通过使用help
命令查看运行规则。
在robot/plugins
中创建zadig/zadig.go
文件,用来注册 zadig 执行流水线指令,内容如下:
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() {
// 注册脚本
rboot.RegisterPlugin(`pipeline`, rboot.Plugin{
// 脚本处理函数
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`: `执行[\w ]+环境[\w- ]+流水线`}, // 脚本规则集
Usage: map[string]string{
"pipeline": "执行dev环境devops-chatops流水线",
},
Description: `example '执行dev环境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]+环境`)
tb := br.FindStringSubmatch(content)
env := strings.Split(tb[0], "环境")[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 环境: %v 服务:%v 服务类型:%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("执行%s环境的流水线%s失败", env, pipelineName)
}
return fmt.Sprintf("执行%s环境的流水线%s成功", env, pipelineName)
}
其中:
- init 方法就是插件注册的实现
- Action 脚本的处理函数
- Ruleset 是指令规则
- Usage 使用方式
- Description 描述信息
- createWorkflowTask 执行工作流方法,主要用来获取指令的关键词,然后调用 zadig.CreateWorkflowTask 执行工作流
- zadigInfo 用来定义 zadig 的环境信息
- workflow 是工作流名称
- branch 是分支名
- serviceType 是服务类型,有 k8s 和 helm 服务
- env 部署环境信息
上面的匹配规则、环境信息等比较简单粗暴,最好是把这些数据存到数据库里,我这里为了不引入额外的组件就直接放代码中了。
业务代码开发完,我们需要把 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"
)
至此,执行流水线业务开发完成。
部署 ChatOps
开发完成就要部署,部署要分几个阶段:
- 创建聊天机器人
- 部署应用
创建聊天机器人
该聊天机器人不是钉钉的普通自定义机器人,而是需要在钉钉开发者后台【4】创建机器人,具体操作见文档【5】,这里不再赘述。
创建到内部机器人过后,就会在钉钉上生成一个测试群并创建了一个机器人,如下: 该机器人和普通机器人的不同之处在于多了一个 POST 地址,该地址是我们创建机器人的时候配置的,也是应用的访问地址。
随着机器人的不断开发,关键词会越来越多,所以我这里选择的是加签校验。
部署应用
(1)修改配置文件,为了简单,我直接将配置文件放到代码仓库,推到镜像中。在代码根目录下创建.env 文件,内容如下:
# 机器人名称
ROBOT_NAME=DEVOPS-CHATOPS
# 聊天转接器名称
ROBOT_ADAPTER="dingtalk"
# 缓存器名称
ROBOT_BRAIN=bolt
# 消息秘钥
ROBOT_SECRET=
# 是否开启DEBUG
DEBUG=false
# 缓存位置
DATA_PATH=.data
# bolt数据保存地址
BOLT_DB_FILE=db/rboot.db
# web 服务监听端口
WEB_SERVER_PORT=9000
# 是否启用TSL
WEB_SERVER_TLS=false
# CA证书位置
WEB_SERVER_CERT=
# CA秘钥位置
WEB_SERVER_CERT_KEY=
# 钉钉机器人秘钥
DING_ROBOT_SECRET="xxxx"
# 钉钉webhook机器人access_token
DING_ROBOT_HOOK_ACCESS_TOKEN="xxxx"
# 钉钉webhook机器人秘钥
DING_ROBOT_HOOK_SECRET="xxxx"
配置转接器名称以及钉钉机器人相关信息。
(2)添加 Dockerfile,用于制作应用镜像,如下:
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)添加应用 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 上部署应用 由于我们这里使用的 YAML 类应用,所以先在 Zadig 上创建一个 YAML 类项目,如下:
然后在项目中创建添加服务,我们选择从代码仓库中同步,如下:
接下来我们需要给该应用增加构建操作,配置如下:
接着我们把服务添加到环境即可。
现在就可以执行工作流发布任务了,如下:
测试机器人
现在我们可以在群里进行测试了,先测试简单的help
,看能不能输出我们想要的帮助信息,如下:
我们发现可以得到我们想要的信息。
接下来测试发布 Zadig 流水线,如下:
可以看到给我们反馈的是流水线创建成功,那到底有没有成功呢?
我们到 Zadig WEB 端查看如下:
我们可以看到有一个由 openAPI 触发的流水线正在运行,这表示流水线已经触发成功。
为了得到工作流执行的最终结果,我们可以在 Zadig 上为工作流添加 IM 通知,同样可以使用该机器人,这样就形成闭环了。
最后
到此,我们把 Zadig 和 ChatOps(聊天机器人)结合就算完成了,当然,这种机器人需要我们根据规则来玩,如果你输的指令和规则不匹配,就没法进行下一步了。
在整个过程中,还是发现一些问题:
- 使用 openAPI 触发 Helm 项目目前存在问题,无法正常获取到服务,导致流水线无法进行
- 使用 openAPI 触发的工作流不会进行 IM 通知
聊天机器人,可以接入很多能力,如果某种操作比较频繁且无趣,可以考虑做成各种自动化,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】钉钉开发者后台 https://open-dev.dingtalk.com
【5】钉钉内部机器人文档 https://open.dingtalk.com/document/robots/enterprise-created-chatbot