ランキングデータを作ったときのAWSサービス選定について

zomです。

この記事はLIFULL Advent Calendar その1の23日目の記事です。

いつもはアドベントカレンダーの日に向けて、前々から興味があったテーマについて調査検証した内容を書いたりしているのですが、今回は年の瀬ということもありますので今年担当したWebアプリケーションについて書いてみます。

Webアプリケーションにおける技術選定という工程は、そのときの要件、リソース（時間や人、金）、会社・組織・あるいはレガシーなシステムとのしがらみといった変数によって大きく変化します。
そのためQiitaなどで検索してもなかなか参考になる記事や考え方が見つからず、どうしたものかなぁと思ったので、このときの設計や技術選定の過程について書いてみようと思います。初めてこういったことをする人の参考になればいいな、と思います :sun_with_face: 

なお、このプロダクトの作成時期は2019年の3月頃です。
また私の部署ではAWSを利用した環境が主なのでAWSのサービスで考えました。

# 要件

- 駅/市区町村のランキングを表示する
    - 駅数は2018年4月1日時点で9516駅（[5/29(火)日本中の駅の数、全部でいくつ？　【駅DB2018年度、整備完了いたしました！】 | ナビットブログ](https://www.navit-j.com/blog/?p=35865)調べ）
    - 市区町村数は2019年時点で1724市区町村（[市区町村数を調べる | 政府統計の総合窓口](https://www.e-stat.go.jp/municipalities/number-of-municipalities)調べ）
    - つまり合計1万超のデータを用意する
        - もちろん本当は弊社所有のデータで計算していますが、Qiitaに載せていいのか分からないので参考値として↑を挙げさせていただきました :bow_tone1: 
- ランキングの元データは3ヶ月分のデータを利用すること
  - 具体的に3ヶ月分の何なのか、はロジックに関するので非公開とさせてください :bow_tone1: 

# 前提条件（裏目標）

- 技術選定は自由
  - その代わりに使い慣れたレガシーなシステムをなるべく使わないようにすること
  - 古いレールに乗らない、次のレールを作るつもりでやること
- 人員は1人、納期はなる早であること

この前提は特に要件にはなかったのですが、当時の上司（に近い人）からのオーダーでした。
自由にやっていいので後輩の手本になるようなやり方をしてくれ、というものです。
:hotsprings: 実際にはもっと柔らかいニュアンスのオーダーですが、対外的に書くとこうなります:joy:

# 最初に考えたこと

前提条件を抜きに一旦考えてみます。

## データストアを何にするか

- S3？
- RDB（RDS）？
- KVS（ElastiCache or DynamoDB）？

## データをどう集めるか
- 既存のログを集計する？
- 新規に専用のデータを取り始める？

## データを集計処理をどうやるか

- EC2でcron？
- Lambda？
- AWS Batch？
- AWS ECS？

:pencil2: RDBのORDER BYなどでリアルタイムに計算すれば処理は不要、データストアで完結するというやり方もあるかと思います。

上記のようなことが頭の中をよぎります。
他にもあるかもしれませんが、当時の自分が最初に思い浮かんだ選択肢はこんなところです。
ここからサイトでのレスポンス速度、既存の構成との兼ね合い、納期、拡張性（他のページでの利用しやすさ）、保守性(運用の楽さ)、前提条件などから最適解を見つけます。

# 選定

自由とはいえ、やはり金・納期のことは無視できません。
そのためいくつかが選択肢から外れます。

> - 新規に専用のデータを取り始める
> - ランキングの元データは3ヶ月分のデータを利用すること

正直、自由にやっていいというところでログ設計から自分でできるチャンス！と思ったのですが、3ヶ月分のデータを表示するということから、リリースしてからランキングが表示されるまでさらに3ヶ月待たせてしまうことになります。
これでは前提条件の「なる早」にも反してますので却下となります。

> - RDB

RDBは人によっては全然選択肢に入っていい内容かと思います。
ただ、RDBのRelationalな部分をそこまで活かすこともなく、KVSで十分なのでは？と考えたことや、すでに使っているデータベース上に（社内事情的な制約で）新規にテーブルを作る積極的な理由が見つかりませんでした。

> - EC2でcron

これは社内では実績がある作り、バッチ用インスタンスなるものが存在します。なのでそこで開発すればリリース速度はおそらく一番早く、納期重視だったらこの選択肢を選んでいたことでしょう。
しかしレールが古く私自身にとっても学びが少ない作り方になってしまうので採用しませんでした。

> - Lambda

ランキングの集計処理に関して言うと、真っ先に思い浮かんだのはLambdaでした。
そのつもりでデータ設計などをして試しにデータを集計する処理などを書いたりして検証を始めたのですが、3ヶ月という膨大なデータ量がLambdaの制限である15分の壁をクリアできないということが発覚しました。
たしか1ヶ月分のデータでも15分の壁を突破できなかったと記憶しています。
検証はしていませんがメモリ量（CPU）を多く割り当てれば行けた可能性はあります。ただ、メモリ量を増やしたところでデータ量次第でさらにメモリを必要とする、安定稼働しないリスクがあるということになりますので別の方法を考えておくべきだと考えました。

※データ量がネックになっているので、あれば例えば10日分のデータなど細かい単位で実行し、中間データみたいなものを作るという手法も検討しました。
ただそうなると中間データを生成するバッチ処理と最終的なデータを生成するバッチ処理の2つが必要になります。
この場合前後関係ができてしまい、順番を考慮する必要があるなど要件に対して管理コストが高くなってしまうのでは？ということで却下しました。

> - AWS Batch

Lambdaが選択肢から外れかけたので使ったことのないAWS Batchも視野に入れてみましたが、やりたいことに対して機能が過剰な気がしました。
当時参考にさせていただいた記事は[AWS Batch とは何か@pottava](https://qiita.com/pottava/items/d9886b2e8835c5c0d30f)です。

> これがマネージドされると僕らは何がうれしいのか
>
> - 本来やりたい、ジョブの実行依頼（submit）と実処理だけを考えればよくなる
> - クラスタ全体で利用可能なリソースの把握、過不足に応じたその調整が不要3に
> - 前処理、集約処理、後処理といった流れのある処理も基盤側に制御を移譲できる
> - 依頼者や状況に応じた優先的リソース配分が容易に実現できる
> - CPU / GPU でそれぞれクラスタを作り、前処理 CPU、本処理 GPU なども簡単
> - クラスタごとに可用性、パフォーマンス、コストのバランスが定義しやすい
> - Docker イメージにさえしてしまえばどんな処理も AWS Batch に乗せられる
> - すでにデータ分析パイプラインの定義があれば移行しやすい（かもしれない）

> 上記記事より一部引用

3ヶ月分のデータがサクッと集計できるのであれば **前処理、集約処理、後処理といった流れのある処理** も不要ですし、 **状況に応じた優先的リソース配分** や **CPU / GPU でそれぞれクラスタを作り、前処理 CPU、本処理 GPU なども簡単** といった、そんなことをせずに済むのです。

ということでAWS Batchも選択肢から外れました。

# 採用したもの

- 既存のログを
- AWS ECSで集計し
- KVS(Redis(ElastiCache)のSortedSets)に入れる

という構成です。

ここからは今まで以上に既存のシステム構成によるので汎用性のない話になります。

## 既存のログ

既存のログというのがS3上に格納されていました。
S3上にあるデータを集計するといえば[AWS Athena](https://aws.amazon.com/jp/athena/)ですね！

これをAWS ECSで集計します。

## AWS ECS

ECSはDockerコンテナアプリケーションをAWS上で実行できるサービスです。またFargateを使えばサーバレスで実行できるのでサーバの管理コストも減らせます。

プログラムは個人的に好きなRubyで書きAWS SDKを使ってAthenaの実行をしました。データベースの作成からテーブルの作成など、すべてSQLライクに書けるので多くのエンジニアでもとっつきやすいのではないかと思います。
が、S3のログの内容からデータ型の設定がうまく行かないとか、思ったよりいろんなポリシーを必要としてIAM周りをバコッと食わせる必要があるとか、サービス間の連携の部分でのハマり事はあるかもしれません。

プログラムからRedisに接続する際の接続情報をセキュアに管理したほうが良いよね、となったときは[ParameterStore](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html)を使うことで難なく実現できました。便利。

肝心の定期実行はCloudWatchEventsのcron式で実現可能です。
ハマりどころとしては、UTCの時間であることや、通常のLinuxのcronとちょっとだけ構文が異なったり、CloudWatchEvents独自の記法があるのが少しだけある程度です。

## RedisのSortedSets

これもドチャクソ便利でした。
過去にMemcacheの利用からRedisに移り変わるときに[ソート済みセット型 — redis 2.0.3 documentation](http://redis.shibu.jp/commandreference/sortedsets.html)を目にしていて「いつかこれ使うぞ〜〜〜:laughing:」という気持ちだったので、ついに使う時が来たという気持ちでいっぱいでした。

上記の日本語ドキュメントは2.0.3時点のものなので存在していませんが、
[zrevrangebyscore](https://redis.io/commands/zrevrangebyscore)が今回最高に使えました。
スコアの高い順にソートして返してくれる、上限下限も設定できるのでスコアによる足切りもできちゃうし、offsetとcountがあるのでランキングのページング機能もお手の物。スコアも返せちゃうので同点だったときにちょっと入れ替えようか、なんてときもアプリケーションで制御可能です。（同点だったときのデフォルトはmemberのASCIIコード順になるはず）

# 最終的に利用したAWSのサービス

- ログ置き場としてS3
- 集計のメイン処理としてAthena
- 実行環境としてECS
- コンテナイメージ管理としてECR
- ECS起動＆ログ出力のためにCloudWatch（Events）
- データの保存先としてElastiCache

このようになりました。

# 結びに

今回は様々な事情からこのような構成になりましたが、LambdaやAWS Batchを使う可能性も全然ありえたとは思います。例えばLambdaのところで触れた中間データも、他のアプリケーションで使い回すことがあるなどの要件や展望があれば採用されていたと思います。
また今回は自分の想定の中にありませんでしたが、多くのWebサービスでも使われているであろうGoogleAnalyticsから集計するという手法もあったのではないかと思います。

AWSのサービスを使い倒して手軽に、安全で、安定した価値提供を行っていきたいですね。