メタラーまとんがハイソにやらかすようです

東大理系修士卒JTBCエンジニアのハイソサイエティ(上流階級)な日常

「レアカード売り時お知らせ君」をクラウドモダンアーキテクチャで作った!AWS Lambda, DynamoDB, EventBridge

ども!AWS認定ソリューションアーキテクトアソシエイトのまとんです。

「レアカード売り時お知らせ君」を半分くらい作りました!

TL; DR

  • 自分が持っているレアカードを登録しておくと、売り時を教えてくれるLINEボットを作る
  • サーバーサイドはサーバーレス構成(Lambda, DynamoDB, EventBridge, API Gateway)で実装した
  • フロントエンド(LINEボット回り)はまだ未完成

MTGプレイヤーの悩み

僕はMTG(Magic: The Gathering)というトレーディングカードゲームのゆるふわプレイヤーです。

新しいパックが発売されると、友達の家に集まってリミテッド戦(その場でパックを開けて出たカードでデッキを組んで遊ぶ)を楽しんでいます。

トレーディングカードゲームユーザーにとって、永遠の悩みがあります。それは、

f:id:highso:20210117005220j:image

レアカードがどんどん溜まっていくことです。

高額なカードは売ればよいのですが、わざわざ売るためにショップに行くのもめんどうだし、タンスの肥やしになりがちです。

しかし、カードの価格というのは水物でして、環境が変化すると、昔は見向きもされなかったカードにプレミアがついて高額になったりします。

とはいえ、タンスの肥やしになっていては、それに気が付かず、売り時を逃してしまいます。

誰かが「今が売り時だ!売れーー!!」と教えてくれたらいいのに!

これが永遠の課題です。

作りたいもの「レアカード売り時お知らせ君」

「今が売り時だ!売れーー!!」と教えてくれるロボットを作ります。

具体的には、以下のようなロボットを作ります。

  • 自分が持っているレアカードを、事前に登録しておく
  • 1日に1回、レアカードの価格をwebで調査
  • 価格が急激に上がったカードがあれば、LINEで「売れーー!!」と通知してくれる

MTGのカードの価格は、大昔から運営されているWisdom Guildさんのサイトから調べることにしました。

f:id:highso:20210123190520p:plain

出展:歩行バリスタ/Walking Ballista:シングル価格 - Wisdom Guild
Wisdom Guildさんの「トリム平均」の値を取得させていただくことにしました。

アーキテクチャ

f:id:highso:20210117010950p:plain

ロボットの維持費にお金がかかっては元も子も無いので、コストを下げるためにサーバーレスアーキテクチャ仮想マシンを常時稼働することをしない)で実装します。

フロントエンド

フロントエンドはLINE botとnotifyの仕組みで実装します。

友達とリミテッド戦をしたら、引いたレアカードをその日のうちに、LINE bot経由で登録します。

僕はずぼらな性格をしているので、「いつか暇なときに登録しよう」だと絶対に登録を忘れするなと思いました。

なので、帰りの電車の中でスマホでポチポチやって登録できるようにします。

LINE bot回りは、以前に実装したことがあるので、やればできると思います。

LINE Botに生命保険の解約返戻金を教えてもらえるようにした!Messaging API, Amazon API Gateway, Lambda - メタラーまとんがハイソにやらかすようです

サーバーサイド

データはNoSQLのDynamoDBを使います。

使った分だけ課金されるオンデマンド型で利用できるため、データ量が極めて小さい今回のユースケースには適任と考えました。

DynamoDBとのやり取りや、Wisdom Guildのサイトから価格をスクレイピングする機能は、サーバーレスでPythonコードを実行できるLambdaで実現します。

EventBridgeでLambdaを定期的に実行することで、一通りの機能は実現できます。

「売り時」の基準は難しいですが、さしあたりは「1か月前より50%高騰したとき」のような簡単なルールにしようと思います。異常検知などの機械学習アルゴリズムを組み入れることができれば、よりナウいのですが。

f:id:highso:20210117011005p:plain

今回はサーバーサイドの実装をしました。

フロントエンドは、以前にやったことがあるので、やればできるだろうと思って、後回しです。

習得したいスキル

エンジニアとしてスキルアップという観点では、今回習得したいスキルは三つです。

  • NoSQLデータベースのDynamoDBを使ってみたい。
  • Pythonウェブスクレイピングをやってみたい。
  • Lambda関数をGitHubで公開するのに慣れたい。GitHubで公開する習慣を身に着けたい。

実装

カードのリストを保持するDynamoDBテーブルを作成

自分が持っているレアカードを登録しておくテーブルを用意します。

公式サイトのチュートリアルが十分わかりやすいです。

NoSQL テーブルを作成してクエリを実行する方法 – AWS

以下のような設定でテーブルを作りました。

キャパシティーモードは、従量課金制のオンデマンドと、固定費がかかるプロビジョニングがあります。

今回のような小規模アプリケーションにおいては、書き込み/読み出しの回数に応じてコストを計算しやすいオンデマンドが適していると考えました。

書き込みと読み込み、それぞれ100万回あたり1.25USDと0.25USDです。(正確には回数ではなく書き込み/読み込みユニット数に依存)

今回のケースでは、せいぜい月に1000回程度なので、月に0.15円程度でしょうか。

データストレージは25GBまで無期限無料なので、保存にかかる料金は無しです。

 

DynamoDBはKey-Value型のNoSQLのため、後からなんでも項目を追加できるのですが、大事なのはパーティションキーとソートキーの設計です。

パーティションキーとソートキーでレコード(DynamoDBでは項目という)を指定するため、DBのパフォーマンスを左右します。

今回はカード名のCardと、保有している人名のOwnerにしました。

Ownerは、将来的にこのシステムをMTG仲間数人で共有するとき、誰が持っているカードかを設定できるようにするために用意しました。

(ただ、この組み合わせだと、同じカードを複数名が保有しているケースに対応できません。どうしたらよいのだろうか・・・)

完成したテーブルに、項目を追加するとこんな感じで表示されます。

f:id:highso:20210117011958p:plain

PicesやURLの列については、次のLambda関数で項目を新規追加する際に生成しています。

新しいカードをDynamoDBに追加するLambda関数を作成

作成したDynamoDBに、項目を追加するLambda関数「SellTimeRemainder_InsertItem」を用意します。

Lambda関数の設定は以下のようにします。

  • 一から作成
  • 関数名:SellTimeRemainderGetItems
  • ランタイム:Python3.8
  • AWSポリシーテンプレートから新しいロールを作成
  • ロールを新規作成:SellTimeRemainder_GetItems_Role
  • ポリシーテンプレート:シンプルなマイクロサービスのアクセス権限 DynamoDB

こうすると、DynamoDBにアクセスできる権限を持ったLambda関数が作成されます。

しかし、このLambda関数は、本アカウントのあらゆるDynamoDBテーブルにアクセスできるようになっていました。

DynamoDBは今後ほかの案件にも使うかもしれないので、全てのテーブルにアクセスできてしまうのはセキュリティ上望ましくありません。

そこで、今回作った「SellTimeRemainder」テーブルだけにアクセスできるようにします。

IAMで「SellTimeRemainder_GetItems_Role」を選択し、ポリシーをJSONで表示します。

"Resource"の最後が「table/*」になっており、全てのテーブルにアクセスできることになっていたので、「*」を「SellTimeRemainder」に変更すればOKです。

f:id:highso:20210117012046p:plain

Lambda関数の中身は、GitHubに公開しました。

github.com

READMEにも書いてありますが、以下のようなテストを設定して実行すると、テーブルに新規項目が追加されます。

{
  "card": "弧光のフェニックス",
  "owner": "まとん",
  "url": "http://wonder.wisdom-guild.net/price/Arclight+Phoenix/"
}

urlはWisdom Guildの「弧光のフェニックス」のシングル価格ページのURLです。

カードの価格を問い合わせてDynamoDBに更新するLambda関数を作成

次に、テーブルに保存したカードを全て読み出し、Wisdom GuildのURLにアクセスして本日の価格をスクレイピングで読み出し、テーブルの"Prices"マップに本日の日付をキーにして価格を格納するLambda関数「SellTimeRemainder_GetItems」を作ります。

ソースコードGitHubに公開しました。

github.com

ウェブスクレイピングのために、PythonのrequestsライブラリとBeautiful Soupライブラリを使います。

LambdaはPython標準でないライブラリを使うためには、ライブラリごとソースをzipに固めてアップロードしなければなりません。

具体的な手順はREADMEにも書いたのでご参考ください。

僕はWindows10を使っていて、WSL2(Ubuntu)環境でpipやらzipやらしました。

zipをアップロードした際のLambdaの画面はこんな感じです。

f:id:highso:20210117012031p:plain

次に、このLambda関数を1日1回、日本時間お昼12時に起動する設定をします。

LambdaのConfiguration→トリガーで、EventBridgeを選びます。(以前はCloudWatch Eventsでしたが、名前が変わったようです)

周期の設定は、細かい設定ができるcron文を使いました。

EventBridgeのタイムゾーンUTCのため、JSTUTCで9時間の時差があることを考慮して、

cron(0 14 * * ? 2021)

としました。cron文については以下のサイトを参考にしました。

AWS_Cron式のワイルドカード - Qiita

Lambda→概要画面で、以下のように可視化されていればOKです。

f:id:highso:20210117012059p:plain

1日に1回、カードの価格がDynamoDBに追加されることを確認

数日ほど待って、Pricesのマップに毎日1件ずつ価格が追加されていることが確認できました。

f:id:highso:20210123190704p:plain

ウェブスクレイピングについての注意

ウェブスクレイピングは、相手のサーバーに負荷をかけるため、実施する前に相手に許諾が必要です。

今回は、Wisdom Guildさんに連絡し、「基本的に問題はないが、サーバーに極力負荷がかからないよう配慮すること」というお返事をいただきました。

 

さて、次回はフロントエンド回りを実装します。

以上、メタラーまとんでした。

ではでは。

過去に作ったエンジニア作品

highso.hatenablog.com

highso.hatenablog.com

highso.hatenablog.com

highso.hatenablog.com

highso.hatenablog.com

highso.hatenablog.com

過去のMTG記事

highso.hatenablog.com

highso.hatenablog.com