優れた 3 種のサーバーレス プラットフォームへのデプロイ

Google Cloud Japan Team
※この投稿は米国時間 2021 年 1 月 28 日に、Google Cloud blog に投稿されたものの抄訳です。
このたび Hot Maze というウェブアプリを作成し、App Engine、Cloud Functions、Cloud Run へのデプロイを試しましたので、その中で学んだことについてご紹介します。
Hot Maze とは
Hot Maze は、パソコンに保存した写真またはドキュメントを、QR コードを使用してスマートフォンと共有できるウェブページです。
アカウントの作成もパスワードも必要とせず、高速、スムーズ、安全にリソースを転送することができます。
実物を見ていきましょう。ここで、青いホリネズミを送信します。

概念的には、Hot Maze のウェブページにファイル(カンガルーなど)をドロップすると、数秒以内に次のようなことが行われます。








リクエスト シーケンスの正確さは、ファイル転送をオーケストレートするために選択したクラウド コンポーネントに依存します。まず採用すべき、Google Cloud を用いた優れたワークフローは次のようになります。
ファイル F をドロップします。
ウェブページが、Cloud Storage 内の一時的ロケーション L にファイルをアップロードするための URL U と、L からファイルをダウンロードするための URL D の 2 つの一意かつ 1 回限り使用できる URL をクラウド サーバーに要求します。
ウェブページで、U を使用して F を L にアップロードするようになります。
ウェブページには、D が QR コード Q にエンコードされて表示されます。
QR コードスキャナのモバイルアプリで Q をスキャンします。
モバイルが D をデコードします。
モバイルが D を使用して L から F をダウンロードします。
数分後、F が L から恒久的に削除されます。
このシナリオでは、モバイルが標準的な QR コードリーダー アプリ(Play ストアや App Store で多くのアプリが入手可能)を使用して、デフォルトのブラウザにスマートフォン側で追加ロジックを使用せずファイルをダウンロードさせます。
秘密の URL である U と D は誰にも知られていないため、ファイル F を見たり、それを改ざんしたりすることはできず、安全性が保たれます。U と D は HTTPS プロトコルを使用しています。クラウドのロケーション L は、一般公開されていません。
ここでは、アプリケーション Hot Maze を設計し、Google Cloud の複数のサーバーレス プロダクトにデプロイする方法をご紹介します。App Engine、Cloud Functions、Cloud Run の 3 つの方法を見ていきましょう。


サーバーレスに関する注意事項
以下で説明する 3 つのデザインの選択肢には、ステートレスであるという共通点があります。つまり、与えられたリクエストを処理する特定のサーバー インスタンスがデータを保持せず、信頼できるソースとはみなされません。これは、自動スケールされるサーバーレス アーキテクチャの基本的特性です。ユーザーデータ(存在する場合)は、データベース、ファイル ストレージ、分散メモリ キャッシュなどの別個のステートフル コンポーネントに格納されます。
App Engine 用の設計
App Engine Standard は、ステートレス アプリケーションが以下の基準を満たす場合に効果的です。
HTTP(S) リクエストを処理する場合
ステートレスである場合
Go、Java、Python、Node.js、PHP、Ruby で記述されている場合
ローカル ファイルシステムを使用したり、バイナリ依存関係をインストールしたりする必要がない場合
他の Google Cloud コンポーネントと通信する場合


Hot Maze は、この条件に大変よく当てはまります。Go で記述されており、ユーザー ファイルの一時リポジトリとして Google Cloud Storage(GCS)が使用されています。Hot Maze は、以下の 2 つのコンポーネントで構成されています。
フロントエンド(静的アセット): HTML、JS、CSS、画像。
バックエンド: アップロードとダウンロードのロジックを処理するハンドラ。
ソースコードは GitHub で入手できます。リポジトリでは、この実装(App Engine + Cloud Storage)を B1 と呼んでいます。
プロジェクト構造
Go アプリがフロントエンドとバックエンドの両方にサービスを提供するのは、簡単かつ通常のことです。




このウェブアプリのコードは App Engine(GAE)特有ではありません。ローカルで開発およびテストできる標準的な Go アプリです。app.yaml だけが App Engine 特有のもので、どの App Engine ランタイムを使用するか、受信したリクエストをどのようにルーティングするかを指定しています。
このウェブアプリでは HTTPS について心配する必要はありません。HTTPS ターミネーションが上流の「Google Front End サービス」(GFE)で行われるため、Go コードでは証明書や暗号化を扱う必要がありません。
Go モジュールのルートにある独自のパッケージでサーバー ロジックのコードを記述し、サーバーをインスタンス化して特定のポートをリッスンする「cmd」内に「main」パッケージの実行可能なプログラムを格納することをおすすめします。たとえば、私がはじめて Hot Maze に実装した App Engine では、「github.com/Deleplace/hot-maze/B1」は、モジュール名とサーバー パッケージのパスの両方を表しています。
ストレージ
私は一時データ ストレージを確保するために Cloud Storage を選んだので、GCS ライブラリ cloud.google.com/go/storage への依存性があるといえます。セキュリティ上(プライバシー)の理由から、匿名のインターネット ユーザーには GCS バケットへの読み書きアクセスを許可せず、匿名のユーザーが特定のファイルをアップロードしたりダウンロードしたりできるように、サーバーで署名付きの URL が生成されるようにします。
フロー


コンピュータ ユーザーが Hot Maze のウェブページにファイルをドロップすると、最初のリクエストがバックエンドに 2 つの URL を生成して返すように要求します。1 つはブラウザがユーザーのファイルを GCS にアップロードするため、もう 1 つはスマートフォンが GCS からファイルをダウンロードするためのものです。
作業を進めるうえで必要な情報は、以下のみです。
ファイル F を「安全なアップロード URL」 U にアップロードします。
次に、安全なダウンロード URL D を QR コードでエンコードして表示します。


ダウンロード URL さえあれば、GCS から直接ファイルを取得できます。ユーザーのスマートフォン(QR コードスキャナ アプリをインストール済み)から App Engine のバックエンドに通信する必要がまったくない状態が理想です。後述の短縮 URL セクションを参照し、Google が実際にモバイル デバイスに App Engine サービスを検出させる理由についてご確認ください。


静的アセットの提供
アプリのフロントエンド部分は、HTML ファイル、JS ファイル、CSS ファイル、複数の画像で構成されています。バックエンド部分と同じサーバー インスタンスによって提供されることもあれば、異なるドメイン名で異なるサーバーによって提供されることもあります。
Go サーバー
Go サーバーに(静的な)フロントエンドと(動的な)バックエンドへのすべてのリクエストを処理させるとシンプルです。所定のフォルダのあらゆるコンテンツが、この方法で提供されるようにしています。


この方法は、スタンドアロンの開発や本番環境においてもまったく同様に機能します。静的ファイルに対する受信リクエストは、サーバー インスタンスの Go コードで処理されます。
app.yaml の静的ファイル ハンドラ
app.yaml で静的アセットを宣言することもできます。

これには、次のような複数の利点があります。
静的アセットのリクエストは、App Engine インスタンスからではなく、CDN のようなファイル サーバーで処理されます。
静的アセットの方が若干速く処理されます。
静的アセットは、コールド スタートの対象になりません。稼働インスタンスがゼロの状態でも高速に動作します。
Cloud Logging コンソールにリクエストログが引き続き表示されます。
その結果、App Engine インスタンスへの負荷の軽減、動的リクエストのスループット向上、スケールアップのためのコールド スタートの総発生回数(ローディング レイテンシ)の減少につながる可能性があります。
この最適化を行う場合は、ローカル開発のために動作し続ける静的ファイル用の Go ハンドラ(http.FileServer を使用)を維持することをおすすめします。これは現在、開発と本番環境では動作が異なるコンポーネントであるため、QA テスト中はそのことを念頭に、app.yaml と Go コードの不一致(ファイルとフォルダのセット、キャッシュ応答ヘッダーなど)を発生させないように注意する必要があります。
組み込み CDN は、新しいプロジェクトのサーバーレス オプションを検討する際に考慮すべき、App Engine の便利な機能です。
短縮 URL
ここで 1 つ気になる問題があります。署名付き URL の暗号パラメータは長く、500 文字を超えるということです。
長い URL はコンピュータにとっては問題ありませんが、人間にとっては(入力する必要がある場合)厄介で、QR コードにも不向きです。


前述の完全な長さの署名付き URL は、技術的には標準的な QR コードに収まりますが、結果として得られる画像は非常に複雑で、小さなパターンでいっぱいになるため、モバイルの QR コードスキャナ アプリがコードを適切に読み取れなくなる可能性があります。
アップロード URL U は、そのまま最も長い形式で使用されます。ブラウザはこれを JSON レスポンス内で受信し、すぐに JS 内で使用してリソースをアップロードします。
ただし、完全な長さのダウンロード URL の D は実際、QR コードでのエンコードには適していません。これに対応するため、リソース ファイルの UUID のみを含む短縮ダウンロード URL という追加の間接的対処を行います。ユーザーのモバイル デバイスが QR コードをスキャンして短縮 URL を抽出すると、App Engine に Cloud Storage 内のリソースと一致する完全な長さの URL の問い合わせが行われます。


この URL 短縮処理により、App Engine サービスに 1 往復分のわずかな遅延が発生します。また App Engine サービスで、短縮 URL の D と長い署名付き URL の D の間の対応関係をなんらかの形で保持する必要があるため、いくつかの中程度の)サーバー ロジックが追加で使用されます。実際に使える QR コードを活用できるという大きなメリットがあります。
クリーンアップ
クラウド サーバーにはユーザーデータを数分以上保存しておく必要はなく、保存したくもありません。
9 分後に自動削除されるよう、Cloud Tasks でスケジュール設定してみましょう。これは次の 3 段階で行われます。
新規タスクキューの作成
●リソースを直ちに削除する /forget ハンドラ: ソース
●署名付き URL 生成から 9 分後に /forget?uuid=<id> を検出するタスク オブジェクト: ソース
リクエスト ヘッダー チェック機能により、ファイル削除が公開サービスにならないことが保証されます。ファイル削除は、タスクキューによってのみトリガーされます。
署名付き URL は短時間しか有効でないため、URL 生成後 5 分間だけアップロードとダウンロードを行うことができます。クリーンアップを行うことで、以下の目的も果たされます。
古いデータが削除され、ストレージ コストが削減されます。
クラウド サービスが必要以上に長くユーザーデータのコピーを保持しないようにすることで、プライバシー保護が強化されます。
プライバシー
このサービスでは、アカウント作成、認証、パスワードなどは一切不要です。
ユーザーデータが第三者に傍受されないため、安全です。
通信暗号化: アップロードとダウンロードで HTTPS を使用。
Cloud Storage で保存データを暗号化。
ノンス UUID を使用して、crypto/rand を使用するパッケージ github.com/google/uuid が生成するリソースを識別。
偽造できない安全な署名付き URL を生成。
署名付き URL は 5 分後に失効。
ユーザーデータは 9 分後に削除。
ただし、これはいわゆる「エンドツーエンドの暗号化」(E2EE)とは異なります。Hot Maze サービスのオーナー(私)は、GCS バケットにアクセスして、削除される前のユーザー ファイルを見ることができました。E2EE の実装は面白いプロジェクトになりそうです。今後の記事の材料になるかもしれません。
Cloud Storage バケットの構成
適切な署名付き URL を使用していても、ウェブブラウザから特定の Cloud Storage バケットへのアクセスは許可しておく必要があります。そのように構成されていない場合、ブラウザがアクセスを拒否してスローする場合があります。
アクセス元からの XMLHttpRequest へのアクセスは CORS ポリシーによりブロックされました。プリフライト リクエストへの応答がアクセス制御チェックを通過していません。要求されたリソースには 'Access-Control-Allow-Origin' ヘッダーが存在しません。
正当な書き込みアクセス(PUT)が必要となる可能性のあるすべてのドメイン名をバケットに割り当てるよう CORS を明示的に構成する必要があります。このために、bucket_cors.json ファイルを作成します
それから、次のコードを使用してファイルを適用します。
「hot-maze.appspot.com」はドメイン名のようですが、ここでは一時ファイルを保存するために使用するバケットの名前を表します。
ローカル開発環境のためのクラウド ストレージへのアクセス
前述の JSON には localhost ドメインが含まれていることにご注意ください。このドメインは、開発段階で必要になります。アプリが本番環境にデプロイされた後も、この構成を維持して問題ありません。これで新しいセキュリティ リスクが生じることはありません。
サービス アカウント
Go クライアント ライブラリを使用して GCS 署名付き URL を生成するには、適切な権限を持つサービス アカウントの秘密鍵が必要であることがわかりました。そこで私は、IAM にアクセスし、アカウント ephemeral-storage@hot-maze.iam.gserviceaccount.com を作成して、これに「ストレージ管理者」のロールを与えました。
これでその秘密鍵をダウンロードして使用できるようになりましたが、私のコード リポジトリにそのような機密性の高い情報を方法がありませんでした。そこで代わりに、秘密鍵を Secret Manager に保存しました。次に、App Engine のデフォルト サービス アカウントに「Secret Manager のシークレット アクセサー」というロールを付与しました。かなり多くの間接的な対応を行いましたが、その理由は次のようなものです。
機密性の高い作業を行う場合、バックエンドがなんらかの方法で認証されている必要があるが、それだけでは十分ではない。
また、そのような作業では、シークレットである秘密鍵が必要となる。
これにより、このサービスの匿名ユーザーが使用するアップロード URL を生成できる。
本番環境では、App Engine バックエンドが自動的に認証される。
ローカル開発環境では、Secret Manager からシークレットを読み取るために、明示的なサービス アカウントを扱う必要がある。
ローカルでの開発
フォルダ B1 から、以下を実行します。
メイン プログラムが cmd/backend にある場合でも、Go モジュールのルート ディレクトリから実行することで、静的フォルダが正しく検出されます。
サービス アカウント キー sa.json が IAM ウェブ コンソールからダウンロードされ、私のローカル ファイル システムのどこかに保存されました。ソースコードでチェックインすることを意図したものではありません。
デプロイメント
前提条件: コマンドライン ツール gcloud で認証を受けており、アクティブなプロジェクトが hot-maze であること。フォルダ B1 から、以下を実行します。
2 分で完了します。ウェブ コンソールで確認できるように、新しいバージョンがデプロイされています。対応する 2 つの URL でアクセスできます。




フロントエンドの構造(静的アセット)
最初の方法との主な違いは、Cloud Functions が HTML ページやリソースを提供するために設計された「ウェブ バックエンド」ではないことです。そのため、Firebase Hosting を使用します。以下を行います。
フロントエンド プロジェクトのフォルダで firebase init を実行し、指示に従います。
index.html と静的フォルダを新しい firebase public フォルダ内に格納します。
次を入力して、フロントエンドをデプロイします。
注: デフォルトでは、Firebase によって提供されるアセットは、ヘッダーの cache-control が max-age=3600(1 時間)となっています。
Firebase Hosting はグローバル CDN からのリクエストに対応しています。
バックエンドの構造
もう一つの違いは、Go で書かれた Cloud Functions は「標準的な」Go のウェブサーバーではないということです。ローカルで開発してテストするには、Functions フレームワークが必要です。
私のアプリの場合は次のとおりです。
サーバーのビジネス ロジックを、Go モジュールのルートの「hotmaze」パッケージに組み込んでおくことをここでもおすすめします。
今後ハンドラは http.HandleFunc で登録しません。
ローカル開発向けには、公開されている関数ごとに funcframework.RegisterHTTPFunctionContext を呼び出すメイン パッケージを用意しています。
なお、一部の構成値は、「main」パッケージの代わりに「hotmaze」パッケージ内で提供されるようになりました。理由は、実行ファイル cmd/backend/main.go が本番環境では使用されないためです。GCF には、実行ファイルもフルサーバーもデプロイしません。代わりに、各関数を個別にデプロイします。
バックエンドに 3 つの動的ハンドラが存在するため、3 つのデプロイ コマンドが用意されています。URL を保護するためのもの、短縮 URL から GCS の完全な長さの署名付き URL にリダイレクトするもの、そしてGCS からリソースを削除するためのものです。
フル アプリケーションをデプロイするコマンドは、全部で 4 つです。フロントエンドのみの再デプロイも、単一の関数のみの再デプロイも可能です。


実画面
ローカルでの開発
フロントエンドとバックエンドが異なるポートで動作しています。
firebase.json で明示的にフロントエンドをポート 8081 でホストしてみましょう。


次に、最初のターミナルで次を実行します。
最後に、2 つ目のターミナルで、デフォルトの 8080 番ポートでバックエンドを実行します。
Cloud Functions for Firebase
Firebase には、JavaScript の関数向けに、役に立つ Cloud Functions との統合機能が組み込まれています。
私のバックエンドは Go で記述されているため、「従来の」 Cloud Functions を使用しています。
Cloud Run 向けの設計
Cloud Run ではフロントエンドとバックエンドを Docker イメージでパッケージ化し、このイメージを本番環境にデプロイできます。
ソース リポジトリでは、この実装(Cloud Run + Cloud Storage)は B3 と呼ばれています。


Cloud Run には、コンテナを使用することで得られるさまざまな利点に加えて、以下のようなメリットがあります。
好きな言語でコードを記述できる。
任意のバイナリ依存関係を使用している(Linux で動作可能)。
ローカル開発、QA、本番環境用に同じコンテナを使用できる。
静的アセットと動的ハンドラを 1 個のコンテナでホストできる。
ゼロから多数まで自動スケーリングが可能。
迅速なコールド スタートが可能。
複雑なクラスタを管理する必要がない(これはクラウド ベンダーが行います)。
プロジェクト構造
Go ウェブアプリは通常、フロントエンドとバックエンドの両方に機能します。その構造は、B1(App Engine)の実装に似ています。


Dockerfile を使用する
サーバー(フロントエンド + バックエンド)を Docker イメージにパッケージ化するには、まずターゲット プラットフォームである linux amd64 向けにサーバーをコンパイルします。
次に、実行可能バイナリと静的アセットのフォルダを送ります。Go のソースコードを送る必要はありません。以下のクイックスタートのサンプルは、Dockerfile を記述して適切な CA 証明書付きのイメージを生成するために役立ちます。


注: Go 1.16 では、すべてのリソースをサーバー実行ファイル内にバンドルすることが可能になり、Dockerfile 内の静的フォルダをコピーする必要がなくなります。
サービス アカウント
デプロイされた Cloud Run サービスでは、デフォルトのサービス アカウントが有効になっています。


IAM では、「Secret Manager のシークレット アクセサー」というロールを付与するために、少し時間を割く必要がありました。


B1 セクションでご説明したとおり、シークレットにアクセスすることで、GCS バケットへの署名付き URL の生成を許可されているサービス アカウント ephemeral-storage@hot-maze.iam.gserviceaccount.com を取得できます。
ローカルでの開発
ローカルで Docker コンテナを「起動するだけ」では、サービス アカウントは自動的に追加されません。また、コンテナ レジストリのソース リポジトリに登録するリスクを冒して、サービス アカウントの秘密鍵を Dockerfile にコピーすることは、間違いなくおすすめできません。
ローカルテスト用の Cloud Run のドキュメントに従って、サービス アカウントの JSON 鍵ファイルを docker run コマンドの引数として渡します。
サーバーが起動し、http://localhost:8080 で利用可能になります。
サービス アカウント キー sa.json が IAM ウェブ コンソールからダウンロードされ、私のローカル ファイル システムのどこかに保存されました。ソースコードでチェックインすることを意図したものではありません。
デプロイメント
次のコマンドを使用すると、Go ウェブサーバーのコンパイル、Docker イメージの構築、Docker イメージの Container Registry への push、Cloud Run へのデプロイを行うことができます。
これにより、デプロイするたびに latest タグが上書きされます。適切なバージョニングを行うには、release-1.0.0 のような、より具体的なタグ名を使用した方がよいでしょう。
注意: -allow-unauthenticated フラグがなんらかの理由でうまく機能しない場合(「IAM ポリシーの設定に失敗しました…」)、エラー メッセージに示されている追加の gcloud コマンドを実行するか、ウェブ コンソールで [Cloud Run] > [Service] > [Permissions] > [Add member "allUsers"] の順に移動し、「Cloud Run 起動元」ロールを付与する必要があります。


実画面
Buildpacks を使用する
正しい Dockerfile を記述してコンテナをデプロイすることは、App Engine などに「コードをデプロイするだけ」よりも複雑な作業です。
しかし Buildpacks は、アプリをソースコードからスムーズにコンテナ化してデプロイするクラウド ネイティブな方法です。前のセクションとの主な違いは、Dockerfile が必要ないということです。
こちらの手順に従い、以下の方法で Hot Maze を Cloud Run にデプロイすることができます。


こちらも実画面です
静的ファイル
前述の戦略上の理由から、Google Cloud CDN のような静的アセットに対応する CDN を利用することをおすすめします。この部分はオプションで、アプリが多くのトラフィックを処理している場合に特に便利です。
その他の作業
ここまで、以下の 2 つのパフォーマンス最適化方法について見てきました。
静的アセットに CDN を利用する。
URL 短縮により QR コードの複雑さを軽減する。
UX、わかりやすさ、パフォーマンスの改善方法は他にも多数考えられます。機会があれば今後の記事でご紹介します。
まとめ
この記事では、中程度の複雑さのアプリを Google Cloud の 3 種類のサーバーレス プラットフォームにデプロイするためどのように構成できるかについて見てきました。
以下のすべてが有効な方法です。
App Engine + Cloud Storage
Cloud Functions + Firebase hosting + Cloud Storage
Cloud Run + Cloud Storage
ご自身のワークフローの詳細を考慮し、または各方法が備える固有の機能に応じて、ご自身のニーズに合ったものをお選びください。たとえば、アプリがすでに Docker コンテナとしてパッケージ化されている場合は、Cloud Run が最適です。
前述のサービスの詳細については、以下のリソースをご確認ください。
-Google Cloud デベロッパー アドボケイト Valentin Deleplace


