Google Cloud
署名付き URL を活用して Cloud Storage に画像ファイルを直接アップロードするアーキテクチャを設計する
2019年6月3日
Google Cloud Japan Team
※この投稿は米国時間 2019 年 5 月 29 日に Google Cloud blog に投稿されたものの抄訳です。
本稿では、主にエンドユーザーに対してコンテンツを配信するサービスを提供している方に向けて、クラウド上のリソースを理解し、その強みを最大限活用することによって、従来の課題をスマートに解決し、サーバの開発や運用にかかるコストを削減する方法を、具体的なアーキテクチャの設計を通してご紹介します。全体を通して、エンドユーザーが署名付き URL を使って直接 Cloud Storage に画像ファイルをアップロードし、そのファイルを配信するシステムを Google Cloud Platform ( GCP )上に構築することを目指します。
本稿でご紹介するアーキテクチャのように、エンドユーザーからの画像ファイルのアップロードを受け付けるサーバを、クラウドを活用せず、大規模に開発・運用することは非常に困難な場合があります。考慮しなければならないものとして、例えばファイルのアップロードを担当するサーバのプロセスへのリクエストを一度キューに入れてフロー制御をしたり、リクエストの過負荷によるシステムダウンを防ぐ必要があります。さらに、関連するサーバの有限のリソース( RAM など)に対して、適切な制限を設定する必要もあるでしょう。
加えて、非常に大きなファイルをサーバにアップロードできるようにするためには、サーバの開発と運用にさらに大きなコストが発生する可能性があります。また、アップロードを担うサーバを何百万ものユーザーが使用する場合、そのスケーラビリティやユーザーエクスペリエンスを確保するために多くの作業が必要になります。
このような課題に対応するために、 Cloud Storage を画像ファイルのアップロードサーバとして活用するシステムアーキテクチャを提案します。次項から実際にその設計方法を見ていきましょう。なお、本稿で使用するソースコードはすべて GitHub 上で管理しています。
画像アップロード機能の構築をはじめましょう
このアーキテクチャに必要な GCP コンポーネントを掘り下げる前に、まずは要件を定義しましょう。- オペレーションコストを削減するために、マネージドサービスを可能な限り使用する。
- 認証されたユーザのみがファイルをアップロードできるようにする。
- アップロードされたコンテンツに対して適切にバリデーションを行う。
上記の要件を満たすために、次のアーキテクチャを提案します。
- App Engine がユーザーからのリクエストを受け取ったら、 Cloud Storage の特定のバケット・特定のオブジェクトキーに対して PUT リクエストを実行可能とするための署名付き URL を生成し、ユーザーに返却する。このとき、 App Engine 上では任意のアプリケーションロジックに基づいて、認証済みのユーザーに対してのみ、当該署名付き URL を払い出すようにする。
- 署名付き URL を返却されたユーザーは、それを用いて Cloud Storage の特定のバケット・特定のオブジェクトキーに対してファイルをアップロードする。
- Cloud Storage へのファイルのアップロードを終えると、 Cloud Storage の
finalize
イベントをトリガーとして Cloud Functions を起動する。Cloud Functionsではアップロードされたファイルをバリデーションする。 - Cloud Functions上で、アップロードされたファイルが適切な形式、かつ適切なサイズであることを確認したら、 Cloud Vision API を用いて不適切なコンテンツをフィルタリングする。
- 手順の 3 と 4 の検証が完了したら、アップロード先のバケットから配信用のバケットにファイルをコピーする。
- コピーが完了した画像はインターネットに公開される。
次に、上記の手順を実現するための実装について解説します。
署名つき URL を App Engine Standard runtime 上で生成する
Cloud Storage には、個々のエンドユーザーが特定のリソースに対し、特定のアクションを実行できるようにするための署名付き URL と呼ばれる機能があります。署名付き URL を使用すると、ファイルを安全にアップロードするために、特定のエンドユーザーにのみ有効な一時認証情報を生成することができます。これについて、 Google Cloud 公式のクライアントライブラリを使用すると、簡単にこの機能を実装することができます。本稿ではこの機能を用いて、特定のエンドユーザーのために署名付き URL を生成させる API サーバを App Engine Standard runtime 上に作成します。実装は公式のライブラリに頼るとしても、署名付き URL を生成する実際の流れについては理解しておくことが望ましいため、処理の流れを次に示します。
1. 任意のバイト列を署名するためのサービスアカウントを用意する。
- 新たなサービスアカウントを作成するか、 App Engine のデフォルトサービスアカウントなどの既存のサービスアカウントに必要なパーミッションを付与する。
Content_MD5
と Canonicalized_Extension_Headers
を省略する)。- ファイルのアップロードを受け入れる HTTP_Verb には、
PUT
を選択する。 Content-Type
の値はアップロードされるファイルの MIME type によって異なる。この値はエンドユーザーから送信される API リクエストによって決定することとする。- 有効期限をUnixタイムで X-Goog-Expires にセットする。 API リクエストを受け取った時点から未来の時間を設定する。本稿では15分と設定する。
Canonicalized_Resource
としてアップロード対象のバケットとオブジェクトキーを設定する。オブジェクトキーは UUID などを用いて、動的に生成することで、既存のオブジェクトキーとの重複を回避する。
各手順の詳細については、公式ドキュメントを参照してください。本稿では上記の手順の例として、 Go を用いて実装します。
読み込んでいます...
一つはサービスアカウントに紐付いた秘密鍵を使って署名を行う方法です。この方法は自分自身の手で秘密鍵を管理する必要があります。これは Google Compute Engineや Google Kubernetes Engine などのフルマネージドでないランタイム上で API サーバを開発・提供するケースに適しています。
二つ目の方法は、 Cloud Identity and Access Management が提供する Service Account API の中の一つである
serviceAccounts.signBlob
を用いる方法です。これを用いることによって、アプリケーションランタイムで秘密鍵を管理せずに署名をすることができます。本稿では、秘密鍵の管理を避けるために、この方法を採用しています。補足として、 Cloud Storage で署名付き URL を生成するためにサービスアカウント付与すべきパーミッションを次に列挙します。
storage.buckets.get
Storage.objects.create
Storage.objects.delete
二つ目の方法に示した
serviceAccounts.signBlob
API を使用する場合には、 roles/iam.serviceAccountTokenCreator ロールを付与します。このロールにまとめられたパーミッション群は公式ドキュメントで確認することができます。署名付き URL を使ってファイルをアップロードする
ここまで説明した機能によって、ユーザーは App Engine 上で生成された署名付き URL を使って Cloud Storage にファイルを直接アップロードできるようになりました。 ここでは、署名付き URL を介してPut Object
と呼ばれる API を呼び出すことで、実際にファイルをアップロードします。次に示すのは、モバイルアプリケーションや Web ブラウザなどで実行される処理を想定した Go によるサンプルコードです。前出の App Engine 上で動かすことを想定したソースコードと対応するものになります。
読み込んでいます...
Cloud Storage の Bucket Lock と Object Lifecycle を署名付き URL と組み合わせる
前出のアーキテクチャ図からもわかるように本稿では、ファイルのアップロード先を担うバケット (Uploadable Bucket
)と、ファイルの配信を担うバケット (Distribution Bucket
)の二つの Cloud Storage バケットを定義しています。しかしながら、これらのバケットをデフォルトのまま取り扱うと、大きく分けて二つの問題が発生します。一つ目に、アップロードされた全てのオブジェクト(ファイル)は、それにかかる検証を終えた後に Distribution Bucket にコピーされますが、何もしなければそれらのファイルは Uploadable Bucket に存在し続けることになります。しかしそれらのオブジェクトが、それ以降の処理でいずれのコンポーネントからも参照されることはありません。したがって、コピーが完了したオブジェクトを Uploadable Bucket に残し続けておくことには意味がありません。言い換えれば不要な費用が発生し続けるということになります。
二つ目に、エンドユーザーは署名付き URL の期限内であれば、何度も特定のバケット・オブジェクトキーに対してファイルをアップロードできてしまうということです。言い換えれば、アップロードされたファイルをサーバ側のコンポーネントが参照する度に、その中身が書き換わっている可能性があるということになります。これにより、後述の Cloud Functions で動かすアプリケーションにおいて、リトライと共に冪等性を担保することが困難になります。用途によってはそのような仕様を持っていても構わないのですが、本稿ではよりシンプルな仕様を維持するために、これを問題と捉えて対策します。
さて、これらの問題を解消するために、Object Lifecycle Management と Retention Policy という二つの Cloud Storage 固有の機能を活用します。
最初に、ライフサイクルを定義することによって、不要なオブジェクトが Uploadable Bucket に残り続けないようにします。これを実現するためには、バケットや署名付き URL の有効期限などに合わせて Cloud Storage 上に存在するオブジェクトのライフサイクルを定義します。本稿では次のように、オブジェクトが Uploadable Bucket にアップロードされてから1日が経過したら、それを不要と判断して削除するライフサイクルを定義します。
読み込んでいます...
バケットロックと保持ポリシーを使用して、あるバケットにおけるオブジェクトの保持期間を設定し、その期間中はオブジェクトの上書きや削除ができないようにします。この保持期間を署名付き URL の有効期限に定める値と一致させることで、署名付き URL を介してアップロードされたオブジェクトが、エンドユーザーによって上書きされることを防ぐことができます。なお、前述のライフサイクルの定義によって、オブジェクトは1日が経過すると削除されるため、ライフサイクルとの競合が起こらないようにしてください。仮にライフサイクルとの競合が起きてしまうと、正常にオブジェクトを削除できないなどの問題が発生します。補足として、バージョン管理が有効となっているバケットでは、このバケットロックと保持ポリシーを有効化できない点に注意してください。
上記の設定を有効化したバケットを作成するためには、次に示すコマンドを実行します。
読み込んでいます...
ファイルを検証し、コピーする
これまで、署名付き URL を生成してファイルを直接 Cloud Storage にアップロードし、アップロードされたファイルを適切に管理する方法について解説してきました。しかし、アップロードされたファイルを実際に多くのエンドユーザーに配信する前に、アプリケーションの仕様に合わせてそのファイルの妥当性を検証する必要があります。ここでは、認証されたユーザーによってアップロードされたファイルの有効性・妥当性を検証する方法と共に、検証が済んだオブジェクトを Distribution Bucket にコピーする方法について解説します。まず、本稿ではこれを行うために検証・コピーを行う処理を Cloud Functions 上に実装します。Cloud Functions を実行するためには、特定の Cloud Storage のイベントをトリガーとして、予めデプロイしておいた Cloud Functions を呼び出します。対象となる Cloud Storage のイベントには、オブジェクトの書き込み完了を意味する
google.storage.object.finalize
イベントを用います。次に、どのようにバリデーションやそれに付随する処理を行うべきかを考える必要があります。これを順序立てて整理しましょう。
1. アップロードされたオブジェクトと同一のオブジェクトキーを持つオブジェクトが Distribution Bucket に存在しないことを確認する。
- オブジェクトキーが存在する場合は、以降の処理を停止する。
- これは Cloud Functions が at least-once の実行を保証することを考慮し、同様の処理を不要に実行させないためのものである。
2. Uploadable Bucket 内のオブジェクトのメタデータから、Content-Type
と ファイルのサイズを取得する。
- サイズが規定サイズを超過するのものでないことを確認する。規定サイズはアプリケーションの仕様に基づいて決定する。超過している場合は以降の処理を停止する。
- 本稿ではアップロードされたファイルを Cloud Functions のメモリ上に一度展開するため、Cloud Functions のRAM上限値を考慮して規定サイズを決定するとよい。
Content-Type
に基づいて、そのオブジェクトが適切なファイルフォーマットに準拠していることを確認する。- 署名付き URL の生成時に指定する
Content-Type
はあくまでリクエストに指定すべきContent-Type
を規定するものであり、アップロードされたオブジェクトがそこに指定されたフォーマットに準拠している保証はない。 - オブジェクトが指定されたフォーマットに準拠していないと見做された場合は、以降の処理を停止する。
- 暴力描写・アダルト表現・人種差別などの可能性があるものを検閲する。
- 本稿では
likelihood
がPOSSIBLE
以上のものを該当すると判定し、以降の処理を停止する。
上記の手順を実装するために、本稿では Cloud Functions Go 1.11 ランタイムを使用します。次にソースコードを示します。
読み込んでいます...
UploadImage
という関数を Cloud Functions にデプロイするために、次のコマンドを実行します。読み込んでいます...
ファイルをアップロードする
ここまでに設計し、実装してきたシステムに対して、実際にファイルをアップロードしてみましょう。テストは非常に簡単です。前述の署名付き URL を App Engine にリクエストし、そのURLに対してファイルをアップロードするために示したソースコードを実行します。Cloud Functions の実行が完了したら、Uploadable Bucket 内のオブジェクトが Distribution Bucket にコピーされていることを確認します。 Distribution Bucket へのアクセスを全体に対して公開していれば、コピーが完了した時点でそのオブジェクトはインターネットに公開されていることを意味します。なお、あなたが設計するシステムの要件によっては、Cloud Functions の処理は非同期に実行されるため、配信の開始を Push 通知などの方法でエンドユーザーに伝える必要があるかもしれません。もしくは、エンドユーザーからのリクエストを同期的に処理するシステムのデータベースの更新が必要となるかもしれません。マイクロサービス・アーキテクチャを採用しているならば、Cloud Functions から元のアプリケーションに HTTP リクエストを送信しても良いでしょう。
本稿で提案したシステムをカスタマイズして、あなたの目的に合わせたファイルアップロード機能を開発することができるはずです。 Google Cloud Platform には、ここで紹介したコンポーネントや機能以外にも様々な選択肢があります。例えば、ファイルをアップロードする際の条件をより詳細に規定したい場合や、POST Object を使いたい場合には、署名付きポリシードキュメントを署名付き URL の代わりに使用することができます。また、モバイルアプリや Web サービスの開発に Firebase を使用している場合には、ここで紹介した署名付き URL の代わりに Cloud Storage for Firebase を使用することができます。
本稿の内容があなたのアーキテクチャ設計の一助となれば幸いです。
Google Cloud のエキスパートと繋がり、本稿で提案したアーキテクチャについてより深く学びたい方は Google Cloud のコンサルティングサービスを確認してください。
-By Kunpei Sakai, DevOps and Infrastructure Engineer, Subhasish Chakraborty, Product Manager