こんにちは、SCSKでインフラエンジニアをやっております潮です。
今回、最近触れる機会のあったReactを使って、サーバレスなウェブサイトをアマゾンウェブサービス(AWS)上に構築してみました。
全体構成
今回は、オンライン英会話での勉強時間を記録・表示することができる機能(オンライン英会話応援機能と呼称)と、英文ニュース記事をクロールしてきて翻訳・表示する機能(英語ニュース提供機能と呼称)を持つウェブサイトを作りました。
サイトのページはJavaScriptで記述していて、.jsファイルのホスティングにはAmazon Simple Storage Service (S3) + Amazon CloudFrontを利用しています。開発環境にはAWS Cloud9、コード保存にはAWS CodeCommitを利用し、コードの更新があるとAWS CodePipelineが変更を検知し、AWS CodeBuildでビルドしてAmazon S3にデプロイする、というCI/CDの仕組みになっています。
サイトではユーザ認証を行っており、そのユーザプールはAmazon Cognitoに格納しているのですが、Undifferentiated Heavy Liftingを削減するため、AWS Amplifyが提供するログインインターフェースを活用することにしました。そのため、Amazon CognitoはAWS AmplifyのAuthリソースとして存在しています。なお、今回は極力色々なサービスを直接触ってみたいという方針だったので使っていませんが、AWS Amplifyでは上述のCloudFront+Amazon S3のサイトホスティングやCI/CDの仕組みを提供することができ、マニュアルでこれらを作っていくよりも早く構成することができます。
オンライン英会話応援機能も英語ニュース提供機能も、データの格納・表示には、Amazon API Gateway+AWS Lambda+Amazon DynamoDBの構成を利用していますが、ログインしていないユーザでもAPI GatewayのAPIを叩けてしまっては困るので、API GatewayにAmazon Cognitoと連携したオーソライザーを作り、認証トークンを渡さないとAPIが利用できないように認証をかけています。なお、DBアクセスについてはGraphQL(AWSではマネージドサービスとしてAWS AppSyncを提供)を使う構成も考えられ、もしクエリの数が増えてくるようであればこちらの方が使い勝手が良いかもしれません。
英語ニュース提供機能については記事をクロールしてくる仕組みが必要なので、記事をクロールするLambda関数とそれを翻訳するLambda関数を作り、これらをAWS Step Functionsでワークフロー化して実施するようにしました。スケジューラにはAmazon EventBridge、翻訳にはAmazon Translate、クロールにはpythonモジュールであるRequestとBeautifulSoupを使いました。
ユーザインターフェース
上の図はオンライン英会話応援機能のインターフェースです(ログイン後)。
今回はタイトルにもある通りReactを使ったSPAサイトとしました。機能間の画面遷移の利便性等を考えてReact Routerを導入し、レスポンシブデザインにしたかったのと、最低限の見た目を整えたかったので、React-Bootstrapを使用し、画面幅が一定以下の時(スマホで閲覧した時等)は、「システム一覧」の下にオンライン英会話応援機能の機能部分が配置されるようにしました。
仕組み上、ユーザ操作によるデータ表示や更新をするWebサーバやAPサーバが存在しないので、データの取得・更新はAPI経由で行うことになります。今回は、データの表示はGETメソッド、更新(追加・削除)はPOSTメソッドで行うことにし、API自体はREST APIとして単一にしました。GET・POSTメソッドでのアクセスの裏にはそれぞれLambda関数を作ってあり、実際のDBアクセスはここに記述しました。追加処理なのか削除処理なのかの区別は、POSTデータのBody(JSON)でope_fを処理の識別子として追加し、Lambda側でif文による場合分けをすることで行っています。
機能が動作するエリア以外はオンライン英会話応援機能と同じものを使いまわしています。API Gateway+Lambda+Amazon DynamoDBで表示するデータを取得している点についてもオンライン英会話応援機能と同様です(図では省略)。英文記事の取得と翻訳においては、Amazon EventBridgeでスケジュール実行されるAWS Step Functionsのステートマシン中で、まず英文ニュース記事をクロールするLambda関数を実行します。記事取得はRequests+BeautifulSoupで行い、そのデータはAmazon DynamoDBに格納すると同時に、戻り値としてAWS Step Functionsに返します。この戻り値を入力として英文記事翻訳のLambda関数を実行し、翻訳結果をAmazon DynamoDBに格納します。
この処理の別法としては、DynamoDB Streamを使って、英文記事取得関数がAmazon DynamoDBに翻訳データを格納することをトリガーとして英文記事翻訳関数をキックする、という方法が考えられます。この方法はLambdaのチェーン実行が不要になるのでAWS Step Functionsを使う必要がなくなる、というメリットがありますが、今回のいち機能のためにAmazon DynamoDB全体の設定であるDynamoDB Streamをオンにする、というのは不要に影響範囲を広げる点で望ましくないだろう、と考えたため、上述の仕組みとしました。
なお、今回AWS Step Functionsを使用してはいますが、2関数のチェーン程度であればわざわざAWS Step Functionsを使わずとも、直接Lambda関数同士を連携させた方がシンプルで良いアーキテクチャになると思います。AWS Step Functionsを使った理由は、単に使ってみたかったというのが1点と、今後処理を追加する場合(例えば記事を格納したことをメール通知する、など)に拡張が容易になる、というのがもう1点です。
費用
今回の構成はサーバレスにしているので、ランニングコストはほとんどかかりません。大量のPVがあるようなサイトでは当然無視はできませんが、常に起動しているコンピューティングインスタンス(Amazon Elastic Compute Cloud (Amazon EC2)やAmazon Relational Database Service (Amazon RDS)など)が構成上存在しておらず、今回のような場末のサイトにPVはほとんどないため、コストとしては微々たるものです。今回はドメインを取得しましたので、その費用が年間で10USD前後(ドメインによる)かかっていますが、AWSではSSL証明書はCloudFrontで使用する分には無償提供されますので(Amazon Certificate Manager (ACM)から発行できます)、SSL証明書の費用はかかりません。
構築のコストも個人のお小遣いの範囲で十分賄える範囲です。構築時特有の費用としては、AWS Cloud9の利用費用(=内部使用されるAmazon EC2インスタンス利用費用)、AWS CodeBuild利用料などがあり、個人でこの程度のサイトの開発をしている前提であれば、気にするとすればAWS Cloud9利用費用かと思います。AWS Cloud9利用費用は、使用しているAmazon EC2インスタンスタイプとその起動時間によるのですが、AWS Cloud9の機能として、利用しなくなってから指定の時間が経過したらAmazon EC2をシャットダウンする、という機能が提供されていますので、是非活用すべきです。参考までに、今回のケースではt3.smallを使用していましたので、1か月起動しっぱなしにすると、概ね20USDほどかかります。
感想
インフラエンジニアである私としては、コーディングが最も時間のかかるタスクで、以前少し触れたことがあったとはいえ、Reactのレンダリングのトリガーの設定やStateの理解などはかなりの時間を要しました。一方で、AWSのアーキテクチャ部分ではあまり時間はかかりませんでした。理由としてはGUIが充実している点が大きく、今回はあまり積極活用しなかったAWS Amplifyを使えば、さらに基盤整備の時間は短縮できるのではないかと思います。
少し文中でも触れましたが、今回の構成では使っていないサービスに一部を入れ替えてみる(例えばAPI Gateway+LambdaをAWS AppSyncにする、など)、というのもやってみたら面白いのではないかと思います。
今回の記事が読者の皆様の参考になれば幸いです。最後までお読み頂き、ありがとうございました。