koukiblog

たぶんweb系の話題

署名付きURLを作成してGCSに直接ファイルをアップロードする

署名付きURLを作成して、GCSに直接ファイルアップロードを試してみました。意外とドキュメントが少なかったので、残しておきます。

下記の記事にサンプル付きで紹介されているのですが、2019年の記事で一部Deprectedになっている箇所もありました(ServiceAccounts.SignBlob を利用している箇所)。 cloud.google.com

署名付きURLを発行するには、GCSバケットにオブジェクト作成権限のあるサービスアカウントで署名する必要があります。このサービスアカウントの権限でGCSにアクセスするできるURLを発行するので、サービスアカウントに付与する権限はオブジェクト作成権限のみなど、必要最低限にするのがよいはずです。 サービスアカウントに紐づく秘密鍵を作成して署名することもできますが、SignBlobという機能を使うと秘密鍵を作成することなく署名することができます。

URLを生成するプログラムを実行しているユーザーに、アップロード用サービスアカウントに対するroles/iam.serviceAccountTokenCreatorロールが必要です。

署名付きURLを作成する関数は以下のようになります。

import (
    "context"
    "time"

    credentials "cloud.google.com/go/iam/credentials/apiv1"
    "cloud.google.com/go/iam/credentials/apiv1/credentialspb"
    "cloud.google.com/go/storage"
)

var (
    serviceAccountName = "アップロード用サービスアカウント名@GCPプロジェクト名.iam.gserviceaccount.com"
    uploadableBucket   = "アップロード先バケット名"
)

func create_signed_url(filename string, content_type string) (string, error) {
    key := generate_key(file_name)
    url, err := storage.SignedURL(uploadableBucket, key, &storage.SignedURLOptions{
        GoogleAccessID: serviceAccountName,
        Method:         "PUT",
        Expires:        time.Now().Add(15 * time.Minute),
        ContentType:    content_type,
        SignBytes: func(b []byte) ([]byte, error) {
            resp, err := sign_blob(b)
            if err != nil {
                return nil, err
            }
            return resp.GetSignedBlob(), nil
        },
    })
    return url, err
}

基本的には、storage.SignedURL関数でURLを作成します。作成時に、GCS上に配置する際のパスとコンテンツタイプを設定します。

アップロードしようとしているファイルのファイル名と、拡張子からパスとコンテンツタイプを設定することになります。上記の例では、generate_key 関数でファイル名をGCSのパスに変換しています。特定のprefixをつけたり、UUIDでファイル名の衝突を避けたりするのがよいと思います。 SignBlobを使った署名は、SignedURLOptionsのSignBytes要素に関数を設定することによって行います。上記のサンプルではクロージャを設定しています。

sign_blob関数は以下のようになります。

func sign_blob(payload []byte) (*credentialspb.SignBlobResponse, error) {
    ctx := context.Background()
    c, err := credentials.NewIamCredentialsClient(ctx)
    if err != nil {
        return nil, err
    }
    defer c.Close()

    req := &credentialspb.SignBlobRequest{
        Name:    "projects/-/serviceAccounts/" + serviceAccountName,
        Payload: payload,
    }
    resp, err := c.SignBlob(ctx, req)

    if err != nil {
        return nil, err
        }

    return resp, nil
  }

SignBlobRequestで、サービスアカウント名を設定するときに、"projects/-/serviceAccounts/{ACCOUNT_EMAIL_OR_UNIQUEID}" というフォーマットにする必要があることに注意が必要です。

pkg.go.dev

生成された署名付きURLを利用してGCSにファイルアップロードするコードは以下のようになります。 この例ではGoでそのままアップロードしていますが、生成したURLをブラウザやアプリのようなクライアントに渡し、クライアントから直接アップロードすることもできます。 この例では、 "hello gcs" という文字列のファイルが "sample.txt"から生成されたパスでGCS上に作成されます。

file_name := "sample.txt"
payload := []byte("hello gcs")
content_type := "text/plain"

url, _ := create_signed_url(file_name, content_type)

req, _ := http.NewRequest("PUT", url, bytes.NewReader(payload))
req.Header.Add("Content-Type", content_type)
client := new(http.Client)
resp, _ := client.Do(req)

Signblob が何かわからず検索すると serviceAccounts.signBlobがヒットするのですが、これがdeprected なのが迷ったポイントでした。

cloud.google.com