【AWS】漫画Webサイトをサーバーレスで作ってみたVer2.0


前回の続き。
早くも構成を変えましたという話。

漫画サイトはこちら:RESIGN THREAT


ハイレベルアーキテクチャ

便宜上前回の投稿の構成をVer1.0として話す。

  • Ver1.0ではLambdaの前段にELBを据えていたのだが、この子が思ったより金食い虫だったので、安いと評判(?)のAPI Gatewayに切り替えた。
    ELBのときと同様で、API GatewayではAPIキーを発行し、CloudFrontにカスタムヘッダとして登録することで、API Gatewayのオリジンへの直接アクセスを基本防止させた。
  • Athena、DynamoDBを追加した。
    それに伴いS3のバケットをひとつ追加。
    Ver1.0ではCloudFrontのログはS3に垂れ流しでライフサイクルルールも何も未設定だったが、さすがになんかするかと思って色々試行錯誤した結果、
    1. CloudFront自身が出力する生ログを別のバケット(この絵でいうところのWork-Bucket)にする
    2. Work-BucketへのオブジェクトPUTをトリガーにLambdaを動かして、Athena用に意識したパーティションを付けてMain-Bucketに移動
    3. 別途用意した日次のLambdaが日本時間のAM1:00にMain-Bucketに入ってるログを集計してDynamoDBに書き込み
    4. ログページへのアクセスがあったときにDynamoDBからデータ取ってきてレスポンス

    …という構成になった。

構成変更1.ELB→API Gatewayの変更

Ver1.0ではLambdaの前段に置いていたのはELBだったが、今回それをAPI Gatewayに変えた。
というのもELBの利用料金が思ったより高かったからだ。
こいつの利用料金がもっと安ければ別にわざわざAPI Gatewayに移らずともELBのままでも良かったのだ。
構成変えるの面倒くさいし。
API GatewayがELBより安いっていう確証はなかったが、なんとなく「API Gatewayは安い」という各所からの断片的な情報を頼りに、そっちに移行してみることにした。
API Gateway→Lambdaを一度作ったことがあるというのも腰が重くならなかった理由の一つでもある。

料金を比較してみよう

(1)サイト開設した8月中旬~8月31日までの約2週間のCostレポート

(2)8/31の終わりにAPI gatwayにへ変更してから9月中旬までの約2週間のCostレポート


(1)の画面ショットで、わざとしらくELBの金額の棒にカーソル合わせてるが、ELBだと毎日必ずこの金額が計上された(この日のレートで1.44$)
毎日固定で1.44$かかる計算で、1ドル105円(※2020年9月のレート)でひと月30日換算で計算すると、結果はなんと4,536円になる。
たっか!!
せっかくNAT Gatewayの高額請求から逃れたと思ったのに今度はてめーかELB!!
という感じ。
4536円なんて推しバンドのツアーグッズとかならカスみたいな金額なんだけどこういうところは「たけーんだよ!!」と思っちゃうのは不思議だね。(?)
俺は自身の漫画や絵を載せるだけのただの個人用趣味サイトを存続させるためだけに4536円も払うつもりはない。
究極無料で運用したいくらい基本的にケチな人間なのだ。
一応様子見で8月中はこのまま見守ったが、以後もこの金額を払うつもりはなかったので、8月中でおさらばさせていただいた。

なお、ELBはNAT Gatewayと違って「置いておく」だけで金のかかるものではないようだ。
ELBを使ったIN/OUTの通信が発生しなければずっと0円のようである。(※ただ個人の体験に基づくものなので公式情報ではない点をご理解ください)
通信した量に応じて課金が発生する、という仕組みのように見える。
ただ毎日必ず1回はCloudFrontのキャッシュがきれてLambdaまで通信が行くことがあるので、その分の通信料がほぼ一定だった(少なくとも次の課金金額を超えるほどの通信料ではなかった)ため、毎日同額が課金されている、ということのようである。
まあいずれにしても1日1.44$を払ってまで君を生かしておく価値は俺にはない、というか君のことはそこまで使いこなせないのだ、ごめんね。
サヨウナラELB…


で、(2)の構成だが、こちらもわざとらしく棒グラフにカーソル合わせてるが、こちらはWAFの金額で、1日0.2$。
↑と同じ計算したとしてもひと月630円にしかならない計算である(個人的にはそれでも少し高いんだけどね)。
ドメインの維持費で毎月1日に更新料として2.5$かかるので、ひと月分で合計すると1000円弱になるが、それでもELBを配置しておくだけに比べたら圧倒的に安い。
そもそも肝心のAPI gatewayそのもののは棒グラフに表れていない=つまり0円(!!)である。
やすい。
こういうのを求めていた。
まあ大したアクセス数ではないということと、一応まだ12か月の無料期間内にいるというのもアドバンテージになってそうだが、それにしてもこの構成の安さは魅力的だ。
もうAPI Gateway以外使う気になれない。
少なくともELBには手を出せそうにない。
大幅値上げとかしない限りはしらばくこの構成で行くと思う。

構成変更2.ログページ追加に伴うAthena、DynamoDBの追加

Athena+LambdaとDynamoDBを使って実装したページ。
しかし最初はAthenaもDynamoDBも積極的に使うつもりがなかった。
やろうと思えばAthenaもDynamoDBも使わずにできるよな、という思惑があったからだった。
たかだかログを見るためだけのページに、あまり大掛かりな凝った仕組みを使いたくない、という思いがあったのも理由の一つ。
まあ結果的に使うことになったんだが。
ここではその苦悩の経緯を紐解く。

そもそもログページで何をしたいのか

まずは自分自身で要件定義wである。
俺はログページを作って何をしたいのか?を自分に問いかける。
Webサイトを持ってる人からすればやはり自サイトへのアクセス数は気になるものである。
しかし単純に「日に◯◯件アクセスがありました」だけでいいのか、「日に◯◯件で、内訳は、Aページに△△件/Bページに■■件…」くらいのものが欲しいのか、それとも集計結果ではなく生ログが欲しいのか?
色々ある。

古き良きインターネッツ時代のアクセスカウンタでいけば「日に◯◯件アクセスがありました」が累計・当日・前日くらいあれば十分なのだろうが、個人的には一歩踏み込んで「日に◯◯件で、内訳は、Aページに△△件/Bページに■■件…」が欲しかった。
CloudFrontのログ項目を観ればその辺を分類して集計することは(多少集計の手間が必要だが)十分可能に思えた。
というわけで、最終的にユーザーにどう見せるのかは別にして、「日に◯◯件で、内訳は、Aページに△△件/Bページに■■件…」を目指すことにした。

Athenaを使わずにやろうとしていたこと

CloudFrontのログはS3にポンポン吐かれているので、S3へのPUTをトリガーにLambdaを呼び出し、中身を開いてその場で集計し、S3上に別途保持しているテキストファイルかなんかにその値を計上。
こうすることで多少タイムラグはあるが当日のアクセスカウンタが出来上がる。
日が経てば自動で昨日、一昨日、の即席アクセスカウンタの出来上がりだ。
累計情報は別途どこかで保持しておいて、直近のアクセスはS3のライフサイクルルールで1か月とかで消せば良い。

今思うとかなりガリガリしたことを考えていたなと思うが、Athenaを使ってCloudFrontのログを集計するやり方を知らなくて、一方でS3に吐かれたCloudFrontのログ-gzファイルを、Node.jsのzlibで開いて中身を集計するのは、手元で軽く実装したツールで実装検証ができていたので、それがまたこのやり方をやろうとする心構えに拍車をかけた。
まあS3からGetしすぎて「お前のFree Tierもうすぐ切れるで」って警告メールきたりしたのだが(ローカルからAWS SDKを使って色々APIやるもんじゃあないなと思い知った)

ただ真面目にこの案をやろうと思ったところでいくつか考えなければいけない点や確認しておくべき点が出てきた。

  1. CloudFrontのログは不定期にS3に吐かれるので、S3のPUTをトリガーにした場合、前のLambda処理が終わってないうちにまた次のLambda処理が動くようなケースがあるかもしれない。
    Lambdaでの処理(集計)結果をS3に吐くことを考えていたので、S3上のファイルの排他ができるのかが気になった。
    前のLambdaが終わってないうちに次のLambdaの結果更新をかけようとするとデグレや集計漏れが起きる可能性があるからね。
    で、調べてみるとS3上のファイルを更新する場合に「排他」という考え方はないようだった。(あるのかもしれないが見つけられなかった。ちなみにオブジェクトロックという機能があったが求めているようなものではなかった)
    まあ、それほどアクセス数もないサイトなので、いらぬ心配である可能性はあるが。
    とはいえ、Lambdaで結果を集計するのはいいにしても、集計結果の出力先をS3にするのはなんか少しイケてないな、と思った。
  2. 集計処理をLambdaにする場合、なんか集計の条件を変えたいとか、そういう仕様変更wが発生するたび、毎回Lambdaを変更する必要があり、手間だと思った。
    加えて、CloudFrontのログがS3に吐かれたことをトリガーにするため、集計する対象が単一のCloudFrontのログになるので、「その回」分しか対象にできない。
    よって集計条件を変更した場合の過去分の遡及が限りなく難しい(できなくはないが糞面倒である)ことに気づいた。
    毎日日ごとの累計カウンタを持ってる程度ならこの心配はないのだが、実際のところ、各コンテンツページごとにどれくらいのアクセスがあったかというのを知りたかったので、今回のように「log」というコンテンツページを増やしたり別の条件を追加するたびにこういうことを考えなければいけなくなるのは非常に面倒だな、と思うに至った。

やはり「集計」等の観点ではDBっぽいリソースが欲しくなる。
そしてググってみるとAthenaを使ったやり方がいくつか出てくる。
最初は「DBもないのにSQL??」と思ったし、パーティションに「year=2020」のようなGETパラメータのような名前を付けるのにすごく違和感があり「本当にこんなことするのかあ?」と懐疑的だったし、かといってノンパーティションでフルスキャンするとスゲー量の課金が発生するぜとかいう恐怖の記事もいくつか見つけて、Athenaを使うのに戸惑っていたが、なんとなくQiitaやらブログやら参考にしたり真似していじくってたら、他の「いいやり方」を模索しているより先にAthenaでの集計のほうがいい感じにまとまってきてしまい、結果的にAthenaを使う方式に至ってしまった。
セフレとなんとなく関係続けてたら子供できちゃって結婚しちゃいましたみたいなグズグズの展開に近しいものを感じた(?)

ただまあ実際使ってみるとこれはやはり便利で、CloudFrontのログを集計するならこのやり方がなんだかんだ一番いいのだろうなと感じた。
この辺は実際自分でやっていくつか蓄積できたノウハウをQiitaにあげたので参考になれば。

Athenaで何発もクエリを投げるのを避けたい(課金対策)ので、今のところ集計は日に1回だけにしている。
このため「今日のアクセス数がいくつか」を当日内でWebページ上で把握することはできない。
ここは個人的には一つの課題であり、後述するCloudFrontdのリアルタイムログ機能を活用してもう少し改善できないかは、今狙っているところではある。
現時点では、そこまで大量のアクセスのあるサイトではないし、「当日のログ」は、Athenaのクエリで裏から探り当てることはできるので、それで満足しているので、今すぐにどうこう、とする予定はないのだが。
まあ、おいおいやっていくつもりである。

DynamoDB導入に至るまでに無駄に色々試行錯誤した経緯

DynamoDBを使いだした今となってはなんで抵抗してたのか馬鹿馬鹿しい感じもするが、心理的に何か抵抗していた部分でいうと

  1. ログ以外のコンテンツはLambdaのデプロイパッケージに含めてあるのに、ログだけ外のリソースに持たないといけないというガチャガチャ感がなんとなく嫌だった
  2. コネクションプールのような概念がない(と思ってるが正確には知らない)サーバレス環境において、リクエストの度にDynamoDBに接続して表示するつくりだと、Lambdaのデプロイパッケージにコンテンツ情報を抱え込んで動かしている他のコンテンツに比べれば圧倒的に遅いはずで、そういう「(他と比べて相対的に)遅い」とわかりきってる処理を自ら作るのが嫌だった
  3. CloudFrontがいるんだし、そもそもログページというコンテンツの性質上大した数のリクエストは来ないだろうが、とはいえ、正確なリクエスト数が見積もれないまま、つまりDynamoDBにどれほどのリクエスト数が来るのかわからないまま使いだすのに漠然とした恐怖感があった

といったところか。

なので最初は「DynamoDBを使わずに、以下にしてアクセスログの集計結果をWebサイト側に渡すか」を色々考えていた。
集計結果をJSONにしてS3に出力し、画面側からAjaxとかで読み込むとか、集計結果をtextにしてS3に出力し、画面側からObjectタグで読み込むとか、なかなか香ばしいガリついた実装を考えて、実際両方ともPoC(というか?wとにかく実現性検証)くらいまでのことはやってはいたのだ。
ただ、実装・検証してみた結果、前者(JSONを吐いてAjaxで読み込み)は、他コンテンツがSSR形式なのにログページだけSPAっぽくなってしまうのに強い違和感があったり、後者(TEXTを吐いてObjectタグで読み込む)は、スタイルを適用する場合HTMLタグを中に書く必要があるとのことでアクセスログの集計結果の管理形態としてふさわしくないなと思ったり、という理由等から、実際自分でやってみて「イマイチ」感を感じて、やめた。
やはりこういうのはなんらかの形でDBが欲しくなるものだ。

それに、未知なる技術領域への興味というか好奇心というのが、一応技術者らしくもとから心の根底にはあって、最終的にはそれが勝った(上述していた抵抗のポイントをこの好奇心で押し殺した)形にはなると思う。
1.は、まあそれはそうだが、かといって集計結果で毎回Lambdaのデプロイするってのも無理がある感じするし、漫画や絵等とログではコンテンツの性質がそもそも違うんだから、それは仕方ないんじゃないの、と納得させて、
2.は、これもまあそうなんだが、ログページは絶対にメインコンテンツではないんだから、ここが遅くたって特別問題あるまいと思ったのと、人柱がいれば(人柱も大抵俺だしw)あとはCloudFrontがいい感じにキャッシュをレスポンスしてくれるから一般的には速いよ多分、と思って納得させた。

3.の部分が心理的には一番ウエイトが大きかったかもしれない。
1.2.は「好奇心」で無理やり納得できるレベルの「ただの食わず嫌い」な部分があったが、実際に課金が発生する可能性に対する抵抗は現実問題なので結構深刻なのだ。
なるべく安く動かしたいケチな人間にとってこの問題は死活問題だった。

DynamoDBで最初にテーブル作るときのマネジメントコンソール上の選択モードがプロビジョニングで、RCUとWCUを入力させられるのがそれに拍車をかけていた。
Calculatorもこの数値いれないと料金見積もれないし。
いや知らんよ現時点でそんな数見積もれないよ、という。。。
それにこれ超えたらどうなっちゃうの?超絶課金発生すんの?スケーリングせずに死ぬの?とか、その辺よくわからずもんもんとした状態が続いていて、不安が解消されなかった。

オンデマンドというモードは、もっとこう、よく知らずに勝手に大規模なシステムで使うものだと勝手に思ってたのだが、どうもそうではないらしい。
俺のように「どれくらいのアクセスが来るか分からない人向け」のようだ。
だったら最初にテーブル作るときの設定をプロビジョニングじゃなくてオンデマンドにしておけばいいのに、とは思う。
まあちゃんと読んでなかったのも悪いんだけども。
流れてくるリクエスト数とかほぼ完全に把握してからDynamoDBを作ろうと思い当たる人ってそんなにいるのかなあ?
初期の選択モードとか「おすすめ」の選択モードをオンデマンドにしといたほうがいい気がするんだが。

料金に関しては、こちらのサイトで見積もってみたのだが、オンデマンドモードだと、相当多めの容量と読み込み・書き込み単位で見積もってみてもひと月0円だった。
ほんとかこれ~?って感じだがこっちで試してみてもそうだったので実際そうなのかも。
実質無料ってことに。。。
まあでも今のところ1テーブルしかないし、その1テーブルも、書き込みはバッチで集計したデータの書き出しで1日1回、読み込みはCloudFrontがある分全部はLambdaにいかないことを考えるとせいぜい1日2~3回(予想)だからな。
こんな程度の使い方だとまあこんなもんなのかもしれない。
大規模にOLTPっぽく使っていくと変わっていきそうではあるな。

というわけで3.に関しては「オンデマンドモードという存在を知った」ことと「見積もってみたら無料だった」ことで納得どころか不安が解消された形になった。

次に目指すべきところは

CloudFrontには「リアルタイムログ機能」なる機能がつい最近リリースされているようである。
ログの出力が「S3に不定期」ではなく「Kinesisにほぼリアルタイムで」になるようだ。
Kinesisは名前くらいしか知らなくて使ったことがないのだが、見てる感じこれをそのままLambdaに流すことは可能っぽいし、DynamoDBを連携させれば、今は出来ていない「当日中に当日のアクセス数を集計」が可能になるのではと期待している。

例えばCloudFront→Kinesis→Lambda→DynamoDBの流れで「当日のアクセス数」を集計して結果を書き出せれば、今のようにS3にCloudFrontのログを吐いておく必要性がなくなる。
日次でLambdaを動かす必要もなくなり、Athenaもいらなくなる。(まあAthenaで生ログ見るのは分析と個人的興味wでしばらく続けそうだけど)
今の構成だと、CloudFrontが出力した「生のフラットなログ」を、一度Athenaで集計する用にLambdaでパーティション付けて別バケットに移動させる処理を組んでる都合上、S3バケットを最低2つ使う必要があり、これがちょっと個人的に嫌なのだ。(ライフサイクルルールとかポリシーとかの管理が面倒だからあんまりバケット持ちたくない) 「たかがログのためにあまり複雑な構成を組みたくない」という最初の動機に、正直に言うと若干反している。(まあ一度作って把握してしまうと「複雑な構成」ではなくなってしまうという点があるが…)

ただ今は連携先のKinesisがUS-EAST-1リージョンにしか対応していない等制限があるようなのでちょっと様子見する。
Kinesisのこともよく知らないし。
まずは簡単な検証を手元でやってみてからかな。
それにしてもCloudFrontは証明書もUS-EAST-1で作った奴しか使えないとか、何かとUS-EAST-1縛りが多いな。
もしかしたらKinesisもずっとUS-EAST-1限定だったりして。 そうなるとLambdaもDynamoDBも全部US-EAST-1で作らなきゃいけなくなる、のかな?
それともKinesisってリージョン跨いで他リソースに流せたりする?
う~ん。
色々調べないといけないことがある。
そのうち。

おわりに

とりあえずVer2.0としてリリース完了である。
正確には9月の上旬~中旬には一通り完了していたが、まあ、ブログ記事としてまとめたというのを一つの区切りにして。

まだVer1.0時代からの積み残し課題wはあるし、今回変更した構成の中で改善したいところや試してみたいところは出てきているので、漫画と並行して色々やっていきたいとは思っているが、一応稼働当初のゴタゴタ期は抜けた気が個人的にしており、今後爆発的な課金増やアクセス数の傾向変化がない限りは、細かい部分での変更はあるにせよ、基本的にはこの構成でしばらく様子を観ようと思う。

前回も思ったけど、こういう風に、要件定義して、設計して、検証して、実装して、という「システム開発」を自分一人でやるのは楽しいな。
これはこれで本当にいい趣味だと思う。
そこに「漫画」や「絵」というもう一つの趣味を合流させられたのもでかい。
なにかWebサイトをつくるときに、目的もなくただテストやデモ用に作りました、じゃ面白くないし、Webサイトは何か作りたいけどそうした「具体的なサイト構築の目的」を見いだせないで悩んでいるという人もTwitterで見ている限りではちょこちょこ見かけているので、そこにもう一つの「漫画」や「絵」という趣味を利用できたのは個人的にラッキーだと思った。
漫画も絵もサイトもまだまだ素人気分の抜けないレベルものだが、これはこれで個人で楽しんでやらせてもらってるのでw、今後も飽きないように続けていきたい。