DiffDaily

Deep & Concise - OSS変更の定点観測

[rails/rails] GCS Active StorageのIAM署名でADC認証を復元し、クライアントをメモ化

rails/rails

Context

#55353 で導入された変更により、デフォルトのADC(Application Default Credentials)認証を使用しているアプリケーションで署名付きURLの生成が失敗する問題が発生していました。以前のコードでは Google::Auth.get_application_default を明示的に呼び出していましたが、これがメタデータサーバーへの過度な呼び出しを引き起こしていたため削除されました。しかし、その結果として認証が適切に行われず、Google::Apis::AuthorizationError: Unauthorized エラーが発生するようになっていました。

このPRは、ADC認証を復元しつつ、IAMクライアントをメモ化することで過度なAPI呼び出しを抑制し、両方の問題を解決します。

Technical Detail

IAMクライアントのメモ化

新たに iam_client メソッドが追加され、IAMクライアントがインスタンス変数でメモ化されるようになりました。

def iam_client
  @iam_client ||= Google::Apis::IamcredentialsV1::IAMCredentialsService.new.tap do |client|
    client.authorization ||= Google::Auth.get_application_default(["https://www.googleapis.com/auth/iam"])
  rescue
    nil
  end
end

この実装により、以下の利点が得られます:

  • インスタンス化時に一度だけ認証を取得: クライアント生成時にADCで認証情報を取得し、それ以降は再利用します
  • 自動的な認証更新: Google APIクライアントが認証情報の有効期限を管理し、必要に応じて自動的に更新します
  • 過度なメタデータサーバー呼び出しの抑制: 以前の実装では signer ラムダが呼ばれるたびに認証が行われていましたが、メモ化により初回のみに制限されます

署名処理の簡素化

signer ラムダ内のコードが大幅に簡素化されました。以前は毎回新しいIAMクライアントを作成していましたが、メモ化されたクライアントを使用するようになりました。

変更前:

def signer
  lambda do |string_to_sign|
    iam_client = Google::Apis::IamcredentialsV1::IAMCredentialsService.new
    # 認証設定なし(意図的)
    request = Google::Apis::IamcredentialsV1::SignBlobRequest.new(
      payload: string_to_sign
    )
    # ...
  end
end

変更後:

def signer
  lambda do |string_to_sign|
    request = Google::Apis::IamcredentialsV1::SignBlobRequest.new(
      payload: string_to_sign
    )
    # iam_clientはメモ化済み
    # ...
  end
end

カスタム認証のサポート

ADCがデフォルトで使用されますが、アプリケーション固有の認証方法に切り替えることも可能です。

ActiveStorage::Blob.service.iam_client.authorization = Google::Auth::ImpersonatedServiceAccountCredentials.new(options)

このアプローチは Google::Apis::RequestOptions.default.authorization を設定するよりも安全です。なぜなら、Active Storageのみに適用され、他のGoogle APIクライアントには影響しないためです。

テストの追加

2つの新しいテストが追加され、認証の動作を検証しています:

  1. デフォルトのADC動作: WebMockを使用して、ADCによる認証トークン取得が行われることを確認
  2. カスタム認証: iam_client.authorization を上書きした場合、デフォルトのトークン取得が行われないことを確認
test "default IAM client ADC" do
  WebMock.enable!
  config_with_adc = { gcs: SERVICE_CONFIGURATIONS[:gcs].merge({ iam: true }) }
  service = ActiveStorage::Service.configure(:gcs, config_with_adc)

  stub_request(:post, "https://www.googleapis.com/oauth2/v4/token").to_return_json(body: {})
  stub_request(:post, %r{https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/.*:signBlob})
    .to_return_json(body: { "signedBlob" => "test_signed_blob" })
  key = SecureRandom.base58(24)
  service.url(key, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
  assert_requested :post, "https://www.googleapis.com/oauth2/v4/token"
ensure
  WebMock.disable!
end

Impact

この変更により、GCS Active Storageを使用するアプリケーションは以下の恩恵を受けます:

  • ADC環境での正常動作: サービスアカウントキーやWorkload Identityを使用する環境で署名付きURLが正常に生成される
  • パフォーマンス向上: 認証クライアントのメモ化により、メタデータサーバーへの呼び出し回数が大幅に削減される
  • 柔軟な認証設定: 必要に応じてカスタム認証方法を設定可能