Back
Featured image of post Webhook实践:打造高效的服务集成

Webhook实践:打造高效的服务集成

我们在对接很多平台时,很经常会需要涉及Webhook。本文不宣读概念,而是聊几个有帮助的实践方案。我们可以用低代码的Pipedream,也可站在巨人肩膀上,直接花几分钟用webhook项目(外加上几行配置和几行代码)来解决一个原本要花费半天时间的通用问题。它还可成为众多小工具的集散地,或是个不错的选择。

背景

在没有Webhook情况下,有些情况会比较复杂且不合理。比如有代码推送了,我们会想进行构建,此时有两种选择,自己主动调用构建(持续集成),或者依赖于代码平台的CI/CD功能来触发。前者信息传递都是个问题更遑论优雅,后者稍优雅一些,但它得依赖平台,如果你的代码托管平台和构建平台不一样,鸿沟就存在了。再比如你费心在博客写的文章好不容易有人点个赞,你想快点感受到这份喜悦,需要及时接受这个事件,然后找一个自己喜欢的通知平台把它传递于你。

如果你接触过一些IM的机器人,比如飞书/企业微信,其机器人也多数是提供一个Webhook接口,让你方便使用。当然用是方便了,安全又是另一回事,文末会探讨这一块。

总的来说,它把可能需要轮询的东西事件化了,更高效了。基于HTTP的请求,也非常易于使用。

实践

场景描述

我们在使用Gitlab或Github时,有时会关注一些事件。比如我就特别关心仓库的Workflow(Actions)有没有执行成功。为什么呢?因为我博客作为静态网页是在它上面构建的,提交完文章后自动构建刷新,但有没完成,啥时能查看到最新鲜出炉的文章,对我而言那种不确定性感受太差啦:) 那么,我们看如何比较好的解决这个小小的困扰。你要是来争论,有其它方案,何必要用Webhook呢,这我就要说了,咱是来学技术的嘛!

为什么不直接在CI流程中集成通知能力?聪明的你猜一下为什么不呢?事实上在工作中我们有不少流水线是在执行完在最后,添加一些额外操作来进行消息通知的,但这有两个弊端:

  • 表面上延长了流水线执行时间。但其实能否节省还取决于你的webhook设计。
  • 修改不方便,未来对于通知的重构,将是霰弹式的修改。这是最致命的!

那么,我们可以定义一个规则,传递一些必要信息到远程某个API,由它处理后续逻辑,这便是Webhook经典场景之一。我们甚至可以继续YY一下,这个流水线信息如果带上执行时间等,那么监控的数据源也有了,Webhook可以处理入库等,避免将这些逻辑放置在核心的流水线模块。

好的,再次明确本次实践目标:接受Github的Actions执行状态并且发消息通知我们,以让我们及时的掌握流水线开始/结束的信息。

低代码平台Pipedream使用

如果你没有一个自己的公网域名和计算集群,我确信你可能需要像ZapierPipedream这样的平台。它向你提供了域名,并帮你集成了很多Web服务和API。你只要点点点便可实现不错的功能。我在试用后,发现Zapier对于Github的webhook处理还是麻烦,相对来说Pipedream会简单不少,以下用它来介绍整个过程。

在我们于https://pipedream.com/注册后,按指引创建一个Project,然后在其内创建一个workflow,接下来只要三步工作就完工了。

添加触发器

我们选择New HTTP/Webhook Requests,然后选择如下: trigger 继续后,会弹出一个URL: trigger 这就是我们填到Github里的Webhook一栏的,如下: trigger 这时候Pipedream会友善的提醒你,可以帮你生成一个测试的数据,或者是由你从外部调用触发一下。我们可以去Github触发一次事件,对后面流程有大帮助。触发后你会看到这边触发器提示收到消息了,并且解出了相关的内容在Exports中可见(上面红框的payload等)。

创建一个解析器

可惜的是在Pipedream中对于webhook也没有现成的解析器,但是我们创建一个代码自定义代码的组件:Run custom code,选择bash语言,简单写如下shell代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
cat $PIPEDREAM_STEPS
payload=$(cat $PIPEDREAM_STEPS | jq .trigger.event.payload)
title="github workflow notify"
wf_name=$(echo $payload | jq -r . | jq .workflow_run.name)
status=$(echo $payload | jq -r . | jq .workflow_run.status)


# Write data to $PIPEDREAM_EXPORTS to return it from the step
# Exports must be written as key=value
echo title="$title" >> $PIPEDREAM_EXPORTS
echo wf_name="$wf_name" >> $PIPEDREAM_EXPORTS
echo status="$status" >> $PIPEDREAM_EXPORTS

这样我们就很简单通过jq提取出了我们想要的字段。这里要特别点赞一下pipedream的调试体验,因为每一步都有确定的输入源了,可以在这个代码框中一直调试代码,直到输出exports符合你的预期。这避免了以往流水线总是需要从头执行的困扰,大大提升了效率!!(也可能我用的低代码平台不多,孤陋寡闻啦)

借pushover通知

这块他们已经集成了pushover组件,直接选择,填上账户,设置title,message等即可。我们可以方便的通过UI选择字段,也可以输入: trigger

至此,后续每次github的workflow构建,我们都会及时收到它的状态变化啦~~

使用webhook镜像实现

上面已经快速的解决了消息通知的问题了,但我们若想更进一步,具备更多可能性。欢迎来到方案二:) 我之前文章提到过,我在腾讯云CVM上搭建了一套价格低廉的K8S集群,其实际是基于k3s这个轻量的k8s发行版本,同时以istio为ingress和内部服务治理。现在我向你隆重介绍一位新朋友adnanh/webhook,如它所言:

webhook is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. webhook also allows you to specify rules which have to be satisfied in order for the hook to be triggered.

我们当然是想用容器的版本,这个仓库提供了三种容器镜像,我推荐使用thecatlady/webhook,它集成了curl/jq等在写webhook脚本时常用的命令。另外我的朋友soulteary对它作了一系列bugfix和一些feature,也欢迎去试用。不过本示例用thecatlady/webhook足够。

我们要做三件事:

  • 部署webhook服务并暴露于公网。
  • 实现具体逻辑,主要是配置webhook服务以及实现几行脚本。
  • 在Github中添加webhook让其调过来。

部署webhook服务并暴露之

这里我们先定义一个deployment和service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
apiVersion: v1
kind: Service
metadata:
  name: webhook
  labels:
    app.kubernetes.io/name: webhook
    app.kubernetes.io/instance: webhook
spec:
  type: ClusterIP
  ports:
    - port: 9000
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: webhook
    app.kubernetes.io/instance: webhook
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook
  labels:
    app.kubernetes.io/name: webhook
    app.kubernetes.io/instance: webhook
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: webhook
      app.kubernetes.io/instance: webhook
  template:
    metadata:
      labels:
        app.kubernetes.io/name: webhook
        app.kubernetes.io/instance: webhook
    spec:
      securityContext: {}
      containers:
        - name: webhook
          command:
            [
              "webhook",
              "-verbose",
              "-debug",
              "-hooks=/etc/webhook/hooks.json",
              "-hotreload",
            ]
          securityContext: {}
          image: thecatlady/webhook
          imagePullPolicy: IfNotPresent
          volumeMounts:
            - mountPath: /etc/webhook/
              name: hooks
          ports:
            - name: http
              containerPort: 9000
              protocol: TCP
          resources: {}
      volumes:
        - name: hooks
          configMap:
            name: hooks
            defaultMode: 0755

因为我们要暴露于公网,方便其它外部服务调用,所以在Istio中我们定义一个VirtualService即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: webhook
spec:
  hosts:
    - "webhook.example.com"
  gateways:
    - istio-ingress/webhook-example-gateway
    - mesh
  http:
    - name: to-webhook
      route:
        - destination:
            host: webhook.webhook.svc.cluster.local
            port:
              number: 9000

修改配置&脚本

在使用这个webhook镜像时,我们的核心还是在配置其所要求的hooks.json文件。对于Github的处理,它的Webhook描述可以在这里查看到一些注意事项。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
[
    {
        "id": "github_workflow",
        "execute-command": "/etc/webhook/github-workflow.sh",
        "command-working-directory": "/etc/webhook",
        "response-message": "I got the payload!",
        "response-headers": [
            {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
            }
        ],
        "pass-arguments-to-command": [
            {
                "source": "payload",
                "name": "workflow_run.name"
            },
            {
                "source": "payload",
                "name": "workflow_run.status"
            },
            {
                "source": "payload",
                "name": "workflow_run.conclusion"
            },
            {
                "source": "payload",
                "name": "workflow_run.html_url"
            },
            {
                "source": "payload",
                "name": "repository.full_name"
            },
            {
                "source": "payload",
                "name": "repository.html_url"
            }
        ],
        "trigger-rule": {
            "and": [
                {
                    "match": {
                        "type": "payload-hmac-sha1",
                        "secret": "your-secret-modify-it",
                        "parameter": {
                            "source": "header",
                            "name": "X-Hub-Signature"
                        }
                    }
                },
                {
                    "match": {
                        "type": "regex",
                        "regex": "queued|completed",
                        "parameter": {
                            "source": "payload",
                            "name": "workflow_run.status"
                        }
                    }
                }
            ]
        }
    }
]

这个配置别看它挺长,也是很简单,直达一个webhook的核心:

  • execute-command 指定了要执行的程序位置。可以用shell,python,或者其它来写。
  • pass-arguments-to-command 是从请求中提取的一些字段,然后会按这里定义的顺序传递给上面脚本。
  • trigger-rule 就是一些规则了,定义了两个规则同时(AND)满足:
    • 其一便是校验header中的X-Hub-Signature。这是github要求的,由此确保消息是来源于github而不是其它。
    • 其二是一个正则匹配,如果状态是queued|completed才满足条件。我们对workflow的这两个状态才做通知。

然后是脚本,具体的消息通知由它来发,我们同样用Pushover推送消息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/sh

title=$1
status=$2
conclusion=$3
url=$4
repo=$5
repo_url=$6

echo "Pushover =>  title:$title, status:$status, conclusion:$conclusion, url:$url, repo:$repo, repo_url:$repo_url"

token=<your-pushover-token>
user=<your-pushover-user>

title_emoji="🚀"
result=""
if [ "$status" = "queued" ]; then
    title_emoji="📥"
elif [ "$status" = "in_progress" ]; then
    title_emoji="🏁"
elif [ "$status" = "completed" ]; then
    if [ "$conclusion" = "success" ]; then
        title_emoji="✅"
    else
        title_emoji="❌"
    fi
    result="结果:$conclusion"
fi

message="仓库:<font color=\"#cccccc\">${repo}</font><br>状态:$status<br> $result<br>仓库地址:${repo_url}" 
curl \
--form-string "token=$token" \
--form-string "user=$user" \
--form-string "title=$title_emoji $title $status" \
--form-string "message=$message" \
--form-string "url=$url" \
--form-string "html=1" \
https://api.pushover.net/1/messages.json

脚本这块的部署在前面已经带上了,它是以configmap方式挂载到容器内的,在此不再罗嗦。

配置Github Webhook

最后,我们要修改Github对应仓库的Webhook地址,如下: github settings

结果

现在我们可以启动一个workflow,很快就收到消息通知了: github settings 当然啦,为了更方便的部署,我们可以将上述资源写到kustomize中,定义一个kustomization.yaml文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
resources:
  - deployment.yaml
  - virtualservice.yaml
namespace: webhook

generatorOptions:
  disableNameSuffixHash: true

# 在这里目录结构最后会被平铺,注意使用时的路径问题
configMapGenerator:
- name: hooks
  files:
  - hooks.json
  - scripts/github-workflow.sh

然后只要kubectl apply -k .就可以部署完工啦。

一些思考

有几点有意思的想法,可以一起交流一下:

  • pipedream亮点
    • 如前面文章提到,pipedream的交互还有调试方式令我眼前一亮,还是懂用户痛点的嘛,不知道我司工具流程设计者啥时可以更有用户feel一些。基于这种理念,明确了输入和输出的概念,组件之前传递变量也清晰了。
  • 关于安全性
    • 安全性上来看,上面我们示例的pipedream是不够的。为保障通信安全,第一是确保使用HTTPS而非HTTP,其次要有一个简单认证。比如Github所使用的secret,要求我们进行签名校验。也可以通过Authorization Bearer的方式来写。至于我所了解到企业微信的机器人在URL中带上token的方式,它简化了调用者使用,但如果这个视为敏感信息,就不应该明文在URL中,如果单独保存secret然后运行时组装URL,那何必呢,容易出错是不?
  • 关于webhook
    • 有两个想法,就像我们在做一些设计,非核心逻辑得剥离。这样我们业务只关注它自己的实现,而把外围东西丢出去。这样即使外围有问题,业务也不致受大影响。其次,webhook产生了一个调用的端点(endpoint),它可以保持稳定不变,但是它对应的功能还是可不停迭代。这意味着,我们有很多想象空间。

PS: 本文标题和配图感谢chatGPT提供帮助:)

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

trigger