基于 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 的聊天应用: 企业微信、飞书等等)

系统拓扑

ChatOPS
ChatOPS

懒汉只会和机器人以聊天的形式交互,其余的脏活累活重复劳动交给机器人做。大致流程如图,重点实现:

  • 迎接聊天机器人的 HTTP 请求
  • 根据请求文本触发行为
    • Git Workflow
      • Approve Pull Request
      • Test Pull Request
      • Merge Pull Request
      • Show PR list
    • CI / CD
      • CI Test
      • Deploy to Production

很显然,核心的开发工作的都在 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 选项,只提供 mergesquash ,在所谓的社区企鹅群里和他们提了一下建议,理都不理)。

如果机器人收到类似这样的指令:

/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 的参数做出相应的变动就实现了。

此项实践已作为 ThoughtWorks 员工构建的知识体系 Ledge DevOps 对 ChatOps 这一模式的展示案例: