グリーを支える通知システム
はじめに
このエントリは GREE Advent Calendar 2014 24日目の記事です。
こんにちは、インフラストラクチャ本部の高野(@takano32)です。
いつも社内では GitHub:Enterprise の運用、 デプロイの改善、 大規模なインフラを操作するためのツール作成、 レガシーなサーバのセキュリティ対策、 コミュニケーションツール向けシステムの構築・運用、 などの仕事をしています。節操がありませんね。はい。
そのうち、今回は「コミュニケーションツール向けシステムの構築・運用」のうち「グリーを支える通知システム」という題目について書きたいと思います。
グリーとリアルタイムコミュニケーションツール
まず、通知システムについてお話する前に、グリーでどのようなリアルタイムコミュニケーションツールが利用されてきたかを簡単に説明したいと思います。
リアルタイムコミュニケーションツールというのは、わかりやすいところでいうとチャットやメッセンジャーなどとして分類されるものとします。
歴史と概要をおさらいしてみると以下のようになっています。
- Skype 時代
- IRC 時代
- ChatWork 時代
適当に時期を名付けてみました。それぞれの時代について、簡単に説明します。
Skype 時代
Skype はとにかく、導入から基本的なコミュニケーションまでは使いやすいツールです。
グリーも Skype を最初のコミュニケーションツールに選びました。
利用者視点では、とくにオンラインではないときの会話のログが、次のオンライン時に読み込まれて話の流れが追える、という仕組みが革新的でした。
また、在籍しているエンジニアの多くも Skype 向けのボットを作ったりしていました。
弊社 CTO の藤本が PHP の DBus 拡張を作成し、Skype のボットを作っていたりもします。
ただ、Skype というツールは組織として考えると、メリットだけではありませんでした。Skype は使いやすいというのはあるのですが、逆に統制をとるという意味では組織に向かないツールになっていました。組織がチャットルームを管理して、発言などをエビデンスとして残す、といったことを考えたときに、定点観測をさせるために常にオンラインなクライアントが必要となり、やりにくいです。それに、 Skype のネットワークの詳細は公開されておらず、これを実現したとしてもエビデンスとして十分なのかわかりません。
ほか、当時よくありがちだったのは、チャットルームを作った管理者が違う部門に異動などで不在になったときに、移動元の部門の管轄にあるチャットルームから退出してしまい、チャットルームの管理者がいなくなってしまう問題がありました。
管理者がいなくなってしまったチャットはメンバーを強制退出させることなどができなくなってしまうため、新しくチャットルームを作りなおして、ほぼ同じメンバーを新しいチャットルームに入れて、既存のチャットルームを廃止するというオペレーションなどがよく行われていました。不便です。
このような機能的な問題や、 Skype はネットワークのトラフィックの挙動が完全に把握できないなどの理由からコミュニケーションツールを移行するモチベーションが組織的に高まっていきました。
IRC 時代
そんなモチベーションの高まりと同時期、ある日、唐突に Skype ではコミュニケーションを続けていくことが難しい壁にぶつかりました。
グリーの社員やエンジニアも増えていく中、参加しているメンバー数が最大のチャットルームで「新しいメンバーを追加できない」という現象が起こったのです。これは Skype のチャットルームの仕様で、チャットルームには300アカウントまでが同時に参加できるのですが、それを越えるアカウントはチャットルームに追加できないのです。
これをきっかけに、300以上のメンバーが会話できる、入退室が社員であることを保証できる、などの理由から社内に IRC サーバを構築し、 IRC を利用することになりました。IRC 古くからあるプロトコルによって実現されていますし、エンジニアにも馴染みのあるツールで、ボットも多く作成されました。
組織的にもチャンネルのオペレータ権限の配布を管理できたり、LDAP を利用することでニックネームと社員の対応づけを明確なものとし、ログインパスワードを要求することでセキュアな環境を作れるといった利点がありました。
そういった利便性と同時に IRC は「ログインしていないときのログをみるのが大変」「社内のサーバを利用しているので、出先で緊急の連絡の送受信などができない」といった問題もありました。
ChatWork 時代
IRC の問題を考えた上で、さまざまな検討の後にエンジニア、非エンジニア職問わず、全社的な導入に至ったのが ChatWork です。
現在、 KDDI ChatWork を利用しています。権限の管理などを細かく設定できますので、組織的な異動にともなうチャットルームからのメンバーの出入りなどを統括して管理できています。
ChatWork のようなデータをサーバサイドに蓄積しており、クライアントで必要なときに取り出すというシステムであれば、ログインしていないときのログをさかのぼることもできますし、エビデンスとしてログを集めたいときにも過去にさかのぼって集めることができ、安心といえます。
グリーのアラート通知システム
コミュニケーションツールの変遷たどってみました。
次はその変遷にあわせて通知の方法も変化していく様子などを紹介します。
通知の例としてアラート通知システムについてご紹介します。
アラートはどこからくるの?
グリーでは大規模なインフラシステムの監視に AWACS という自社製のシステムを利用しています。
下記に AWACS による Passive 監視の図を抜粋します。
上図の Critical Sender というモジュールがアラートを受信したいひとたちにアラートを配信する役割を担っています。
Critical Sender がアラートを受信したい方たちのメールアドレスに向けてメールを配信しています。
Critical Sender
Critical Senderは、Summarizerによって宛先が決定され、集約されたアラートを、ひたすらメールで送信します。
アラートはメールで配信するだけなの?
上記の説明では登場していませんが、メールでの受信のほか、リアルタイム性のあるコミュニケーションツールでのアラートの受信にも需要があったため、 ひたすらアラートの情報をしゃべり続ける Skype のボットという形の Critical Sender も存在していました。
通知はアラートだけなの?
業務を円滑に進めるためにはアラートのような情報のみではなく、さまざまな情報の通知を適切に行う必要があります。
ところが、たいていの既存のシステムはメールによる通知はサポートしているものの、他の手段をサポートしていないことが多いです。筆者が知る限りではデフォルトで Skype への通知をサポートしている、といったシステムはないと思います。
WebHook などモダンな通知方法が世の中で使われていたとしても、それをコミュニケーションツールにつなげることができなければ、利用できません。
また、独自にちょっとしたツールなどを作成したときに通知のために、 Skype ボットを操作して発言させるということを都度行っているのでは効率が悪すぎます。
グリーの通知システム
アラート通知が Skype ボットで行われていたとき、次のような状況がありました。
- さまざまなシステムが通知に対応しているのはメール
- 通知はメールとは別にリアルタイム性のあるコミュニケーションツールでも受信したい
要望としてまとめると「メールを発信するとリアルタイム性のあるコミュニケーションツールに配信するシステムがあるとよい」といったところでしょう。
この要望を満たすシステムを筆者が開発しました。
- メールによる通知をリアルタイム性のある方法で受信するためのシステム
- 当時の主要なコミュニケーションツールが Skype であった
上記の背景から、このシステムは Skype と Mail を組み合わせた造語である Skail という名前になりました。読み方ですが、気持ち Skype の方を意識しているときは「すかいる」で、メールの方を意識しているときは「すけいる」と読んでみるといいと思います。特に呼び方をドキュメントに書いた記憶がないので、読み方について語るのは、はじめてですね。 Skail の読み方がわからなかった方々、すみませんでした。
Skail
Skype のチャットルームにメールでメッセージをお届けするので Skail という名前まではよいものの、どのように実現するのか、さまざまな案がありました。
主要なところだと 'X-Skype-Conversation' といったメールヘッダをもたせ、このメールヘッダに Skype チャットルームの識別子を入れたメールを送信すると、チャットに参加しているボットがメールの内容を発言する、というものとかありました。
この方法のよくないところは、 'X-Skype-Conversation' というヘッダを入れ込む、ということに既存のシステムが対応しているかを考えると、メールヘッダの指定などに対応しているシステムは希ということです。
結局のところ、便利に使えるシステムにするためには、普通のメール通知になんらかの方法で Skype チャットルームの識別子をふくませる、ということを満たさなければなりません。
そのために、採用した方法が拡張メールアドレスに識別子を入れるという方法です。
拡張メールアドレス
拡張メールアドレスとは、Gmail のメールアドレスなどで使える「配送先は同じなんだけれど、複数のアドレスを保持しているようにみえる」ような機能です。
たとえば、 takano32@gmail.com というメールアドレスがあったときに、これは、わたしの Gmail アカウントに配送されるわけですが、 takano32+gree@gmail.com というアドレスを配送先に指定しても takano32@gmail.com に配送されるという機能です。
この機能、 Postfix でも使えたりします。
ちょっと解説するの面倒だな、と思ったら、ちょうど良い記事がありましたよ。
この拡張メールアドレスの recipient_delimiter
以降の部分を Skype チャットを識別するための名前部分として使うことにました。
概要図
都合、細かい部分を省くとこんな感じになっています。
システムのデータ構造
どのような方法でごく普通のメールを Skype のチャットルームに配送するか説明したところで、実際にそのデータがどのように管理されているかを説明します。
コア部分で利用しているデータ構造は以下のようになります。
データ構造で説明しているデータは実際には運用していないデータです。
1 2 3 4 5 6 7 8 9 10 11 |
--- - id: 1 name: jira.notify identity: ! '#mitsuhiro.takano/$17961a235ac6547a' keyword: ! '(alert|SKAIL|TEST|JIRA)' filters: 'JIRA::Notify::RequestIssue' - id: 2 name: gist identity: ! '#mitsuhiro.takano/$4b72e41a9074b10' keyword: ! '.*' filters: '-Subject::AddStar Gist' |
それぞれについて説明します。
id
YAML 形式で書かれているのですが、実は DataMapper の dm-yaml-adapter を使っているだけで、 O/R Mapper から取り出すことができます。
この id
という値はデータベースの主キーです。
name
先ほど説明した拡張メールアドレスの部分に指定するアドレスの部分文字列です。
identity
Skype チャットルームに対して内部的に割り当てられる識別子です。Skype クライアントから /get name
というコマンドを発行することで取得することができます。
Skype クライアントとしては name
なわけですが、システムとしてはチャットルームの id
という意味の名前が使いたかったのです。しかし、 id
は主キーなので identity
という名前になっています。
いま思うと、 room_id
などが正しい名前だったかもしれません。そのうちリファクタリングするかもしれないですね。
keyword
keyword
にはメールアドレスがシステムの関係者ではないところに漏れたときを考慮されたデータです。
メールのサブジェクトが keyword
に指定された正規表現と一致しないときには、届いたメールなどなかったフリをして、システムは何もしません。
スパム避けですね。
filters
Skail のシステムには Filter という概念が存在します。
あらかじめ記述したフィルタをどのように適用していくか、で通知するメッセージを変更したり、メールの内容をみて特定の条件にマッチしなければチャットルームに通知しない、などの機能を実装することができます。
配送の例
example.com
ドメインで Postfix を運用したときに、データ構造として例示した name
にしたがうと、以下のような拡張アドレスがふくまれているか、サーバで調べます。
- skail+jira.notify@example.com
- skail+gist@example.com
メールが配送されたときにメールをプログラムで読み込んで、サーバで処理するためには procmail(1)
を利用したり、 ~/.forward
に記述を行うなどの方法があります。
Skail では procmail(1)
を使っています。
プログラムがメールを読んだときに、例としてあげたデータ構造によって、どのような挙動となるのか説明します。
例. メールアドレス skail+jira.notify@example.com への配送
データ構造は以下のようになっています。
1 2 3 4 5 |
- id: 1 name: jira.notify identity: ! '#mitsuhiro.takano/$17961a235ac6547a' keyword: ! '(alert|SKAIL|TEST|JIRA)' filters: 'JIRA::Notify::RequestIssue' |
メールを読み込んだプログラムの挙動は下記の通りです。
skail+jira.notify@example.com
というメールアドレスからjira.notify
という部分を取り出す- データベースの中に
name
としてjira.notify
がふくまれているレコードがないか調べる - 該当するレコードがある場合はメールのサブジェクトが
keyword
にある正規表現に一致するか確認する - メールの本文を
filters
に書かれているフィルタを通過させる - フィルタを通過させたデータを
identity
を内部の識別子として保持している Skype チャットに送信する
例. JIRA::Notify::RequestIssue フィルタ
JIRA::Notify::RequestIssue フィルタは以下のように書かれています。
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 |
# -*- coding: utf-8 -*- # vim: set noet sts=4 sw=4 ts=4 : # # GREE::Skail::Filter::JIRA::Notify::RequestIssue # author: takano32 # require_relative '../Notify' module GREE::Skail::Filter::JIRA::Notify::RequestIssue include GREE::Skail::Filter::JIRA::Notify def self.filter(data) raise GREE::Skail::NotTransferIssueTypeException if discard? data return GREE::Skail::Filter::JIRA::Notify.filter data end def self.discard?(data) discard = true data.each_line do |line| if line.strip =~ %r!^Issue Type: .*! then discard = false if line.include? 'Request' end end return discard end end |
これは JIRA からの通知メール向けのフィルタで、 Issue Type
が Request
のもの以外の通知はなかったことにしてしまうフィルタです。
例. メールアドレス skail+gist@example.com への配送
データ構造は以下のようになっています。
1 2 3 4 5 |
- id: 2 name: gist identity: ! '#mitsuhiro.takano/$4b72e41a9074b10' keyword: ! '.*' filters: '-Subject::AddStar Gist' |
ふたつめの例なので簡単に説明します。
name
がjira.notify
からgist
になっている- データベースを検索するときに
name
にgist
と記録されているレコードを探す identity
が異なるので違うチャットルームに配信されるkeyword
が/.*/
の表現となるので、スパム避けの機能は停止されているfilters
からSubject::Addstar
フィルタが除外されているfilters
でGist
フィルタを利用している
例. Subject::AddStar フィルタ
Subject::AddStar フィルタは以下のように書かれています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# -*- coding: utf-8 -*- # vim: set noet sts=4 sw=4 ts=4 : # # GREE::Skail::Filter::Subject::AddStar # author: takano32 # require_relative '../Subject' module GREE::Skail::Filter::Subject::AddStar include GREE::Skail::Filter::Subject def self.filter(data) data = "(*) #{data}" return data end end |
Skype チャット向けにサブジェクトの行に (*)
で星をつけて、サブジェクトをわかりやすくするというフィルタですね。
このような基本的かつ、規定で適用すると利便性が高いフィルタについては「特別な記述がなければ、常に適用」というルールがあり、暗黙に適用されます。
後述の Gist フィルタと組み合わせて使う場合には不要なので -Subject::AddStar
という記述をすることで、明示的にこのフィルタの処理を除去しています。
例. Gist フィルタ
Gist フィルタは以下のように書かれています。
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 36 37 38 39 40 41 42 43 44 45 46 47 |
# -*- coding: utf-8 -*- # vim: set noet sts=4 sw=4 ts=4 : # # GREE::Skail::Filter::Gist # author: takano32 # require_relative '../filter' require 'openssl' OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE require 'rubygems' require 'pit' require 'octokit/enterprise' module GREE::Skail::Filter::Gist include GREE::Skail::Filter def self.filter(data) # token config = Pit.get 'github', require: { login: 'your github login name', oauth_token: 'your github oauth token', } options = { login: config[:login], cliend_id: 'Skail', oauth_token: config[:oauth_token], } Octokit::Enterprise.configure do |c| c.hostname = 'github.enterprise.host.name' end client = Octokit::Enterprise.new options request = { description: 'Skail pastes received mail.', public: true, files: { body: {content: data}} } response = client.create_gist request return 'Gist: ' + response.html_url end end |
これは内容が長い通知に対して使うフィルタで GitHub:Enterprise の gist に貼り付けて、その URL を通知するというプラグインだったんですね。
GitHub:Enterprise のホスト名部分については実際のコードと異なります。
Skype 時代以降の変遷
IRC 時代
Skail の姉妹プロダクトとして Skybridge という IRC と Skype をブリッジするプログラムを作成しました。つなげたい Skype チャットルームの識別子と IRC のチャンネル名を対にして登録することで相互の内容をやり取りする、というものになります。
Skype チャットルームに配信したアラートを IRC のチャンネルに流すということをしています。
IRC でもアラートの内容が即時に閲覧できるようになっています。
また、少し手を入れて IRC から Skype ボットを操作したり、その逆を行っていました。
ChatWork 時代
CircleWork という名前で IRC と ChatWork のブリッジを行うものを作ったのですが、ちょっと安定した運用を保証できる状態ではなかったので、ブリッジによる受け渡しで通知するという方法はいったん考えないことにしました。
ごく普通に Skype チャットとはことなる扱いにしてしまいました。
1 2 3 4 5 6 7 8 9 10 11 12 |
if conv.is_a? GREE::Skype::Conversation then # XML RPC require 'xmlrpc/client' daemon = XMLRPC::Client.new3(host: 'xml.rpc.host', port: xxxx) daemon.call(:send_message, conv[:identity], payload) logger.info "Send to Skype Conversation: #{skail.mail.subject.to_s}" end if conv.is_a? GREE::ChatWork::Conversation then require 'gree/skail/filter/Send/ChatWork/Takochan' GREE::Skail::Filter::Send::ChatWork::Takochan.filter(conv[:identity], payload) logger.info "Send to ChatWork Conversation: #{skail.mail.subject.to_s}" end |
Skype チャットの場合は XML-RPC で Skype クライアントが起動しているホストと通信して、そのホストから Skype API を利用して発信されます。
ChatWork の場合は Takochan
フィルタが使われています。
XML-RPC の部分もフィルタにすればいいのに、という気がしたのでそのうちやります。
例. Takochan フィルタ
Takochan
フィルタは以下のように書かれています。
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 |
# -*- coding: utf-8 -*- # vim: set noet sts=4 sw=4 ts=4 : # # GREE::Skail::Filter::Send::ChatWork::Takochan # author: takano32 # require_relative '../ChatWork' module GREE::Skail::Filter::Send::ChatWork::Takochan include GREE::Skail::Filter::Send::ChatWork def self.filter(id, data) require 'nkf' require 'uri' require 'net/http' text = NKF.nkf('-w', data) takochan = URI.parse 'http://localhost:4979/send_chat' Net::HTTP.start takochan.host, takochan.port do |http| request = Net::HTTP::Post.new takochan.path params = {rid: id, text: text} request.set_form_data(params, "&") response = http.request request unless response.code == 200.to_s then raise GREE::Skail::Exception.new 'Cant send text to ChatWork via takochan' end end return data end end |
takochan というのは、これもまた自分が作ったものなのですが、Perl の IRC 向け通知サーバの App::Ikachan をリスペクトして名前をつけた ChatWork 向け通知サーバです。ちなみに、 ikachan のソースコードは参考にしていないので、ライセンスは GPL 汚染されていないです。takochan は比較的スリムなので、オープンソース化とかするとしても、そんなに自社に依存したコードはないと思うので、機会があれば公開するかもしれないです。
参考: ikachan: http://blog.yappo.jp/yappo/archives/000760.html
おまけ的な機能
DataMapper + dm-yaml-adapter という組み合わせでの運用なので、ついでにウェブでデータ構造を管理できるインターフェイスも搭載されています。
まとめ
メールはさまざまな仕様があるので、システムを運用していくことを考慮に入れた設計にしたのはよかったことだと思います。
たとえば、フィルタの機構は意外なところでさまざまな拡張に余裕のある対応ができる仕組みになりました。他のリアルタイムコミュニケーションツールへの投稿などはもちろん、テキストパートをふくまない HTML メールをスクレイピングしてテキスト部分を取り出すといったフィルタなども作成でき、システムが対応できる幅が広くなりました。
あとは、時流に依存した名前を基幹となりうるシステムにつけると、うっかり「支える技術」ほどのシステムとなってしまった時期くらいには、システムの名前と機能が剥離してよくないのでやめましょう。
Skail は ChatWork 時代のいまでは ChatWork への通知がメインの機能となりつつありますが、名前は Skype + Mail の Skail となっております。修正するのも大変そうなので、よい思い出としておきます。
最後にリアルタイムコミュニケーションツールと本日登場してきたツールなどがどのような関係にあるかまとめた図を作ってみました。
反響とかあれば、もう少し詳しい話とかするかもしれないです。
明日の記事は、グリーの神こと、CTO @masaki_fujimoto でこのカレンダーも大トリを迎えます。
というか、今日ってクリスマスイブなんですね。リア充のみなさまにおかれましては爆発にご注意ください。
冗談ですからね。はい。