はじめに
最近AWSのサーバーレスサービスで作るWebサービスの雛形を公開してみた*1。小規模サービスならかなり安く (ほぼ無料になるケースも多そう) で運用できるので、割と良い選択になる場面も多いと考えている。今日はその布教記事。
全体構成
全体の構成は下図のとおり。特徴としては以下が挙げられる:
- CDNでフロントエンド (React SPA) の静的ファイルを配信
- バックエンドAPIは API Gateway + Lambda (Express.js)
- データベースはDynamoDB (詳細は後述)
- ユーザー登録が必要なサービスのため、Eメール認証も付けている
- ありがちな非同期ジョブ、cronジョブ実行の仕組みも用意
- AWS CDKで一発デプロイ
こういう構成を趣味サービスで採ることのおそらく一番のデメリットは、学習コストだろう (VMを使った構成とはかなり違うので。) このサンプルを参考に実装したり、一旦そのまま使ったりすることで、その初期学習コストを回避させることができれば良いなと思う。
他にもVMやコンテナ系サービスと比較したサーバーレス特有の考慮点がいくつかあるので、次の節にまとめる。
サーバーレスの考慮事項
AWSのサーバーレスサービスを使う時に考慮すべきことをまとめる。
メリット
サーバーレスのメリットは枚挙にいとまがないが、趣味サービスという観点で実用的なのは以下のようなところ:
- 初期コストが安い。基本的にリクエスト量に応じた従量課金なので、ユーザーが少ないうちはほぼ無料で運用可能。
- サーバーの運用が不要。VMの面倒を見るような作業から開放される。
- 流行の技術なので、業務での活用も期待できる。
一方でもちろんAWSマネージドになる分考えないといけないポイントもあり、以下にそれらをまとめた。他にもあれば教えてください。
タイムアウトの短さ
Amazon API Gatewayの仕様上、Webリクエストは30秒でタイムアウトする。つまり、リクエストを送ってからレスポンスに30秒以上かかるようなAPIは作ることができない。実際レスポンスに1分もかかるようなAPIがあると、ユーザー側も不安になる・UX下がることもあろうから、妥当な制約ではある。
この制約を回避するための常套手段は、非同期ジョブ化である。APIではジョブをエンキューだけしてレスポンスを返し、非同期でジョブを実行する。今回のサンプルではAmazon SQSにエンキューしAWS Lambdaでデキュー・ジョブ実行する形で、これを実現している。
DynamoDBの特性
小規模サービスで最もコストがかかるのは大抵データベース。例えばAmazon RDS Auroraを使うとしたら、最安のインスタンス (db.t3.small) 1台でも毎月5000円くらいかかる。個人サービスにはなかなか厳しい値だろう。
一方で今回使うDynamoDBはリクエスト量に応じた課金なので、小規模ならかなり格安に使うことができる。具体的には、100万リクエストごとに1.5 USDとかそれくらいのコスト感。もし毎月1万アクセスのサービスなら、1円程度で済む計算。
このようにコスト効率の良いサービスではあるのだが、いかんせんNoSQLなのでRDMBSとは使い勝手が違う。具体的にはJOINができないため、情報を1つのリクエストで一括取得したい場合はテーブル設計にそれなりの工夫が必要になる。正直設計だけでも結構難しいし、DynamoDB Streamsなどを使ったそれなりの仕組みを実装する必要もあったりする。
とはいえ小規模サービスなら、強いて1リクエストで必要な情報をすべて取得することにこだわらなくても良いのでは。上記のように、1万アクセスで1円程のコスト感である。仮に1アクセスごとのリクエスト数が2倍になったとしても2円。1円のためにDynamoDBまわりの設計を複雑化するのは非合理じゃないだろうか。ここの最適化にこだわらなければ、意外とDynamoDBも素直に使える*2。
将来的にもしサービスの規模が大きくなったら、その時にリファクタすれば良い。DynamoDBはテーブルをリファクタする仕組みが十分に整っている。もちろん非常に簡単な作業というではないが、それほどの規模になっているのであればモチベーションを湧かせるのは難しくないはず。
コストの上限
サーバーレス系のサービスでよく言われる気がするのは、なまじスケールアウト性能が高いため、もしDDoS攻撃を受けた時にコストが青天井になるという不安。この点について考えてみる。
まずフロントエンドの静的アセットを配信しているCDN (Amazon CloudFront) だが、ここをDDoSするのは攻撃者視点であまりメリットがないと思われる。CDNに対しての攻撃は多くの場合直接サービス品質に影響を与えられないし、課金攻撃だとしてもデータの転送量に対する支払いとなるため、攻撃にかかるコストとサービス運用者に支払わせるコストがトントンになって効率が悪いだろう。
このため、より気にすべきはバックエンドAPIの方だろう。例えば非同期Jobを走らせるエンドポイントだと1回のリクエストでジョブをエンキューできるので、重いジョブの場合は攻撃の効率が良さそうだ。
しかしこれも運用側で対策ができて、LambdaのReserved Concurrencyを設定すれば大体OK。これはLambdaの同時実行数に上限を設定する機能で、要は無限にスケールアウトするような事態を防止できる。このため、攻撃を受けても一定のコスト増加やサービス品質の低下はあるものの、無限にコストが増えることはない。あとはAWSの請求額を監視するSlack通知でも設定しておけば、早めに異常に気づいてそこから対策を考えることができる。
これらのことはDDoS攻撃だけでなく急にサービスが流行りだしたときもおなじことが言えるかもしれないが、その場合は実ユーザーをお金に変換する仕組み (広告など) を事前に仕込んでおけば、運用コスト以上にリターンがあるはず。
とはいえ多くの個人サービスは攻撃を受けることも急に流行りだすこともないだろうから、アーキテクチャをシンプルに保つ意味でAWS WAFなどは入れていない。ここは考え方次第なので、最初からそういう対策を入れておくのも全然アリだと思う。趣味なのでこだわりたいところをこだわろう。
ローカルでの開発しやすさ
Lambdaを使うとローカルで開発しづらいんじゃないという声もよく聞く気がする。実はそうでもなくて、というのは今回 serverless-express というExpress.jsのサーバーをほぼそのままLambda上で動かせる仕組みを使っているため。 これにより、ローカルではただのExpressサーバーとして検証し、そのコードをそのままLambdaにデプロイするということが可能*3。
SQSなどのサービスが絡む部分は、実際ローカルで検証しづらい。ここは自分だとAWS上のリソースを検証用途でも使うようにしている。最近は cdk watch
や cdk hotswap
といったCDKの機能で、AWS上のリソースが絡む検証も非常に高速にイテレーションを回せるようになった。このおかげで待ち時間も短くストレスもない。コストに関しても今回のサーバーレス系サービスなら無視できるレベル。
ちなみにフロントエンドは、SPAの仕組み上CDNで配信してもローカルで動かしても同じ動作になるので、何も考えずにローカルで動作検証できる。そんな感じで、システム全体をローカルから開発している。
塩漬けの可能性
サーバーレスに代表されるAWSマネージドのサービスは、基本的に塩漬け運用はしづらいと言われる。例えば最近だとPython 3.6のEOLに伴ってLambdaの同ランタイムはサポートが終わったので、開発者たちは対応に奔走したものだ (一応、Lambdaがサポート終了した後も関数実行自体は可能なので動作はする。)
ただし、実は今回利用したサービスの場合はそれほどユーザー影響のある変更があるとは考えづらい。AWSサービスは内部仕様は変わることはあれど、ユーザーの目にするAPIが後方互換性を保たずに変更されることは少ないためだ*4。 基本的にAPIはユーザーとの契約なので、みだりに破壊的な変更は加えられないものなのである。このように、完全にサーバーレスのサービスを利用していれば案外塩漬け性能は高いかもしれない。
念のため、一般論として塩漬けは推奨されない (脆弱性に対応できない、サービスを改善しづらくなるなどが理由) ので、モチベーションが消え去ったけどサービス自体は提供し続けたいような場合の最終手段と捉えておくと良さそう。
システム全体のコストは?
AWSは無料利用枠があり、今回使うサービスの多くは毎月一定までは無料で使える (例えばAPI Gatewayは毎月100万リクエストまで無料など)。つまり、小規模な趣味サービスならほぼ永久無料で運用できる。
無料利用枠の使い果たしたあとも、基本的には単純な従量課金なので、リクエスト量がわかれば見積もりは容易。DynamoDBの項でも書いたが、大抵100万リクエストにつき100円とかのオーダーなので、そんなもんだと思っておけば良い。VMとは異なり使ってない計算リソースが維持され続けることもないので、特に利用者数が時刻により変動するようなサービスは効率よく運用できる。
実際運用してみて
まだ限定公開ながら、この技術スタックで稼働しているサービスがある。 今はかなり小規模なので、目論見通りほぼ無料枠に収まっている。
一点想定外だったのは、Amazon Elastic Container Repository (ECR)の利用料金。素のLambdaランタイムではデプロイの際Node.jsのバンドリングに時間がかかるなど不便だったので、この時はDocker Lambdaを使っていた。
CDKだと cdk watch
という機能を使って、ローカルのファイルに変更があるたびに自動で新しいDockerイメージをビルドしてデプロイするということができるのだが、これによりECRへの保存量が多めになっていた模様。
結果的には、1ヶ月で10GB程度のストレージを消費していたため100円くらい課金された。やっぱり運用してみるまで分からないこともあるね。ここはユーザーの増加に伴って増える金額ではないので、許容している。
まとめ
趣味サービス、AWSのサーバーレスで作ってみても良いんじゃないという話をした。他の手段と同様に考慮すべき事項はあるが、かなり有効な選択だと思う。サンプルも用意しているのでぜひお試しください。