[Rails] CarrierWaveとfog-googleでGoogle Cloud Storageに接続する際にデフォルト認証情報を使う


TL;DR

  • GCPの実行環境(App EngineやCompute Engineなど)内のアプリケーションは、GCPの他のリソースにアクセスする際に、その環境に関連づいたサービスアカウントの認証情報を利用できる = アプリケーションのデフォルト認証情報(ADC)。
  • GCPリソースへのアクセスにfog-googleを利用しているRubyアプリは、fog-googlegoogle_application_defaultオプションをtrueにしてADCを使える。なので手動で認証情報を取り回す必要はない。
    • CarrierWaveのバックエンドをCloud Storageにしてfog-googleを使っている時にも同じ手が有効
      • その際のサービスアカウントには ストレージ オブジェクト管理者(roles/storage.objectAdmin)ロールを付与する

背景: CarrierWave + fog-googleでCloud Storageを使うRailsアプリ

画像アップロードにCarrierWaveを利用しているRailsプロジェクトに出会った。
GCPを利用しており、アプリケーションはApp Engineで実行され、CarrierWaveのバックエンドはCloud Storageだった。
CarrierWaveがCloud Storageを使うためにfog-googleで認証していた。

fog-googleのREADMEで紹介されているコードがほぼそのまま使われていた。

config/initializers/carrierwave.rb
CarrierWave.configure do |config|
    config.fog_provider = 'fog/google'
    config.fog_credentials = {
        provider: 'Google',
        google_project: Rails.application.secrets.google_cloud_storage_project_name,
        google_json_key_string: Rails.application.secrets.google_cloud_storage_credential_content
        # can optionally use google_json_key_location if using an actual file;
    }
    config.fog_directory = Rails.application.secrets.google_cloud_storage_bucket_name
end

もしくはこちらの記事も参照したのかもしれない。

https://qiita.com/arthur_foreign/items/43da529ab3beb760ba4b

サービスアカウントの鍵JSONをそのまま使うことの問題点

上記コードを見て分かる通り、Cloud Storageにアクセス権のあるサービスアカウントの鍵が google_json_key_string オプションに渡されている。
GCPのwebコンソールからサービスアカウントの鍵をJSON形式でダウンロードし、その中身をそのままシークレット管理機構に突っ込んで、ここで読み出している[1]
ここでもう嫌な匂いがするわけだが、
fog-google公式が上記のようなサンプルを載せているし、
metaware/carrierwave-google-storagefog-googleを使わない、CarrierWaveのCloud Storage向けアダプタ)も"How to get the Keyfile?"で同じようなやり方を画像つき↓で説明しているのも事実。

ところが、実際、上記の認証情報の管理方法では、「鍵JSONをそのままシークレットに突っ込むのなんか気持ち悪い」以上の弊害がある。

鍵ファイルの取得、保存に人の手が介在するため、TerraformなどのIaCとすこぶる相性が悪い。
例えばせっかくCloud Storage BucketをIaCで管理しても、

  1. それにアクセスするためのサービスアカウントの設定
  2. サービスアカウントの認証情報(鍵JSON)の生成・ダウンロード
  3. 認証情報をシークレット管理へ投入

はIaCの外側で手作業で行うことになってしまう[2]

どうすべきか。

アプリケーションのデフォルト認証情報(ADC)の利用

Google Cloudの各種実行環境で、アプリケーションのデフォルト認証情報(ADC)を利用できる。

https://cloud.google.com/docs/authentication/production から引用する。

アプリケーションが Google Cloud 環境内で実行されていて、その環境にサービス アカウントが接続されている場合、アプリケーションはそのサービス アカウントの認証情報を取得できます。その後、アプリケーションはこの認証情報を使用して Google Cloud APIs を呼び出すことができます。

Compute Engine、Google Kubernetes Engine、App Engine、Cloud Run、Cloud Functions など、さまざまな Google Cloud サービスのリソースにサービス アカウントを接続できます。これは、認証情報を手動で提供するよりも便利で安全なため、この方法をおすすめします

また、アプリケーションには Google Cloud クライアント ライブラリを使用することをおすすめします。Google Cloud クライアント ライブラリでは、アプリケーションのデフォルト認証情報(ADC)というライブラリを使用して、サービス アカウントの認証情報を自動的に検索します。

(...中略...)

環境変数 GOOGLE_APPLICATION_CREDENTIALS が設定されていない場合、ADC はコードを実行しているリソースに関連付けられているサービス アカウントを使用します。
このサービス アカウントは、Compute Engine、Google Kubernetes Engine、App Engine、Cloud Run、Cloud Functions により提供されるデフォルトのサービス アカウントの場合があります。 また、作成したユーザー管理のサービス アカウントの場合もあります。

(...略...)

今回の例でいえば、App Engine内で実行しているアプリケーション(Railsアプリ)がGoogle Cloud クライアント ライブラリを利用してGoogle Cloudの各種リソースにアクセスする(e.g. APIを呼び出す)とき、自動で認証情報が付与される。
その認証情報は、App Engineに自動で付与されるサービスアカウント<project-id>@appspot.gserviceaccount.com)になる。

この仕組みを使えば、手動で認証情報(鍵JSON)を取り回す必要はなくなる。そしてGoogleもそれを推奨している。

さて、fog-googleのREADMEを見てみると、
実は上で引用したサンプルコードの少し前に、しれっとADCが使えると書いてある。サンプルコードもある。google_application_defaultオプションにtrueを渡せば良いらしい。

As of 1.9.0 fog-google supports Google application default credentials (ADC) The auth method uses Google::Auth.get_application_default under the hood.

connection = Fog::Compute::Google.new(:google_project => "my-project", :google_application_default => true)

なので、CarrierWaveで使うときにはこうすれば良い。

config/initializers/carrierwave.rb
CarrierWave.configure do |config|
    config.fog_provider = 'fog/google'
    config.fog_credentials = {
        provider: 'Google',
        google_project: Rails.application.secrets.google_cloud_storage_project_name,
        google_application_default: true  # HERE!
    }
    config.fog_directory = Rails.application.secrets.google_cloud_storage_bucket_name
end

これで、このRailsアプリケーションをApp Engineで動かしたとき、CarrierWaveはApp Engineのデフォルトのサービスアカウント(<project-id>@appspot.gserviceaccount.com)でCloud Storageにアクセスするようになる。自前で認証情報(鍵JSON)をシークレット管理する手間から解放された。

サービスアカウントに権限を付与

あと一つやることが残っている。

App Engineのデフォルトのサービスアカウント(<project-id>@appspot.gserviceaccount.com)はそのままではCloud Storageのオブジェクトを操作する権限がない。

ストレージ オブジェクト管理者ロール(roles/storage.objectAdmin)を付与する必要がある。

※ストレージ オブジェクト管理者ロールの全ての権限が必要かは分からない。権限をもっと絞りたい人は適宜絞ってみてください。ちなみにStorage オブジェクト作成者(roles/storage.objectCreator)だと storage.objects.delete がなくてエラーになった。

例えばTerraformなら↓のようになる(var.name, var.location, var.projectはプロジェクトに合わせて指定)。

# Bucket作成
resource "google_storage_bucket" "assets" {
  name          = var.name
  location      = var.location
  project       = var.project
}

# AppEngineのデフォルトサービスアカウントにCloud Storageに関する権限を付与
resource "google_storage_bucket_iam_member" "appengine_asset_access" {
  bucket = google_storage_bucket.assets.name
  role   = "roles/storage.objectAdmin"
  member = "serviceAccount:${var.project}@appspot.gserviceaccount.com"
}

これで、上で挙げた鍵JSONを使うことの問題点(IaCとの相性)も解消されていることが分かる。
ADCの仕組みに乗っかることで、必要な情報が全てコード化された。

ローカル開発時

https://cloud.google.com/docs/authentication/production から再度引用する。

環境変数 GOOGLE_APPLICATION_CREDENTIALS が設定されている場合、ADC では、変数が示すサービス アカウント キーまたは構成ファイルを使用します。

従って、ローカル開発でGCPのリソース(Cloud Storageとか)にアクセスする必要がある場合は、適当なサービスアカウントをローカル開発用に用意し、その鍵JSONファイルをダウンロードしてきてパスを GOOGLE_APPLICATION_CREDENTIALS 環境変数に入れれば良い。
ちなみにこういうローカルの境変数管理にはdirenvとかが良いのかな。

脚注
  1. 本稿の主題ではないのでシークレット管理機構そのものの説明は省く。何らかの適当な仕組みを使うこと。実行環境がGoogle App EngineならBerglasとかいいと思う。 ↩︎

    1. はまあコード化できるかもしれないが、結局2, 3に困難があり、趣意は変わらない。
    ↩︎