Google Cloud Platform

署名付き URL を活用して Cloud Storage に画像ファイルを直接アップロードするアーキテクチャを設計する

Data Analytics B1
昨今のクラウドの普及によって、クラウド上にデプロイしたアプリケーションをエンドユーザーに広く提供することが一般的になってきました。それと同時に、不特定多数のエンドユーザーがクラウド上のリソースに直接アクセスすることを念頭に置くことによって、柔軟にクラウドの利点を活かしたシステムアーキテクチャを設計できるようになります。

本稿では、主にエンドユーザーに対してコンテンツを配信するサービスを提供している方に向けて、クラウド上のリソースを理解し、その強みを最大限活用することによって、従来の課題をスマートに解決し、サーバの開発や運用にかかるコストを削減する方法を、具体的なアーキテクチャの設計を通してご紹介します。全体を通して、エンドユーザーが署名付き URL を使って直接 Cloud Storage に画像ファイルをアップロードし、そのファイルを配信するシステムを Google Cloud Platform ( GCP )上に構築することを目指します。

本稿でご紹介するアーキテクチャのように、エンドユーザーからの画像ファイルのアップロードを受け付けるサーバを、クラウドを活用せず、大規模に開発・運用することは非常に困難な場合があります。考慮しなければならないものとして、例えばファイルのアップロードを担当するサーバのプロセスへのリクエストを一度キューに入れてフロー制御をしたり、リクエストの過負荷によるシステムダウンを防ぐ必要があります。さらに、関連するサーバの有限のリソース( RAM など)に対して、適切な制限を設定する必要もあるでしょう。

加えて、非常に大きなファイルをサーバにアップロードできるようにするためには、サーバの開発と運用にさらに大きなコストが発生する可能性があります。また、アップロードを担うサーバを何百万ものユーザーが使用する場合、そのスケーラビリティやユーザーエクスペリエンスを確保するために多くの作業が必要になります。

このような課題に対応するために、 Cloud Storage を画像ファイルのアップロードサーバとして活用するシステムアーキテクチャを提案します。次項から実際にその設計方法を見ていきましょう。なお、本稿で使用するソースコードはすべて GitHub 上で管理しています。

画像アップロード機能の構築をはじめましょう

このアーキテクチャに必要な GCP コンポーネントを掘り下げる前に、まずは要件を定義しましょう。
  • オペレーションコストを削減するために、マネージドサービスを可能な限り使用する。
  • 認証されたユーザのみがファイルをアップロードできるようにする。
  • アップロードされたコンテンツに対して適切にバリデーションを行う。

上記の要件を満たすために、次のアーキテクチャを提案します。
Architecture with GCP services
このアーキテクチャの主な特徴として、ファイルのアップロードからその配信に至るまで、全てのコンポーネントをサーバーレスに使用可能なアプリケーションランタイム、もしくはマネージドサービスによってのみ構成している点が挙げられます。これは一つ目の要件を満たすための設計指針に基づいています。次に、各ステップについて詳しく解説します。
  1. App Engine がユーザーからのリクエストを受け取ったら、 Cloud Storage の特定のバケット・特定のオブジェクトキーに対して PUT リクエストを実行可能とするための署名付き URL を生成し、ユーザーに返却する。このとき、 App Engine 上では任意のアプリケーションロジックに基づいて、認証済みのユーザーに対してのみ、当該署名付き URL を払い出すようにする。
  2. 署名付き URL を返却されたユーザーは、それを用いて Cloud Storage の特定のバケット・特定のオブジェクトキーに対してファイルをアップロードする。
  3. Cloud Storage へのファイルのアップロードを終えると、 Cloud Storage の finalize イベントをトリガーとして Cloud Functions を起動する。Cloud Functionsではアップロードされたファイルをバリデーションする。
  4. Cloud Functions上で、アップロードされたファイルが適切な形式、かつ適切なサイズであることを確認したら、 Cloud Vision API を用いて不適切なコンテンツをフィルタリングする。
  5. 手順の 3 と 4 の検証が完了したら、アップロード先のバケットから配信用のバケットにファイルをコピーする。
  6. コピーが完了した画像はインターネットに公開される。

次に、上記の手順を実現するための実装について解説します。

署名つき URL を App Engine Standard runtime 上で生成する

Cloud Storage には、個々のエンドユーザーが特定のリソースに対し、特定のアクションを実行できるようにするための署名付き URL と呼ばれる機能があります。署名付き URL を使用すると、ファイルを安全にアップロードするために、特定のエンドユーザーにのみ有効な一時認証情報を生成することができます。これについて、 Google Cloud 公式のクライアントライブラリを使用すると、簡単にこの機能を実装することができます。本稿ではこの機能を用いて、特定のエンドユーザーのために署名付き URL を生成させる API サーバを App Engine Standard runtime 上に作成します。

実装は公式のライブラリに頼るとしても、署名付き URL を生成する実際の流れについては理解しておくことが望ましいため、処理の流れを次に示します。


1. 任意のバイト列を署名するためのサービスアカウントを用意する。

  1. 新たなサービスアカウントを作成するか、 App Engine のデフォルトサービスアカウントなどの既存のサービスアカウントに必要なパーミッションを付与する。
2. 署名付き URL の生成に必要なシグネチャを生成する(本稿では Content_MD5と Canonicalized_Extension_Headers を省略する)。
  1. ファイルのアップロードを受け入れる HTTP_Verb には、 PUTを選択する。
  2. Content-Type の値はアップロードされるファイルの MIME type によって異なる。この値はエンドユーザーから送信される API リクエストによって決定することとする。
  3. 有効期限をUnixタイムで X-Goog-Expires にセットする。 API リクエストを受け取った時点から未来の時間を設定する。本稿では15分と設定する。
  4. Canonicalized_Resource としてアップロード対象のバケットとオブジェクトキーを設定する。オブジェクトキーは UUID などを用いて、動的に生成することで、既存のオブジェクトキーとの重複を回避する。
3. 手順 1 で用意したサービスアカウントの秘密鍵を用いて、手順 2 で生成した文字列を署名する。



各手順の詳細については、公式ドキュメントを参照してください。本稿では上記の手順の例として、 Go を用いて実装します。

  package main

import (
        "context"
        "encoding/base64"
        "fmt"
        "log"
        "net/http"
        "os"
        "time"

        "cloud.google.com/go/storage"
        "github.com/google/uuid"
        "golang.org/x/oauth2/google"
        iam "google.golang.org/api/iam/v1"
)

var (
        // iamService is a client for calling the signBlob API.
        iamService *iam.Service

        // serviceAccountName represents Service Account Name.
        // See more details: https://cloud.google.com/iam/docs/service-accounts
        serviceAccountName string

        // serviceAccountID follows the below format.
        // "projects/%s/serviceAccounts/%s"
        serviceAccountID string

        // uploadableBucket is the destination bucket.
        // All users will upload files directly to this bucket by using generated Signed URL.
        uploadableBucket string
)

func signHandler(w http.ResponseWriter, r *http.Request) {
        // Accepts only POST method.
        // Otherwise, this handler returns 405.
        if r.Method != "POST" {
                w.Header().Set("Allow", "POST")
                http.Error(w, "Only POST is supported", http.StatusMethodNotAllowed)
                return
        }

        ct := r.FormValue("content_type")
        if ct == "" {
                http.Error(w, "content_type must be set", http.StatusBadRequest)
                return
        }

        // Generates an object key for use in new Cloud Storage Object.
        // It's not duplicate with any object keys because of UUID.
        key := uuid.New().String()
        if ext := r.FormValue("ext"); ext != "" {
                key += fmt.Sprintf(".%s", ext)
        }

        // Generates a signed URL for use in the PUT request to GCS.
        // Generated URL should be expired after 15 mins.
        url, err := storage.SignedURL(uploadableBucket, key, &storage.SignedURLOptions{
                GoogleAccessID: serviceAccountName,
                Method:         "PUT",
                Expires:        time.Now().Add(15 * time.Minute),
                ContentType:    ct,
                // To avoid management for private key, use SignBytes instead of PrivateKey.
                // In this example, we are using the `iam.serviceAccounts.signBlob` API for signing bytes.
                // If you hope to avoid API call for signing bytes every time,
                // you can use self hosted private key and pass it in Privatekey.
                SignBytes: func(b []byte) ([]byte, error) {
                        resp, err := iamService.Projects.ServiceAccounts.SignBlob(
                                serviceAccountID,
                                &iam.SignBlobRequest{BytesToSign: base64.StdEncoding.EncodeToString(b)},
                        ).Context(r.Context()).Do()
                        if err != nil {
                                return nil, err
                        }
                        return base64.StdEncoding.DecodeString(resp.Signature)
                },
        })
        if err != nil {
                log.Printf("sign: failed to sign, err = %v\n", err)
                http.Error(w, "failed to sign by internal server error", http.StatusInternalServerError)
                return
        }
        w.WriteHeader(http.StatusOK)
        fmt.Fprintln(w, url)
}

func main() {
        cred, err := google.DefaultClient(context.Background(), iam.CloudPlatformScope)
        if err != nil {
                log.Fatal(err)
        }
        iamService, err = iam.New(cred)
        if err != nil {
                log.Fatal(err)
        }

        uploadableBucket = os.Getenv("UPLOADABLE_BUCKET")
        serviceAccountName = os.Getenv("SERVICE_ACCOUNT")
        serviceAccountID = fmt.Sprintf(
                "projects/%s/serviceAccounts/%s",
                os.Getenv("GOOGLE_CLOUD_PROJECT"),
                serviceAccountName,
        )

        http.HandleFunc("/sign", signHandler)
        log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), nil))
}
コード内で言及しているように、署名付き URL を生成するための方法は大きく分けて二つあります。

一つはサービスアカウントに紐付いた秘密鍵を使って署名を行う方法です。この方法は自分自身の手で秘密鍵を管理する必要があります。これは 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 上で動かすことを想定したソースコードと対応するものになります。
  package main

import (
        "bytes"
        "fmt"
        "io/ioutil"
        "log"
        "net/http"
        "net/url"
        "strings"
)

const signerUrl = "<APPENGINE_URL>"

func getSignedURL(target string, values url.Values) (string, error) {
        resp, err := http.PostForm(target, values)
        if err != nil {
                return "", err
        }
        defer resp.Body.Close()
        b, err := ioutil.ReadAll(resp.Body)
        if err != nil {
                return "", err
        }
        return strings.TrimSpace(string(b)), nil
}

func main() {
        // Get signed url from the API server hosted on App Engine.
        u, err := getSignedURL(signerUrl, url.Values{"content_type": {"image/png"}})
        if err != nil {
                log.Fatal(err)
        }
        fmt.Printf("Signed URL here: %q\n", u)

        b, err := ioutil.ReadFile("/path/to/sample.png")
        if err != nil {
                log.Fatal(err)
        }
        // Generates *http.Request to request with PUT method to the Signed URL.
        req, err := http.NewRequest("PUT", u, bytes.NewReader(b))
        if err != nil {
                log.Fatal(err)
        }
        req.Header.Add("Content-Type", "image/png")
        client := new(http.Client)
        resp, err := client.Do(req)
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println(resp)
}

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日が経過したら、それを不要と判断して削除するライフサイクルを定義します。
  {
  "rule": [
    {
      "action": {"type": "Delete"},
      "condition": {"age": 1}
    }
  ]
}
次に、ユーザーが有効期限内であったとしても何度もファイルをアップロードできないようにするため、保持ポリシーを定義します。

バケットロックと保持ポリシーを使用して、あるバケットにおけるオブジェクトの保持期間を設定し、その期間中はオブジェクトの上書きや削除ができないようにします。この保持期間を署名付き URL の有効期限に定める値と一致させることで、署名付き URL を介してアップロードされたオブジェクトが、エンドユーザーによって上書きされることを防ぐことができます。なお、前述のライフサイクルの定義によって、オブジェクトは1日が経過すると削除されるため、ライフサイクルとの競合が起こらないようにしてください。仮にライフサイクルとの競合が起きてしまうと、正常にオブジェクトを削除できないなどの問題が発生します。補足として、バージョン管理が有効となっているバケットでは、このバケットロックと保持ポリシーを有効化できない点に注意してください。

上記の設定を有効化したバケットを作成するためには、次に示すコマンドを実行します。

  REGION="REGION"
PROJECT_ID="PROJECT_ID"
UPLOADABLE_BUCKET="UPLOADABLE_BUCKET"
DISTRIBUTION_BUCKET="DISTRIBUTION_BUCKET"
LIFECYCLE_POLICY_FILE="/path/to/lifecycle.json"

# Creates the uploadable bucket
gsutil mb -p $PROJECT_ID -l $REGION --retention 900s gs://$UPLOADABLE_BUCKET
# Creates the bucket for distribution
gsutil mb -p $PROJECT_ID -l $REGION gs://$DISTRIBUTION_BUCKET
# Set lifecyle for the uploadable bucket
gsutil lifecycle set $LIFECYCLE_POLICY_FILE gs://$UPLOADABLE_BUCKET

ファイルを検証し、コピーする

これまで、署名付き URL を生成してファイルを直接 Cloud Storage にアップロードし、アップロードされたファイルを適切に管理する方法について解説してきました。しかし、アップロードされたファイルを実際に多くのエンドユーザーに配信する前に、アプリケーションの仕様に合わせてそのファイルの妥当性を検証する必要があります。ここでは、認証されたユーザーによってアップロードされたファイルの有効性・妥当性を検証する方法と共に、検証が済んだオブジェクトを Distribution Bucket にコピーする方法について解説します。

まず、本稿ではこれを行うために検証・コピーを行う処理を Cloud Functions 上に実装します。Cloud Functions を実行するためには、特定の Cloud Storage のイベントをトリガーとして、予めデプロイしておいた Cloud Functions を呼び出します。対象となる Cloud Storage のイベントには、オブジェクトの書き込み完了を意味する google.storage.object.finalize イベントを用います。

次に、どのようにバリデーションやそれに付随する処理を行うべきかを考える必要があります。これを順序立てて整理しましょう。


1. アップロードされたオブジェクトと同一のオブジェクトキーを持つオブジェクトが Distribution Bucket に存在しないことを確認する。

  1. オブジェクトキーが存在する場合は、以降の処理を停止する。
  2. これは Cloud Functions が at least-once の実行を保証することを考慮し、同様の処理を不要に実行させないためのものである。

2. Uploadable Bucket 内のオブジェクトのメタデータから、Content-Type と ファイルのサイズを取得する。

  1. サイズが規定サイズを超過するのものでないことを確認する。規定サイズはアプリケーションの仕様に基づいて決定する。超過している場合は以降の処理を停止する。
  2. 本稿ではアップロードされたファイルを Cloud Functions のメモリ上に一度展開するため、Cloud Functions のRAM上限値を考慮して規定サイズを決定するとよい。
3. アップロードされたオブジェクトを実際に取得し、手順 2 で取得した Content-Type に基づいて、そのオブジェクトが適切なファイルフォーマットに準拠していることを確認する。
  1. 署名付き URL の生成時に指定する Content-Type はあくまでリクエストに指定すべき Content-Type を規定するものであり、アップロードされたオブジェクトがそこに指定されたフォーマットに準拠している保証はない。
  2. オブジェクトが指定されたフォーマットに準拠していないと見做された場合は、以降の処理を停止する。
4. Cloud Vision の Safe Search Annotation を用いてアップロードされた画像ファイル(オブジェクト)が不適切な表現を含むものでないことを検証する。
  1. 暴力描写・アダルト表現・人種差別などの可能性があるものを検閲する。
  2. 本稿では likelihood が POSSIBLE 以上のものを該当すると判定し、以降の処理を停止する。
5. 全ての検証をパスしたら、アップロードされたオブジェクトを Distribution Bucket にコピーする。


上記の手順を実装するために、本稿では Cloud Functions Go 1.11 ランタイムを使用します。次にソースコードを示します。

  package function

import (
        "context"
        "errors"
        "fmt"
        "image"
        "image/gif"
        "image/jpeg"
        "image/png"
        "log"

        "cloud.google.com/go/storage"
        vision "cloud.google.com/go/vision/apiv1"
        "golang.org/x/xerrors"
        pb "google.golang.org/genproto/googleapis/cloud/vision/v1"
)

type GCSEvent struct {
        Bucket string `json:"bucket"`
        Name   string `json:"name"`
}

var retryableError = xerrors.New("upload: retryable error")

func validate(ctx context.Context, obj *storage.ObjectHandle) error {
        attrs, err := obj.Attrs(ctx)
        if err != nil {
                return xerrors.Errorf("upload: failed to get object attributes %q : %w",
                        obj.ObjectName(), retryableError)
        }
        // You can enlarge maximum size up to 20MB by modifying this line. 
        if attrs.Size >= 1024*100 {
                return fmt.Errorf("upload: image file is too large, got = %d", attrs.Size)
        }
        // Validates obj and returns true if it conforms supported image formats.
        if err := validateMIMEType(ctx, attrs, obj); err != nil {
                return err
        }
        // Validates obj by calling Vision API.
        return validateByVisionAPI(ctx, obj)
}

func validateMIMEType(ctx context.Context, attrs *storage.ObjectAttrs, obj *storage.ObjectHandle) error {
        r, err := obj.NewReader(ctx)
        if err != nil {
                return xerrors.Errorf("upload: failed to open new file %q : %w",
                        obj.ObjectName(), retryableError)
        }
        defer r.Close()
        if _, err := func(ct string) (image.Image, error) {
                switch ct {
                case "image/png":
                        return png.Decode(r)
                case "image/jpeg", "image/jpg":
                        return jpeg.Decode(r)
                case "image/gif":
                        return gif.Decode(r)
                default:
                        return nil, fmt.Errorf("upload: unsupported MIME type, got = %q", ct)
                }
        }(attrs.ContentType); err != nil {
                return err
        }
        return nil
}

// validateByVisionAPI uses Safe Search Detection provided by Cloud Vision API.
// See more details: https://cloud.google.com/vision/docs/detecting-safe-search
func validateByVisionAPI(ctx context.Context, obj *storage.ObjectHandle) error {
        client, err := vision.NewImageAnnotatorClient(ctx)
        if err != nil {
                return xerrors.Errorf(
                        "upload: failed to create a ImageAnnotator client, error = %v : %w",
                        err,
                        retryableError,
                )
        }
        ssa, err := client.DetectSafeSearch(
                ctx,
                vision.NewImageFromURI(fmt.Sprintf("gs://%s/%s", obj.BucketName(), obj.ObjectName())),
                nil,
        )
        if err != nil {
                return xerrors.Errorf(
                        "upload: failed to detect safe search, error = %v : %w",
                        err,
                        retryableError,
                )
        }
        // Returns an unretryable error if there is any possibility of inappropriate image.
        // Likelihood has been defined in the following:
        // https://github.com/google/go-genproto/blob/5fe7a883aa19554f42890211544aa549836af7b7/googleapis/cloud/vision/v1/image_annotator.pb.go#L37-L50
        if ssa.Adult >= pb.Likelihood_POSSIBLE ||
                ssa.Medical >= pb.Likelihood_POSSIBLE ||
                ssa.Violence >= pb.Likelihood_POSSIBLE ||
                ssa.Racy >= pb.Likelihood_POSSIBLE {
                return errors.New("upload: exceeds the prescribed likelihood")
        }
        return nil
}

// distributionBucket is the distribution bucket.
// It's used for distributing all of passed files.
// TODO: This value MUST be updated before deploying this function.
const distributionBucket = "DISTRIBUTION_BUCKET"

// UplaodImage validates the object and copy it into the distribution bucket.
func UploadImage(ctx context.Context, e GCSEvent) error {
        client, err := storage.NewClient(ctx)
        if err != nil {
                return fmt.Errorf("upload: failed to construct a client, error = %v", err)
        }
        defer client.Close()

        dst := client.Bucket(distributionBucket).Object(e.Name)
        _, err = dst.Attrs(ctx)
        // Avoid proceeding if the object has been copied to destination.
        if err == nil {
                log.Printf("upload: %s has already been copied to destination\n", e.Name)
                return nil
        }
        // Return retryable error as there is a possibility that object does not temporarily exist.
        if err != storage.ErrObjectNotExist {
                return err
        }
        src := client.Bucket(e.Bucket).Object(e.Name)
        if err := validate(ctx, src); err != nil {
                if xerrors.Is(err, retryableError) {
                        return err
                }
                log.Println(err)
                return nil
        }
        // Returns an error if the copy operation failed.
        // Will retry the same processing later.
        if _, err := dst.CopierFrom(src).Run(ctx); err != nil {
                return err
        }

        return nil
}
上記のソースコードにおける UploadImage という関数を Cloud Functions にデプロイするために、次のコマンドを実行します。

  gcloud functions deploy UploadImage --runtime go111 --trigger-resource [UPLOADABLE_BUCKET] --trigger-event google.storage.object.finalize --retry
--retry オプションを付けて実行することで、Cloud Functions や Cloud Storage の一時的なエラーに備えることができます。一方でリトライを有効化する際には、それを考慮してコーディングをする必要がある点に注意してください。また前出のソースコードについても、 Cloud Functions が at-least once の実行を保証することを考えれば、 Distribution Bucket へのコピーが一回以上行われる可能性を否定できないなど、冪等な実装になっているとは言えませんが、この場合は副作用はありません。

ファイルをアップロードする


ここまでに設計し、実装してきたシステムに対して、実際にファイルをアップロードしてみましょう。テストは非常に簡単です。前述の署名付き 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 のコンサルティングサービスを確認してください。