Google App Engine Node.jsでGoogle Cloud StorageのSigned URLを取得する
結論
- 「App Engine default service account」に
roles/iam.serviceAccountTokenCreator
IAM役割を付与する - 「Identity and Access Management (IAM) API」を有効にする
- 公式クライアントの
getSignedUrl()
メソッドを使用する際はaction
とexpires
オプションを必ず指定する
せっかくApp Engine Standard Environment Node.jsがリリースされたので色々いじりつつ、お試しプロダクトを作っていたところ思わぬところで躓いたので備忘録的にまとめる。
とはいえ、改善のプルリクエストも出す(予定)だし、今後は同様の問題で躓くことはないだろうけれど……。
Google App Engineで実行されるService AccountのIAM
GAEやGCPの権限管理が複雑に絡んでくるのでまずはそのあたりの説明から。
GCPにはAWSと同様にIAMというアカウントによって権限が安全かつ明確に管理されている。
このIAMには大きく分けて5つのアカウントの種類があるが、よく使うのは2種類なので、まずはこの2種類を覚えて欲しい。
「Google アカウント」と「サービス アカウント」だ。
「Googleアカウント」はGmailやYouTubeにログインするあのGoogleアカウントのことで、1つのアカウント=現実の人間1人、という想定で作られている。GCPに収まらないGoogleサービス全体で使えるアカウントだ。
「サービスアカウント」は人間ではなくコンピューターのために発行するアカウントだ。GCPのCompute EngineやCloud Functions、もちろんGAEにも発行されるし、オンプレミスのサーバーや開発に使用しているMacにも発行できる。こちらはGCPの中だけで使えるアカウントになる。
さて、GAEにはデフォルトで「App Engine default service account」というものがあり、メールアドレス(実際に受信できるアドレスではなくIDという意味)は PROJECT_ID@appspot.gserviceaccount.com
だ。
このアカウントは設定しなくてもデフォルトでGAEに読み込まれるので「Application Default Credentials (ADC)」と呼ばれている。
もちろんオンプレミスのサーバーでGCPサービスを使用する場合などと同様、新しくサービスアカウントを作成して、そのJSON Keyファイルを一緒にデプロイすれば、別のサービスアカウントを使うこともできるが、設定なしで使えるADCを使った方がシンプルだ。
詳しくはCloud IAMのドキュメントに書いてある。
という訳で、このサービスアカウントがGAEでプログラムを作動させるときの実行アカウントになるので、Google Cloud StorageのSigned URLを発行するために必要な権限を与えればよい。
Signed URLを発行するために必要な権限
ではGoogle Cloud StorageのSigned URLを発行するために必要な権限はなにか。
Node.js以外のGAEについて詳しい人は「App Engine App Identityサービス」を使うことでSigned URLを作成できると知っているかもしれない。
GCSのドキュメントにもそのように書いてある。
しかし、Node.jsはこれまでのApp Engine特有のAPIにほとんどアクセスできず、GCP共通のAPIを通して実行しなければならない。
その権限についてはまだGCSのドキュメントには記載されていないが、GCEなどで実行する場合と同じなので、GCSのNode.js向けクライアントライブラリに記載されていた。
In Google Cloud Platform environments, such as Cloud Functions and App Engine, you usually don’t provide a keyFilename or credentials during instantiation. In those environments, we call the signBlob API to create a signed URL. That API requires either the https://www.googleapis.com/auth/iam or https://www.googleapis.com/auth/cloud-platform scope, so be sure they are enabled.
簡単にまとめると、signBlob APIを使うので iam.serviceAccounts.signBlob
という権限が必要だと書いてある。
IAM API (Node.js)でもApp Identity API (Python, Go, Java, PHP)でもsignBlobをしているだけで、やっていることは同じだが、必要な権限が違うという訳だ。
ドキュメントでは https://www.googleapis.com/auth/iam
というスコープの権限が必要だと書かれているが、権限を管理する方法としてのスコープはレガシーだそうで、現在ではIAM権限で管理するのが一般的となっている。
IAM権限は個別に付与するのではなく、IAM役割というGoogleが事前に定義したロールを付与することでアタッチする。iam.serviceAccounts.signBlob
権限が含まれるのは roles/iam.serviceAccountTokenCreator
役割だ。
この役割を PROJECT_ID@appspot.gserviceaccount.com
に付与すれば必要な権限は満たせている。
有効にしなければいけないAPI
ところがこれで完了ではない。。
GCP上でなにかを実行するためには、権限とAPIが必要だ。
今回実行しようとしている projects.serviceAccounts.signBlob
というメソッドは「Identity and Access Management (IAM) API」というAPIに属しており、これを有効にしなければいけない。
こちらはコンソールから「APIとサービス」 => 「APIとサービスの有効化」にて行える。
ここまで行うと、ようやくクライアントライブラリの signedURL()
というメソッドを使用できるようになった。
普段なら、例えばBigQueryを使い始めると自動的にそのサービスに必要なサービスアカウントが作成され必要なIAM権限が付与され必要なAPIが有効化されるが、都度設定するとここまで複雑だとは思っていなかった。
GCSのNode.js向け公式クライアントの落とし穴
GCSのNode.js向け公式クライアントライブラリは npm install --save @google-cloud/storage
でインストールできる。
TypeScriptの型定義ファイルは npm install --save-dev @types/google-cloud__storage
だ。
このクライアントライブラリにある getSignedUrl()
というメソッドに落とし穴があった。
簡単にまとめるとオプションが必須で、指定しないと以下のような原因を知らせないエラーメッセージを返す。
/srv/node_modules/@google-cloud/storage/src/file.js:1818
callback(null, signedUrl);
^
TypeError: callback is not a function
at /srv/node_modules/@google-cloud/storage/src/file.js:1818:5
at Request._callback (/srv/node_modules/google-auto-auth/index.js:362:9)
at Request.self.callback (/srv/node_modules/request/request.js:185:22)
at emitTwo (events.js:126:13)
at Request.emit (events.js:214:7)
at Request.<anonymous> (/srv/node_modules/request/request.js:1157:10)
at emitOne (events.js:116:13)
at Request.emit (events.js:211:7)
at IncomingMessage.<anonymous> (/srv/node_modules/request/request.js:1079:12)
at Object.onceWrapper (events.js:313:30)
さらにTypeScriptでもオプションは任意と指定されている。
そんな訳で、オプションが必須だと気付くまでかなり時間がかかった。。
TypeScriptを安易に信じない
TypeScriptの型定義ファイルは公式に提供されているものと、コミュニティーが勝手に作成したものがある。
コミュニティー製の型定義ファイルはアップデートに追随していなかったり抜け漏れがあったりするので安易に信じてはいけないと学んだ。
一応、諸々のプルリクエストを作成してコミットしようと思っている。
最後に、Signed URLを取得するためのサンプルコード全体を置いておく。