ActiveStorage の service や name を変更したいとき
こんにちは、 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
old_amazon:
service: S3
access_key_id: xxxx
secret_access_key: xxxxx
bucket: old_bucket
これを後から以下のように bucket
を変更した別の service に変更したくなったとします。
new_amazon:
service: S3
access_key_id: xxxx
secret_access_key: xxxxx
bucket: new_bucket
このときに利用できるのが ActiveStorage のミラーサービスです。
以下のように古い service と新しい service の両方にファイルがアップロードされるようにします。ミラーサービスでは、ファイルのアップロードはプライマリとミラーの両方にアップロードが行われ、ダウンロードはプライマリから行われます。今回は既に old_amazon
でアップロードされたファイルが存在すると仮定し、old_amazon
をプライマリ、new_amazon
をミラーとします。
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::Attachment
の after_create_commit
で、ActiveStorage::Blob#mirror_later
を使用してミラーサービスが存在すればミラーサービスにもファイルのアップロードを行います。
そのため、後はこの ActiveStorage::Blob#mirror_later
を既存レコードに対して実行すれば既存レコードのファイルも新しいサービスにアップロードされそうに見えます。
しかしながら、既存の old_amazon
サービスでアップロードされたファイルは、config.active_storage.service
を変更しても old_amazon
サービスとして扱われてしまうため、 mirror_later
を呼び出してもミラーリングが行われません。これは ActiveStorage がファイルを管理しているテーブル active_storage_blobs
の service_name
カラムに ActiveStorage::Blob
作成時に使用したサービス名が保存されていて、ActiveStorage::Blob
インスタンスが利用するサービスはこのカラムの値をもとに決定されているからです。
そのため、例えば以下の 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_type
、 name
カラムが存在しており、それぞれ関連するモデルのクラス名と、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 を後から変更したいケースはあまりないかもしれませんが、参考になれば幸いです。
Discussion