Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

2018年11月追記: GitHub の Amazon SNS 通知機能は廃止されます。https://developer.github.com/changes/2018-04-25-github-services-deprecation/ したがって、新たに設定する場合は Webhook を利用した実装を行う必要があります。

--

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倍稼働しても無料だと思われます。社内で使う分には全然無料で行けそう。

参考にした記事

ooharabucyou
地球に住んでいます。
http://www.bucyou.net
codeal
即戦力複業ならコデアル。高収入×リモートワークの複業・フリーランス・転職求人多数
https://www.codeal.work
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした