基于 Go 的 ChatOps 实践
聊着天就把活干了。
公司的运维一直由我来帮助实施。去年将所有微服务容器化后,运维的成本面临新的挑战。本着解放生产力的目标,顺带回忆不太熟悉的 Go 基本语法,笔者写了一个钉钉机器人来帮助我做一些运维的活。效果也不错,就把大体做法在这里讲讲。
做这个之前有一系列的必要(也可替换的)基础设施和概念以及 HTTP 服务端开发的认知,能实现这套聊天运维离不开每一个环节以及诸多细节。简单概括一下我是怎么实践的。
技术栈依赖
实践 ChatOps 至少需要的基础设施:一个 CI / CD 服务,一个暴露在公网的 HTTP 服务,一个远程 VCS 和一个能提供收发消息 API 的即时通信工具。
本项目采用:
- Jenkins (CI / CD,经典的持续集成工具,提供公网可访问的 REST API)
- Git (VCS,本项目使用公司正在用的 Gitee.com - 可使用 GitHub.com 或 GitLab,提供 REST API 或 SDK)
- Serverless (用来暴露核心的 HTTP 端点,只是无需运维而已;也不是必须,否则需要有公网服务器)
- DingTalk (限制企业内部机器人,能提供 WebHook 和收发消息的 SDK 即可,或其他类似 Slack 的聊天应用: 企业微信、飞书等等)
系统拓扑
懒汉只会和机器人以聊天的形式交互,其余的脏活累活重复劳动交给机器人做。大致流程如图,重点实现:
- 迎接聊天机器人的 HTTP 请求
- 根据请求文本触发行为
- Git Workflow
- Approve Pull Request
- Test Pull Request
- Merge Pull Request
- Show PR list
- CI / CD
- CI Test
- Deploy to Production
- Git Workflow
很显然,核心的开发工作的都在 Message Handler 那里。根据聊天机器人提供的 Event Hook(每次聊天机器人收到消息时会自动向某个服务器发送 HTTP 请求,会携带发送消息的用户、消息内容等 Payload),可以设计一个 HTTP 端点,在服务端解析这些请求(钉钉需要按照企业内部机器人开发,参考这篇开发文档)。
我们按照 Kubernetes Prow 的设计语言,用一个 /
来作为 Tag,形式如同 /test xxx
.
因此这里必然需要做字符串处理了。除了判断 Tag 的存在之外,要取 Tag 后面的参数。项目用 Go 实现,很简单。贴其中一工具函数的代码,各位猜猜这是做什么的:
func StringIndexOf(originalArray []string, wordToFind interface{}) []int {
length := len(originalArray)
interfaceArray := make([]interface{}, length)
for i, v := range originalArray {
interfaceArray[i] = v
}
var indexArray []int
for i:=0 ; i < length; i++ {
if strings.Compare(wordToFind.(string), originalArray[i]) == 0 {
indexArray = append(indexArray, i)
}
}
return indexArray
}
拿到参数后继续判断,比如机器人收到了这个指令:
/merge 233
即合并第 #233 个 Pull Request。要通过代码来合并,那操作 Git 就需要 SDK 或接口了。考虑到之前写 Issue Ops Bot 的经验,Google 开源的 GitHub 的 Go-GitHub 写起来很方便,Gitee 应该也有人搞吧?果然搜到了华为的 OpenEuler 团队的仓库:Go-Gitee。看来只有大厂才会给这些基础设施写 SDK 啊!果断加依赖:
go get -u gitee.com/openeuler/go-gitee
版本下载下来给的时间戳居然就是这两天的,虽然没有任何测试代码,但看似更新蛮活跃,大胆直接用上了。后面写了一些接口,发现 SDK 提供的一些 API 也不全,比如没有审核通过 PR 和测试通过 PR 的 API,就自己顺带拿 net/http
参考着 Gitee 的 REST API 自己实现了,但是代码太丑陋(本来想模仿项目的写法去写,但是一心只想把功能完成),也没给华为提 issue,等回头有机会写下给它提个 PR 嘿嘿(除此之外我还发现 Gitee 的合并方式没有 rebase
选项,只提供 merge
和 squash
,在所谓的社区企鹅群里和他们提了一下建议,理都不理)。
如果机器人收到类似这样的指令:
/jenkins test myservice
或 /deploy myservice
那就直接对接 Jenkins 的 REST API 了。可以直接凭状态码判断响应,安全方面 Jenkins 自身提供了 HTTP BASIC 认证。就不细说了。
之前没有用 Go 写过 HTTP Server,这次也没有写——如果运维一套服务已经很麻烦了,开发出的一套帮助运维的辅助服务本身也需要运维(公网服务器、域名解析、持续的测试和部署等服务器资源以及注意力资源),我就没有使用传统的后端服务,而是使用了自己熟悉的 Vercel Serveless for Go。公开一个 Handler 方法即可,*http.Request
结构体指针的 Body
就是机器人发来的消息了。
下面是整个 Handler 的大致逻辑,伪代码:
func Handler(w http.ResponseWriter, r *http.Request) {
defer fmt.Fprintf(w, "ok")
chatMessage, _ := ioutil.ReadAll(r.Body)
if strings.Contains(chatMessage, "/deploy") {
serverName := getServerNameFromMessage(chatMessage)
jenkinsSDK.deploy(serverName)
}
if strings.Contains(chatMessage, "/merge") {
pullRequestNumber := getPullRequestNumberFromMessage(chatMessage)
giteeSDK.mergePullRequest(pullRequestNumber)
}
...
}
期待某天 Vercel 能够支持 JVM 语言甚至是 Spring 的 Serverless。
效果
比如让机器人展示下仓库目前的 Pull Request,然后测试这个 PR,通过后批准这个 PR,最终合并 PR,上线生产,聊天记录会是这样的:
Users 命令: /pr
Robot 回复: 当前仓库的 PullRequest 列表...
[#709] fix: typo
[author] username
Users 命令: /jenkins test micro-server 709
Robot 回复: 开始测试 PullRequest 709
CIBot 回复:
[jenkins] micro-server ci
-------------------------
任务:#666
状态:开始
持续时间:0 分 1 秒
执行人:Host 76.76.21.21
Users 命令: /approve 709
Robot 回复: 审核通过 PullRequest #709
Users 命令: /merge 709
Robot 回复: 合并 PullRequest #709
Gitee 回复: User 接受了 Owner/repo 的 Pull Request !709 fix: typo
Users 命令: /deploy micro-server
Robot 回复: 生产发布 micro-server
CDBot 回复:
[jenkins] micro-server cd
-------------------------
任务:#233
状态:开始
持续时间:0 分 1 秒
执行人:Host 76.76.21.21
# 友好地提供帮助
Users 命令: /help
Robot 回复: 当前支持指令列表, 带 * 需要特定人员发起
/pr - 展示仓库发起中的 PR
/jenkins <action> <servername> <pr-number> - 在指定 PR 下发起后端测试
/pass <pr-number> - * 测试通过指定 PR
/approve <pr-number> - * 审核通过指定 PR
/merge <pr-number> - * 合并指定 PR
/test <servername> - 在仅有一个 PR 的状态下发起后端服务测试
/deploy <servername> - * 发布服务至生产环境
/help - 显示此帮助内容
代码写的很丑陋,没脸往外放了,而且也是企业内部使用,就不开源了(所有变量,十个左右,都配置在环境变量中,尽量没有 Hard Code)。
社区也有不少钉钉机器人的 SDK,阿里没有提供 Go 版本的,但写起来也不复杂,顺手提供自己写的钉钉群机器人 Go 语言的 SDK,目前就只用来发文本消息。
最近一次更新,让机器人支持了多个仓库,直接在 /tag
最后加一个可选参数 [repo]
,然后 SDK 的参数做出相应的变动就实现了。
相关链接: