[1年前を振り返って]もうちょっと細かくハピネススカウターを作ってみる
はじめに
この記事では1年前(2021年12月19日)にAWS上に作成したLINE botをレビューしていく記事です。もちろん、過去の自分は知る由もないことです。当時気づけなかった観点や実装できなかったことを反省して実際に検討して実装していきます。
要は昔の記事について実装と運用面から見ていく記事です。ではいってみよう。
何を作ったか3行で
まずは何を作ったかですが、端的に言えば
Amazon Rekognitionの画像認識技術を用いて
表情を分析し、数値化して返信するLINE botを作りました。
というものです。アイデアは拾い物ですが、中の実装は完全オリジナルという代物です。
動作イメージ
技術的には何使ってるん??
そんな代物をどうやってそして何で作ったのかって話をします。
具体的には以下の技術です。
-
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です。
LINEからのメッセージを受信した時にユーザーIDとImageIDのみを保存していますが、これらだけだとデータとしてはあまり価値がありません。わかるとしてもせいぜい、ユーザー数くらいのものだと思います。
ユーザーIDとImageIDだけでなくメッセージを受信した時のイベント情報を保存するということであれば
意味のあるデータ
になるとは思いますが、そうでない場合は意味のないデータ
になる為、費用対効果は高くありません。
データを集めるのであれば、何かしら意味のあるデータセットを構築する
ようにした方が建設的です。
見極めが難しいですが、データに対するドメインをしっかり把握しておくことが見極めることにつながります。
解決策
各テーブルの用途を定めた後、テーブルを1つにしてデータを集約する。この時パーティションキーとソートキーを見直します。
具体的にはパーティションキーはLINEのユーザーID、ソートキーはLINEが作成するImageIDとします。
理由:パーティションキーにLINEのユーザーID、ソートキーをImageIDとすることでどのユーザーどれくらい画像を送信しているかを計測できるからということとキャパシティ管理もシンプルになるからです。
ソースコードに冗長な部分が多い、考慮が足りていない
ソースコード内で環境変数を参照していますが、中身のチェックがないです。
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でl.884を見てみると
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でl.123を見てみると
def emotion_flexmessage(emotions):
message = """
{
# 〜省略〜
}
"""
combat_power = 0.0
解決策
新しくディレクトリを作成して定数値として分けるなどの処理をしましょう。
具体的にはlibディレクトリを作成して設計原則(単一責任の原則)を意識します。
例えば、emotion_flexmessage
というメソッドであれば、LINE
用のモジュールを作成するなどしておくとデバッグの時にどの機能に問題があるのかをモジュール単位で見極めることができます。
zipで圧縮してアップロード、しかもサイズが大きい
Lambdaにアップロードするファイルが大きいです。また、zipでアップロードされていますが
容量が大きいのでソースコードを確認しにくいです。
そもそもですが、事前準備にあるこの手順ってなんか面倒だと思いますよね。
複数のプラットフォームで改修することもあるので動作確認のタイミングで困ることもあると思います。
解決策
コンテナデプロイに変更する。AWS Lambdaはzipで利用する以外でECRにpushされたコンテナを参照することでコンテナによるコンピューティングが可能となります。
こうすることでコンテナランタイムで動作するようになるのでプラットフォーム依存にならず、アプリケーションの起動設定もまとめることができるようになります。
Apple Sillicon MacBookとMicrosoft SurfaceBookで異なるOS、アーキテクチャの端末を持っているので今回はアーキテクチャごとにマルチステージビルドを書くようにしました。
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を使う場合はコマンドラインによる作成がメインとなる。現状はマネージドコンソールでやっている為、まずはマネージドコンソールによる開発から脱却することを目指すことが良いから
補足:
zipを使いつつ、AWS CLIでアプリケーションを更新することもできます。
アラームと監視の設計がない
結婚式の披露宴だけ動けばそれで問題ないという要件からアラームと監視の設計をしていませんが、このLINE botをある程度長い期間(例えば、1ヶ月以上)稼働させるといくつか課題があることに気づくと思います。
具体的には以下の3つです。
- 各AWSサービスの利用制限(クウォータ)
- AWSのサービスの正常性確認(各サービスのヘルスチェック)
- LINE APIの回数制限
補足:
アラームは何のために出すのか
とサービス監視なのかインフラの監視なのか
も重要です。
解決策
サーバーレスのアラーム設計については以下の項目について監視すると良いです。
- API Gatewayの呼び出し回数
- httpステータスのエラー
- Lambdaのメモリ使用率を監視
- DynamoDBのスロットリング (クォータを消費しきったときに発生します)
- 各AWSサービスの利用制限(クウォータ)
また、LINE APIにはAPIの利用回数制限があり、監視すべき項目としてはAWSからLINEアプリへのpush
メッセージの回数です。
LINE APIで利用中のメッセージを取得する場合は当月のメッセージ利用状況を取得するを実行することで取得できます。
ゆえに
- 定期監視でDBに当月のメッセージ利用状況を記録する
- 累計のメッセージ数が1000通を超えたらシステムが混み合っているreply メッセージを返信するように改修する
という動きをとると安心して使えるLINE botになるかと思います。
コスト管理と見積もりがない
LINE botを運用していくにしても利用料金に関する記述がないので初学者にとってはとてもハードルが高いと思われます。
料金の度合いを感じ取ることのできる内容があると良いとは思うので月に1,000回くらい使ってみたらどうかについて考えてみます。
※Pricing Calculatorを利用
料金
AWS Lambda(画像処理用)
リクエスト回数:1000回/月
稼働日数:30.5(日/月)
メモリ:128MB
実行時間:15秒
Lambda costs - Without Free Tier (monthly): 0.03 USD
AWS Lambda(LINE bot返信用)
リクエスト回数:1000回/月
稼働日数:30.5(日/月)
メモリ:128MB
実行時間:15秒
Lambda costs - Without Free Tier (monthly): 0.03 USD
Amazon API Gateway
1000(回/月) 1(APIs) 30.5(日/月)
REST API cost (monthly): 0.00 USD
Amazon Rekognition
DetectFace:1,000 枚 30.5 (日/月)
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(日/月)
S3 Standard cost (monthly): 0.21 USD
Amazon DynamoDB
書き込み要求 1000(回/月) 1(Functions) 30.5(日/月)
Total Monthly cost: 0.46 USD
補足:
ユーザー数は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を付加する)
AWS XRayを導入するとアプリケーション上でどこに遅延が発生しているか分かるのでより詳細にデバッグしたい場合は導入を検討すると良いです。
例えば、Lambdaの料金を設定するときは平均の実行時間がわかっていると良いです。AWS XRayではそういうパフォーマンスの部分を調査することができます。
まとめ
他にも考慮すべきことがいろいろとあります。
例えば、
- 保存した画像の保存期間を設定しておく
- ストレージクラスなど
- 同時実行数を考慮しておく
- 同時接続数やユーザー数を意識する
- 今回は1~50人くらいを想定しています
- Lambdaの仕様を意識したコードにする(events データの精査など)
1年前に作ったものを見て思うのが、作ったままで終わっているというところです。
結婚式の披露宴で出す出し物なのでその場限りで動作すれば良いという側面はありますが
もう少し上手く作り込んでいれば、LINE APIだけでなくいろんなAPIにも対応できるので少し勿体無いかなと思いました。
また、実務経験ありで作っている人
のと実務経験なしで作っている人
との間では大きな差があると感じました。特に、実務経験ありの人だとサービスクウォータの部分やAPI実行回数制限、データベースのキャパシティマネジメントなどなど実務においては重要な観点だなと思いました。
リポジトリ
linebot
ブランチで絶賛改修中です。