ほぼゼロコスト！AWSとSlackbotを使った勤務管理アプリを開発してみた

面倒くさい作業、自動化したくありませんか？(挨拶)

現在参画しているプロジェクトでは、毎月末にExcelの勤務表(勤務日の勤務時間を記入したファイル)を作成して提出しています。
ただでさえ忙しい月末の勤務終了後に作っているので毎回バタバタしちゃいます...

正直面倒くさい！...ということで夢を叶えてくれるSlackbotを作成しました。

::: note warn
プロジェクトの勤務表は基本的に社外秘です。
本記事でもサンプルテンプレートから勤務表を作成していますので、情報の扱いにはくれぐれもご注意ください。
:::
## Botの紹介
作成したのはSlackbot「勤務管理くん」です！
(※一般公開はしていません)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/caa98128-dee1-d940-db24-e6c4181cb8f2.png)
用意した勤務ファイルをSlackにアップロードすると、勤務表に変換して返してくれる機能があります。

### 使い方
Botが追加されているSlackのチャンネルに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/f90476bb-da75-1869-2da5-868822258c17.png)
勤務ファイルをアップロードすると...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/e994e8dc-028a-bef0-c76b-7c49692fe256.png)
Botからデータを受け付けメッセージが来て
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/cea4d298-c224-2ac6-6282-e0b3ba81ba5c.png)
しばらく待つとExcelファイルに勤務データが転記された勤務表がアップロードされます！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/8a8251e2-0783-ec3b-5ad3-f126755729bf.png)
簡単ですね！

## アプリケーションの中身
AWSをふんだんに使っているので、AWSサービスをベースにしたアーキテクチャ図を紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/7b147a54-6dd4-a052-0df9-1b8183bc2853.png)

大体の流れは以下のとおりです。
1. ファイルが共有されたら勤務表共有メソッドが起動する
1. 勤務ファイル受付メッセージをSlackへ投稿し、勤務表作成バッチを非同期で起動する
1. Slackから勤務ファイルを取得し、ストレージに格納する
1. 勤務ファイルを加工し、データベースに登録する
1. ユーザ設定・勤務表テンプレート設定をデータベースから取得する
1. テンプレートを加工し、勤務表を作成する
1. 勤務表のURLを含むメッセージをSlackへ投稿する

シーケンス図にすると以下のようになります。
```mermaid
sequenceDiagram
    actor user as ユーザ
    participant bot as 勤務管理くん
    participant api as API
    participant async_bat as 非同期バッチ
    participant strage as ストレージ
    participant db as DB
    autonumber

    user->>+bot: 勤務ファイル共有
    bot->>+api: SlackbotハンドラAPI呼び出し要求
    Note over api: Slackbotハンドラ
    api-)+async_bat: 勤務表作成非同期バッチ呼び出し要求
    async_bat-->>api: 勤務表作成非同期バッチ呼び出し結果

    api-->>-bot: 受付メッセージ
    bot-->>-user: 受付メッセージ投稿

    
    Note over async_bat: 勤務ファイル取得処理
    async_bat->>+bot: 勤務ファイル取得要求
    bot-->>-async_bat: 勤務ファイル取得結果
    async_bat->>+strage: 勤務ファイル格納要求
    strage-->>-async_bat: 勤務ファイル格納結果
    
    Note over async_bat: 勤務データ加工・格納処理
    async_bat->>+strage: 勤務ファイル取得要求
    strage-->>-async_bat: 勤務ファイル取得結果
    async_bat->>async_bat: 勤務データ加工
    async_bat->>+db: 勤務データ格納要求
    db-->>-async_bat: 勤務データ格納結果

    Note over async_bat: 設定取得処理
    async_bat->>+db: ユーザ設定設定取得要求
    loop ユーザ設定が存在する
        alt ユーザ設定が存在しない
            db->>db: ユーザ設定作成
        else ユーザ設定が存在する
            db-->>-async_bat:　ユーザ設定設定取得結果 
        end
    end
    async_bat->>+db: テンプレート設定取得要求
    db-->>-async_bat: テンプレート設定取得結果
    
    Note over async_bat: 勤務表作成処理
    async_bat->>+db: 勤務データ取得要求
    db-->>-async_bat: 勤務データ取得結果
    async_bat->>+strage: テンプレートファイル取得要求
    strage-->>-async_bat: テンプレートファイル取得結果
    async_bat->>async_bat: 勤務表作成(テンプレートファイル加工)
    async_bat->>+strage: 勤務表格納要求
    strage-->>-async_bat: 勤務表格納結果

    Note over async_bat: 勤務表共有処理
    async_bat->>+strage: 勤務表取得要求
    strage-->>-async_bat: 勤務表取得結果
    async_bat->>+bot: 勤務表共有要求
    bot-->>-async_bat: 勤務表共有結果
    async_bat-->>-bot: 勤務表共有メッセージ
    bot-->>user: 勤務表共有メッセージ投稿
```
アップロードした勤務ファイルや登録した勤務データを使う時に再度取得しているのは、各処理ごとに関数に分けてマイクロサービスとしているためです。

### 使用技術
アプリケーションを作成する上で使用した技術をリストアップします
- AWSサービス
    - [コンピューティング] Lambda
    - [ストレージ] S3
    - [アプリケーション統合] Step Functions
    - [データベース] DynamoDB
    - [マネジメント] SystemsManager, CloudFormation, CloudWatch
    - [セキュリティ、アイデンティティ] KMS, IAM
    - [デベロッパーツール] CDK
- プログラミング言語
    - Python: 3.9.6
        - boto3: 1.26.118
        - numpy: 1.24.3
        - openpyxl: 3.1.2
        - pandas: 2.0.2
        - requests: 2.31.0
        - slack-bolt: 1.18.0 
        - slack-sdk: 3.21.3
    - TypeScript: ES2021
        - aws-cdk: 2.85.0
        - aws-cdk-lib: 2.85.0

## こだわりポイント
### AWSリソースをコード化している(IaC)
本アプリケーションはAWS　CDKを使ってAWSリソースをプログラミング言語（今回はTypeScript）で定義しています。
AWS Lambda関数で作っている勤務管理くんハンドラ関数の定義を見てみます。
```typescript:lambda.ts
...
// Lambda Function
const handleWorkforceBuddy = new lambda.Function(
  this,
  "handleWorkforceBuddy",
  {
    functionName: "HandleWorkforceBuddy",
    runtime: lambda.Runtime.PYTHON_3_9,
    code: lambda.Code.fromAsset("src/lambda/handle_workforce_buddy"),
    handler: "handle_workforce_buddy.lambda_handler",
    layers: [slackBoltLayer],
    timeout: cdk.Duration.minutes(1),
    environment: {
      SLACK_SIGNING_SECRET: slackSigningSecret,
      SLACK_BOT_TOKEN: slackBotToken,
      SLACK_BOT_ID: slackBotId,
      WORKSCHEDULE_MAKER_KEY: props.workscheduleMakerKey,
    },
  }
);
...
```
宣言的なコードになっているのも特徴の一つで、上記のコードを見ればどんな設定になっているのかがわかります！
例えばLambda関数のタイムアウト設定を見ると
```typescript:lambda.ts
...
timeout: cdk.Duration.minutes(1),
...
```
タイムアウトが1分であることが想像できると思います。

### ほぼゼロコストで作成できた
使っているAWSサービスは多くがサーバーレスというキーワードを持っているサービスです。
サーバーレスとは、AWS公式サイトには以下のように書かれています。
> AWS でのサーバーレス AWS はサーバーを管理することなく、コードの実行、データの管理、アプリケーションの統合を行うための技術を提供します。 サーバーレス技術には、オートスケーリング、ビルトインの高可用性、使用量に応じた課金モデルなどの特徴があり、俊敏性の向上とコストの最適化を実現します。

https://aws.amazon.com/jp/serverless/

コスト面で言えば、使っていない時は料金がかからず使っている間だけ課金が発生する仕組みです。
自分でサーバを用意した場合は、常にレンタルサーバ代やサーバの電気料金等がかかってくるため、需要の少ない個人開発であれば特に従量課金制の恩恵を得られます。

アプリが完成した月のAWS支払い料金は以下のとおりです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/e2fa1904-d029-c1d4-fabb-f5c1615ec4f1.png)
100回以上アプリケーションを実行しましたが、一番多いS3でさえ$0.09なので格安です。
(S3は他でも使っているので少し高めに出ています)
アプリケーションの核となるLambdaやStep Functionsは名前も挙がっていません。

主要サービスの料金表を掲載します。
(2023/7/29時点 東京リージョン)
|サービス|単位|料金|備考
|-|-|-|-
|Lambda|1ミリ秒|0.0000000021USD|メモリ128MB
|Step Functions|状態遷移1000回|0.025USD|4000回/月まで無料
|S3|1GB(ストレージ)|0.025USD|
|S3|1000リクエスト|0.0047USD|
|DynamoDB|100万書き込み要求単位(WRU)|1.4269USD|
|DynamoDB|100万読み込み要求単位(RRU)|0.285USD|
|DynamoDB|1GB(ストレージ）|0.285USD|25GBまで無料


ユーザ数を1000人、ユーザあたりの使用数を1回/月、平均格納勤務データが12ヶ月とした場合の計算をしてみます。
- Lambda起動秒数:
    - 2200ms(勤怠管理くんハンドラ) + 2500ms(ファイル取得) + 3500ms(勤務データ格納) + 5300ms(勤務表作成) + 3600ms(勤務表共有) = 17100ms
    - 17100ms x 1000ユーザ x 0.0000000021USD/ms = 0.0359USD
- Step Functions状態遷移数:
    - 10回(状態遷移数/バッチ)　x 1000ユーザ → 4000回 x 0USD + 6000回 x 0.025USD/1000回 = 0.1500USD
- DynamoDBストレージ:
    - 5.5KB(1ヶ月分) x 12ヶ月 x 1000ユーザ　 ≒ 0.06GB
    - 0.06GB x 0.285USD = 0.0171USD
- DynamoDB書き込みリクエスト:
    - ６WRU x 1000ユーザ x 1.4269USD/100万WRU = 0.0086USD
- DynamoDB読み込みリクエスト:
    - (1RRU + 1RRU + 2RRU) x 1000ユーザ x 0.285USD/100万RRU = 0.0011USD
S3ストレージ:
    - 5.6KB(1ヶ月分) x 12ヶ月 x 1000ユーザ ≒ 0.06GB
    - 0.06GB x 0.025USD = 0.0015USD
- S3リクエスト:
    - 10リクエスト x 1000ユーザ → 10000リクエスト x 0.00047USD/1000リクエスト = 0.0047USD

合計: 0.2189USD
なんと1ドルにも満たない計算です！これは安い...
 


### 拡張性が高い
ユーザ設定、テンプレート設定を持たせていることで、変換する勤務表を柔軟に変更できるようにしています。
使っているテンプレートは以下のExcelファイルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/f6012c75-1a6f-ba2c-f112-e872776cc216.png)
テンプレートに対して設定の例を見てみます。
|項目|設定値
|-|-
|ファイル名|template_01.xlsx
|データの並び|縦
|勤務日-先頭セル|B7
|勤務曜日-先頭セル|C7
|開始時間-先頭セル|D7
|...|

新しいテンプレートが必要な時は、設定とテンプレートファイルを作れば追加で使えるようになるのです。
ソースコードを書き換えずにテンプレートを追加できるので、お手軽に使えます。

## 苦労したこと
アーキテクチャの構成検討に苦労しました。
当初は以下のように、ファイルを格納するイベントを起点に非同期バッチを呼び出していました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/e5cae819-5225-bff9-c8d5-252c4386cfac.png)
この構成では以下の点に困っていました。
- 「勤務ファイルの取得→格納」も勤務表作成の一連の流れなのにバッチの外に出ている
- Slackの情報(チャンネルIDやファイルを共有したユーザID)が渡せない
- Slackbotに他の機能を追加する際、機能が煩雑になる

そこで以下の構成に変更しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703623/285efa78-c731-ac55-6ab0-2706f5b002f2.png)
「勤務ファイルの取得→格納」がバッチ内に移動したので、処理の流れがシンプルになりました。
Slackの情報も勤務表作成バッチ内のペイロード(各関数間の受け渡し変数)で渡せるようになりました。
また、Slackbotでの処理もSlackの操作に関する処理(メッセージの送信など)に限定したので、今後機能を追加する際もわかりやすくなりました。
Slack APIのフレームワークであるBoltでは、イベントやアクション毎にルーティングできるので、管理しやすかったです。

## 今後やりたいこと
- バリデーション
    - ファイル形式が合っている、必要なカラムが存在している...などをチェックしたい。現状はファイル共有した人を信じるしかない。。。
- モニタリング
    - 処理中にエラーが発生した時はもとより、何らかの原因で非同期バッチが動かなかった時とかLambdaやDynamoDBに過剰に要求を送ってしまいエラーを起こした時...とかを監視して、問題報告用のSlackチャンネルに投稿したい。
    - サービスの稼働状態がわかるダッシュボードも作りたい。
- CI/CDパイプラインの構築
    - masterブランチへプッシュすることを起点とした自動デプロイ、単体テストの自動化を行いたい。
    - CDKワークショップにCI/CDパイプラインのサンプルがあったので、うまく組み込みたい
- 新しいAPIの作成
    - ユーザ設定を追加するAPI
    - テンプレートを登録するAPI
    - 月を指定して勤務表を取得するAPI
    - 月を指定してサマリ情報を取得するAPI

## おわりに
アプリケーションを1本作ってみて、業務で行う開発とは異なる新たな経験できました。
業務では使い慣れた独自のライブラリを活用したり、他のアプリケーションからの流用を行うことが一般的です。
今回は完全新規開発なので、例えばログの出力設定など、普段考えることのない課題に頭を悩ませました。
一方で、普段の開発に対しても新たなアプローチや改善点を見つけることができ、新鮮な気持ちを味わいました。

また、自分で作ったアプリケーションが動き始める瞬間はやっぱり最高でした…！
ファイル格納が動作した！Slack上にメッセージ連携できた！など、一つ一つの成果を目にするたびにモチベーションが高まりました！
まだまだ改修したい点や追加したい機能があるので、これで開発を終わらせずにどんどんアプリケーションを成長させていきたいと思っております！！