はじめに
AWSのアーキテクチャ設計って難しいですよね。私も今でこそ少し慣れてきましたが、AWSを学び始めた時はどこから手をつければいいのか混乱した思い出があります。誰かのものを参考にしようとネットでアーキテクチャ例を検索しても出てくるのは1つのサービスに絞った解説か、企業事例としての大きなアーキテクチャで困った思い出があります。
そこで今回は新卒研修のチーム開発で作成したサーバレスなAWSアーキテクチャを段階別でそれぞれの詰まりポイントと共に紹介していきます。AWSでのサービスの構築初期からどのように拡張していくか参考になれば幸いです。
要件
はじめに今回作成したアーキテクチャの要件を記載します。この後のアーキテクチャで作りたいものとしてざっくりと理解いただければ幸いです。
サービスは生成AIを活用したSNSです。Webサイトとして動作し、ユーザーは自身の書いた文章をAIによって変換し、その変換後の文章を投稿します。各投稿にはコメント欄がついており、ユーザー同士で会話ができます。誰が会話しているかを明らかにするため、メールアドレス、パスワード、ユーザー名のみの簡単なログインが必要です。
以後の記事ではこの要件を前提にアーキテクチャを構成していきます。
開発初期
開発初期の詰まりポイントと解決策
まずはじめにAIによる変換の部分に注目し、AWS LambdaからAmazon Bedrockに対して生成要求できる関数を作成しました。少し詰まった部分として、ap-northeast-1リージョンにあるモデルに対してInvokeをしようとした時にエラーが発生しました。(この記事を書いた時点で)東京リージョンではまだ利用できない操作だったようで、us-east-1のリージョンに変更することで解決しました。
次にAPI Gatewayとの接続を行いました。ここではメッセージの受け取りで詰まりました。リクエストボディやクエリパラメータの取得には複数の方法があり混乱しがちですが、そのLambda関数をAPI Gatewayとの接続のみで使用し、他のサービスなどとの連携で再利用しない場合は、API GatewayはRest APIにしてLambda統合を有効化し、Lambda関数側のプログラムで event.get('body')
という呼び出しで受け取れるように調整すると良いでしょう。
他のサービスと連携する場合はLambda統合をやめて、マッピングテンプレートを使用してAPI Gatewayから渡されるデータの方を加工することで、Lambda関数を変更することなく連携できます。
フロントエンドにはReactを使用しました。まずはローカルからAPIを呼び出せることを検証し、その後にS3&CloudFrontという構成にすると良いでしょう。
詰まりポイントとしては、React Routerを使用している場合、Reactのビルド後のdistをS3に上げてCloudFrontからアクセスする際にエラーになります。これはS3にアップロードされたファイルがindex.htmlとjsのファイルだけなのにも関わらず、パスパラメータのフォルダを参照しようとするのですが、実際にはそのファイルがないためにエラーとなる状況です。
解決法としてはCloudFront Functionsを利用してURLを変更することで、パスパラメータはそのままにアクセス先をindex.htmlにすることができます。
開発初期で参考になった記事
この段階で参考になった記事を以下にまとめます。
開発中期
開発中期の詰まりポイントと解決策
大きな変更点は、DynamoDBが追加されたことです。今回はDynamoDBのシングルテーブル設計を行いました。この設計をするためにGSIという概念が必要になります。このGSIの理解に最も時間を要しました。後述の参考記事も役立ちますが、GSIは新たなPKとSKを張ることによって視点を変えるという考え方が一番理解しやすいでしょう。Black Beltもとても分かりやすいのでぜひ見てみてください。
私は以下の構造でやってみました。
種類 | PK | SK |
---|---|---|
投稿 | POST#<postId> | DETAILS |
コメント | POST#<postId> | COMMENT#<createdAt>#<commentId> |
いいね | POST#<postId> | LIKE#<userId> |
ユーザ | USER#<userId> | PROFILE |
この構造により、例えばユーザーごとの投稿一覧や、投稿ごとのコメント一覧を効率的に取得できるようになります。
またDynamoDBに対してアクセスを行うLambda関数は5つ作成しました。これはAPI Gatewayのリソース(APIのパス)につき1つ作成しました。理由としてはLambda関数のコード行数を減らすためと、責任分離を行うためです。これによってプログラムの変更が容易になっています。
また実際にこの方式でやったことによって、APIのパスが変更になってもすぐに変えることができるメリットもありました。
API Gatewayに関連することとしてもう一つ変更点があります。それはAPI GatewayをCloudFrontから提供していることです。これには2つ狙いがあります。
1つ目の狙いはWAFでの管理の統合で、アクセス制限やリクエストブロックのACLを導入する際にCloudFrontからまとめて管理することができるようになります。
2つ目の狙いはドメインの統合で、次の開発後期でドメインを取得する際にAPIに対して別でドメインを適用するのは管理しづらくなるかと思ったのでCloudFrontからの提供にしました。
開発中期で参考になった記事
開発後期
最後に開発後期です。もともと作っていた図からとってきたので色々増えてます。
開発後期の詰まりポイントと解決策
ドメイン、ログイン機能とCloudWatchによる監視が増えています。
この段階ではドメイン登録を行いました。中期でCloudFrontにまとめられているので、複雑な設定は不要でした。SSL証明書を発行する時に、Route 53にCNAMEを設定しないといつまでも作成されないので注意しましょう。
Cognitoに関しては、ユーザー情報をDynamoDBで管理したかったためLambda関数を起動して書き込む仕組みを追加しています。
CloudWatchはまずLambda関数のレイテンシとエラー率だけダッシュボードで確認できるようにしました。
各種周辺のサービスが増えることによって保守性が上がっています。
ちなみにここまでの内容で1日あたり2ドル程度のコストになっています。
CloudWatchが0.98ドル、WAFが0.5ドルかかっているので、ほぼその2つしかコストがかかっていないですね。
個人で作るには少し高く感じるかもしれないので、CloudWatchのモニタリングは無くすことも検討できるでしょう。
開発後期で参考になった記事
おわりに
以上、開発初期から現在までのアーキテクチャの変遷と詰まりポイント、参考になった記事をまとめてみました。
今回はIaC(Infrastructure as Code)やCI/CDについては言及していませんが、ここから先はLambdaのコードをS3で管理したり、CDKでリソースの管理をするといった進め方をするとさらに管理をしやすくなるでしょう。
サーバレスであまりコストをかけずに入門してみたところから、本格的なサービスに至るまでの架け橋となる記事になれば幸いです。