3行で
- マネージャーが喜ぶ承認システム、 CTO いわく「マジ便利すぎて鼻血出る」
- 5時間かかっていた承認リードタイムが58分に短縮
- コストは月当たり1ドル未満
はじめに
はじめまして、 IT 戦略部システム開発グループの長谷川です。入社以来一貫して社内システムの内製開発に携わっています。今では当部署のマネージャーを務める身ですが、基本的には開発大好きアプリケーションエンジニアであり、メンバーとああでもないこうでもないと意見交換しながらコーディングに没頭する日々を過ごしています。
社内システムの内製開発?
社内システムは会計、人事、総務、法務…と多領域にわたり、担当者あたりのシステム数が必然的に嵩むため、それぞれの運用効率というものが強く求められます。その意味で、社内の主軸となるシステムは原則 SaaS で賄うべしというのが世の流れです。しかしながら SaaS が提供してくれる機能はあくまで汎用性重視であり、得てして特定の業界や会社の独自ニーズを満たすことは難しいものです。そのため要所要所で、アドオン開発やスクラッチ開発といった内製開発を組み合わせ、より高い品質のサービスを社内に提供することが求められます。
今、簡単に「組み合わせ」という言葉を使いましたが、これは大きな視野で捉えると社内システム群の全体設計に他ならず、単体システムを適切に作り上げるだけに留まらない、実に取り組み甲斐のある側面が含まれています。特に対向にある各種 SaaS の仕様を正しく理解し、その特性に沿った設計を行うことは必須でありながら、そこに時間をかけ過ぎると一体何のために SaaS を採用したんだっけという自己矛盾に襲われかねません。今回のこの記事は、こうした社内システム特有の前提を踏まえて読んで頂けるとさらに嬉しいです。
リモートワークとビジネスチャットツール
本題に入る前に、 Slack ワークフロー承認機能をテーマに選んだのかについても少しだけ話しておきます。
昨今のコロナ禍により、リモートワークメインな働き方に移行した企業は数知れず、といった状況になりましたね。この移行の中で重要な役割を果たしたシステムのうちの一つとして、所謂ビジネスチャットツールを挙げることができます。電子メールよりもリアルタイム性が高く、ビデオ会議よりも心理的ハードルの低いビジネスチャットツールは、従業員同士の地理的な断絶で生じるコミュニケーションの諸問題に対してとても有効に機能しました。
DeNA では Slack がそれにあたりました。 Enterprise Grid 導入 でワークスペース横断の連携が実現されていたこともあり、 Slack はコロナ禍以前より既にメジャーなコミュニケーションツールとして社内に浸透していましたが、コロナ禍を経て フルリモートに移行 してからは、まさに全従業員のコミュニケーションインフラとして欠かせない存在になっていました。
これはとても自然な流れとしてどなたも同意してくださることと思いますが、コミュニケーションの中心 (となるツール) が定まると、日々の業務は必ずそこに集約されていきます。リモートワークに後押しされ、すべての従業員が Slack 上でプロジェクトの進捗を伝え、資料の URL を共有し、ときにはソースコードやエラーメッセージを貼り付けて眺めているうちに、意識するとしないとに関わらず、その目が、耳が、未読表示やメンション通知を逃さないよう最適化されました。速やかなリアクションを求められる業務ほど、この感度の最適化されたツール…すなわち Slack を通すことで効率を上げられることをみんな自覚しています。裏を返せば、それ以外のツールで業務を急かされても困るという至極当然の主張を誰しもが抱いているということでもあります。口には出さないかもしれませんが。
実のところ Slack ワークフロー承認機能は、コロナ禍よりもずっと前、件の Enterprise Grid 導入後間もない時期に実装したものなのです。決して今日のような、全従業員が Slack を常に目の端に置いているような未来を想像していたわけではなかったのですよね。先見の明といいましょうか、結果オーライといいましょうか…
ともあれそんなわけで、社内システム内製開発について語ってみる自分内初ブログ記念として、直近思うところの多いこの機能を選んでみた次第です。
Slack ワークフロー承認機能とは
前置きも長くなってしまったので、 Slack ワークフロー承認機能が一体どういったものなのかを駆け足でご紹介してみます。
NetSuite 上の金額系ワークフロー
- DeNA は財務会計システムとして NetSuite という SaaS を利用している
- NetSuite は JavaScript ベース言語でサーバーサイドの挙動をカスタマイズできるという PaaS 的側面も持っている
- NetSuite のインフラ上に独自の RESTful API endpoint を実装してインターネット公開することすら可能
- 高いカスタマイズ性を最大限活かして、 DeNA では稟議や発注といった金額にまつわる申請と承認のワークフローも NetSuite 上に構築している
マネージャーが捕まらない
- 申請者や経理の立場として、ワークフローの承認を得ずに後続の業務を進めるわけにはいかない
- 業者への発注行為、月末月初の計上処理、…
- 大抵遅延するとプロジェクトや会社が危険にさらされるものばかり
- 急いで承認を得たい高額案件ほど、より上位のマネージャーまでワークフローが回る
- グループリーダーの次の部長、部長の次の統括部長、統括部長の次の本部長、…
- マネージャーは上位にいくほど忙しく、申請されてきたワークフローの一覧を眺める時間も取れないので、なかなか承認できない
- 申請者や経理は承認が滞っているワークフローがないか絶えず監視し、今承認できていないマネージャーが誰なのかを特定し、どうにかして連絡を取るために日々奔走する
細切れな時間しか空かないマネージャーにワークフローを見てもらうために、できることは何か
- スマートフォン »»> 越えられない壁 »»> ノート PC
- マネージャーはそもそも、たとえ持ち歩いていても、ノート PC を開きもしない時間が存外長い
- 片手でチラ見できるスマートフォンはやはり強い
- PC 前に張り付けられがちな昨今ですら、ビデオ会議ツールに奪われた画面面積を補填してくれるスマートフォンは相変わらずビジネスツールとして健在で、ちょっと面白い
- 必要最小限で凝集された申請情報表示
- さしもの NetSuite も残念ながら GUI コンポーネントを完全に自由にカスタマイズできるわけではない
- スマートフォンブラウザで NetSuite にアクセスさせただけでは、視認性よくワークフロー申請内容を表示することはかなわない
そこで Slack
しかないですよね。
告白すれば、そもそものきっかけは当時 Enterprise Grid 導入を推進した部長からの「長谷川さん、 Slack 上で NetSuite ワークフローを承認できるようにしたいんです」という依頼だけでした。特にそれ以上の検討や検証もありませんでしたが、まあ直感的に素敵だし妥当なアイディアですよね。ということで、ちくちく隙間時間で作ってみることにしたのです。
作ってみたもの (機能概要)
- リアルタイム通知
- ワークフローで自身が承認者となると bot がダイレクトメッセージ (個人に閉じたチャンネル) で教えてくれる
- 勿論その場で承認できる
- ファーストビューは視認性重視、「詳細表示」ボタンで追加情報を展開してくれる
- 稟議、発注、債務…などワークフローの種類ごとにリアルタイム通知の ON/OFF を自分で設定できる
- リマインド通知
- ワークフローで自身が承認者となってから1営業日以上放置されているものがあると、日次で教えてくれる
- こちらの通知はあえて ON/OFF できないようにしています
- 一覧表示
- bot にダイレクトメッセージで「承認一覧」と話しかけると、自身が承認者となっているワークフローの一覧を返してくる
- 一件指定表示
- bot にダイレクトメッセージで「申請番号xxx」と話しかけると、自身が承認者となっている、ないし引き上げ承認 (将来的に自身に承認者が回ってくるワークフローを、現在の承認者達をスキップして承認してしまう行為) できるワークフローがあればそれを返してくる
- クレーム転送 (所謂イースターエッグ)
- 罵倒するとサポートチームにその内容を転送するか尋ねてくる
結果どうなったのか?
金額系ワークフローが申請されてから完全に承認されきるまでのリードタイム平均値 (土日を挟むなどした外れ値を弾きたかったので、遅い方から20%は集計から除外しました) を Slack ワークフロー承認機能の導入前後で比較してみたところ、約5時間だったものが58分に短縮されました。5倍ですよ。当時の部長も長谷川も、ご満悦というものですよね。
実装上の工夫
まずはシステム構成
さて、 NetSuite が PaaS 的側面を持っているということについては既に触れました。サーバー上で JavaScript が動作し自作 API endpoint も公開できるとなれば、 NetSuite と Slack とが直接通信し合う大変シンプルな構成になりそうなものですが、実際にはそうはなりませんでした。
ご覧の通り Google App Engine (GAE) によるデータベース (Datastore) 込みのアプリケーションを挟んでいます。この構成の一番の目的は、 NetSuite 宛のリクエストを従前から稼働していた独自のジョブキュー (図中で Queueing System と表示しているもの) に通すことでした。
SaaS 時代の API Rate Limit と社内システム的対処法
ここでわざわざ詳述するまでもなく、今日のきちんとした SaaS 提供者であれば大抵その API endpoint を利用者に開放し、システム的な連携や操作の自動化が可能であることを売りにしていますし、あわよくばエコシステムの発展を期待しているものです。このとき彼らは同時に、利用者から投げられてくるリクエストの流量に怯えてもいます。場合によっては自分達のサービスがダウンしかねないからです。そこで一定以上のリクエストは受け取らないよう、最初から制限を設けているのが一般的です。制限の仕様は SaaS によって様々ですが、これが API Rate Limit と呼ばれるものです。
API Rate Limit を超えたリクエストは 403 Forbidden や 429 Too Many Requests であしらわれることが多いですが、普通に 5xx で失敗することもあります。そしていずれにせよ、これに対処するためのベストプラクティス…というかもはや一般常識となっているのが、 Exponential Backoff です (とても大雑把に説明するなら、リクエストが失敗したとき同じものを再送するのだけれど、都度再送までの間隔を延ばすことでシステム負荷を下げてあげるいい感じのやり方です)
ただし…この Exponential Backoff (のみ) に頼っていいのはあくまで対象となる SaaS の
- リクエスト総流量
- API Rate Limit の仕様
が把握できないときだけだと長谷川は思うのです。特に社内システムとして利用するような SaaS の場合、両者とも明らかであることが殆どです。再送も何も、そもそもリクエスト流量が制限を超えなきゃいいのです。制限値ぴったりの流量にコントロールしてリクエストを投げることで、リクエスト失敗率が最小化され、全体のスループットは最大化されます。
NetSuite の定める API Rate Limit は、独自 API endpoint(s) への同時接続数に上限を設けているというものでした。具体的な上限値は契約に依存していてお金である程度解決できるものの、劇的に増やせるわけでもありません。しかし NetSuite にデータを投入/連携したい会計システムは幾つもありました。限られたリソースを最大限活用するために、長谷川は NetSuite 導入後間もなく手前に専用のジョブキューを設け、 NetSuite 宛のリクエストはすべてここを通すようルールを定めました。新参者である Slack ワークフロー承認機能も例外ではありません。ユーザーの利用に応じて発生する Slack からのリクエストを一度 GAE で受けてからこのジョブキューに流すことで、 NetSuite へのリクエスト流量は常に最適にコントロールされます。
ちなみにこのジョブキューは優先度付きで実装していたので、月に一度連携される大量仕訳データがバックグラウンドでゆっくりと投入されている傍らで、素早く反応してほしい Slack 由来のジョブは優先的に処理させる、ということが最初から実現できました。かつての自分を一番褒めてあげたいポイントです。
JWT によるデータの署名
GAE からジョブキューを通じて NetSuite に渡されてくるデータには、承認者名義で特定のワークフローを承認/差戻しするという なりすまし に当たる効力があります。万一このデータをどこかで奪取され、あまつさえ改変されて送り込まれてしまうと、コーポレートガバナンスを大きく損ねる事態に発展します。
そこで今回の実装では、安全のためにデータ中のなりすまし効力のある箇所をごっそり JWT で署名することにしました。多少長めながら有効期限も付けています。このなりすましデータは元を正せば NetSuite 本体で生成しているものなので、結局 NetSuite は自分で署名したデータを自分で検証していることになります。当然システム間の通信箇所にはそれぞれ認証がかかってはいますが、たかだか JWT を導入するだけでセキュリティーレベルがきっちり一層分追加されるのですから、活用しない手はないと思います。
Slack からの Event API 再送対策
説明順序が若干前後しましたが、 Slack 上でユーザーが bot に話しかけると、その事象 (イベント) が HTTP リクエストの形で GAE に飛んできます。これが Event API です。対話型の bot ではこのイベントデータを受け取るたびに解析し、内容に応じたメッセージをユーザーに返すような処理を実装するわけです。ちなみに Slack 上でボタンを押したときも HTTP リクエストが発生し、これは Interactive Messages と呼ばれますが、挙動は Event API と同じです。
さて、この Event API は受け手が3秒以内に 2xx レスポンスを返さないと
- 直後
- 1分後
- 5分後
という時系列で最大3回、同一イベントデータを再送してきます。これはインフラレイヤーに障害が起きていてもイベントを取りこぼさなくて済む、という意味で親切に見えるのですが、正直ユーザーというものは bot が反応しなければもう一度話しかけるだけなので、そこまでしてイベントの取りこぼしを防がずとも実害はないことが多いです。むしろ律儀に再送を受け取って処理するような実装にしていると、たまたま処理時間が3秒を超えたときに初回と再送の両方に反応してしまって、ユーザーからすれば一度の呼びかけに二度同じ返事をよこすという大変心象の悪い振る舞いをさらすことになります。ということで耐障害性はそこまで気にせず、再送はばっさり無視するような設計でよいと思います。
などと語っておきながら、当時の長谷川はある程度丁寧に実装しておこうと考えたらしく、有効期限6分で memcache に受け取ったイベントを覚えておいてもらい、同一イベントデータの重複処理を回避するようにしていました。またイベントデータの処理自体もその場では行わず Cloud Tasks のキューに積んでしまうことで、なるたけ早く Slack に 2xx を返せるようにもしていました。健気ですね。
ユーザーアクション (承認や差戻し) の競合回避
先にも出た通り、ワークフローの承認者は複数人いる場合があります。申請が出されると全員に対して承認と差戻しのボタンが付いたダイレクトメッセージが送られます。そのうち誰か一人がボタンを押せばよいのですが、もしかすると別の誰かは違うボタンを押すかもしれません。 NetSuite 本体のワークフロー機能には元から楽観的ロックによる排他制御を組み込んでいたので、承認と差戻しが競合して後勝ちになる…というようなことは最低限防げるのですが、それにしてももっと早いタイミングで、誰かが承認していることをその他の承認者たちに教えてあげたいものです。具体的に言うなら、誰かが承認した時点で他の承認者に表示されている承認と差戻しのボタンを削除するべきです。そのためには、 Slack 上のどこにボタン付きメッセージが表示されているのかを知る必要があります。
今回の実装では Slack に送信するすべてのメッセージを GAE 上のデータベースに保持するようにしました。というより、 GAE 上のデータベースにメッセージ情報を保存することをフックとして、 Slack に対してその内容を
chat.postMessage
メソッド
で送信 (既に送信済みの場合は
chat.update
メソッド
で更新) するようにした、という方が正確です。データの更新が表示の更新を呼ぶ設計は処理の流れが整理されるので気に入っていて、大きく言えば MVVM パターンに影響を受けているのだと思います。こうした設計のおかげで、誰かが承認ボタンを押したとき、同一のワークフローについて承認や差戻しのボタンがまだ残っているメッセージはあるか?という洗い出しが、いちいち Slack 側に問い合わせたりせずともすべて GAE 上で完結でき、パフォーマンス的にも有利です。ビジネスロジックとしては見付けたメッセージデータからボタンを削除して保存するだけで、 Slack 上のメッセージへの反映は統一的に行われるのでプログラム記述としてもシンプルです。
複数人のアクション競合を例にとって説明しましたが、一人の承認者が同一ワークフロー情報を複数回呼び出してボタン付きメッセージを複数受け取った場合でも、いずれか一つのメッセージでボタンを押せば他のメッセージのボタンは同じ仕組みで即座に削除されるようになっています。もし bot とのダイレクトメッセージの履歴を遡ってみたときにボタンの残ったメッセージを見付けてしまったら、きっと承認漏れを疑って厭な気持ちになりますよね。社内業務はえてして抜け漏れのなさをとても重視するものなので、そうした「ちょっとしたこと」を救うことでユーザー体験はきっと向上する筈です。
どのチャンネルでの呼びかけに反応させるか?
稟議や発注など、ワークフローには他の人には知られてはいけないプロジェクトや取引先の内容が含まれることが往々にしてあります。そのため承認者本人と bot との間のダイレクトメッセージ用チャンネルでのみワークフローの情報を返すことにしました。ただしメンションされても何も反応しない bot もどうかと思うので、 Event API の
app_mention
イベントと
message.im
イベントの両方を受信しておきつつ、前者が飛んできた場合には忠告だけするようにしました。
ユーザーの使用言語判定
ここまで一言も触れませんでしたが、実は NetSuite を使うのは国内 DeNA グループ会社所属の日本人だけとは限りません。英語や中国語を母国語とする従業員のために、 NetSuite は日英中の3ヶ国語で不自由なく操作できるよう設定されています。となると NetSuite と連携する Slack ワークフロー承認機能でも当然のように多言語対応できることが求められます。
Slack は一般的なアプリと同様、各ユーザーが使用言語を設定することができ、その値は
users.info
メソッド
で取得することができます。ですがこれはあくまでユーザーが Slack を利用するときの言語 であって、ユーザーがコミュニケーションそのものに使いたい言語と一致するとは限りません。チームメンバーとの会話はすべて日本語であっても、メニュー表記やフォントの好みの問題で Slack の言語設定を英語にする人は決して少なくありません (同様の理由で Mac OS の言語を英語にしている日本人は多い筈です…だって自分がそうだしね!)
スラッシュコマンド のみで構成されるような Slack アプリと違い、対話型 bot ではユーザーの呼びかけが自然言語になります。自然言語なら NLP ですよね。折角 GCP に機能構築しているので Natural Language API を使わない手はありません。こうしてユーザーの呼びかけ文を都度言語判定させることで、常に呼びかけと同じ言語で返答メッセージを返すトリリンガル bot をお気軽に実現することができました。お気付きとは思いますが先に紹介したクレーム転送機能もこの NLP 判定結果を利用しています。
まあ…同一ユーザーが複数言語で呼びかけること自体がレアなので、仕様として若干オーバースペックであることは否めません。ですが簡単に実現できるなら実現してしまえばいいという思想でいた方が、エンジニアとしての遊び心を満たす余地のようなものが残されていてよいんじゃないかなあと考えていたりもするのです。これはきっと、社内システムに限った話ではありませんけどね。
おわりに
いかがでしたでしょうか。少なくともこの記事に取り立てて技術特化な挑戦は見当たらなかったとは思いますが、小さく丁寧な工夫の一つ一つが大きな効果を上げうる領域であることを感じ取って頂けたなら幸いです。
つらつらと書いてきた最後の自慢ですが、この Slack ワークフロー承認機能を構築した GCP のコストは月当たり1ドル未満です。そもそも金額系ワークフローが飛び交うのは月初の営業日に集中しており、利用されない日には本当に利用されないので、 GAE の Scale to zero の特性 (負荷がないときゼロインスタンスまで縮退できること) が大きく効いてくるのです。
しかしながら仮にこのコストが1000倍かかっていたとしても、多忙なマネージャーと周りのメンバーの対応工数、そして隠れた心労の削減効果を思えば、まったく無視できる程お安く済んでいるもの。と、信じております。
※記事中の NetSuite 利用方法については DeNA が独自に構築したものをご紹介しています。日本オラクル株式会社や関連パートナー企業がご紹介しているサービスには含まれません旨、ご留意ください。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。