概要
前回の記事でGAE/GoからGCSのコンテンツにアクセスする時のコードがとても良くなかった。
それを解決するために一定時間アクセスが可能になる署名付きリンクを生成し、それをクライアントに渡す。というコードを書いた。
前回は何が悪かったのか
リソースを一瞬で消費する
AppEngine上で画像を展開して、それをリクエストしてきたクライアントに画像ファイルで渡す。というコードになっていた。
これは例えば3MBのファイルに1000リクエスト来たら3GB消費するということになっているのでメモリやプロセスを無駄に消費してしまう。
遅い
1つの画像をクライアントに渡すのに2回ロードが必要になる。
・GCSからAppEngineでロード
・AppEngineからクライアントでロード
とても効率が悪い。
解決策
署名付きリンクを生成する。
その上でAppEngineはクライアントに(画像の代わりに)GCSにアクセスができる署名付きリンクを渡してあげる。
これを行うことで、AppEngineはURL文字列を返すだけになるのでとても安価な処理になる。
また、1つの画像をクライアントに渡すまで、AppEngineでのロードが必要なくなったのでその分早くなった。
署名付きリンクとは
これは、バケットとオブジェクトに対するクエリ文字列認証のためのメカニズムです。署名付き URL は、時間制限のある読み取りや書き込みのアクセス権を、Google アカウントを持っているかどうかにかかわらず、URL を知っている全員に許可するための手段です
つまりこの場合、GCSのコンテンツに対して一定期間だけアクセス可能になるURLを発行し、そのURLが有効な間は誰でもアクセスができるというもの。
注意点としては、クライアントがこのURLをキャッシュするような場合は有効期限が切れてしまいアクセスできない可能性がある点。
署名付きリンクを生成するコード
GAE/GoでWebフレームワークはginを使っています。
func GetPageImageOfSignedURL(g *gin.Context) {
signedURL, err := GCSManager.GetSignedUrl(g)
if err != nil {
g.String(http.StatusBadRequest, err.Error())
}
g.String(http.StatusOK, signedURL)
}
func GetSignedUrl(g *gin.Context) (url string, err error) {
c := appengine.NewContext(g.Request)
//Defaultのバケット名を取得する
bucketName, err := file.DefaultBucketName(c)
if err != nil {
return url, err
}
//fileNameは例えば[bucket名]/foo/bar.jpgというようなディレクトリ構造を持つファイルの場合は"foo/bar.jpg"と指定する。
fileName := "foo/bar.jpg"
//ここでExpireする時間を決める。(今回は60秒とした。)
expires := time.Now().Add(time.Second * 60)
//[projectID]@appspot.gserviceaccount.comを取得する。こうしなくてもstring直書きでも問題ない。
acc, _ := appengine.ServiceAccount(c)
//SignedURLを取得する(Optionについては後述)
url, err = storage.SignedURL(bucketName, fileName, &storage.SignedURLOptions{
GoogleAccessID: acc,
SignBytes: func(b []byte) ([]byte, error) {
_, signedBytes, err := appengine.SignBytes(c, b)
return signedBytes, err
},
Method: "GET",
Expires: expires,
})
//URL文字列を返す。
return url, err
}
storage.SignedURLOptionsについて
参考:https://cloud.google.com/storage/docs/access-control/signed-urls
GoogleAccessID:必須
[projectID]@appspot.gserviceaccount.com を指定する。
PrivateKey または SignBytes:どちらかが必須
今回はSignBytes関数を利用した。
(SignBytes関数はこのアプリケーションがGAE上で動いている時に使えるGAE内部の認証関数とのこと)
PrivateKeyはGoogleDeveloperConsoleからClientIDを作り、そこからP12証明書を生成する。
それをPEMファイルに変換し、それをByteのスライスに変換してPrivateKeyに代入する。
これに関しては自分はやっていないので詳しい解説はここ参照
Method:必須
署名付きURLで使用するHTTP動詞。
Expires:必須
署名の有効期限が切れるタイムスタンプ。UNIXTimeで表現する。
結果
https://storage.googleapis.com/bucketname/foo/bar.jpg?Expires=1485049086&GoogleAccessId=hogeproject%40appspot.gserviceaccount.com&Signature=ZNlN6NzfPb6DiR%2FxOAjUN4LF7f7pkJEusZ3zr8EEnrofquPCNUtd%2FXek2ur9gKA8sEGBbzMMp%2BKzMS6vyBJl9bDCKf6XnL2N3ZBnx7W%2B1anhCXajpgwAsH6FVxYN9OD1EOqgtTlZxv3tyRfvyaipfrtB1d2Ip84gdxVVTsWVAhk2SmqTehNJor94LW%2BrkyBpn%2Bm3M%2BlRGcx9ZAu8BPuywIGyt%2BW4G8ve6cXJzzpJBJYnosJwKx047%2BSoeTdvk2p%2BLyR%2BfyHhS6OV7RDPDaWX4YGXxcw8v2Q0Pnp2iOs5kegMZOiKup1NyqhRHFNo9tX8C%2FPQ0b38FGQAlcOREheFIA%3D%3D
のようなURLが生成される。
60秒間はアクセスできる。しかし、60秒以上経つと
<Error>
<Code>ExpiredToken</Code>
<Message>The provided token has expired.</Message>
<Details>Request has expired: 1485049363</Details>
</Error>
という表示がされる(ブラウザで確認したもの)