見出し画像

気軽にツールを作ってライセンス棚卸しを爆効率化した話

この記事は Money Forward CorporateIT Advent Calendar 202321日目の投稿です。
前回は Sub / さぶ さんの「従業員2000名規模の社内ユーザーサポートが考えていること」でした!


はじめに



 【あいさつ】


マネーフォワード(以後マネフォ)のCIO室でコーポレートエンジニアやってるヒデオミです!

今年はツールを作ってライセンスの棚卸し業務を改善した話をしようと思います!!
(自己紹介は昨年の記事に書いてあるので省略します)

ちなみにヘッダ画像は某社のイベントに呼ばれた際、マネフォ参加組で撮った記念写真です。とっても楽しかったです。
(某社の社長、めちゃくちゃ気さくで良い人でした)

なお、今回の記事とは全く何も関係ないです


 【この記事の内容】


  • 棚卸し業務を効率化するためのbotを作った話です!

  • 業務改善の順番を整理して載せてます!長いです!

  • でも諸事情あってbotの具体的な作り方は載せてません!すみませんmm


 【目次】




1. 開発の背景



 【棚卸し業務辛い問題】



突然ですが、棚卸しってめちゃくちゃ面倒ですよね???


管理部門としての責務があるので重要な業務ですが、情シスが抱えるSaaSや担当領域が増えるほど、比例して必要な棚卸し回数も増えるわけです。
棚卸しって基本的にどれも流れが一緒なので、自動化するのに向いていると思いますが、もし、、、万が一、、、、全部手作業でやろうものなら、、、

そこには死ぬほど面倒で辛い作業が待っている、、、!!!



 【あと、何か開発したい】


コーポレートエンジニアたるもの、何かを開発して改善効果を生み出したい気持ちがあります。
開発当時は時間がなかったので(今もだけどw)とにかくカジュアルにさくっとツールを作りたいと考えていました。
あとはSlack Boltという良さげなフレームワークを知ったばかりだったので、Boltを使ってみたいというのも大きなポイントでした(別に何か作って遊びたかっただけじゃないよ、ホントダヨ)


 【Tips:Slack Boltってなんぞや?】


すごくざっくりいうと、インタラクティブなSlack Bot開発が効率よく行えるプログラミングのフレームワークです。
ググってもらうと色々情報がありますが、Boltを使うと使わないでは開発効率に雲泥の差があるので、試したことが無い方は是非とも使ってみることをおすすめします。
(公式リファレンス: Bolt 入門ガイド for Python


2. 要件定義


いきなりツールを作る前に、先に業務理解と要件定義をするのが大事です
そうすることで効率よく業務改善を行うことが出来ます


 【まずは業務理解】


というわけで棚卸し業務の流れを一般化して、マネフォにおける業務の規模感も確認しました。

1. 棚卸し対象のリストを作成
 どんなライセンスが存在するか
 そのライセンスを誰が使ってるか
2. リスト情報をもとにヒアリングする
 ・
継続利用 / 不要 / そもそもライセンスの利用者が合っているか etc…
 ヒアリング結果を集計してリスト化する
3. ヒアリング情報をシステムに反映する
 ・
継続利用の場合は何もしない
 不要なライセンスは削除やディアクティベートする

棚卸し業務の一般的な流れ

・会社の規模は2000人程度
・棚卸し対象は一度に数百人とか、多くて約1000人とか
・常にヒアリングのコミュニケーションが発生するわけじゃない
・瞬間的にコミュニケーションコストが爆発することがある

利用者の規模


 【現状の課題整理】


業務理解ができたので、弊社CIO室の課題は何があるのか関係者に話を聞いて整理しました。

・基本的にヒアリングを手作業でやっているのが辛い
・棚卸し毎にチャンネルを作成して関係者を招待していて手間がかかる
棚卸しの回数と対象の人数が多い
上記より、いちいち依頼してやりとりするだけでも大変
対象人数が多いので、回答状況を把握して管理するのが大変

なるほど〜〜
弊社は扱っているSaaSが多いし、棚卸しの業務負荷は高そうだ、、、!!

ただ、"棚卸し対象のリストを作成する"とか"ヒアリング情報をシステムに反映する"作業について、弊社にはAdminaという強い武器があり、大体自動化できているため、そこまで課題ではないようでした。
(参考:マネーフォワードAdminaでSaaS管理してコストカットした話


【最後に要件定義】


業務と課題を理解したので、何を自動化するのか(要件は何か)考えます

  • 弊社はAdminaがあるため、対象一覧の用意やライセンスの棚卸しそのものはあまり課題ではない

  • 棚卸しの回数と人数規模が多いので、ヒアリングのコミュニケーションコストと進捗管理が大変で課題になっている。

という事が明らかになったので、今回はヒアリング作業を自動化して効率化することにしました!


3. 設計


改善すべき目標が定まったことでようやく設計と開発のフェーズです。


 【イメージを固める】


まずは色々考えてイメージを固めます
個人的に、最初にイメージを固めると後で迷うことが減るので重要だと思ってます。

最初にコンセプトをイメージして、

Slack Boltで開発コスト低くスピーディに開発する
ランニングコストはできるだけ安くする
カジュアルに再利用できて使い回せる汎用的なBotにする
ツールの運用にエンジニアが必要ない状態にする

コンセプトイメージ

次に改善後の業務UIUX的な部分をイメージします。

1. 棚卸し担当者が棚卸しするリストを用意する
2. ユーザに自動的にヒアリングする
3. ユーザがボタンで回答する
4. 回答データが自動的に集まる
5. 回答データをもとに棚卸しを行う

改善後の業務イメージ

・棚卸しの担当者:スプシをインターフェースとして作業が完結する
・ユーザ:SlackのDMでやりとりが完結する
・開発者だけが裏側のシステムやスクリプトに触れるイメージ

成果物のUIUXイメージ


【ツールと業務の設計】


アウトプットのイメージが固まったので実際にどう改善するか設計します

  • アーキテクチャ図はこちら


アーキテクチャ図
  • シーケンス図はこちら

シーケンス図
上半分がヒアリングを行う流れ
下半分が回答データを取得する流れ



、、
、、、、
、、、、、、、、、

記事がようやく折返し地点、、、

やってる内容をちゃんと書いたらこんな量にw

長くてすみませんmm



4. 実装構築


設計まで終わったので早速ツールを作っていきます!
今回の構築手順は次の通りです。

1. SlackApp作成
2. スプシとGASでツールを作成
  2_1. スプシを作成
  2_2. GASをライブラリとして作成
  2_3. スプシに直接紐づいているGASに連携
3. GASからSlackAPIを利用してメッセージ送信できるか確認
4. Lambda作成
5. SlackAppのチャレンジ承認をする
6. DynamoDB作成
7. 残りのコード実装
8. テストとリリース

構築の順番

諸事情により成果物を共有することができないので、実装のポイントだけ説明します!!


 【SlackAppについて】


■ やること
特別なことはなく普通に作成します。
今回はBotとして動作します。必要なBot Token Scopesは次の通り。

chat:write
im:write
metadata.message:read


■ Tips
Botとして動作するので、Your App’s Presence in SlackページからDisplay Nameの設定するのを忘れずに!
(私は最初にこれを忘れてインストールエラーを起こすことが多いです)


 【スプシについて】


■完成イメージ

ヒアリング状況を管理するシート
ここにリストを記入する
DM内容と回答の選択肢について記入するシート


 シート構成


■ Tips
スプシで完結できることが重要なので、必要な情報はスプシに入力するように、ヒアリングをする時はメニュータブからポチっと実行できるようにしてます!
SlackIDメールアドレス氏名ヒアリングの回答状況関数で参照できるようにします。あと、セルが編集されて機能が壊れるリスクを最小限にしたいのでArrayFormula()関数を使って実装します!


 【GASについて】


■ 完成イメージ

event.js

// ==================================================
// イベント定義
// ==================================================
/** スプシ:アカウント棚卸し */
function onOpenByAccountInventory() {
  ///   "メニュー" タブの追加   ///
  const ui = SpreadsheetApp.getUi();                      /// Uiクラスを取得する
  ui.createMenu('メニュー')                              /// Uiクラスからメニューを作成する
    /// メニューにアイテムを追加する ///
    .addItem('ヒアリングする', 'Main.evt_hearing')
    .addItem('ヒアリング結果を同期する', 'Main.evt_getResult')
    .addToUi();
}

///////   メインイベントの定義   ///////
/** 対象のユーザにヒアリングをする イベント */
function evt_hearing() {
  hearing();
}
/** ヒアリング結果を同期する イベント */
function evt_getResult() {
  getResult();
}
main.js

// ==================================================
// 基本的な関数の定義
// ==================================================
/** slackApiを利用するための 基本的な関数…
function callSlackApi_(method, payload) {
  …
  …
  …
}
/** 指定したチャンネルに指定したメッセージを送信する 基本的な関数…
function postDM(ssid, uid, channelId, msg1, msg2, choises, isDuplicateAnswer) {…
  …
  …
  …
}
/** メンバーIDを受け取りDMチャンネルIDを返す 基本的な関数…
function getChannelID(memberId) {…
  …
  …
  …
}
/** テーブル:take_inventory から 指定したスプレッドシートIDの全てのレコードを取得する 基本的な関数…
function getItemsFrom(ssid) {…
  …
  …
  …
}
// ==================================================
// メイン関数の定義
// ==================================================
/** 対象のユーザにヒアリングをする メイン関数…
function hearing() {…
  …
  …
  …
}
/** ヒアリング結果を同期する メイン関数…
function inputResult() {…
  …
  …
  …
}

コードが全然載せられなくてすみません、、、量が多くなるので全部は書けず、、、



■ Tips
スプシに紐づいているコンテナバインド型ではなく、スタンドアロン型のGASにコードを実装します
スプシ側のGASでは実装したGASをライブラリとして呼び出して利用します。この運用によりスプシをコピーした時に大量にスクリプトが誕生する問題が解消できます
元のコードが1ヶ所に集約されるので仕様改修がとても楽です
効率を優先しているのでバージョンは開発モードで呼び出しますが、スプシ毎にバージョンを変えることも可能です。

スプシ側のGASはこれだけ


 【Lambdaについて】


■コード

lambda_function.py

import os
import logging
import json
import re
import urllib.parse
from datetime import datetime, timedelta, timezone
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from slack_sdk.web import WebClient
from slack_bolt import App, Ack
from boto3.dynamodb.conditions import Key
import boto3


TABLE_NAME = 'gws_dlp_ssid_to_optout'

app = App(
    # リクエストの検証に必要な値
    # Settings > Basic Information > App Credentials > Signing Secret で取得可能な値
    signing_secret=os.environ["SLACK_SIGNING_SECRET"],
    # 上でインストールしたときに発行されたアクセストークン
    # Settings > Install App で取得可能な値
    token=os.environ["SLACK_BOT_TOKEN"],
    # AWS Lamdba では、必ずこの設定を true にしておく必要があります
    process_before_response=True,
)


######################################################################
######################################################################
def just_ack(ack: Ack):
    """ * トリガーに対して、ただ ack() だけを実行する関数
        * lazy に指定された関数は別の AWS Lambda 実行として非同期で実行される
        * このメソッドは 3 秒以内に終了しなければならない
    """
    # ack() は何も渡さず呼ぶとただ今のモーダルを閉じるだけ
    # response_action とともに応答するとエラーを表示したり、モーダルの内容を更新したりできる
    # https://slack.dev/bolt-python/ja-jp/concepts#view_submissions
    ack()


def put_item(ts, ssid, uid, user_name, slack_id, account_name, answer):
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(TABLE_NAME)
    jst = timezone(timedelta(hours=9), 'JST')
    now_datetime = datetime.now(jst)
    str_now = (now_datetime.strftime('%Y/%m/%d %H:%M:%S'))
    item = {
        "ssid": uid,
        "time_stamp": str_now,
        "msg_ts": ts,
        "slack_id": slack_id,
        "account_name": account_name,
    }
    print(item)
    table.put_item(Item=item)


######################################################################
###################         処理の流れ          ###################
######################################################################
def step_answer(body, client, payload):
    jst = timezone(timedelta(hours=9), 'JST')
    now_datetime = datetime.now(jst)
    str_now = (now_datetime.strftime('%Y/%m/%d %H:%M'))
    ts = body['message']['ts']
    blocks = body['message']['blocks']
    answer = urllib.parse.unquote(payload['value'])
    ssid, uid = payload['block_id'].split('//')
    answer_block_section = {
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": "*" + str_now + "に【" + answer + "】と回答しました*"
        }
    }
    blocks.pop()
    blocks.append(answer_block_section)
    put_item(
        ssid= ssid,
        ts=ts,
        uid=uid,
        user_name=body['user']['username'],
        slack_id=body['user']['id'],
        account_name=body['user']['name'],
        answer=answer,
    )
    client.chat_update(
        channel=body['container']['channel_id'],
        ts=ts,
        blocks=blocks,
        text="alt_message"
    )


######################################################################
###################          トリガーの流れ          ###################
######################################################################
# 何かしらボタンが押された
app.action(re.compile('^account_inventory_.+'))(
    ack=just_ack,
    lazy=[step_answer],
)


######################################################################
######################################################################
if __name__ == "__main__":
    app.start()


######################################################################
#### これより以降は AWS Lambda 環境で実行したときのみ実行されます ####
######################################################################
# from slack_bolt.adapter.aws_lambda import SlackRequestHandler
# ロギングを AWS Lambda 向けに初期化します
SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)
def lambda_handler(event, context):
    print("=======  リクエスト受信  =======")
    if event["headers"].get("x-slack-retry-num"):
		## 再送かチェック
        if event["headers"]["x-slack-retry-reason"] == "http_timeout":
			## http_timeoutなのかチェック
            print("Retry called")
            return {"statusCode": 200}
        else:
            print("タイムアウトではないエラーが発生")
            raise
    # AWS Lambda 環境のリクエスト情報を app が処理できるよう変換してくれるアダプター
    slack_handler = SlackRequestHandler(app=app)
    # 応答はそのまま AWS Lambda の戻り値として返せます
    return slack_handler.handle(event, context)

コードはこれだけ、けっこうシンプルです。


■ Tips
チャレンジ承認とリクエスト処理が重要。色々ググったら情報が出てきます。
今回は敢えて認証タイプ:NONEの関数URLを発行して利用します。Slack Boltは実行にSlackApp側のシークレット情報を参照するので最低限のセキュリティは担保されています(※実際には会社のルールに従ってセキュリティ環境を構築しましょう)


 【データベースの選択について】


■ Tips
データベースの選択については、ユーザ体験やコスト、アクセスのトラフィックなどを色々考えて適切なものを選択すると良いです。
例えば、小規模ならスプシでも良いし、もっと大規模な利用ならSQSみたいな仕組みを導入する必要があると思います。
また、スプシを選択する場合には排他処理が必要になるので注意しましょう(これをしないと同時にアクセスがあった時に同じレコードに書き込んでしまう可能性があります)
そもそも、ある程度大規模になったら棚卸しに対応したようなSaaSを導入するというのも大事です。
規模や状況に応じた適切なデータベースを選択しましょう


 【それ以外のTipsについて】


■スプシとGASを利用する
ブラウザで完結するようなWebAppを作成する選択肢もあるのですが、結局社内業務の多くはスプシをインターフェースにして行われるので、スプシを利用したツールを作成することで、全体的に効率よく業務改善を行うことができます

■ライセンスがかぶって困る問題
データベースにはユーザ名とライセンスの情報さえあれば良さそうに見えますが、実際には同じライセンスについて、違うタイミングや意図で聞く可能性が高いので、どのスプシからヒアリングを行っているか区別できることが重要です
保存するレコードには、必ずスプレッドシートのIDを保存しましょう!


 【ツールを使用するヒアリング手順を確認】


■ 改善後の作業手順
下記の通りです

事前準備:ヒアリングするデータを用意する
1. スプシにデータを貼り付ける
2. DMする文面と回答する選択肢を入力する
3. 実際に送信する
4. ユーザ回答を待つ
5. 回答を収集する
後続作業:回答データを元に棚卸しを行う

改善後の作業手順





ここまでお付き合いいただき大変お疲れ様でしたmm

こうしてようやくツールのリリースをしました

しかしながら、コーポレートエンジニア的にはリリース後の改善効果を確認する事こそが本番だったりします




5. 改善効果



 【結論:はっきり言って爆効率化した】


いきなりオチみたいな話ですが、
ぶっちゃけ私が一番最初にこのツールを利用したんですよねw

特定のファイル群の利用状況についてのアンケート
何回もヒアリングする必要があった
一度に2000件ぐらい送るときもあった
対象ファイル群の中で1人が複数ファイル利用している場合があった

最初のユースケース

Googleドライブのファイルなのでオーナーを調べるのは簡単、そして今回のツールがあるのでリスト情報をコピペしてボタンぽちっとするだけで、あとは自動でユーザにDMが届き、適当なタイミングでまたボタンぽちっとして回答を収集したらヒアリング終了、、、


ほぼ作業時間が0だし、めちゃくちゃ便利でした


いや〜〜〜、もしツールが無い状況でSlackでいちいちヒアリングしてたと考えると、、、、地獄だし絶対やらなかった
作ってくれた人本当にありがとう!!!(自画自賛)

もちろん!!
それだけでは終わらせずに担当者に使ってもらったりCIO室内で宣伝をしました!!!

利用者の声を聞く限り、Adminaが担当してない領域(だけど地味に工数がめちゃくちゃかかる)のマイクロサービスだったので、とても良い改善効果を生んだようです

そして、ちょっと使い方覚えたら状況に合わせた利用が誰でも簡単にできるので、エンジニアが運用する必要がないのはとても大きいです。
良い形での現場完結です。

あと、地味に社内の対象者達にとっても良いUIUXだったようで、なんと、仕組みが社内のLGTM賞にも選ばれました
完全に想定外だったけど、やったね!!!!


 【改善効果まとめ】


  1. ヒアリングが自動化されてとても便利になった

  2. 進捗や回答の管理確認が確実かつ簡単になった

  3. 簡単にツールがコピペできて内容が変えられるので汎用性がめちゃくちゃ高い

  4. ヒアリングされる側にとっても体験性が良くなった


所感など



 【業務改善としてめちゃくちゃコスパが良かった】


実は今回の改善って、話が出てからリリースするまで1人日でやったんですよね。要件定義がスムーズにできたのは運とタイミングによるところが大きいですが、ツール開発についてはひたすらにSlack Bolt様々でした。


 【急がば回れ】


忙しい時こそ基本的なセオリーを意識するのは大事だと思いました。
今回に関しては、そのおかげで途中迷うことなくササッと効果的な業務改善ができました(ただ、あえてセオリーを無視するべき状況もあると思いますので、一概には言えない)


 【所感まとめ】


コーポレートエンジニアの本質は業務を改善することだと思ってるので、ツールを作るのはあくまでも手段ですが、とてもコスパ良く業務改善という目的を達成できて本当に良かったです。


最後に



 【今後の展望じゃないけども】


汎用性を高くしすぎた結果、棚卸しに限らず色んなアンケートや連絡などでも使えるbotが爆誕しました。
今回は敢えて自動化しなかったけど、リストの用意や後続作業などもAPIだったりスクリプト書いたりしてツールの拡張をするのも良いかもしれません。
あとは時間が無いから見送ったけど、リマインド機能とか回答期限の機能とかもあったら便利だと思います。


 【今回話せなかったこと】


本当は今回作成したツールについて、OSS化をして全国の皆さまの業務改善に利用していただきたいと思っていたりとか、Adminaの開発者と現状のAdminaが領域外としているマイクロサービスなのでシナジー効果が高そうという話で盛り上がったりしたりとか、書ききれなかったツールのこだわりとか色々な話があります。
もちろんこの記事と一緒に作成したツールを公開したかったのですが、色々な事情があり、、、
いつか機会があれば、、、!!!
ちなみに、もしツールのデータが欲しいとかもっと詳しい話が聞きたい場合、CIO室広報的な画伯にご連絡お願いします!(参考:画伯Twitter
あとは、話すだけでも問題ないのでカジュアル面談するという裏技もありますw


 【おわり】


皆さまも良い業務改善ライフを!
そして、もしマネフォに興味があったらぜひともお話しましょう!!!!

この記事が気に入ったらサポートをしてみませんか?