Could not load the default credentials エラーについて

Cloud Functions でしばしば見るエラー、Firebase Functions も中身は同じなので起きる。

なにやらちゃんと動いていないぞ... と Stackdriver Logging を見にいくと、こういうエラーメッセージが書かれている。

Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (/srv/functions/node_modules/google-auth-library/build/src/auth/googleauth.js:160:19)
    at process._tickCallback (internal/process/next_tick.js:68:7)

これは Application Default Credentials (アプリケーションのデフォルト認証情報)の設定に失敗するせいで発生する。

GCP 上にデプロイした Cloud Functions は、デフォルトの認証情報を使っていい感じに Firestore や Stackdriver Logging など各種プロダクトを利用できるようになっている。

関数の実行中、Cloud Functions はサービス アカウント PROJECT_ID@appspot.gserviceaccount.com を ID として使用します。たとえば、Google Cloud クライアント ライブラリを使用して Google Cloud Platform サービスにリクエストを送信するときに、Cloud Functions は自動的にトークンを取得して使用し、サービスの認証を行うことができます。
関数 ID  |  Cloud Functions のドキュメント  |  Google Cloud

PROJECT_ID@~ のサービスアカウントはプロジェクトの全リソースへの編集アクセス可能な Role を持っているので、デフォルトでは権限の管理や設定を要求されない。実行時にサービスアカウントを解決する挙動は google-auth-library が担っていて、Firestore など各種プロダクトのクライアントライブラリはこれを使って実装されている。

はじめは認証を省略できることに奇妙な感覚があったけど、実際はちょう便利。コードをデプロイする権限があるなら、サービスをめちゃくちゃにする権限もあるのである。規模が大きくなってくると事故防止のために分けたくなるが、最初の段階で Role や IAM の切り分けに悩まされないのは楽ちんである。...だけど、この権限を解決するための通信が失敗することがある。

google-auth-library の実装を追っていくと、getApplicationDefaultAsyncで実行環境が GCP 内かどうかを解決しようとしている。_checkIsGCE を経て gcp-metadatametadataAccessor を呼び出して GCPメタデータサーバーにアクセスしているのだけど、ここの通信が失敗 or Timeout することがあり、GCP 上での実行と判定されなくなってしまう。(実際に GCP 上であれば、Compute clientメタデータサーバーからアクセストークンを取得して各種 API を叩けるようになる)

そしてこの判定結果はキャッシュされるので、このエラーが出続けたり、コールドスタートを経て新たに失敗するようになったりする。

もちろんこの挙動が問題になってないわけはなくて、この issue で長期に渡って盛り上がっている。
Google Cloud Function - Error: Could not load the default credentials. · Issue #798 · googleapis/google-auth-library-nodejs

対策

対策は今のところ、サービスアカウントキーを同時にデプロイして Cloud Functions 上からも参照するしかない。

サービス アカウント – IAM と管理から、PROJECT_ID@appspot.gserviceaccount.com のキーを発行する(もちろん新しく別のサービスアカウントを作ってもよい)。Cloud Functions をデプロイする際、同時にキーファイルを含め、ディレクトリルートからのパスを GOOGLE_APPLICATION_CREDENTIALS 環境変数にセットする。これで google-auth-ilbrary はこのキーを利用し、GCE メタデータサーバーへ通信する必要がなくなりエラーが起きなくなる。

$ gcloud functions deploy FUNCTION_NAME ... \
  --set-env-vars="GOOGLE_APPLICATION_CREDENTIALS=./service-account-key.json"

サーバー間での本番環境アプリケーションの認証の設定  |  Google Cloud

サービスアカウントキーファイルの管理

強力な権限のキーを公開しちゃうと大事故になる。Cloud KMS でキーファイルを暗号化しておくと安全にリポジトリに含められる。 Cloud Build でデプロイを行って、デプロイフローで復号すると良さそう。暗号化されたリソースの使用  |  Cloud Build のドキュメント に例がある。

そもそもリポジトリに含めたくない場合は、Cloud Storage に置いておいて、デプロイフローでダウンロード & 復号する手もありますね。

啓蒙

この挙動を啓蒙するためのパーカーを独自に作成して着用しています。

f:id:pokutuna:20200219054326p:plain:w400