# 在两个半公有云上实现 Github Webhook

## 背景

[Service Mesher](http://www.servicemesher.com) 社区牵头启动 [Istio](https://istio.io/zh) 文档翻译工作之后，为降低维护工作量，我们开发了一个 [Github Webhook 项目](https://github.com/servicemesher/webhook)，用 Github Issue 的方式对社区翻译工作流程提供自动化支持。同时也开发了一个 Chatbot 来完成任务的维护工作。

在上海 KubeCon 上，经过和 [Kubernetes](https://kubernetes.io/zh/) 文档工作组进行一番交流之后，决定将这一套方法推行到 Kubernetes 文档的本地化工作之中。

经过一番准备之后，两个项目用相似的 Flask 代码，以在 VPS 上运行的 Docker Image 的形式支撑了两个本地化工作组的工作流程。

然而两组代码始终是一个隐患，并且工作流程固化在代码之中，也给流程改进带来很大阻碍；另外使用高配 Linode 运行 Webhook 是个非常奢侈的事情 : smile :。因此也就有了利用公有云 Free Tier 提供 Webhook 响应的想法。

未解决这些问题，新建了 [Webhook 项目](https://github.com/fleeto/issueflow)，经过对代码的修改，将流程定制工作全部转移到配置文件之中，并将流程处理代码进行了固化，在此基础上，分别实现了 Flask、AWS Lambda 以及 GCP Function 的三个版本。

## AWS Lambda

### 入口代码

Lambda 版本的 Webhook，使用 [`lambda.py`](https://github.com/fleeto/issueflow/blob/lambda/lambda.py) 作为入口文件，入口函数为 `webhook`，在创建 Lambda 的页面中，可以指定 `lambda.webhook` 为入口。

`def webhook(event, context):` 中的 `event` 参数中包含了请求数据，`context` 顾名思义，包含 Lambda 的上下文信息。例如用下面的代码获取 METHOD、POST Data 以及 Header（强烈鄙视标头这个译法）：

```python
if event["httpMethod"] != "POST":
    return {
        "isBase64Encoded": "false",
        "statusCode": 405,
        "headers": {},
        "body": "Method Not Allowed!"
    }

data = json.loads(event["body"])
event_type = event["headers"]["X-GitHub-Event"]
event_id = event["headers"]["X-GitHub-Delivery"]
```

### 日志

如下代码可以将日志写入 CloudWatch Log。

```python
logger = logging.getLogger()
logger.setLevel(int(LOG_LEVEL))
```

需要注意的两个问题：

* CloudWatch Log 不属于 Free Tier。因此可以考虑使用 S3 存储文件的方式来完成日志记录。
    
* AWS 为 Lambda 分配的缺省权限中不包含 Log 的内容，需要在 IAM 中进行授权。
    

### 返回

选择 API Gateway 作为 Lambda 触发器，其返回内容需要是一个固定的 JSON 格式，例如：

```python
return {
    "isBase64Encoded": "false",
    "statusCode": 405,
    "headers": {},
    "body": "Method Not Allowed!"
}
```

### 部署

Lambda 没有为 Python 提供依赖处理功能，需要自行下载依赖包，并统一打包为 ZIP 文件上传，代码中提供了 [build.sh](https://github.com/fleeto/issueflow/blob/lambda/build.sh)，用于生成发布包。

## GCP Function

### 入口代码

GCP Function 版本的 Webhook 以 [`main.py`](https://github.com/fleeto/issueflow/blob/gcp-function/main.py) 为入口，这是强制规定。可以指定入口函数，我在这里指定使用 `webhook` 入口，其中的 `request` 参数实际上就是 [Flask](http://flask.pocoo.org/docs/1.0/) 的 Request 对象。因此可以很方便的查找文档。

### 日志

这里的日志稍嫌复杂，但是和 AWS 不同的是，StackDriver Log 是免费的，因此可以忍。

创建 ServiceAccount：

```bash
gcloud iam service-accounts \
create [account] --project [project-id]
```

为新账号赋权：

```bash
gcloud projects add-iam-policy-binding [project-id] \
--member "serviceAccount:[account]@[project-id].iam.gserviceaccount.com" \
--role "roles/owner"
```

获取账号文件：

```bash
gcloud iam service-accounts keys create permission.json \
--iam-account [account]@[project-id].iam.gserviceaccount.com
```

应用中需要定义 `GOOGLE_APPLICATION_CREDENTIALS` 环境变量，指定上传的 permission.json 文件的位置。

日志需要使用 Google 自己的库来完成：

```python
from google.cloud import logging
...

logging_client = logging.Client()
log_name = "github-webhook-{}".format(WORKFLOW)
logger = logging_client.logger(log_name)
...
logger.log_struct(
    {"workflow": WORKFLOW, "admins": ADMINS}
)
...
```

`requirments.txt` 中需要加入如下依赖：

```bash
google-cloud
google-cloud-logging
```

### 返回

返回值无需像 Lambda 一样特别处理，直接 `return` 即可。

### 部署

GCP Function 提供了依赖处理能力，只需要在 `requirements.txt` 中写明依赖包即可。无需下载上传大量的依赖包文件。

## Azure Function

Azure 提供了 func cli 来完成初始化工作，并通过 VS Code 提供了 Azure Function 的开发支持。**然而 func cli 只支持 Python 3.6.x，测试未能完成。**

## 一点对比

* GCP Function 的 HTTP 触发器没有提供对网址的定义功能。
    
* AWS 日志不免费提供，但是比 GCP 更方便。
    
* AWS 没有提供 Python 的依赖处理。
    
* GCP Function 部署似乎有一点延迟，不会立即生效。
    
* AWS Lambda 的默认超时时间为 3 秒，对很多任务来说，可能无法顺利完成。
