🔏

署名付きURLを使用してCloud Storageにファイルアップロードをする方法

2024/05/13に公開

対象者

  • 署名付きURLを使用してCloud Storeageへのファイルアップロード処理を行いたい
  • そのためのバックエンド環境をGoで実装し、Terraformで構築したい

という人向け。

署名付きURLとは

署名付きURLとは、一定期間だけファイルストレージに対してアクセス権が付与されたURLのこと。

この署名付きURLを使用することで、以下のようにサーバで認証をはさみながら、フロントエンドから直接ファイルアップロードを行うことができる。

単純なテキストファイルであればサーバで認証を行った後にそのままファイルアップロードを行う方がシンプルな実装になる。
しかし、音声ファイルや動画ファイルのような重たいファイルではサーバに負荷がかかってしまう。

そのような場合、署名付きURLを使用してフロントエンドからファイルをアップロードすることで、サーバの負荷を抑えることができる。

Terraformでインフラ構築

# バケットの作成
resource "google_storage_bucket" "upload_bucket" {
  name                        = "upload_bucket"
  location                    = "ASIA-NORTHEAST1"
  uniform_bucket_level_access = true

  cors {
    origin          = ["https://foo-bar.com"]
    method          = ["PUT", "OPTIONS"]
    response_header = ["*"]
    max_age_seconds = 3600
  }
}

# サービスアカウントの作成
resource "google_service_account" "upload_sa" {
  account_id   = "service-account-id"
  display_name = "Service Account"
}

# ロールを付与
resource "google_project_iam_member" "upload_member" {
  for_each = [
    "roles/iam.serviceAccountTokenCreator",
    "roles/storage.objectUser",
    ]
  role     = each.value
  project  = "projectID"
  member   = "serviceAccount:${google_service_account.upload_sa.email}"
}

# (おまけ) GKEを使っている場合、k8sとGCPのSAを紐づけ
resource "google_service_account_iam_binding" "workload_identity_binding" {
  service_account_id = google_service_account.upload_sa.name
  role               = "roles/iam.workloadIdentityUser"
  members = [
    "serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"
  ]
}

上記は一例。フィールドは条件によって変わるため以下を参考に適宜編集。

google_storage_bucket:バケットの作成

name(Required)

バケットの名前

location(Required)

バケットのロケーションを指定

https://cloud.google.com/storage/docs/locations?hl=ja

force_destroy(Optional, Default: false)

オブジェクトがある場合にバケットを削除できるかどうか

project(Optional)

リソースが属するプロジェクトの ID。指定されない場合は、providerのプロジェクトが使用される。

storage_class(Optional, Default: 'STANDARD')

バケットのストレージクラスを指定

https://cloud.google.com/storage/docs/storage-classes?hl=ja

autoclass(Optional)

特定の期間アクセスされないオブジェクトを、よりコスト効率の良いストレージクラスに自動的に移行させるかどうか。

lifecycle_rule(Optional)

オブジェクトのライフサイクル。一週間で削除するかどうかなど。

https://cloud.google.com/storage/docs/lifecycle?hl=ja#configuration

versioning(Optional)

バケットに格納されるオブジェクトのバージョニング設定

https://cloud.google.com/storage/docs/object-versioning?hl=ja

website(Optional)

バケットをwebサイトとして動作させるか

cors(Optional)

バケットのCORS(オリジン間リソース共有)の設定。
CORSとは別オリジンのサーバーへのアクセスをオリジン間 HTTP リクエストによって許可できる仕組みのこと(参考)。

署名付きURLを用いたフロントエンドからのファイルアップロードでは、CORSの設定が必須

origin(Optional)

許可されるオリジンのリスト。

method(Optional)

許可されるHTTPメソッドのリスト。

署名付きURLを用いたアップロードではPUTメソッドが使われる。

またPUTメソッド以外に、実際のリクエストを送信する前にリソースへのアクセスが許可されているかを確認するためのプリフライトリクエストが行われる可能性があるため、OPTIONSメソッドも追加しておく。

response_header(Optional)

ブラウザが読み取りを許可されるヘッダー

max_age_second(Optional)

ブラウザがプリフライトリクエストの結果をキャッシュする時間を指定。キャッシュすることで同じリソースへの後続のリクエストに再利用できる。

default_event_based_hold(Optional)

オブジェクトを特定のイベントが発生するまで保持するかどうか

retention_policy(Optional)

バケット単位での保持ポリシー(Bucket Lock)の設定

labels(Optional)

バケットへのラベル付与

logging(Optional)

ロギングの設定

https://cloud.google.com/storage/docs/access-logs?hl=ja

encryption(Optional)

オブジェクトの暗号化設定。デフォルトで暗号化はされている。

https://cloud.google.com/storage/docs/encryption?hl=ja

enable_object_retention(Optional, Default: false)

オブジェクト単位での保持ポリシー(Object Lock)の設定

requester_pays(Optional, Default: false)

リクエスト元に支払いさせるか

https://cloud.google.com/storage/docs/requester-pays?hl=ja

rpo(Optional)

クロスリージョンでのレプリケーションのrpo(recovery point objective)設定。
デュアルリージョンとマルチリージョンのバケットにのみに適用される。

uniform_bucket_level_access(Optional, Default: false)

Cloud Storage リソースへのアクセスを均一に管理するか。

均一なバケットレベルのアクセスを有効にすると、アクセス制御リスト(ACL)が無効になり、バケットレベルの IAM 権限だけがそのバケットとその中のオブジェクトへのアクセス権を付与するように制限される。

有効にすることが推奨されている。

https://cloud.google.com/storage/docs/uniform-bucket-level-access?hl=ja#should-you-use

public_access_prevention(Optional)

publicアクセスの拒否

custom_placement_config(Optional)

ディアルリージョンの場合のリージョン指定

soft_delete_policy(Optional, Computed)

オブジェクトが削除されてから保持される期間

google_service_account:サービスアカウントの作成

account_id(Required)

サービスアカウントID

display_name(Optional)

サービスアカウントの表示名

description(Optional)

サービス アカウントの説明

disabled(Optional)

サービスアカウントを無効にするかどうか。デフォルトはfalse。このフィールドは作成時には影響しない。サービスアカウントを無効にするには、作成後に設定する必要あり。

project(Optional)

サービスアカウントが作成されるプロジェクトのID。デフォルトはproviderのプロジェクト。

create_ignore_already_exists(Optional)

同じemailが既に存在していた場合、サービスアカウントの作成をスキップするかどうか。

google_project_iam_member:ロールの付与

member(Required)

ロールを付与する ID

role(Required)

上記のmemberに付与するロール

project(Required)

ターゲットのプロジェクトID。
providerから推測してくれるわけではないため注意する

condition(Optional)

条件付きのアクセス制御設定項目。

本番環境に関する問題を解決するために、ユーザーに一時的なアクセス権を付与したり、会社のオフィスからリクエストを行う従業員にのみアクセス権を付与することなどができる。

https://cloud.google.com/iam/docs/conditions-overview?hl=ja

(おまけ)google_service_account_iam_binding:k8sのSAとGCPのSAの紐づけ

GKEを使っている場合、GKEのサービスアカウントとGCPのサービスアカウントを紐づけ(Workload Identity 連携)する必要がある。

k8sのyamlファイルにも修正が必要。この記事では割愛。

https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity?hl=ja#kubernetes-sa-to-iam

実装

ほぼ公式ドキュメントと一緒。

package main

import (
	"context"
	"fmt"
	"io"
	"os"
	"time"

	"cloud.google.com/go/storage"
)

func generateV4PutObjectSignedURL(bucket, object string) (string, error) {
	ctx := context.Background()
	client, err := storage.NewClient(ctx)
	if err != nil {
		return "", fmt.Errorf("storage.NewClient: %w", err)
	}
	defer client.Close()

	opts := &storage.SignedURLOptions{
		Scheme: storage.SigningSchemeV4,
		Method: "PUT",
		Expires: time.Now().Add(15 * time.Minute),
	}

	u, err := client.Bucket(bucket).SignedURL(object, opts)
	if err != nil {
		return "", fmt.Errorf("Bucket(%q).SignedURL: %w", bucket, err)
	}

	return u, nil
}

func main() {
	u, err := generateV4PutObjectSignedURL("bucketName", "objectName")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return
	}

	fmt.Printf("%q\n", u)
}

ローカルで試す場合はサービスアカウントキーを発行し、ダウンロードした認証情報ファイルのPATHを通す必要がある。

export GOOGLE_APPLICATION_CREDENTIALS=hoge_fuga.json

PATHを通すことでGoogleAccessIDPrivateKeystorage.SignedURLOptionsに含めなくても自動で認証情報が読みこまれる。

参考

https://pkg.go.dev/cloud.google.com/go/storage#hdr-Credential_requirements_for_signing

アップロード方法は以下のようにcurlを叩く。

curl -X PUT --upload-file ~/Downloads/upload.csv "signedURL"

Discussion