SlackのGitHub連携をやめGitHub->AmazonSNS->Lambda->Slackで連携する

  • 97
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Slackには元から、GitHub連携が用意されている。 しかし、あまり良い物ではないと思う。

  • GitHub の Issue Comment のほうで Mention しても、Slackのほうでは通知されない。もちろん、GitHub側の通知では出ているのだが、メールを見ない人もいて気づかない場合がある。
  • GitHubのアカウント名とSlackのアカウント名が違う。例えば、私の場合GitHubではkawaharaを利用しているが、Slackの場合は@ooharabucyouを使っている。なんとややこしい!これは、自分だけでなく他の人も言える。
  • メッセージ周りを自分好みにカスタマイズしたい。

そんなこんなで、SlackのAppDirectoryにある、GitHub連携をいっそこのことやめて、自分で連携する道(なるべく最短でいきたい)を選んだ。

AmazonSNS -> Lambda -> Slack

そこで取り出したるはAWS。AmazonSNSとLambdaを組み合わることにより、サーバ無しでコードを実行させることができる。幸いなことに、GitHubは最初からAmazonSNSとの連携をサポートしている。

Slack設定

Slack側で、Incoming WebHooks 設定をしておく。

https://slack.com/apps/A0F7XDUAZ-incoming-webhooks

設定は適当に。

スクリーンショット 2016-01-27 15.28.32.png

AmazonSNS設定

SNSのTopicを作成。

スクリーンショット 2016-01-27 15.31.12.png

スクリーンショット 2016-01-27 15.31.23.png

スクリーンショット 2016-01-27 15.31.42.png

スクリーンショット 2016-01-27 15.31.56.png

Lambdaとの連携はあとでできるので、とりあえずここまで作成して終わり。ここで作成した、TopicARNが後々必要になる。

Lambdaコード

(追記 2016-01-30)
https://github.com/kawahara/github2slack-lambda
GitHub にコードを追加しました。デプロイ用のgulpタスクも用意しております
(追記終わり)

ローカルに適当なフォルダを作って、その中に index.js を作成。今のところ、4つのEventにのみ対応させているが、以下のコードを書き換えるだけで、いろいろ対応は可能。EventTypeに関しては、SNS Message ではなく、MessageAttributes のほうに乗るので注意が必要。
結構適当に書いたので、Issueをアサイン者・ラベル付きで開いた時に、opended, labeled, assigned の3回メッセージ投稿されてしまう挙動になっている。後で治す予定。 (1/30 修正済み)

index.js
/* jshint: indent:2 */
var request = require('request'),
    config  = require('./config.json');

var convertName = function (body) {
  return body.replace(/@[a-zA-Z0-9_\-]+/g, function (m) {
    return config.account_map[m] || m;
  });
};

var link = function (url, text) {
  return '<' + url + '|' + text + '>';
};

exports.handler = function (event, context) {
  console.log('Received GitHub event: ' + event.Records[0].Sns.Message);
  var msg = JSON.parse(event.Records[0].Sns.Message);
  var eventName = event.Records[0].Sns.MessageAttributes['X-Github-Event'].Value;
  var text = '';

  switch (eventName) {
    case 'issue_comment':
    case 'pull_request_review_comment':
      var comment = msg.comment;
      text += comment.user.login + ": \n";
      text += convertName(comment.body) + "\n";
      text += comment.html_url;
      break;
    case 'issues':
      var issue = msg.issue;
      if (msg.action == 'opended' || msg.action == 'closed') {
          text += 'Issue ' + msg.action + "\n";
          text += link(issue.html_url, issue.title);
      }
      break;
    case 'push':
      text += 'Pushed' + "\n";
      text += msg.compare + "\n";
      for (var i = 0; i < msg.commits.length; i++) {
        var commit = msg.commits[i];
        text += link(commit.url, commit.id.substr(0, 8)) + ' ' + commit.message + ' - ' + commit.author.name + "\n";
      }
      break;
    case 'pull_request':
      var pull_request = msg.pull_request;
      if (msg.action == 'opended' || msg.action == 'closed') {
          text += 'Pull Request ' + msg.action + "\n";
          text += pull_request.title + "\n";
          text += pull_request.body + "\n";
          text += pull_request.html_url;
      }
      break;
  }

  if (!text) {
    context.done();
    return;
  }

  request({
    url: config.slack_web_hook_url,
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    json: {text: text, link_names: 1}
  }, function () {
    context.done();
  });
};

うっかり、クセで request を使ってしまったので (別に使わないでhttpを使う手もある), 以下のコマンドを叩き、依存解決をする

npm init
npm install --save request

設定ファイルは、以下のような感じで作るとよろし。WebHookURLと、GitHubアカウントとSlackアカウントのマップを記す。

config.json
{
  "slack_web_hook_url": "https://hooks.slack.com/services/dummy",
  "account_map": {
    "@kawahara": "@ooharabucyou",
    "@shotaatago": "@shota-a"
  }
}

以下でパッケージ化して、github-bot.zip をアップロードする。 この辺は、http://dev.classmethod.jp/cloud/aws/how-to-deploy-a-lambda-function-with-gulp/ とか使えば楽そうね。 (1/30 対応しました。GitHubリポジトリにあるほうでは、以下のコマンドではなく gulp deploy で作成・更新ができます。)

zip -r github-bot.zip index.js config.json node_modules/

スクリーンショット 2016-01-27 18.42.00.png

テスト時には、以下のデータを使った。SNSのテストデータのテンプレートを元に、API Document にあるPayloadを送るような挙動になっている。

{
  "Records": [
    {
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:EXAMPLE",
      "EventSource": "aws:sns",
      "Sns": {
        "SignatureVersion": "1",
        "Timestamp": "1970-01-01T00:00:00.000Z",
        "Signature": "EXAMPLE",
        "SigningCertUrl": "EXAMPLE",
        "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
        "Message": "{\"action\":\"created\",\"issue\":{\"url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2\",\"labels_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/labels{/name}\",\"comments_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/comments\",\"events_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/events\",\"html_url\":\"https://github.com/baxterthehacker/public-repo/issues/2\",\"id\":73464126,\"number\":2,\"title\":\"Spelling error in the README file\",\"user\":{\"login\":\"baxterthehacker\",\"id\":6752317,\"avatar_url\":\"https://avatars.githubusercontent.com/u/6752317?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/baxterthehacker\",\"html_url\":\"https://github.com/baxterthehacker\",\"followers_url\":\"https://api.github.com/users/baxterthehacker/followers\",\"following_url\":\"https://api.github.com/users/baxterthehacker/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/baxterthehacker/subscriptions\",\"organizations_url\":\"https://api.github.com/users/baxterthehacker/orgs\",\"repos_url\":\"https://api.github.com/users/baxterthehacker/repos\",\"events_url\":\"https://api.github.com/users/baxterthehacker/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/baxterthehacker/received_events\",\"type\":\"User\",\"site_admin\":false},\"labels\":[{\"url\":\"https://api.github.com/repos/baxterthehacker/public-repo/labels/bug\",\"name\":\"bug\",\"color\":\"fc2929\"}],\"state\":\"open\",\"locked\":false,\"assignee\":null,\"milestone\":null,\"comments\":1,\"created_at\":\"2015-05-05T23:40:28Z\",\"updated_at\":\"2015-05-05T23:40:28Z\",\"closed_at\":null,\"body\":\"It looks like you accidently spelled 'commit' with two 't's.\"},\"comment\":{\"url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/comments/99262140\",\"html_url\":\"https://github.com/baxterthehacker/public-repo/issues/2#issuecomment-99262140\",\"issue_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2\",\"id\":99262140,\"user\":{\"login\":\"baxterthehacker\",\"id\":6752317,\"avatar_url\":\"https://avatars.githubusercontent.com/u/6752317?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/baxterthehacker\",\"html_url\":\"https://github.com/baxterthehacker\",\"followers_url\":\"https://api.github.com/users/baxterthehacker/followers\",\"following_url\":\"https://api.github.com/users/baxterthehacker/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/baxterthehacker/subscriptions\",\"organizations_url\":\"https://api.github.com/users/baxterthehacker/orgs\",\"repos_url\":\"https://api.github.com/users/baxterthehacker/repos\",\"events_url\":\"https://api.github.com/users/baxterthehacker/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/baxterthehacker/received_events\",\"type\":\"User\",\"site_admin\":false},\"created_at\":\"2015-05-05T23:40:28Z\",\"updated_at\":\"2015-05-05T23:40:28Z\",\"body\":\"You are totally right! I'll get this fixed right away.\"},\"repository\":{\"id\":35129377,\"name\":\"public-repo\",\"full_name\":\"baxterthehacker/public-repo\",\"owner\":{\"login\":\"baxterthehacker\",\"id\":6752317,\"avatar_url\":\"https://avatars.githubusercontent.com/u/6752317?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/baxterthehacker\",\"html_url\":\"https://github.com/baxterthehacker\",\"followers_url\":\"https://api.github.com/users/baxterthehacker/followers\",\"following_url\":\"https://api.github.com/users/baxterthehacker/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/baxterthehacker/subscriptions\",\"organizations_url\":\"https://api.github.com/users/baxterthehacker/orgs\",\"repos_url\":\"https://api.github.com/users/baxterthehacker/repos\",\"events_url\":\"https://api.github.com/users/baxterthehacker/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/baxterthehacker/received_events\",\"type\":\"User\",\"site_admin\":false},\"private\":false,\"html_url\":\"https://github.com/baxterthehacker/public-repo\",\"description\":\"\",\"fork\":false,\"url\":\"https://api.github.com/repos/baxterthehacker/public-repo\",\"forks_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/forks\",\"keys_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}\",\"collaborators_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}\",\"teams_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/teams\",\"hooks_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/hooks\",\"issue_events_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}\",\"events_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/events\",\"assignees_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}\",\"branches_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}\",\"tags_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/tags\",\"blobs_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}\",\"trees_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}\",\"statuses_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}\",\"languages_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/languages\",\"stargazers_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/stargazers\",\"contributors_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/contributors\",\"subscribers_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/subscribers\",\"subscription_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/subscription\",\"commits_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}\",\"git_commits_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}\",\"comments_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}\",\"issue_comment_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}\",\"contents_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}\",\"compare_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}\",\"merges_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/merges\",\"archive_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}\",\"downloads_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/downloads\",\"issues_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}\",\"pulls_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}\",\"milestones_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}\",\"labels_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}\",\"releases_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}\",\"created_at\":\"2015-05-05T23:40:12Z\",\"updated_at\":\"2015-05-05T23:40:12Z\",\"pushed_at\":\"2015-05-05T23:40:27Z\",\"git_url\":\"git://github.com/baxterthehacker/public-repo.git\",\"ssh_url\":\"git@github.com:baxterthehacker/public-repo.git\",\"clone_url\":\"https://github.com/baxterthehacker/public-repo.git\",\"svn_url\":\"https://github.com/baxterthehacker/public-repo\",\"homepage\":null,\"size\":0,\"stargazers_count\":0,\"watchers_count\":0,\"language\":null,\"has_issues\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\":true,\"forks_count\":0,\"mirror_url\":null,\"open_issues_count\":2,\"forks\":0,\"open_issues\":2,\"watchers\":0,\"default_branch\":\"master\"},\"sender\":{\"login\":\"baxterthehacker\",\"id\":6752317,\"avatar_url\":\"https://avatars.githubusercontent.com/u/6752317?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/baxterthehacker\",\"html_url\":\"https://github.com/baxterthehacker\",\"followers_url\":\"https://api.github.com/users/baxterthehacker/followers\",\"following_url\":\"https://api.github.com/users/baxterthehacker/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/baxterthehacker/subscriptions\",\"organizations_url\":\"https://api.github.com/users/baxterthehacker/orgs\",\"repos_url\":\"https://api.github.com/users/baxterthehacker/repos\",\"events_url\":\"https://api.github.com/users/baxterthehacker/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/baxterthehacker/received_events\",\"type\":\"User\",\"site_admin\":false}}",
        "MessageAttributes": {
          "X-Github-Event": {
            "Type": "String",
            "Value": "issue_comment"
          }
        },
        "Type": "Notification",
        "UnsubscribeUrl": "EXAMPLE",
        "TopicArn": "arn:aws:sns:EXAMPLE",
        "Subject": "TestInvoke"
      }
    }
  ]
}

テストに成功すると、Slack側に以下の様なメッセージが投稿される。

スクリーンショット 2016-01-27 18.47.14.png

次にAmazonSNSによってLambdaが実行されるようにするために設定。

Lambda側の設定から Event source -> Add event source からトリガを作ることができる。

スクリーンショット 2016-01-27 18.44.27.png

topic名は、先ほど作成したものを設定。なんて楽ちんなんだ。

スクリーンショット 2016-01-27 18.44.38.png

IAMアカウント追加

GitHubから使うためのIAMアカウントを追加する。権限は、最低限のもので以下。

Resource には、AmazonSNSのARNを適用するのを忘れずに。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "sns:Publish"
      ],
      "Sid": "Stmt0000000000000",
      "Resource": [
        "arn:aws:sns:ap-northeast-1:xxxxxxx:github2slack"
      ],
      "Effect": "Allow"
    }
  ]
}

GitHub側設定

最後にGitHubからの通知を、AmazonSNSに転送するための設定を行う。対象のリポジトリの設定画面から、Webhooks & Service を選び、Add Services から AmazonSNSを選択する。

スクリーンショット 2016-01-27 18.51.21.png

先ほど作成したbot用IAMアカウント、SNS topic (ARNを指定)、リージョン、アカウントのSecretを登録する。

スクリーンショット 2016-01-27 18.51.56.png

上記を設定しても、まだ push イベントしか受け取れない問題がある。そこで、GitHubのAPIを直接コールして、issues, issue_comment, pull_request_review_comment のイベントも送るような設定を送る。

設定画面URLから hook IDが取得できるので、それを元にAPIをコール。(アクセストークンは、https://help.github.com/articles/creating-an-access-token-for-command-line-use/ を参考に発行できる。scope は organization の場合、 read:org, repo の2つが必要)

スクリーンショット 2016-01-27 19.03.37.png

以下の YOUR_TOKEN, OWNER, REPOSITORY_NAME, HOOK_ID は置き換える。

curl -X PATCH -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" -d '{
    "active": true,
    "events": ["push", "issues", "issue_comment", "pull_request", "pull_request_review_comment"]
}' 'https://api.github.com/repos/OWNER/REPOSITORY_NAME/hooks/HOOK_ID'

完成

これで、@kawahara がGitHub上で呼びかけられたときに Slack上で @ooharabucyou に変換して呼んでくれるbotが出来上がった。
上が、デフォルトのGitHub連携、下が今回作ったものにより作られたメッセージ。

スクリーンショット 2016-01-27 18.56.29.png

まとめ

  • botづくりに関してはHubotもいいけど、サーバ用意しない&メンテしたくないならLambdaを使えば楽そう。API Gateway + Lambda + Slack Outgoing WebHook で何らかのコメントに応答するものを作ったり、CloudWatch Scheduler + Lambda で定期的に何か調査して Slack に通知したりと、サーバレスでいろいろできることがあり良い。
  • GitHub側のAmazonSNS設定は、Event設定をAPIから直接やることを忘れない。これで私の1時間くらいがもってかれ、一時期はAPIGateway経由でWebhook受けようかと迷ったほどであった。

【追記】料金について

こちらを適用して、1ヶ月間様子を見たところ無料でした。利用頻度としてはこんな感じ。

  • SNSへの通知 = GitHubの活動量: 3,000回くらい
    • 1,000,000回まで無料
    • Lambdaへの通知は無料
  • Lambda の実行時間: 100秒くらい
    • 1,000,000回実行まで無料
    • 400,000秒まで無料 (厳密には1GBのメモリを400,000秒専有という感じで計算する。今のところ、128MBのメモリ設定で元気いっぱい動いている)

ということで、2016年3月現在あと300倍稼働しても無料だと思われます。社内で使う分には全然無料で行けそう。

参考にした記事