Google Cloud Functions と MongoDB Atlas を併用するためのベスト プラクティスとチュートリアル
Google Cloud Japan Team
サーバーレス機能をサードパーティ データベースに接続することにより、クラウド アプリケーションの効率と費用対効果を改善できます。
※この投稿は米国時間 2023 年 4 月 15 日に、Google Cloud blog に投稿されたものの抄訳です。
デベロッパーの間で次第に人気が高まっているサーバーレス アプリケーションは、アプリケーション ロジックとデータ ストレージを処理するための費用対効果の高い効率的な方法を提供します。Google Cloud Functions と MongoDB Atlas の組み合わせは、サーバーレス アプリケーション構築で高い人気を誇るテクノロジーです。
デベロッパーは、Google Cloud Functions を使用することで、基盤となるインフラストラクチャを管理しなくても、データの変更や HTTP リクエストなどのイベントに応じてコードを実行できます。したがって、スケーラブルでパフォーマンスの高いアプリケーションを簡単に構築できます。一方、MongoDB Atlas は、グローバルに分散された可用性の高いフルマネージドのデータ プラットフォームを提供します。したがって、デベロッパーは信頼性の高い安全な方法でデータを簡単に保存および管理できます。
この記事では、Google Cloud Functions でデータベースを操作するための 3 つのベスト プラクティスを紹介します。最初に、グローバル スコープでデータベース接続をオープンするメリットを確認します。次に、データベース オペレーションをべき等にして、イベント ドリブン関数でデータの整合性を確保する方法を取り上げます。最後に、データを不正アクセスから保護するために安全なネットワーク接続を設定する方法について説明します。以上のベスト プラクティスを実践することにより、データベースとシームレスに連携する信頼性の高い安全なイベント ドリブン関数を構築できます。
前提条件
このチュートリアルを実践するための最小要件は次のとおりです。
データベース ユーザーと適切なネットワーク構成が存在する MongoDB Atlas データベース。
課金が有効になっている Google Cloud アカウント。
Cloud Functions、Cloud Build、Artifact Registry、Cloud Run、Logging、Pub/Sub API が有効になっていること。必要な API を有効にするには、リンク先をご覧ください。
この記事で紹介しているテストをご自身でお試しになれます。最初の 2 つの例については、MongoDB Atlas と Cloud Functions のそれぞれの無料枠を利用して試すことができます。最後の例(VPC ネットワークまたは Private Service Connect の設定)については、有料の Atlas 専用データベースを設定し、おなじく有料の Google Cloud 機能を使用する必要があります。
グローバル スコープでデータベース接続をオープンする
MongoDB に接続する従来の自己ホスト型アプリケーションを構築しているとします。データベースと通信する必要が生じるたびに、新しい接続をオープンしてすぐにクローズする方法もありますが、接続をオープンまたはクローズするたびにデータベース サーバーとアプリの両方に追加のオーバーヘッドが発生します。データベースにリクエストを送信する際に、毎回同じ接続を再利用する方がはるかに効率的です。そこでアプリの起動時に MongoDB ドライバを使用してデータベースに接続し、その接続をグローバルにアクセス可能な変数に保存して、リクエストの送信に使用するのが一般的な手法です。アプリが実行されている間、接続はオープンなままです。
より正確に言うと、接続時に MongoDB ドライバが接続プールを作成します。これにより、同時に発生するリクエストそれぞれがデータベースと通信できるようになります。ドライバはプール内の接続を自動的に管理します。必要なときに新しい接続を作成し、アイドル状態になったらクローズします。プーリングにより、単一のアプリケーション インスタンスから取得できる接続の数も制限されます(デフォルトの接続数は 100 です)。

一方、Cloud Functions はサーバーレスです。複数の同時リクエストを受信したときは自動的にスケールアップし、需要が減少したときは自動的にスケールダウンするため、非常に効率的です。
デフォルトでは、個々の関数インスタンスが一度に処理できるリクエストは 1 つのみです。しかし、Cloud Functions(第 2 世代)では、同時リクエストを処理するように関数を構成できます。たとえば、同時実行パラメータを 10 に設定すると、1 つの関数インスタンスが同時に最大 10 個のリクエストを処理できます。データベースへの接続方法を慎重に検討すれば、リクエストの処理において MongoDB ドライバが作成した接続プールによるメリットを活用できます。このセクションでは、接続を再利用する具体的な戦略について説明します。
デフォルトでは、Cloud Functions は最大 1,000 個の新しいインスタンスをスピンアップできます。ただし、各関数インスタンスはそれぞれの分離された実行コンテキストで実行されます。つまり、インスタンスはデータベース接続プールを共有できません。そのため、データベース接続をオープンする方法に注意を払う必要があります。同時実行パラメータを 1 に設定し、リクエストごとに新しい接続をオープンすると、データベースで不要なオーバーヘッドが発生したり、最大接続数の上限に達したりします。


これは非常に非効率に思えます。幸いなことに、もっと良い方法があります。Cloud Functions が開始済みのインスタンスを再利用する際の挙動を活用するのです。
前述のように、Cloud Functions は受信リクエストを処理するために新しいインスタンスをスピンアップして、スケーリングを行います。まったく新しいインスタンスを作成することを「コールド スタート」と呼びます。そのプロセスは次のとおりです。
ランタイム環境を読み込みます。
関数のグローバル(インスタンス全体の)スコープを実行します。
「エントリ ポイント」として定義された関数の本体を実行します。
インスタンスがリクエストを処理した後もすぐにはクローズされません。別のリクエストが数分間以内に届いた場合、高い確率で「ウォームアップ」済みの同じインスタンスにルーティングされます。ただし、今回は「エントリ ポイント」関数のみが呼び出されます。さらに重要なのは、関数が同じ実行環境で呼び出されることです。これは、実質的には、グローバル スコープで定義したものはすべて(データベース接続も含めて)再利用できることを意味します。したがって、関数を呼び出すたびに新しい接続をオープンするオーバーヘッドが削減されます。
グローバル スコープを利用して再利用可能な接続を格納することはできますが、再利用可能な接続が使用される保証はありません。
この理論を検証してみましょう。次のようなテストを行います。
ドキュメント 1 件を MongoDB Atlas データベースに挿入する Cloud Functions の関数を 2 つ作成します。また、新しいデータベース接続が作成されるたびにメッセージをログに記録するイベント リスナーもアタッチします。
1 つ目の関数は関数スコープで Atlas に接続します。
2 つ目の関数はグローバル スコープで Atlas に接続します。
各関数に 50 個の同時リクエストを送信し、完了するのを待ちます。理論的には、Cloud Functions は少数のインスタンスをスピンアップした後、それらを再利用して一部のリクエストを処理します。
最後に、ログを調べて、それぞれのケースで作成されたデータベース接続の数を確認します。
開始する前に、Atlas デプロイメントに戻って接続文字列を見つけます。また、ネットワーク設定で任意の場所からのアクセスを許可していることを確認してください。その代わりに、安全な接続を確立することを強くおすすめします。
関数スコープのデータベース接続を使用して Cloud Functions の関数を作成する
Google Cloud コンソールを使用してテストを行います。Cloud Functions ページに移動し、ログインしていること、プロジェクトを選択していること、必要な API をすべて有効にしていることを確認します。次に、[ファンクションを作成] をクリックして次の構成を入力します。
環境: 第 2 世代
関数名: create-document-function-scope
リージョン: us-central-1
認証: 未認証の呼び出しを許可


[ランタイム、ビルド、接続、セキュリティの設定] セクションを開いて、[ランタイム環境変数] で新しい変数 ATLAS_URI を追加し、値を MongoDB Atlas の接続文字列とします。ユーザー名とパスワードのプレースホルダを実際のデータベース ユーザーの認証情報に置き換えることを忘れないでください。
> 認証情報をクリアテキストで環境変数として追加する代わりに、シークレットとして Secret Manager に簡単に保存できます。保存すると、Cloud Functions の関数から認証情報にアクセスできるようになります。
[次へ] をクリックします。ここで、いよいよ関数の実装を追加します。左側のペインから `package.json` ファイルを開き、内容を次のように置き換えます。
ここでは、`mongodb` パッケージを依存関係として追加しました。このパッケージは、データベースへの接続に使用する MongoDB Node.js ドライバを配布するために使用されます。
ここで、`index.js` ファイルに切り替えて、デフォルト コードを次のように置き換えます。
選択したランタイムが Node.js 16 であることを確認し、エントリ ポイントの helloHttp を createDocument に置き換えます。
最後に [デプロイ] をクリックします。
グローバル スコープのデータベース接続を使用して Cloud Functions の関数を作成する
関数のリストに戻り、もう一度 [ファンクションを作成] をクリックします。関数に create-document-global-scope という名前を付けます。構成の残りの部分は、前の関数とまったく同じにする必要があります。接続文字列に ATLAS_URI という名前の環境変数を追加することを忘れないでください。[次へ] をクリックし、`package.json` の内容を前のセクションで使用したものと同じコードに置き換えます。次に、`index.js` を開いて次の実装を追加します。
エントリ ポイントを createDocument に変更して、関数をデプロイします。
ご覧のように、2 つの実装の違いはデータベースに接続する場所だけです。繰り返すと、次のようになります。
関数スコープで接続する関数は、呼び出しのたびに新しい接続を作成します。
グローバル スコープで接続する関数は、「コールド スタート」のときだけ新しい接続を作成します。そのため、一部の接続を再利用できます。
関数を実行して、どうなるか見てみましょう。Google Cloud コンソールの上部にある「Cloud Shell をアクティブにする」アイコンをクリックします。次のコマンドを実行し、50 個のリクエストを create-document-function-scope 関数に送信します。
コマンドを実行すると、Cloud Shell による認証情報の使用を承認するよう求められます。[承認] をクリックします。数秒後、ターミナル ウィンドウで、ドキュメントの作成に関するログの表示が開始されるはずです。コマンドの実行が停止するまで待ちます。すべてのリクエストが送信されると、実行が停止します。
次に、以下のコマンドを実行して、関数からログを取得します。
ここでは、`grep` を使用して、新しい接続が作成されるたびにログに記録されるメッセージのみをフィルタしています。新しい接続が多数作成されたことがわかります。


これは `wc -l` コマンドで数えることができます。
ターミナル ウィンドウに 50 という数字が表示されます。これは、リクエストごとに接続が作成されるという私たちの理論を裏付けています。
同じプロセスを create-document-global-scope 関数に対して繰り返してみましょう。ドキュメントの作成に関するログメッセージが再び表示されます。このコマンドが完了したら、次のコマンドを実行します。
今回は、新規作成される接続が大幅に減少します。今回も `wc -l` で数えることができます。これで、データベース接続を確立するにはグローバル スコープより関数スコープを使用する方が効率的であることが証明されました。
前述のように、Cloud Functions の関数の同時リクエスト数を増やすと、データベース接続の問題を軽減できます。この点をもう少し掘り下げてみましょう。
Cloud Functions(第 2 世代)と Cloud Run による同時実行
デフォルトでは、Cloud Functions は一度に 1 つのリクエストしか処理できません。これに対して、Cloud Functions(第 2 世代)は Cloud Run コンテナで実行されます。これにより、特に大きなメリットとして、複数の同時リクエストを処理するように関数を構成できます。同時実行のキャパシティを増やすと、Cloud Functions がデータベースと通信する方法が従来のサーバー アプリケーションに近づきます。
関数インスタンスが同時リクエストをサポートしている場合は、接続プーリングも利用できます。使用している MongoDB ドライバは、同時リクエストが使用する接続を含むプールを自動的に作成して管理することを思い出してください。
ユースケースと予想される関数の作業量に応じて、以下を調整できます。
関数の同時実行設定。
作成できる関数インスタンスの最大数。
MongoDB ドライバによって管理されるプール内の接続の最大数。
また、すでに証明したとおり、呼び出し間でデータベース接続を永続化するために、常にグローバル スコープでデータベース接続を宣言する必要があります。
イベント ドリブン関数でデータベース オペレーションをべき等にする
イベント ドリブン関数で再試行を有効にできます。これを有効にすると、Cloud Functions は、関数が正常に完了するか再試行期間が終了するまで、関数の実行を何度も試行します。
この機能は多くのケース(断続的なエラーに対処する場合)で役立ちます。ただし、関数にデータベース オペレーションが含まれている場合は、複数回実行すると、ドキュメントの重複などの望ましくない結果が生じる可能性があります。
次の例を見てみましょう。関数 store-message-and-notify は、指定された Pub/Sub トピックにメッセージがパブリッシュされるたびに実行されます。この関数は、受信したメッセージを MongoDB Atlas にドキュメントとして保存した後、サードパーティ サービスを使用して SMS を送信します。しかし、SMS サービス プロバイダでの処理は頻繁に失敗し、関数はエラーをスローします。再試行を有効にしているため、Cloud Functions は関数の実行を再試行します。慎重に実装を作成しないと、データベースでメッセージが重複する可能性があります。
このようなシナリオをどう処理すればよいかを考えてみましょう。関数を安全に再試行できるようにするには、関数をべき等にする必要があります。べき等の関数は、一度実行されたか複数回実行されたかにかかわらず、まったく同じ結果を生成します。一意性チェックなしでデータベース ドキュメントを挿入すると、関数はべき等でなくなります。
このシナリオを試してみましょう。
非べき等のイベント ドリブンな Cloud Functions の関数を作成する
Cloud Functions に移動し、新しい関数の構成を開始します。
環境: 第 2 世代
関数名: store-message-and-notify
リージョン: us-central-1
認証: 認証が必要
次に、[Eventarc トリガーを追加] をクリックし、表示されたダイアログで以下を選択します。
イベント プロバイダ: Cloud Pub/Sub
イベント: google.cloud.pubsub.topic.v1.messagePublished
[Cloud Pub/Sub トピックを選択してください] を開いて、[トピックを作成する] をクリックします。トピック ID として test-topic と入力し、[作成] をクリックします。
最後に、[失敗時に再試行する] を有効にして、[トリガーを保存] をクリックします。関数は失敗すると常に再試行し、それは実装のバグが原因である場合も同じであることにご注意ください。
ATLAS_URI という名前の新しい環境変数を追加し、接続文字列を値として、[次へ] をクリックします。
`package.json` を前に使用したものに置き換えてから、`index.js` ファイルを次の実装に置き換えます。次に、先ほど作成した Pub/Sub トピックに移動し、[メッセージ] タブに移動します。メッセージ本文が異なるいくつかのメッセージをパブリッシュします。
Atlas デプロイメントに戻ります。クラスタタイルで [Browse Collections] をクリックし、データベース test とコレクション messages を選択して、データベースに保存されたメッセージを調べることができます。先ほどパブリッシュしたメッセージの一部が重複していることがわかります。これは、関数が再試行されると、同じメッセージが再度保存されるためです。
関数のべき等性を修正するうえですぐ思いつく方法は、2 つのオペレーションを入れ替えることです。最初に `notify()` 関数を実行し、成功したらメッセージをデータベースに保存する方法が考えられます。しかし、データベース オペレーションが失敗した場合はどうでしょうか?それが実際の実装であれば、SMS 通知の送信を取り消すことができません。したがって、関数はまだ非べき等です。別の解決策を見つけましょう。
イベント ID と一意のインデックスを使用して Cloud Functions の関数をべき等にする
関数が呼び出されるたびに、関連するイベントが一意の ID とともに引数として渡されます。関数が再試行されたときも、イベント ID は同じままです。イベント ID はフィールドとして MongoDB ドキュメントに保存できます。フィールドには一意のインデックスを作成することができ、これにより、イベント ID が重複するメッセージの保存は失敗します。
MongoDB Shell からデータベースに接続し、次のコマンドを実行して一意のインデックスを作成します。次に、Cloud Functions の関数で [編集] をクリックし、実装を次のように置き換えます。
Pub/Sub トピックに戻り、さらにいくつかのメッセージをパブリッシュします。Atlas でデータを調べると、新しいメッセージでは重複がなくなったことがわかります。
べき等性に関する万能の解決策はありません。たとえば、挿入オペレーションではなく更新オペレーションを使用している場合は、`upsert` オプションと `$setOnInsert` 演算子について確認してみてください。
安全なネットワーク接続を設定する
Atlas クラスタと Google Cloud Functions の関数で最大限のセキュリティを確保するには、安全な接続を確立することが不可欠です。幸いにも、Atlas を介して利用できるいくつかのオプションがあり、プライベート ネットワークを構成できます。
そのようなオプションの一つが、MongoDB Atlas データベースと Google Cloud の間にネットワーク ピアリングを設定することです。または、Private Service Connect を利用してプライベート エンドポイントを作成することもできます。どちらの方法でも、接続を保護するための堅牢なソリューションが提供されます。
ただし、こうした機能は無料の Atlas M0 クラスタでは使用できないことにご注意ください。このような強化されたセキュリティ機能を利用するには、M10 ティア以上の専用クラスタにアップグレードする必要があります。
まとめ
まとめると、Cloud Functions と MongoDB Atlas は、効率的でスケーラブルな費用対効果の高いアプリケーションを作成するための強力な組み合わせです。この記事で概説しているベスト プラクティスを実践すると、堅牢でパフォーマンスの高いアプリケーションを作成し、どのような量のトラフィックでも処理できます。適切なインデックスの使用やネットワークの保護などのヒントを参考にすると、これら 2 つの強力なツールを最大限に活用し、真にクラウドネイティブなアプリケーションを作成できます。今すぐ実装に今回ご紹介したベスト プラクティスを応用して、クラウド開発を次のレベルに進化させましょう。MongoDB Atlas をまだ利用していない場合は、Atlas に登録したうえで Google Cloud Marketplace からすぐに最初のクラスタを無料で作成できます。
- Google、デベロッパー アドボケイト Abirami Sukumaran