morishitaです。
Github Service が使えなくなったので対応しました。
次の記事には2019年1月末に止めるよって書いてあるので、今月中になんとかすればいいかなと思っていました。
ところが、2019/01/08 から突然動かなくなったのです。
最初は月末に廃止する作業でなにかミスがあったのだろう、じきに回復するだろうと思っていました。
しかしGitHub Statusを確認すると、「1/7 19:00 UTCから止め始めるよ」って書いて
あるではないですか!
廃止のスケジュールが前倒されたの?こちらの勘違い?と思ったものの、どうせ今月中にはやらない行けない作業なのでこちらも予定を変更して対応しました。
なにに利用していたのか?
Github Serviceを何に使っていたのかというと、 以前、このブログでも紹介した いこレポの自動テストで利用していたのです。
詳しくは上記の記事を見てほしいのですが、ざっくりなにをやっているかというと次の通りです。
- Github の PullRequest のブランチにPushする
- PushがAmazon SNSに通知される
- Amazon SNSがLambda関数をトリガーして、CodebuildによりRSpecを実行する
- RSpecの結果をGithubのPullRequestとSlackに通知する
このなかで、PushをAmazon SNSに通知されるという処理に利用していたのです。
移行方法の検討
移行方法を検討するにあたって、もともとの仕組みを少し調べました。
API的には、GithubServiceの設定内容も Webhook APIで取得できます。 なので、ちょっと設定をいじればできちゃうんじゃないかと思いましたが、どうやらその方法はなさそうでした。
で、Webhookに移行する方法として次の3案を考えました。
現行の処理の流れは次のとおりです。
Github Services => AWS SNS => Lambda => CodeBuild => 以下略。
これを次の3案のいずれかに移行しようと考えました。
- Github Webhook => AWS SNS => Lambda => CodeBuild
- Github Webhook => API Gateway => Lambda => AWS SNS => Lambda => CodeBuild
- Github Webhook => API Gateway => Lambda => CodeBuild
案1はAWS SNSにHTTPのエンドポイントがあれば、と思ったのですがどうやらトピックに直接パブリッシュできる エンドポイントはなさそうです。なので、却下。
案2はAWS SNSにパブリッシュする必要なHTTPエンドポイントをAPI Gateway+Lambdaで作ってしまうというもの。
SNS以降の処理に変更が不要なところが利点。
案3はAWS SNSの利用をやめ、CodeBuildをトリガーしているLambdaを変更して直接Webhookのリクエストを受けるようにするもの。
現行の仕組みを変更するけれど、仕組みは簡素になるという案。
いずれにせよLambdaでWebhookからのリクエストを受ける必要があるので、その部分をプロトタイピングしてどんなリクエストが来るのか調査しました。
調査した結果、AWS SNS経由で受け取っていたGithubのPullRequest情報と内容と形式が同じ1だったので、変更も軽微で済むと判断して、案3を採用しました2。
Webhookへの移行
Webhookの設定
Webhookは次のように設定します。
項目 | 設定値 |
---|---|
Payload URL | API GateWayのURL |
Content-type | application/json |
Secret | 推測されにくい文字列を設定 |
SSL verification | Enable SSL verification |
Which events would you like to trigger this webhook? | Let me select individual events. > PullRequest |
説明が前後しますが、Payload URL には後述の Serverless Framework の設定を追加すればAPI Gatewayが登録されるので、そのエンドポイントURLを設定します。
プルリクに対する操作で実行したいので、送信するイベントはPullRequest
だけで必要十分です。
Webhookからのリクエストの受けの変更
Webhookからのリクエストのヘッダやボディは Lambda関数にはハンドラーの引数 event
オブジェクトに格納されて渡ってきます。
次のような感じです。
{ "event": { "resource": "/test", "path": "/test", "httpMethod": "POST", "headers": { 〜略〜 "X-Hub-Signature": "sha1=<bodyをsecretでハッシュ化した値(後述)>" // 利用する }, "multiValueHeaders": { 〜略〜 }, "queryStringParameters": null, "multiValueQueryStringParameters": null, "pathParameters": null, "stageVariables": null, "requestContext": { 〜略〜 }, "body": "〜後述〜", "isBase64Encoded": false } }
で、リクエストボディは次の様な感じ。
実際にはJSON.stringify された文字列が入っているのでJSON.parse
して読み込みます。
{ "body": { "action": "labeled", // 利用する "number": <PullRequestの番号>, // 利用する "pull_request": { "url": "https://api.github.com/repos/〜略〜", "id": 242573289, "node_id": "〜略〜", "html_url": "https://github.com/〜略〜", // 利用する "diff_url": "https://github.com/〜略〜", "patch_url": "https://github.com/〜略〜", "issue_url": "https://api.github.com/repos/〜略〜", "number": 883, "state": "open", "locked": false, "title": "<PullRequestのタイトル(略)>", // 利用する "user": { <PullRequestのユーザ情報(略)> }, "body": "<PullRequestの本文(略)>", "created_at": "2019-01-07T07:21:06Z", "updated_at": "2019-01-09T03:28:00Z", "closed_at": null, "merged_at": null, "merge_commit_sha": "〜略〜", "assignee": null, "assignees": [], "requested_reviewers": [], "requested_teams": [], "labels": [ { "id": 936758322, "node_id": "〜略〜", "url": "https://api.github.com/repos/〜略〜", "name": "自動テスト対象", // 利用する "color": "fbca04", "default": false } ], "milestone": null, "commits_url": "https://api.github.com/repos/〜略〜", "review_comments_url": "https://api.github.com/〜略〜", "review_comment_url": "https://api.github.com/repos/〜略〜", "comments_url": "https://api.github.com/repos/〜略〜", "statuses_url": "https://api.github.com/repos/〜略〜", "head": { "label": "〜略〜", "ref": "<ブランチ名>", // 利用する "sha": "<コミットID>", // 利用する "user": { 〜略〜 }, "repo": { 〜略〜 } }, "base": { 〜略〜 }, "_links": { 〜略〜 }, "author_association": "CONTRIBUTOR", "merged": false, "mergeable": true, "rebaseable": true, "mergeable_state": "clean", "merged_by": null, "comments": 0, "review_comments": 0, "maintainer_can_modify": false, "commits": 5, "additions": 50, "deletions": 49, "changed_files": 1 }, "label": { "id": 936758322, "node_id": "〜略〜=", "url": "https://api.github.com/repos/〜略〜", "name": "自動テスト対象", "color": "fbca04", "default": false }, "repository": { 〜略〜 }, "organization": { 〜略〜 }, "sender": { 〜略〜 } } }
// 利用する
とコメントした項目を処理の中で利用しています。
幸い、もともとの実装が、AWS SNSから渡されるオブジェクトからPullRequestの情報を取り出す部分を関数化していたので、event.body
から取り出す様に変更するだけでほぼ対応できました。
API Gatewayの追加
ServerlessFrameworkを利用しているので次のようなイベント設定を追加するだけです。
簡単!
events: - http: path: pull_request method: post
デプロイするとエンドポイントが決まるので、そのURLをGithubのWebhookの設定の Payload URL に設定します。
不正リクエストの判別
これまで、どうしてGithub Serviceを使っていたのかというと、手軽だったということに尽きます。
いくらRSpecを実行するだけとはいえノーチェックのAPIエンドポイントをパブリックなインターネットに晒すのは避けたいなぁと思うと、簡易的にでも認証処理をしたり不正なリクエストを判別できないといけません。
Github Serviceだと、設定するだけでIAMによる認証付きでSNSのトピックにパブリッシュできたのです。
しかし、WebhookではIAMによる認証はできません。
そこで、WebhookのSecretを利用して、不正なリクエストを判別しています。
Github Webhookの設定画面で任意項目としてSecretという項目があります。
この Secret に適当な文字列を入力しておくとその文字列をキーとしてリクエストボディのHMAC値を計算してX-Hub-Signature
ヘッダの値に入れてリクエストしてくれます。
リクエストを受けた際に、Lambda内部で再度リクエストボディのHMACを計算してX-Hub-Signature
ヘッダの値と比較します。
するとリクエストの改ざんを検知できますし、Secretの値を知っているということで送信元の認証にもなります。
具体的には次のようなコードでチェックします。
/** * @param {String} secret Secretの値 * @param {Object} body Webhookからのリクエストボディ */ function signRequestBody(secret, body) { return `sha1=${crypto.createHmac('sha1', secret).update(body, 'utf-8').digest('hex')}`; } /** * Github webhoookのリクエストが正しいことをチェックする。 * X-Hub-Signature と リクエストホディのハッシュが一致することを確認する * @param {Event} event Lambdaのイベントオブジェクト * @return 一致するとき true。それ以外はfalse */ function isValidRequest(event) { const signature = event && event.headers && event.headers['X-Hub-Signature']; const secret = process.env.GITHUB_WEBHOOK_SECRET; const signedBody = signRequestBody(secret, event.body) return signature === signedBody }
上記の isValidRequest
関数を最初に実行し、true
が帰ってきたときのみ処理するようにしました。
ということで、プルリクにpushすればテストが回る状況が回復しました3。
参考
最後に
アクトインディではエンジニアを募集しています。