简介
大多数选择 Hugo 这类静态博客的用户,会选择将博客存储到 GitHub Pages 之类的方案,其中初学者一般会在本地生成网站,而进阶用户或者懒人一般会使用 CI 来实现自动部署。而我,由于自身性格别扭,以及将网站源代码放在了私有库中,并基于一些其它的神秘原因选择了通过 GitHub Webhook 再加上用 Golang 编写的后台服务来在自己的 VPS 上实现自动部署。
流程
- 抽象而言
Git 仓库更新后触发钩子 => (通过反向代理) => 接收钩子的服务 => 进行处理 - 具体到个人的选择
GitHub Webhook => (Nginx) => Golang API Server => ...
配置 GitHub Repository
将整个网站添加到仓库
基本操作就不详谈了,记得把这两行加进 .gitignore
,因为我们只需要网站的源代码,而不需要生成后的内容。
/public
/resources
添加 Webhook
首先前往
https://gitHub.com/{user}/{repository}/settings/hooks/new
(自行替换{user}
和{repository}
)。Payload URL
这里填写接收钩子的 URL,例如https://api.coda.world/webhookpath
,实际上强烈建议用一串复杂的字符代替webhookpath
(例如我后面的代码中的webhookpath_zbthvqemo08kvhds
),可以防止钩子被别人探嗅到然后恶意触发。Content type
建议使用application/json
。Secret
可以使用下方的命令生成一个head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo ''
如果前面设置了复杂的 URL 或者使用防火墙限制 IP 等其他方式以确保钩子不会被恶意触发,不设置 Secret 也是可以的。
其它选项保持默认,最后点
Add webhook
添加即可。
设置 Deploy keys (可选)
- 由于我使用的是私有库,所以不能直接 Pull,而响应钩子的服务应当使用独立的密钥来进行 Pull Request,所以要在这里进行设置
https://gitHub.com/{user}/{repository}/settings/keys/new
(同样自行替换{user}
和{repository}
)。 - 新生成一个 SSH Key,并将 Public Key 填写到这里,而 Private Key 自然由响应钩子的服务来使用。
Allow write access
通常不需要选上,当然如果你的服务需要进行 Push 的话除外。
配置 Nginx 反向代理(推荐)
反向代理还是推荐配置一下的,毕竟也不复杂,比起来直接使用 IP 加端口号也更安全,毕竟给 IP 地址直接配上 SSL 证书可以说是不现实的。
直接贴一下部分 Nginx 配置
server {
server_name api.coda.world;
listen 443 ssl http2;
ssl_certificate /.../cert.pem;
ssl_certificate_key /.../key.pem;
location / {
proxy_pass_request_headers on;
proxy_pass http://127.0.0.1:11235;
}
}
Golang 后台服务
准备工作
首先准备相应的用户,添加之前生成的 SSH 密钥,把 github.com 的 fingerprint 提前写入 known_hosts,将仓库克隆下来。
useradd githubwebhook
sudo su - githubwebhook
cd
ssh-add ...
ssh-keygen -F github.com || ssh-keyscan github.com >> ~/.ssh/known_hosts
git clone ...
最后记得把自己存放网站的目录的所有者设为 githubwebhook:githubwebhook,以及设置好相应的权限,否则可能会出现生成完网站后因为权限问题没法 rsync,或者 Nginx 因为权限不足没法读取内容的尴尬情景。
Golang
先放出实现最低功能的代码,后面再解释。
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 |
|
代码就这么短,做的事情也很简单:
- 监听 11235 端口,收到 HTTP 请求后返回一个简单的响应(这样 GitHub Webhook 那边就不会显示响应超时)。
- 然后把
/home/githubwebhook/myrepopath/
作为工作目录依次执行git pull
和hugo ...
。 - 仅仅这些!没错,根本没管前面设置的
Content Type
以及Secret
,事实上除了收到请求这一点以外其它的全被忽略了。当然实际上在这种简单的场景下也确实用不到 Payload 中的那些信息,而忽略Secret
的条件是代码中webhookpath_zbthvqemo08kvhds
这个 path 设置的比较复杂,很难收到仿造的请求。
自行更改一下高亮部分中的变量,然后测试一下没什么大问题后就可以编译二进制文件了。
systemd
既然是后台服务,unit file 自然少不了,同样直接放简化版。
# /etc/systemd/system/githubwebhook.service
[Unit]
Description=GitHub Webhook Server
Require=syslog.target network.target Nginx.service
After=syslog.target network.target Nginx.service
[Service]
Type=simple
User=githubwebhook
Group=githubwebhook
ExecStart=/usr/local/bin/githubwebhook-go-bin
Restart=always
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=GitHubWebhook
[Install]
WantedBy=multi-user.target
可以改进的地方
- 由于改进会导致代码的急速膨胀,所以只会大概提一下思路。
- 首先 HTTP 响应只是个简单的 "hook received",事实上可以根据情况不同返回相应的 status code 以及内容,不过需要注意的是响应前耗费的时间太多可能被 GitHub 认为响应超时。
- 没有使用 Secret 进行哈希验证,如果想进行验证,可以参考官方文档来了解如何对比使用 Secret 生成的哈希值。
- 可以在更新完网站后 Ping 一下 googlesitemap 等服务,告知搜索引擎网站更新了。
- 为了精简代码,Golang 代码中的一些路径和端口号等都是写死在代码中的,实际上更推荐在 service file 里加上
EnvironmentFile=...
,然后把这些变量写到 EnvironmentFile 中,并在程序中使用os.Getenv("VARIABLE")
来读取。注意 EnvironmentFile 的权限,因为可能还包含了其它 API 的密钥之类的东西。 - 在进行大量后续处理后,总是希望可以方便的看到日志的,然而就像前面提到过的,这时候再发送响应肯定是已经超时的。最省事的办法是直接把日志做成一个网页,可以用简单的文件名以方便访问,也可以用复杂的文件名然后把链接放到 readme.md 中。由于我使用的是私有库,所以我直接把后端的所有输出都写进了 readme.md,然后再 push 回去,当然这样做需要处理两个问题。首先是 push 回去的时候也会触发一遍 webhook,所以需要在收到请求时检查一下是不是由于更新 readme.md 导致。其次是会导致 Race Condition,这个问题当然可以用分支之类的方案解决,不过实际使用中由于只是个人站点所以其实注意一下即可。
- 最后还是再次强调一下,如果网站本身不是放在私有库中或者愿意为 CI 掏钱,直接使用成熟的 CI 方案会更省心省力。