[Rails] CarrierWaveとfog-googleでGoogle Cloud Storageに接続する際にデフォルト認証情報を使う
TL;DR
- GCPの実行環境(App EngineやCompute Engineなど)内のアプリケーションは、GCPの他のリソースにアクセスする際に、その環境に関連づいたサービスアカウントの認証情報を利用できる = アプリケーションのデフォルト認証情報(ADC)。
- GCPリソースへのアクセスに
fog-google
を利用しているRubyアプリは、fog-google
のgoogle_application_default
オプションをtrue
にしてADCを使える。なので手動で認証情報を取り回す必要はない。- CarrierWaveのバックエンドをCloud Storageにして
fog-google
を使っている時にも同じ手が有効- その際のサービスアカウントには ストレージ オブジェクト管理者(
roles/storage.objectAdmin
)ロールを付与する
- その際のサービスアカウントには ストレージ オブジェクト管理者(
- CarrierWaveのバックエンドをCloud Storageにして
背景: CarrierWave + fog-googleでCloud Storageを使うRailsアプリ
画像アップロードにCarrierWaveを利用しているRailsプロジェクトに出会った。
GCPを利用しており、アプリケーションはApp Engineで実行され、CarrierWaveのバックエンドはCloud Storageだった。
CarrierWaveがCloud Storageを使うためにfog-google
で認証していた。
fog-google
のREADMEで紹介されているコードがほぼそのまま使われていた。
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
もしくはこちらの記事も参照したのかもしれない。
サービスアカウントの鍵JSONをそのまま使うことの問題点
上記コードを見て分かる通り、Cloud Storageにアクセス権のあるサービスアカウントの鍵が google_json_key_string
オプションに渡されている。
GCPのwebコンソールからサービスアカウントの鍵をJSON形式でダウンロードし、その中身をそのままシークレット管理機構に突っ込んで、ここで読み出している[1]。
ここでもう嫌な匂いがするわけだが、
fog-google
公式が上記のようなサンプルを載せているし、
metaware/carrierwave-google-storage
(fog-google
を使わない、CarrierWaveのCloud Storage向けアダプタ)も"How to get the Keyfile?"で同じようなやり方を画像つき↓で説明しているのも事実。
ところが、実際、上記の認証情報の管理方法では、「鍵JSONをそのままシークレットに突っ込むのなんか気持ち悪い」以上の弊害がある。
鍵ファイルの取得、保存に人の手が介在するため、TerraformなどのIaCとすこぶる相性が悪い。
例えばせっかくCloud Storage BucketをIaCで管理しても、
- それにアクセスするためのサービスアカウントの設定
- サービスアカウントの認証情報(鍵JSON)の生成・ダウンロード
- 認証情報をシークレット管理へ投入
は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で使うときにはこうすれば良い。
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
とかが良いのかな。
Discussion