GCS(Google Cloud Storage)へファイルアップロードについて、仕組みの理解とRust言語でそれを実装する時にはまったことをメモする。
1. Google Cloud Storage 基本概念
1-1. バケット
1-1-1. バケットとは
-
ファイルをフォルダを格納するロジカル的なストレージ領域である。
ローカルPCだと、ドライブ名的なもの。(WindowsシステムのCドライブ、Dドライブ) -
アクセス方法:GCP > Cloud Storage > ブラウザ > バケット名をクリック
1-1-2. オブジェクト
不正確かもしれないが、バケット中のファイル名やフォルダ名のことを思えばよい。
1-1-3. 権限
- 当たり前、基本はバケットは非公開
- プリンシパル・ロールで強力的に色々コントロールできる印象
1-2. サービスアカウント
- GCPの普通のユーザと違って、バックエンドAPIで利用するユーザのこと。
- プライベート鍵JSONファイルをダウンロードして、署名付きURL作成時に利用する
Google Cloud Platformサービスアカウントを作成する
1-3. 署名付きアップロードURL
-
下記情報が含まれる
- アップロード先のバケットとオブジェクト名
- http通信メソッド名(PUTかPOST)
- http通信コンテンツタイプ(application/pdfなど)
-
サービスアカウントのプライベート鍵で暗号化する
- サービスアカウント名や暗号化アルゴリズムなどをURLのリクエストパラメータに追加
1-4. GCS側デフォルトで行う署名内容の確認(Validator)
- 指定サービスアカウントの公開鍵で認証する
- http通信メソッドなど一致しているかをチェック
- チェックOKだったらアップロードが行われる
1-5. 対象バケットをサービスアカウントのアクセスを許可する
次のどちらも行ける
1-5-1. サービスアカウントで広い範囲を設定
- Cloudストレージ管理者権限を与えば、全てのGCSをアクセスできてしまう
- バケットごとに細かく設定が不要になるメリットがある
- 広すぎで、やられたら、影響範囲が広いデメリットが大きい
- 管理者権限を与えるための権限が必要で、上長の承認を通らないいけないので、諦める
1-5-2. バケット画面の権限・プリンシプルで該当サービスアカウントの権限を追加
2. アーキテクチャー
- フロントからバックエンドの署名付きURL作成apiを叩く
- 取得されたURLへファイルをアップロードする
3. Rustで署名付きURL作成APIとはまったこと
3-1.軽量クレートtame_gcs
を利用
- 下記はactix-webフレームワークを利用してcontroller側の実装である
- GCS_SERVICE_ACCOUNT_JSON_FILE環境設定には実際のプライベート鍵JSONファイルのパスが設定される
extern crate diesel;
use dotenv::dotenv;
use std::env;
use serde::Serialize;
use serde::Deserialize;
use actix_web::HttpResponse;
use actix_web::web;
use tame_gcs::{signed_url, signing, types};
use http;
#[derive(Serialize)]
struct GoogleCloudStorageUrl<'a> {
signed_file_upload_url: &'a str,
}
#[derive(Deserialize)]
pub struct Param {
file_name: String,
content_type: String,
}
fn signed_url(file_name: &str, content_type: &str) -> Result<url::Url, tame_gcs::Error> {
dotenv().ok();
let gcs_service_account_json_file = env::var("GCS_SERVICE_ACCOUNT_JSON_FILE").expect("GCS_SERVICE_ACCOUNT_JSON_FILE must be set");
let sa = signing::ServiceAccount::load_json_file(gcs_service_account_json_file)?;
let mut headers = http::HeaderMap::default();
headers.insert(http::header::CONTENT_TYPE, http::HeaderValue::from_str(content_type).unwrap());
// let options = signed_url::SignedUrlOptional::default();
let options = signed_url::SignedUrlOptional {
method: http::Method::PUT,
duration: std::time::Duration::from_secs(60 * 60),
headers,
region: "auto",
query_params: Vec::new(),
};
let bucket = types::BucketName::try_from("hoge-upload-test")?;
let object = types::ObjectName::try_from(file_name)?;
let url_signer = signed_url::UrlSigner::with_ring();
let signed_url = url_signer.generate(&sa, &(&bucket, &object), options)?;
Ok(signed_url)
}
pub async fn file_upload_url(query: web::Query<Param>) -> HttpResponse {
let file_name = query.file_name.as_str();
let content_type = query.content_type.as_str();
println!("file_name:{}", file_name);
println!("content_type:{}", content_type);
let url = signed_url(file_name, content_type).unwrap();
println!("url:{}", url.as_str());
HttpResponse::Ok().json(GoogleCloudStorageUrl {
signed_file_upload_url: url.as_str(),
})
}
Cargo.toml依頼の抜粋
[dependencies]
actix-web = "4.0.1"
actix-rt = "2.7.0"
tame-gcs = { version = "0.11.3", features = ["signing"] }
url = "2.2.2"
http = "0.2"
3-2. はまったこと
この記事を参考してすぐに署名付きURLができましたが、それを使ってアップロードすると、理由不明で403エラーになった。
- chromeのコンソールにはなぜかCORSエラーと403が同時に表示される
- CORSエラーがGCSかchromeのバグっぽい、それも目が惹かれってしまった
- 具体的なレスポンスが何もないため、原因究明は難航
- Firefoxならエラーレスポンスがちゃんと見れた。なるほど、SignatureDoesNotMatch署名が一致しないことでした。
<?xml version='1.0' encoding='UTF-8'?><Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message><StringToSign>GOOG4-RSA-SHA256
20220421T163054Z
20220421/auto/storage/goog4_request
c87e06a70044bf8632a035fa36def8e8804085a76bc15574bc95a5f1241bc0d4</StringToSign><CanonicalRequest>POST
/hoge-upload-test/test.pdf
X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=cloud-storage-account-01%40hogehoge-stg.iam.gserviceaccount.com%2F20220421%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220421T163054Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host
host:storage.googleapis.com
host
UNSIGNED-PAYLOAD</CanonicalRequest></Error>
- 原因判明
上記Qiita記事はアップロードためのURL作成ではないからだ。
- デフォルトのSignedUrlOptionalを使ってGETための署名付きURLになる
- GCS側でValidatorする時に、実際はPUTなので、当然通らない
let options = signed_url::SignedUrlOptional::default();
- 修正内容
ネット記事Google Cloud Storage】署名付きURLによる画像のアップロードをみると、http method名やcontent-typeなども設定する必要があるようで、デフォルトをやめてSignedUrlOptionalを生成したら、上手くいった。
4. フロント側の実装とCORSエラーの対処
フロント側がaxiosを使ってファイルアップロード通信を行なうところ、GCSと自サービスのドメインが違うためCORSエラーが発生する。
それを通るには、違うドメインでもいいよとフロント側通信設定とGCSサーバ設定が必要。
4-1. axiosの設定
- Access-Control-Allow-Originをaxiosのdefaults設定に追加
- 下記はputでアップロードする場合の設定
- axios通信メソッドのその場のheader設定が上手くいかない
axios.defaults.headers.put['Access-Control-Allow-Origin'] = '*';
- 1つのファイルをアップロードする場合は、FormDataを使わない方良い
Google Cloud Storageの署名付きURLに対してaxiosでPUTするときにFormDataを使おうとしてはならない
- content-typeがアップロードファイルのタイプである
- pdfファイルなら「application/pdf」
4-2. GCSサーバ設定
gsutilsコマンドラインで行なう。(WEB画面で設定するかと思ったが、できなさそう。)
- cors設定ファイル(cors_setting.json)を用意
[
{
"origin": ["*"],
"responseHeader": ["*"],
"method": ["*"],
"maxAgeSeconds": 3600
}
]
methodやresponseHeaderは必要によって後で変更でOK。
- gsutilsコマンドで対象バケットのCORSを設定
gsutil cors set cors_setting.json gs://hoge-upload-test
- CORS設定内容の確認
gsutil cors get gs://hoge-upload-test
const handleUpload = async () => {
if (!file) return;
axios.defaults.headers.put['Access-Control-Allow-Origin'] = '*';
console.log('file:', file?.name);
console.log('fileType:', file?.type);
const idToken = await getIdToken();
console.log('idToken:', idToken);
const uploadUrl = await axios.get('/api/file_upload_url', {
headers: {
Authorization: `Bearer ${idToken}`,
},
params: {
file_name: file.name,
content_type: file.type,
},
}).then((response) => {
return response.data.signed_file_upload_url;
});
console.log('uploadUrl:', uploadUrl);
const res = await axios.put(uploadUrl, file, {
headers: {
'Content-Type': file.type,
},
});
console.log('upload response:', res);
};
4-3. CORSエラーにはまったことと心得
はまったこと
CORSエラーが解決したのに、GCS403エラーが返された場合、chromeはなぜかCORSエラーログが出力されるので、正しく導かれない。
心得
Chromeのdevtoolsでエラーレスポンスを確認できない場合、Firefoxを使ってエラーレスポンスを確認することが大事。