📁

ActiveStorage の service や name を変更したいとき

2024/10/23に公開

こんにちは、 simomu です。今日は ActiveStorage の service や name を変更したいときの話をします。
以後断りのない場合は

  • Ruby on Rails 7.1

の環境下での話とします。

ActiveStorage の service を変更したいとき

ミラーサービスを利用する方法

例えば、以下のような ActiveStorage の設定があるとします。

config.active_storage.service = :old_amazon

class User < ApplicationRecord
  has_one_attached :avatar
end
storage.yml
old_amazon:
  service: S3
  access_key_id: xxxx
  secret_access_key: xxxxx
  bucket: old_bucket

これを後から以下のように bucket を変更した別の service に変更したくなったとします。

storage.yml
new_amazon:
  service: S3
  access_key_id: xxxx
  secret_access_key: xxxxx
  bucket: new_bucket

このときに利用できるのが ActiveStorage のミラーサービスです。
https://railsguides.jp/active_storage_overview.html#ミラーサービス

以下のように古い service と新しい service の両方にファイルがアップロードされるようにします。ミラーサービスでは、ファイルのアップロードはプライマリとミラーの両方にアップロードが行われ、ダウンロードはプライマリから行われます。今回は既に old_amazon でアップロードされたファイルが存在すると仮定し、old_amazon をプライマリ、new_amazon をミラーとします。

storage.yml
old_amazon:
  # 中略
  bucket: old_bucket

new_amazon:
  # 中略
  bucket: new_bucket

migrate:
  service: Mirror
  primary: old_amazon
  mirrors:
    - new_amazon

次に、環境で使用するサービスを定義したミラーサービスに変更します。

config.active_storage.service = :migrate

これで新しくアップロードされるファイルに関しては、ミラーサービスによって両方のサービスにファイルがアップロードされることになります。プライマリを古いサービスにしたので、既存のファイルはそのままアクセスすることができます。

ActiveStorage がミラーサービスにファイルをアップロードするタイミングは、ActiveStorage::Attachmentafter_create_commit で、ActiveStorage::Blob#mirror_later を使用してミラーサービスが存在すればミラーサービスにもファイルのアップロードを行います。
https://github.com/rails/rails/blob/7-2-stable/activestorage/app/models/active_storage/attachment.rb#L36
https://github.com/rails/rails/blob/7-2-stable/activestorage/app/models/active_storage/attachment.rb#L130-L132

そのため、後はこの ActiveStorage::Blob#mirror_later を既存レコードに対して実行すれば既存レコードのファイルも新しいサービスにアップロードされそうに見えます。

しかしながら、既存の old_amazon サービスでアップロードされたファイルは、config.active_storage.service を変更しても old_amazon サービスとして扱われてしまうため、 mirror_later を呼び出してもミラーリングが行われません。これは ActiveStorage がファイルを管理しているテーブル active_storage_blobsservice_name カラムに ActiveStorage::Blob 作成時に使用したサービス名が保存されていて、ActiveStorage::Blob インスタンスが利用するサービスはこのカラムの値をもとに決定されているからです。
https://github.com/rails/rails/blob/7-2-stable/activestorage/app/models/active_storage/blob.rb#L343-L346
そのため、例えば以下の SQL で既存のファイルに登録されているサービスをミラーサービスに切り替える必要があります。

UPDATE active_storage_blobs SET service_name = 'migrate';

これで既存のレコードのファイルもミラーサービスとして扱われるようになったので、以下のようなスクリプトで既存ファイルのミラーリングを行います。

User.find_each { |user| user.avatar.mirror_later }

mirror_later によって全てのファイルが新しいサービスにアップロードされたことが確認された後は、移行時と同じように config.active_storage.service と、active_storage_blobs.service_name をそれぞれ新しいサービスに変更すれば移行が完了します

config.active_storage.service = :new_amazon
UPDATE active_storage_blobs SET service_name = 'new_amazon';

ミラーサービスを利用しない方法

上記ではミラーサービスを利用してサービスの変更を行いましたが、ミラーサービスを利用しなくても移行すること自体は可能です。
先ほど書いた通り、既にファイルが作成されている場合、ActiveStorage::Blob インスタンスが利用するサービスは active_storage_blobs.service_name によって決定されるという話をしました。

そのため、config.active_storage.service を切り替えたとしても、storage.yml に古いサービスの設定が残っている限りは古いサービスでアップロードされたファイルはそのまま問題なく扱うことができます。

user.avatar.attach(params[:avatar]) # `old_amazon` サービスを利用してアップロード

# ------
# サービスを新しいもに切り替える
config.active_storage.service = :new_amazon
# ------

user.avatar.download # 既存のレコードは`old_amazon` の設定をもとに DL
User.create.avatar.attach(params[:avatar]) # 新しいレコードは `new_amazon` サービスとしてアップロード

後は古いサービスでアップロードされた画像を、新しいサービスとして再アタッチすれば移行が完了します。attach を呼び出す際に、avatar.blob をアタッチしてしまうと、ActiveStorage::Blob が使い回されてしまい、新しいサービスにアップロードされないので、IO としてアタッチします。

User.find_each do |user|
  user.avatar.attach(
    io: StringIO.new(user.avatar.download),
    filename: user.avatar.filename
  )
end

以上がミラーサービスを利用しない service の変更方法でした。
ミラーサービスを利用するパターンと比べて、移行作業時の active_storage_attachments テーブルへの書き込み回数が増える点や、再アタッチによって移行後は古いサービスでアップロードされたファイルが ActiveStorage::Blob#purge によって消えてしまう点があるため、ミラーサービスを利用したくない特別な理由がない限りはミラーサービスを利用した変更をおすすめします。

ActiveStorage の name を変更したいとき

次は ActiveStorage の name 、つまり以下のような変更をしたい時の話です。

class User < ApplicationRecord
  has_one_attached :avatar
  # これを `has_one_attached: :image` に変更したい
end

単純に has_one_attached :image に変更しリリースしてしまった場合、
既存の has_one_attached :avatar でアップロードされたファイルが参照できなくなってしまいます。

user.avatar.attach(params[:avatar])
user.avatar.attached? #=> true
# ------
# `has_one_attached :image` に変更したものをリリース
# ------
user.image.attached? # => false

これを解決するには、ActiveStorage を利用しているモデルと ActiveStorage::Blob をつなげる active_storage_attachments テーブルの情報を修正する必要があります。active_storage_attachments には record_typename カラムが存在しており、それぞれ関連するモデルのクラス名と、has_one_attached で指定された名前が保存されています。そのため、変更したいモデルの active_storage_attachments.name を更新すれば name を変更した後でも引き続き既存のファイルを利用することができるようになります。

UPDATE active_storage_attachments SET name = 'image' WHERE record_type = 'User' AND name = 'avatar'

ただし、上記の方法では has_one_attached :image の変更をリリースしてから、
active_storage_attachments.name を更新するまでの間は正常にファイルにアクセスできない時間になるため、リネームによるダウンタイムを許容できない場合は利用できません。
ダウンタイムを許容できない場合は下記のように一時的に両方の has_one_attached を用意することになると思われます。

class User < ApplicationRecord
  # attach する際には avatar, image 両方行う
  has_one_attached :avatar
  has_one_attached: :image
end

まとめ

今回は ActiveStorage の service や name を後から変更したい場合の手法を紹介しました。運用中に service や name を後から変更したいケースはあまりないかもしれませんが、参考になれば幸いです。

SocialPLUS Tech Blog

Discussion