始めに
こんにちは、ニフティ新卒1年目の高田と村山です。
現在はジョブローテーション期間中で、いろいろな部署を回っています。
現在は、情報システムチームに配属されており、社内向けシステムの開発や運用を行っています。その業務の一環として便利ツールを2人で作成したので、それについて話していきたいと思います!SlackBotを作ってAWSにデプロイしたい人はぜひ読んでみてください!
(編集部より)
2023年10月27日に実施されたニフティ全社員が集う「ニフティ会議」にて、この記事で紹介されたSlackアプリが「創意工夫賞」に選ばれました🎉
創意工夫賞は、業務の改善活動を行ってお客様満足度の向上、業務の効率化、社員満足度向上に貢献した個人を表彰するニフティの社内制度です。
ご参照:自己成長を促す風土 | 採用情報 | ニフティ株式会社
背景
ニフティでは出勤時に勤務場所と仕事内容をSlackに投稿し、Slackのステータスも勤務地に応じたものに変えるというルールがあります。また、退勤時にもSlackへの報告が必要です。これは、リモートワークによるハイブリッドな働き方により、社員全員がオフィスにいるわけではないので、どこで働いているか、今日のタスクはどうなっているかなどを共有する必要があるためです。
従来は、すべて手動で行わなければならなかったため、他の出勤時のタスクと合わせて行わなければならず、忘れがちだったり、作業の負担になっていたりしました。
そこで、我々新人はそんな状況を打破すべく、先に挙げたSlackへの投稿・ステータス変更を自動化するシステムである、「出退勤楽楽くん」の制作に取り組みました。
出退勤楽楽くんとは?
出退勤楽楽くんとは、以下のように、Slackの出退勤報告用チャンネルに報告用スレッドが毎日自動的に投稿され、
メッセージ内にあるボタンを押すだけでSlackへの出勤報告やSlackのステータス変更が行われるというシステムです。
これにより、先に挙げた報告やステータス変更をボタン一つで完了できるため、楽に出退勤時のタスクをこなすことができます。
また、社内で使用されているスケジュール管理システムのIDとPWを事前に登録しておくことで、
報告内容に当日の業務内容を自動的に含めることができます。
システム構成
アーキテクチャ
本システムは、対象がニフティの社員(数百人程度)&1日に一人当たり2回利用されることを想定した比較的小規模なツールです。ですので構成は、呼び出しが少ない場合に低コストで運用できるサーバレスとし、AWS Lambda上にデプロイを行いました。詳細なアーキテクチャは以下の通りです↓
デプロイにはServerless Frameworkを用いました。設定ファイルは以下の通りです(一部省略)↓
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
service: attendance-slack frameworkVersion: '3' provider: name: aws runtime: python3.10 region: ap-northeast-1 iam: role: statements: - Effect: Allow Action: - lambda:InvokeFunction - lambda:InvokeAsync Resource: "*" - Effect: Allow # ParameterStore関連 Action: - ssm:PutParameter - ssm:GetParameter Resource: "*" - Effect: Allow Action: - kms:Decrypt Resource: "*" - Effect: Allow # DynamoDB関連 Action: - dynamodb:UpdateItem - dynamodb:GetItem - dynamodb:DeleteItem Resource: "*" environment: TZ: Asia/Tokyo SERVERLESS_STAGE: ${opt:stage, 'dev'} package: patterns: - "!**" - "function/**" functions: app: # メイン関数 handler: function.slack_bolt_function.lambda_handler name: attendance-slack-${sls:stage} url: true maximumEventAge: 21600 maximumRetryAttempts: 0 post_attendance_message: # 出勤報告用メッセージ投稿関数 handler: function.post_attendance_message.lambda_handler name: attendance-slack-post-attendance-message-${sls:stage} maximumEventAge: 21600 maximumRetryAttempts: 0 events: - schedule: cron(0 22 ? * 1-5 *) # 平日朝7時(JST)に実行 post_leaving_message: # 退勤報告用メッセージ投稿関数 handler: function.post_leaving_message.lambda_handler name: attendance-slack-post-leaving-message-${sls:stage} maximumEventAge: 21600 maximumRetryAttempts: 0 events: - schedule: cron(50 2 ? * 2-6 *) # 平日朝11時50分(JST)に実行 manage_channels: # チャンネル管理用関数 handler: function.manage_channels.lambda_handler name: manage-channels-${sls:stage} url: true maximumEventAge: 21600 maximumRetryAttempts: 0 # 外部パッケージをLambda上で動かすプラグイン plugins: - serverless-python-requirements custom: pythonRequirements: zip: true slim: true useDownloadCache: false useStaticCache: false # DynamoDBのインスタンス作成 resources: Resources: ondemanddb: Type: 'AWS::DynamoDB::Table' Properties: TableName: user AttributeDefinitions: - AttributeName: slack_id AttributeType: S KeySchema: - AttributeName: slack_id KeyType: HASH # オンデマンドキャパシティモード BillingMode: PAY_PER_REQUEST |
Bolt for Pythonによるアプリ制作
出退勤楽楽くんはSlackの公式ライブラリであるBolt for Pythonを使って作られています。これによってユーザからのアクション(スラッシュコマンドやボタンクリックなど)と処理の紐づけを非常に簡単に実装できます。
メッセージ等アプリ内コンポーネントの成形にはBlock Kit Builderを使用しています。たとえば↑のメッセージブロックのうち、ワンクリック版は以下のようなソースでできています。
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 48 49 |
blocks = [ { "type": "section", "text": { "type": "mrkdwn", "text": ":syukkin: *おはようございます!*:syukkin:\nボタンを押して出勤報告しましょう!", }, }, {"type": "divider"}, {"type": "section", "text": {"type": "mrkdwn", "text": "*ワンクリック版*"}}, { "type": "actions", "block_id": "place_oneclick_block", "elements": [ { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "新宿17F"}, "value": "新宿17F", "action_id": "button_17f", }, { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "新宿18F"}, "value": "新宿18F", "action_id": "button_18f", }, { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "在宅"}, "value": "在宅", "action_id": "button_home", }, { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "横浜"}, "value": "横浜", "action_id": "button_yokohama", }, { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "その他"}, "value": "その他", "action_id": "button_others", }, ], }, {"type": "divider"}, # 詳細版も同様に... ] |
これをSlackのchat.postMessage APIにblocksとして渡してAPIを叩くことで指定のチャンネルに送ります。ボタンのアクションは以下のようにblock_idとaction_idを指定して受取り、値はaction[“value”]で受け取ります。また、bodyにリクエスト情報が一通り入っています。
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 48 49 |
def attendance_oneclick_button_clicked( action: dict, body: dict, client: WebClient ) -> None: # ユーザid取得 user_id = body["user"]["id"] schedule = "some schedule" # ここでスケジュール管理ツールから予定をとってくる schedule = "\n--やること--\n" + schedule msg = f"{body['user']['username']}が勤務を開始しました。\n場所: " + action["value"] + schedule # 出勤報告メッセージ投稿 client.chat_postMessage( channel=body["channel"]["id"], text=msg, thread_ts=body["message"]["ts"], username="出退勤楽楽くん", ) # ステータス変更 place = action["value"] if place == "新宿17F": status_text = "オフィスワーク中 17F" status_emoji = ":17f:" elif place == "新宿18F": status_text = "オフィスワーク中 18F" status_emoji = ":18f:" elif place == "在宅": status_text = "リモートワーク中" status_emoji = ":working-from-home:" elif place == "横浜": status_text = "オフィスワーク中 横浜" status_emoji = ":yokohama:" else: status_text = None status_emoji = None # 勤務場所が「その他」のとき以外は変更する if status_text and status_emoji: user_id = body["user"]["id"] client.users_profile_set( token=ps.get_parameter("SLACK_USER_TOKEN"), user=user_id, profile={ "status_text": status_text, "status_emoji": status_emoji, "status_expiration": 0, }, ) # Lazy Listener設定 app.action({"block_id": "place_oneclick_block", "action_id": re.compile("button_.+")})( ack=just_ack, lazy=[attendance_oneclick_button_clicked] ) |
Boltで構築したSlackアプリをLambda上で動かす場合はLazy Listenerの設定が必要です。これについては先輩がすでに記事を書いているので見てみてください。なお、users.profile.set APIを用いて他人のステータス変更を行う場合、その人のメンバー種別より上位のメンバーが発行したユーザトークンが必要です。今回はプライマリーオーナー権限で払い出してもらっています。
使ってみてくれた人の感想
ニフティではSlackに個人の分報チャンネルを作成してつぶやきを発信する文化があります。そこに投稿されていた感想の一部をご紹介します。
好評ですね!
工夫した点
AWSにデプロイするにあたり、比較的安い構成を採用
規模を考慮した完全サーバレス構成
社内ツールであり、ユーザが多くて数百人程度なので、サーバは立てずにリクエストベースのLambda+DynamoDB構成にしました。
シークレットの管理をParameter Storeで行う
ローテーション機能を要さないことなどから、Secrets ManagerでなくParameter Storeを使うことでコストを抑えました。
API GatewayでなくLambda Function URLsを利用
HTTPエンドポイントとしてLambda Function URLsを直接利用するようにし、認証等はSlack SDKに任せています。
開発効率を上げるためにIaCを組んだ
本プロジェクトでは、AWSを採用しているため、コンソールを使用した手動でのデプロイには時間や工数がかかります。
そのため、上記のようにServerless Frameworkを使用して、デプロイできるようにしました。また、Serverless FrameworkはTerraform等と比べて簡単に設定を書けるので、新人エンジニアがIaCを始めるきっかけとしては、とても良いのではないでしょうか。
苦労した点
AWS, SlackAPI初心者2人での設計・開発
私たちはどちらもAWSやSlackAPIを用いた開発の経験がなかったため、仕様の検討には時間がかかりました。 AWSでいえば、DBの選択や設計、シークレットの管理方法、その他必要なサービスの取捨選択などに苦労しました。わからない点は社内分報チャンネルでぼやいていると先輩方が教えてくれたり、あとはChatGPTに相談したりして決めていきました。GPT4は最強でした。
またSlackAPIでは、Tokenの権限、Tier(1分当たりの呼び出し可能回数)やインタラクションの実装方法(ショートカットやスラッシュコマンドなど)の検討に悩まされました。
ジョブローテによる開発期間の制約
9月下旬には他の部署へ移るため、開発はもちろん、企画や仕様の決定、諸々の調整などを含めて3か月で開発しなくてはなりませんでした。また先述の通り私たちには技術を調べる時間も必要だったため、うまくことを進めていくのが大変でしたが、先輩に頼ったり、IaCによる効率化などにより何とか乗り切りました。
まとめ
今回は出退勤時のタスクを自動化するSlackアプリを作成しました。
今回、ふたりともAWSやBoltを使用した開発は初めてでしたが、3か月という短い期間の中でひとつ形にできたのでよかったです。先ほど紹介した感想を含め、チームの方々がみんな楽になったと言ってくれてうれしかったです。
もしみなさんにも、こういったSlackでの毎日のタスクがあるなら、私たちのように自動化を考えてみてはいかがでしょうか。