nikkie-ftnextの日記

イベントレポートや読書メモを発信

ライブラリGCSFSを使う時、クレデンシャルを環境変数 GOOGLE_APPLICATION_CREDENTIALS でもapplication default credentials(gcloud auth application-default login)でも指定できるのはどういう仕組み? 縁の下にはgoogle-authがいたのです

はじめに

グラスゴー・コーマ・スケールというGCSもある。nikkieです。

Python製のライブラリGCSFSを使っていて気になったことを、ソースコードを読んで調べました。

目次

結論(タイトルへの回答)

  • GCSFSが独自に、環境変数やapplication default credentialsを読み込む実装をしているわけではない
  • google-authライブラリのdefaultメソッドを呼び出している(google.auth.default
  • defaultメソッドが環境変数もapplication default credentialsも扱う

バージョン情報

以下のバージョンのソースコードを読みました。

  • GCSFS 2023.6.0
  • google-auth 2.21.0

GCSFS

Pythonic file-system for Google Cloud Storage

GCS(Google Cloud Storage)のファイル操作に使っています。
ドキュメントには「This software is beta, use at your own risk.」(ベータ版です。ご自身の責任において使ってください)とあります1

pip install gcsfsでインストールでき、簡単に使い出せます

https://gcsfs.readthedocs.io/en/latest/#examples

>>> import gcsfs
>>> fs = gcsfs.GCSFileSystem(project='my-google-project')

GCSFSのCredentials

GCSへの接続情報(クレデンシャル)の設定も簡単です。
例えば以下の2つの方法があります

  • 環境変数 GOOGLE_APPLICATION_CREDENTIALS
  • gcloud auth application-default login

サポートするクレデンシャルについては、ドキュメントにも記載があります。
https://gcsfs.readthedocs.io/en/latest/#credentials

どういう仕組みで環境変数でもgcloud authコマンドでもクレデンシャルを設定できているのかが、私、気になりました!

Exampleのコードから始めるソースコードリーディング

GCSFileSystem初期化

GCSFileSystemの実装はcore.pyにあります。
https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/core.py#L153
このdocstringはCredentialsのドキュメントと同内容です。

初期化する際(__init__)に、GoogleCredentialsを初期化して設定しています。
https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/core.py#L318

self.credentials = GoogleCredentials(project, access, token)

GoogleCredentials初期化

GoogleCredentialsの実装はcredentials.pyにあります。
https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/credentials.py#L39

__init__で初期化する中でconnectメソッドを呼び出しています。
https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/credentials.py#L50

self.connect(method=token)

ここでtokenが指す値ですが、

  • ExamplesのコードでGCSFileSystemのtoken引数は指定していない
    • デフォルト値のNone2
  • GoogleCredentialsの初期化に渡るtokenの値はNone
  • つまり、self.connect(method=None)と呼び出している

GoogleCredentialsのconnectメソッド

https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/credentials.py#L208

実装は、Credentialsのドキュメントの説明と同様でした(一致しているのは当たり前ですね)。

method引数の値がNoneの場合の動きを見ていきます。
method is Noneですから、230行目からのブロックが実行されます。
https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/credentials.py#L230

これは(Credentialsのドキュメントに概要があるように)

  1. google_default
  2. cache
  3. cloud
  4. anon

の順でconnectメソッドを(再帰的に)呼び出します。
https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/credentials.py#L232

connectメソッド呼び出しで例外が送出されなければ、そこでクレデンシャルが取得できたので反復は終了です(for文をbreak)。
どの呼び出しでも例外が送出された場合は、クレデンシャルが取得できなかったということであり、connectメソッドも例外を送出します3

では、再帰的な呼び出しでself.connect(method="google_default")のようになった場合はどうなるのでしょうか。
実行されるブロックは以下です。
https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/credentials.py#L249-L250

self.__getattribute__("_connect_" + method)()
self.method = method

この中の__getattribute__がポイント4
ここでは_connect_google_defaultメソッドを取得し、続くカッコ()で呼び出します。
文字列を渡して__getattribute__でメソッドを取得できるのか〜!

GoogleCredentialsの_connect_google_defaultメソッド

https://github.com/fsspec/gcsfs/blob/2023.6.0/gcsfs/credentials.py#L76

google.auth.defaultメソッドを呼び出します5
ここが今回の疑問の答えです。

google-authのdefaultメソッド(google.auth.default

https://googleapis.dev/python/google-auth/2.21.0/reference/google.auth.html#google.auth.default
ドキュメントに書かれています。

This function acquires credentials from the environment in the following order:

全部で5つ書かれていますが、今回の疑問に関係するのは次の2つ。

  1. If the environment variable GOOGLE_APPLICATION_CREDENTIALS is set to the path of a valid service account JSON private key file, then it is loaded and returned. (略)

  2. If the Google Cloud SDK is installed and has application default credentials set they are loaded and returned.

第一に環境変数GOOGLE_APPLICATION_CREDENTIALSに指定されたパスのサービスアカウントファイル(JSONファイル)を探し、それがない場合はgcloud auth application-default loginで有効にされたapplication default credentialsを探す(どちらもないときの動きは今回は省略)わけですね!

実装は以下にあります。
https://github.com/googleapis/google-auth-library-python/blob/v2.21.0/google/auth/_default.py#L543
_get_explicit_environ_credentials環境変数で指定したパスのファイルから読み込む関数です。
その実装の中では6environment_vars.CREDENTIALSGOOGLE_APPLICATION_CREDENTIALSという名前の環境変数にアクセスしています!
https://github.com/googleapis/google-auth-library-python/blob/v2.21.0/google/auth/environment_vars.py#L36

終わりに

GCSFSを使っていて覚えた疑問からソースコードを読み、google-authライブラリのdefaultメソッドを知りました。
defaultメソッドが、環境変数GOOGLE_APPLICATION_CREDENTIALSが指すJSONファイルから読み込んだクレデンシャルやapplication default credentialsを返しています。
環境変数からもapplication default credentialsからもという振る舞いはgoogle-authライブラリによるものなので、google-authライブラリを使えばこの振る舞いを再利用できるわけですね!

ソースコードリーディングとしては、connectメソッドの4回の呼び出しの実装をそれぞれざっと眺めた結果、_connect_google_defaultメソッドに当たりがつきました。
__getattribute__で文字列からメソッドを取得でき、それを呼び出すのはちょっとメタくもあり、学びとなりました。
またconnectメソッドを再帰的に呼ぶ実装も興味深かったです。