[1年前を振り返って]ハピネススカウターをレビューする

# [1年前を振り返って]もうちょっと細かくハピネススカウターを作ってみる

## はじめに

この記事では1年前（2021年12月19日）にAWS上に作成したLINE botをレビューしていく記事です。もちろん、過去の自分は知る由もないことです。当時気づけなかった観点や実装できなかったことを反省して実際に検討して実装していきます。

要は[昔の記事](https://qiita.com/ymd65536/items/f1d50596cdd1d76d1c7b)について実装と運用面から見ていく記事です。ではいってみよう。

### 何を作ったか3行で

まずは何を作ったかですが、端的に言えば

> Amazon Rekognitionの画像認識技術を用いて
表情を分析し、数値化して返信するLINE botを作りました。

というものです。アイデアは拾い物ですが、中の実装は完全オリジナルという代物です。

動作イメージ

<iframe width="560" height="380" src="https://www.youtube.com/embed/Pnq_b8Iyvaw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

### 技術的には何使ってるん？？

そんな代物をどうやってそして何で作ったのかって話をします。
具体的には以下の技術です。

- AWS
    -　AWS Lambda
    -　Amazon DynamoDB
    -　Amazon S3
    -　Amazon Rekognition
    -　Amazon API Gateway(REST API)

- 言語
    - Python3

- LINE API
    - Messaging API

はい。要するにサーバーレスアーキテクチャのLINE botです。

とてもシンプルに言えば

`アプリケーションが提供するAPIをAWS上で実行できるように調整して他のAWSサービスと連携した`

ということになりそうです。昨今では最も基本的でかつ運用にまで持っていくにはいろんな試行錯誤が必要な内容になります。

ただ作るだけならおそらく誰にでもできる内容かと思いますが

これを実際の運用までに持っていくのであれば、それなりにセキュリティ対策とかアーキテクチャはこれで良いのかとかコストはどうするんだとか監視はどうするんだとか色々考えるべきことがあります。

いくつか観点がありますが、そもそもここがイケてないよねという`良くないイケていない部分をピックアップしていきます。

## LINE botのここがイケてないよ - 実装編

### 同じ情報が2つのテーブルに保存されている(DB設計/構築)

記事内ではDynamoDBテーブルを2つ作成しており、同じ意味持つ情報が2つのテーブルに保存されています。具体的にはユーザーIDとImageIDです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527543/5436543b-8d3e-2e29-9463-351d59f442b6.png)

LINEからのメッセージを受信した時にユーザーIDとImageIDのみを保存していますが、これらだけだとデータとしてはあまり価値がありません。わかるとしてもせいぜい、ユーザー数くらいのものだと思います。

ユーザーIDとImageIDだけでなくメッセージを受信した時のイベント情報を保存するということであれば
`意味のあるデータ`になるとは思いますが、そうでない場合は`意味のないデータ`になる為、費用対効果は高くありません。

データを集めるのであれば、何かしら`意味のあるデータセットを構築する`ようにした方が建設的です。
見極めが難しいですが、データに対するドメインをしっかり把握しておくことが見極めることにつながります。

#### 解決策

各テーブルの用途を定めた後、テーブルを1つにしてデータを集約する。この時パーティションキーとソートキーを見直します。

具体的にはパーティションキーはLINEのユーザーID、ソートキーはLINEが作成するImageIDとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527543/f16cf42f-ef35-40ae-0301-9b69134ae515.png)

理由：パーティションキーにLINEのユーザーID、ソートキーをImageIDとすることでどのユーザーどれくらい画像を送信しているかを計測できるからということとキャパシティ管理もシンプルになるからです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527543/0965503c-5ec9-efa6-1a2a-25156ce92348.png)

### ソースコードに冗長な部分が多い、考慮が足りていない

ソースコード内で環境変数を参照していますが、中身のチェックがないです。

```python
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
table_name = os.getenv('DYNAMODB_TABLE_NAME', None)

# 〜省略〜

def lambda_handler(event, context):

    records = event['Records']

# 〜省略〜

    except Exception as e:
        print(e)
        return

```

また、変数についても配列アクセスを複数回実行していますが、変数一つにまとめられる部分があります。

例えば、[画像分析用のLambda](https://github.com/ymd65536/happiness_scouter/blob/main/reko_api/lambda_function.py)でl.884を見てみると

```python
    flex_message_json_dict['body']['contents'][0]['contents'][1]['contents'][1]['text'] = emotion_happy
    flex_message_json_dict['body']['contents'][0]['contents'][2]['contents'][1]['text'] = emotion_calm
    flex_message_json_dict['body']['contents'][0]['contents'][3]['contents'][1]['text'] = emotion_surprised
    flex_message_json_dict['body']['contents'][0]['contents'][4]['contents'][1]['text'] = emotion_fear
    flex_message_json_dict['body']['contents'][0]['contents'][5]['contents'][1]['text'] = emotion_confused
    flex_message_json_dict['body']['contents'][0]['contents'][6]['contents'][1]['text'] = emotion_disgusted
    flex_message_json_dict['body']['contents'][0]['contents'][7]['contents'][1]['text'] = emotion_angry
    flex_message_json_dict['body']['contents'][0]['contents'][8]['contents'][1]['text'] = emotion_sad

    flex_message_json_dict['body']['contents'][0]['contents'][1]['contents'][2]['text'] = happy_score
    flex_message_json_dict['body']['contents'][0]['contents'][2]['contents'][2]['text'] = calm_score
    flex_message_json_dict['body']['contents'][0]['contents'][3]['contents'][2]['text'] = surprised_score
    flex_message_json_dict['body']['contents'][0]['contents'][4]['contents'][2]['text'] = fear_score
    flex_message_json_dict['body']['contents'][0]['contents'][5]['contents'][2]['text'] = confused_score
    flex_message_json_dict['body']['contents'][0]['contents'][6]['contents'][2]['text'] = disgusted_score
    flex_message_json_dict['body']['contents'][0]['contents'][7]['contents'][2]['text'] = angry_score
    flex_message_json_dict['body']['contents'][0]['contents'][8]['contents'][2]['text'] = sad_score
    flex_message_json_dict['body']['contents'][0]['contents'][9]['contents'][2]['text'] = 

```

こんな感じで配列へのアクセスが実行されていますが、これはどう見ても修正の余地があります。

### 解決策

変数のチェックを入れる。変数をまとめる。可能であれば、次に説明するFlexMessageと合わせて改善すると良いです。

### FlexMessageを表現するjson文字列がベタ書き

LINEのFlexMessageをJSONで表現していますが、エントリーポイントのあるLambdaにベタで書いています。

例えば、[画像分析用のLambda](https://github.com/ymd65536/happiness_scouter/blob/main/reko_api/lambda_function.py)でl.123を見てみると

```python
def emotion_flexmessage(emotions):
    message = """
{
# 〜省略〜
}
    """

    combat_power = 0.0
```

### 解決策

新しくディレクトリを作成して定数値として分けるなどの処理をしましょう。
具体的にはlibディレクトリを作成して設計原則(単一責任の原則)を意識します。

例えば、`emotion_flexmessage`というメソッドであれば、`LINE`用のモジュールを作成するなどしておくとデバッグの時にどの機能に問題があるのかをモジュール単位で見極めることができます。

### zipで圧縮してアップロード、しかもサイズが大きい

Lambdaにアップロードするファイルが大きいです。また、zipでアップロードされていますが
容量が大きいのでソースコードを確認しにくいです。

そもそもですが、[事前準備にあるこの手順](https://qiita.com/ymd65536/items/f1d50596cdd1d76d1c7b#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99)ってなんか面倒だと思いますよね。
複数のプラットフォームで改修することもあるので動作確認のタイミングで困ることもあると思います。

#### 解決策

コンテナデプロイに変更する。AWS Lambdaはzipで利用する以外でECRにpushされたコンテナを参照することでコンテナによるコンピューティングが可能となります。

こうすることでコンテナランタイムで動作するようになるのでプラットフォーム依存にならず、アプリケーションの起動設定もまとめることができるようになります。

Apple Sillicon MacBookとMicrosoft SurfaceBookで異なるOS、アーキテクチャの端末を持っているので今回はアーキテクチャごとにマルチステージビルドを書くようにしました。

```docker
FROM public.ecr.aws/lambda/python:3.8-arm64 AS arm64
COPY ./app.py   ./
RUN pip3 install LINE-bot-sdk
CMD ["app.lambda_handler"]

FROM public.ecr.aws/lambda/python:3.8-x86_64 AS x86_64
COPY ./app.py   ./
CMD ["app.lambda_handler"]
```
本番用Dockerfileなのか検証用Dockerfileなのかという部分はあるかと思いますが
現状、このDockerfileは環境毎に別の動作をするような想定はなく、環境はアカウント毎に分離できていれば、問題ありません。

## このLINE bot、ここがイケてないよ - 運用編

### 再構築のしやすさ

なし。現在はマネージドコンソールをポチポチしながら作成する感じです。

#### 解決策

具体案としてはAWS CLIによるデプロイ手順を作ります。Lambdaの作成、API Gatewayの作成をコマンドラインできるようにします。なお、Lambdaはコンテナベースで開発する為、ECRへのpushもコマンドラインでやります。

究極を言うとSAM(サーバーレスアプリケーションモデル)を使うと良いですが
`現段階の改善ではそこまで不要である`認識です。

理由：SAMを使う場合はコマンドラインによる作成がメインとなる。現状はマネージドコンソールでやっている為、まずはマネージドコンソールによる開発から脱却することを目指すことが良いから

:::note info
補足：
zipを使いつつ、AWS CLIでアプリケーションを更新することもできます。
:::

### アラームと監視の設計がない

結婚式の披露宴だけ動けばそれで問題ないという要件からアラームと監視の設計をしていませんが、このLINE botをある程度長い期間(例えば、1ヶ月以上)稼働させるといくつか課題があることに気づくと思います。

具体的には以下の3つです。

- 各AWSサービスの利用制限([クウォータ](https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-service-information.html))
- AWSのサービスの正常性確認(各サービスのヘルスチェック)
- LINE APIの回数制限

:::note info
補足：
`アラームは何のために出すのか`と`サービス監視なのかインフラの監視なのか`も重要です。
:::

#### 解決策

サーバーレスのアラーム設計については以下の項目について監視すると良いです。

- API Gatewayの呼び出し回数
- httpステータスのエラー
- Lambdaのメモリ使用率を監視
- DynamoDBのスロットリング (クォータを消費しきったときに発生します)
- 各AWSサービスの利用制限(クウォータ)

また、LINE APIにはAPIの利用回数制限があり、監視すべき項目としてはAWSからLINEアプリへの`push`メッセージの回数です。

LINE APIで利用中のメッセージを取得する場合は[当月のメッセージ利用状況を取得する](https://developers.line.biz/ja/reference/messaging-api/#get-quota)を実行することで取得できます。

ゆえに

- 定期監視でDBに当月のメッセージ利用状況を記録する
- 累計のメッセージ数が1000通を超えたらシステムが混み合っているreply メッセージを返信するように改修する

という動きをとると安心して使えるLINE botになるかと思います。

### コスト管理と見積もりがない

LINE botを運用していくにしても利用料金に関する記述がないので初学者にとってはとてもハードルが高いと思われます。

料金の度合いを感じ取ることのできる内容があると良いとは思うので月に1,000回くらい使ってみたらどうかについて考えてみます。

※Pricing Calculatorを利用

#### 料金

AWS Lambda(画像処理用)

リクエスト回数：1000回/月
稼働日数：30.5(日/月)
メモリ：128MB
実行時間：15秒

```txt
Lambda costs - Without Free Tier (monthly): 0.03 USD
```

AWS Lambda(LINE bot返信用)
リクエスト回数：1000回/月
稼働日数：30.5(日/月)
メモリ：128MB
実行時間：15秒

```txt
Lambda costs - Without Free Tier (monthly): 0.03 USD
```

Amazon API Gateway
1000(回/月) 1(APIs) 30.5(日/月)

```txt
REST API cost (monthly): 0.00 USD
```

Amazon Rekognition
DetectFace：1,000 枚　30.5 (日/月)

```txt

Image pricing (monthly): 1.30 USD
```

Amazon S3
ストレージ容量:1000(枚/月) × 30.5(日/月) × 5(MB/枚) × 0.001(GB 換算)
PUT リクエスト:1,000(回/月) 30.5(日/月)
GET リクエスト:1000(回/月) 30.5(日/月)

```txt
S3 Standard cost (monthly): 0.21 USD
```

Amazon DynamoDB
書き込み要求 1000(回/月) 1(Functions) 30.5(日/月)

```txt
Total Monthly cost: 0.46 USD
```


:::note info
補足:
ユーザー数は1〜50人、月に1000回の実行で計算しています。

Lambdaを15秒に設定している理由としてはAmazon Rekognitionの処理を返す前にLambdaが終了する可能性があるので長くしています。（検討の余地あり）

API Gatewayのタイムアウトは30秒なのでその点についても注意する。
:::

### デバッグの方法が定められていない

特に記載はしていませんが、記事内で利用されているS3バケットに対して画像をPUTすることでAWS Lambdaを発火することが可能です。がしかし、明文化されていません。

#### 解決策

ローカルデバッグはせず、クラウド上で全てデバッグする。

テストする項目としては
- Webhookの動作確認(LINE bot <=> AWS)
- 受信用Lambdaの動作確認(Webhook 実行時にLINE ユーザーIDで返信をする)
- FlexMessageを返すLambdaはS3のPUTイベントを使って動作確認をする(画像にはLINE ユーザーIDを付加する)

:::note info
AWS XRayを導入するとアプリケーション上でどこに遅延が発生しているか分かるのでより詳細にデバッグしたい場合は導入を検討すると良いです。

例えば、Lambdaの料金を設定するときは平均の実行時間がわかっていると良いです。AWS XRayではそういうパフォーマンスの部分を調査することができます。
:::

## まとめ

他にも考慮すべきことがいろいろとあります。

例えば、

- 保存した画像の保存期間を設定しておく
    - ストレージクラスなど
- 同時実行数を考慮しておく
- 同時接続数やユーザー数を意識する
    - 今回は1~50人くらいを想定しています
- Lambdaの仕様を意識したコードにする(events データの精査など)

1年前に作ったものを見て思うのが、作ったままで終わっているというところです。
結婚式の披露宴で出す出し物なのでその場限りで動作すれば良いという側面はありますが
もう少し上手く作り込んでいれば、LINE APIだけでなくいろんなAPIにも対応できるので少し勿体無いかなと思いました。

また、`実務経験ありで作っている人`のと`実務経験なしで作っている人`との間では大きな差があると感じました。特に、実務経験ありの人だとサービスクウォータの部分やAPI実行回数制限、データベースのキャパシティマネジメントなどなど実務においては重要な観点だなと思いました。

## リポジトリ

`linebot`ブランチで絶賛改修中です。

[happiness_scouter_v2](https://github.com/ymd65536/happiness_scouter_v2)