こんにちは、技研の「むらたん」です。

 

ChatOpsやってますか?「乗るしかない このビッグウェーブに」ということで、今期の開発技術SIGでも「定形作業の自動化 powered by ChatOps」をテーマとしています。

ChatOpsするなら、

ChatツールとしてSlackを導入して、HubotでBotスクリプトを開発して、「PING」って書き込むと「PONG」って返ってきたり、「PUG」って書き込むと、可愛いワンちゃんの画像を探してくれるんです。

みたいな内容はよく見かけますが、エンタープライズ利用を考えると、そう甘くはありません。

弊社の現状における課題と対策

「Slack」のエンタープライズ利用

  • GmailやOffice365を利用しているのに、これ以上、コミュニケーションインフラを増やすことはできません。
  • 今回はOffice365に含まれる「Yammer」でやってみることにしました。

「Hubot」でBotスクリプトを開発

  • Hubotって、Javascriptを便利にしたCoffeeScriptで開発する必要がありますが、そんな言語を書いたことはありません。
  • HubotのRuby版であるLitaなどの利用も考えましたが、Yammerとの接続アダプタが公開されていません。
  • 今回は開発量を最小限とするため、CoffeeScriptを覚えながら、Hubotで開発することにしました。

「PING」って書き込むと「PONG」って返ってくる

  • 特定の入力に対して特定の反応をするのは簡単そうですが、状態の管理はどうするのでしょう。
  • Yammerは「チャット」ではなく「ソーシャル」ですが、Slackと同じ感覚で使えるのでしょうか。
  • 今回のエントリーはこのあたりの課題をどのように乗り越えたか、を紹介します。

Botを導入したいのは社内のQAシステム

弊社にはCET(Cresco technical Expert Team)と呼ばれる、技術・業務・製品といった様々な領域のエキスパートで編成されたバーチャルチームがあります。
困りごとがある社員はCETに質問することで、困りごとが解消したり、解決のヒントが得られる、といった制度を導入しています。
CETとのコミュニケーションはRedmineを使うことでログを残し、先々ではナレッジベースとして活用することを目標に運用しています。

QAシステムの課題

しかし、RedmineのUIが使いづらいため、質問が挙げるまでに至らないという課題があり、質問の敷居を下げるためにChatでQAを実現することにしました。
また、Chatだけではナレッジの再利用が困難なため、QAの結果はRedmineに残す必要があります。

Yammerと連携するQAシステム

システム概要

やりたいこと自体は大したことなく、Yammerの投稿をRedmineに連携するだけです。

環境構築

Qiitaに投稿がありましたので、こちらをご確認ください。
上記のリンクに用意するものも記載いただいておりますが、今回のシステムを構築する上で、以下も用意しました。

開発の流れとハマりポイントの紹介

①YammerとHubotの連携

Qiitaの手順で自分のYammerアカウントのBot化に成功し、本格的なBot開発に着手しようとしたところ、

YammerのグループIDが取れない!

前述のQiitaのおまけのエラー集にも書かれていますが、手順通り、誤字もなくグループを作成しているのですが、グループIDが取れない。
Yammerには「外部ネットワーク」という概念があり、そのネットワークに紐づいたアカウントにネットワークをまたがって、グループを参照することができないのです。

全社ネットワーク(プライマリネットワーク) に 所属するBotアカウント に 紐づくクライアントアプリケーションのAPIキー

では

QAシステム用(外部ネットワーク) の グループ

は見えません。

QAシステム用(外部ネットワーク) に 所属するBotアカウント に 紐づくクライアントアプリケーションのAPIキーを払い出して利用します。

Yammerのことを良く分かってないと、ハマってしまうかもしれません。
上記の問題を乗り越え、HubotがYammerを認識するようになりました。

②HubotとRedmineの連携

Redmineとの連携はHubot謹製のPluginが提供されているので、すぐに出来ました。意図したとおりのチケットが作成され、喜んだのも束の間、

文字化け!

日本語を入力すると、Redmineに登録された文言が文字化けするという。。。
Redmineのチケットの作成・更新はAPIを呼び出すだけなので、HTTP通信用のライブラリを入れればと思っていましたが、HubotのメッセージはHTTPクライアント持ってるんですね。使い方はこんな感じです。

robot.hear /(.*)/i, (msg) ->
  data = {投稿したいチケットの内容をJSONに設定 詳しくはRedmine のAPIリファレンス参照}
msg
.http(process.env.HUBOT_REDMINE_BASE_URL + "issues.json")
.headers
'X-Redmine-API-Key': process.env.HUBOT_REDMINE_TOKEN
'Content-Type' : 'application/json'
.post(data) (err, res, body) ->
if err
res.send "Encountered an error :( #{err}"
return

あとは、作りこむだけですが、ここでも色々な課題が見つかります。

複数行書き込みが取れない!

大抵のサンプルコードは、チャットからの入力を取る方法として、2行目のコードが書かれています。
(Yammerだけの問題かもしれませんが)この方法だと複数行の書き込みが取れないので、オリジナルの入力を取得するために3行目の方法を利用しました。

robot.hear /(.*)/i, (msg) ->
  message = msg.match[1]
message_all = msg.message.text

Yammerの投稿とRedmineのチケットの紐づけ問題① メッセージの判別!

ここまでで、Yammerから投稿された内容をRedmineに投稿するまでは行けましたが、Yammerで質問へ回答を返信すると、回答が新規の質問として投稿されてしまいます。
Yammer上で質問に対して、返信があった場合、既存のRedmineのチケットを更新する必要があります。
HubotがConsoleに出力するログを見ていると、こんなログが出ていました。

received 投稿内容 from Yammerアカウント名 (thread_id: XXXXXXXXXX, sender_id: XXXXXXXXXX)

どうやら、thread_idが投稿のIDなようで、質問に対する返信も同じIDが振られています。これをRedmineのチケットIDと紐付けることで、質問なのか(新規)、回答なのか(既存)を見分けることができます。

で、どこから取るのかというと、これまた、メッセージの中から取り出せます。

robot.hear /(.*)/i, (msg) ->
user = msg.envelope.user.name
thread_id = msg.envelope.user.thread_id

Yammerの投稿とRedmineのチケットの紐づけ問題② データの永続化!

で、取り出したthread_idやRedmineのチケットIDをどこに保管しましょう?
巷のサンプルだとRedisを導入し、Hubotのbrain機能を使うのですが、なかなかの曲者のようで、トランザクション量から考えて、今回はJSONファイルに残すことにしました。

fs = require 'fs'
# File Read [PathToFile]はファイル保存先にしてください
jsontext = fs.readFileSync [PathToFile], 'utf8'
# File Write
fs.writeFileSync [PathToFile], jsontext

③Redmineからの書き込みをYammerに返す

ここまでで、Yammerの新規投稿をRedmineのチケットに登録し、Yammerの投稿への返信をRedmineのチケットに反映できるようになります。
今度はYammerの投稿に紐づくRedmineのチケットが更新されたら、Yammerに通知するようにします。

HubotとRedmineの連携も検索するとたくさん出てくるんですが、チャットはSlackであることが多いんですよね。
こちらの記事をみても分かるんですが、

Yammerにはチャンネルという概念がない!

WebHookのURLのroomというパラメータには、何を設定するのでしょう?
このあたりから、魔改造に足を踏み入れるので、結論としては、「不要」です。

HubotにRedmine Notifierを導入すると、Postリクエストが受け付けられるようになります。
PostmanなどでHubotのサーバに空データのPostリクエストを送ると、エラーが表示されれば、Hubot側は準備完了です。

いざ、Redmineのチケットを更新すると、

Hubotの反応が無い!

何も表示されません。Redmineプラグインとその設定、ネットワーク、ファイアウォールなど、様々な個所を疑いましたが、結論としては、Redmine NotifierとHubot Yammerアダプターの相性が悪く、Yammerに返せないということでした。

そりゃそうだ、どのYammerアカウントで書き込んでよいのか、分からないですからね。。。

  • Yammerアダプター(yammer.coffee)がYammerに書き込む際のユーザ情報
class YammerAdapter extends Adapter
send: (envelope, strings...) ->
user = if envelope.user then envelope.user else envelope
  • Redmine Notifier(hubot-redmine-notifier.coffee)がYammerアダプターに連携するユーザ情報
envelope = {}
envelope.user = {}
envelope.user.room = envelope.room = query.room if query.room
  • Yammerに書き込む際に必要な情報(むらたん調べ)
user =
name: [ Yammer上のユーザ名(メアドのアカウント部分) ]
id: Yammer の sender_id
thread_id: 投稿対象のthread_id

ぜんぜん違うじゃん!もう、魔改造するしか無いっしょ。
ということで、Hubotのプラグインを改造して、RedmineからYammerへの投稿も実装できましたが、最後の問題にぶつかります。

余計な書き込みが増える!

  • Yammerに質問を投稿する → Redmineにチケットを作成する → 作成されたチケットの情報をYammerに返す。
  • Redmineから質問に回答する → 紐づくYammerの投稿にレスを付ける。

これらはOK

  • Yammerから質問に回答する → 紐づくRedmineが更新される = Redmineから質問に回答する → 紐づくYammerの投稿にレスを付ける。

これはNG

対処として、Redmineの更新ユーザがbotの場合はYammerへ連携しないようにして、実装終了。

④Hubotのサービス化

Hubotをアプリケーションとして起動する方法は散見されますが、サービス化しないとターミナルが落とせなくなります。
hubot foreverで検索すると事例が紹介されているので、ご参考ください。

最後に

Hubotはアダプターが多数公開されており、アダプターを切り替えることで簡単にチャットサービスを切り替えられるものだと考えていましたが、簡単ではなかったです。「Yammerがチャットじゃないから」が原因な気がします。

とはいえ、CoffeeScriptは難しくなく、色々と実装できそうなので、今後も機能拡張を続けていきたいと思います。