PythonでGoogle Cloud Storageの署名付きURLを作成する

[Google Cloud Platform その２ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gcp2)の3日目の投稿となります。

Google App Engine(GAE)でアプリ開発をしていたのですが、ファイルアップロード機能を実装していると、アップロードできるファイルサイズに制限があったため、Google Cloud Storage(GCS)の署名付きURLを作成して直接アップロードすることにしました。

署名付きURLを作成するのに1日ほどハマったので、まとめておきます。


## 環境構築

仮想環境を作成して環境を構築します。

ソースはGitHubにアップしていますので、よければご参考ください。
https://github.com/kai-kou/create-cloud-storage-signed-url

```sh
> python --version
Python 3.6.6

> python -m venv venv
>. venv/bin/activate

> touch requirements.txt
> touch main.py
```

Google Cloud Platform(GCP)のサービスアカウントを利用してURLを生成するのに、oauth2clientを利用します。

```py:requirements.txt
oauth2client
```

実装は下記を参考にさせてもらいました。

authentication - Cloud storage and secure download strategy on app engine. GCS acl or blobstore - Stack Overflow
https://stackoverflow.com/questions/29847759/cloud-storage-and-secure-download-strategy-on-app-engine-gcs-acl-or-blobstore


署名付きURLの作成方法については公式ドキュメントが詳しかったです。

Generating Signed URLs with Your Own Program  |  Cloud Storage  |  Google Cloud
https://cloud.google.com/storage/docs/access-control/signing-urls-manually

```python:main.py
import time

import urllib

from datetime import datetime, timedelta

import os

import base64

from oauth2client.service_account import ServiceAccountCredentials

API_ACCESS_ENDPOINT = 'https://storage.googleapis.com'


def sign_url(bucket, bucket_object, method, expires_after_seconds=60):
    gcs_filename = '/%s/%s' % (bucket, bucket_object)
    content_md5, content_type = None, None

    credentials = ServiceAccountCredentials.from_json_keyfile_name('[サービスアカウントキーのファイルパス]')
    google_access_id = credentials.service_account_email

    expiration = datetime.now() + timedelta(seconds=expires_after_seconds)
    expiration = int(time.mktime(expiration.timetuple()))

    signature_string = '\n'.join([
        method,
        content_md5 or '',
        content_type or '',
        str(expiration),
        gcs_filename])
    _, signature_bytes = credentials.sign_blob(signature_string)
    signature = base64.b64encode(signature_bytes)

    query_params = {'GoogleAccessId': google_access_id,
                    'Expires': str(expiration),
                    'Signature': signature}

    return '{endpoint}{resource}?{querystring}'.format(
        endpoint=API_ACCESS_ENDPOINT,
        resource=gcs_filename,
        querystring=urllib.parse.urlencode(query_params))


if __name__ == '__main__':
    url = sign_url('[バケット名]', '[オブジェクト名(ファイル名)]', 'GET')
    print(url)
```

上記で指定する[サービスアカウントキーのファイルパス]はGCPのサービスアカウントを作成すると取得できるjsonファイルとなります。サービスアカウントには必要なGCSの役割を付与します。

サービスアカウントの作成方法は下記が詳しかったです。

Google Cloud Platform のサービスアカウントキーを作成する | MAGELLAN BLOCKS
https://www.magellanic-clouds.com/blocks/guide/create-gcp-service-account-key/


## 実行する

実行するとURLが生成されます。URLにアクセスする、指定したGCSのバケットにあるオブジェクトが取得できることが確認できます。

```sh
> python main.py

https://storage.googleapis.com/[バケット名]/[オブジェクト名]?GoogleAccessId=xxxxx%40xxxxx.iam.gserviceaccount.com&Expires=1542687588&Signature=xxxxxxxxxx
```

## ハマりポイント

### GCPサービス上で```GoogleCredentials.get_application_default()``` が使えない

Google App Engine(GAE)やGoogle Cloud Functions(GCF)だと、実行に利用しているサービスアカウントの情報が取得できるので、```GoogleCredentials.get_application_default()```  で ```Credentials``` を利用しようとしたのですが、```service_account_email``` が空で、```sign_blob``` メソッドも利用できませんでした。


## まとめ

AWSだと署名付きURLの作成はもう少し簡単だった記憶があったので、GCPでも同じ感覚でいたら、~~ひどい目にあいました。~~自前でURLを組み立てる必要があったので、手間がかかりましたが、作成方法さえわかれば、あとは、GAEやGCFでの利用も簡単そうです。


## 参考

authentication - Cloud storage and secure download strategy on app engine. GCS acl or blobstore - Stack Overflow
https://stackoverflow.com/questions/29847759/cloud-storage-and-secure-download-strategy-on-app-engine-gcs-acl-or-blobstore

Generating Signed URLs with Your Own Program  |  Cloud Storage  |  Google Cloud
https://cloud.google.com/storage/docs/access-control/signing-urls-manually

Google Cloud Platform のサービスアカウントキーを作成する | MAGELLAN BLOCKS
https://www.magellanic-clouds.com/blocks/guide/create-gcp-service-account-key/