ボクココ

サービス開発を成功させるまでの歩み

Rails コールバックによる S3 事故と対策

ども、@kimihom です。

f:id:cevid_cpp:20190601165719j:plain

Rails でコードを書いていたら、きっと一度は書いたことがあるだろう before_destroy で起こりうるリスクと対策について記す。

S3 のドメインが変わるお知らせ

先日、S3 の URL が変更されるとのお知らせが届いた。

dev.classmethod.jp

これを読んで、「そのうち対応が必要なら早いうちにやっちゃおう。"s3-ap-northeast-1.amazonaws.com" ってリージョンも長ったるしいし。」ってことでさっと対応してみよう。

User の Image URL の更新

User モデルがあったとして、その User に image_url で画像URLを保存するデータがある。そこで、画像を保存している S3 URL をアップデートする処理を書く。

そもそも、ユーザー画像に S3 のドメインから登録させるってのは、あまりすべきことではないだろう。ただ、Twitter ログインなどから画像を登録したり、CRM の画像を参照する可能性もあったことから、画像URL をドメインから DB に保存するようにするケースもあるだろう。

てことで、以下のようなコードを実行することになる。

prev_s3 = "s3-ap-northeast-1.amazonaws.com/myapp"
after_s3 = "myapp.s3.amazonaws.com"
User.where("image_url is not null").each do |c|
  c.update(image_url: c.image_url.sub(prev_s3, after_s3))
end; ""

画像の自動削除

ユーザーが画像を再度アップロードした時は、前に使っていた画像はもう使わないので削除するような処理を実装することがあるだろう。これをしないと使わない画像がどんどん溜まっていってしまうからである。具体的には以下のような形である。

class User < ApplicationRecord

  before_save :delete_image_if_image_changed

  def delete_image_if_image_changed
    return if self.attribute_was("image_url").blank? || !self.changed.include?("image_url")
    remove_s3_image(self.attribute_was("image_url"))
  end

end

これでユーザーが画像を繰り返しアップロードするたびに、前の画像は無事削除されるようになり、無駄に S3 の画像が残らず、費用の節約にもなる。

さて、このコールバックが定義された状態で、先ほどのコードを実行したらどうなるか、もうお分かりだろう。そう、画像URL だけが変わり、画像の実態が削除されてしまうのである。

S3 のバージョニングを有効にしておこう

画像が一括削除されてしまうような悲劇が起きたとしても、なんとか対応できるようにするために、S3 のバージョニングを有効にすることをお勧めする。S3 の対象のバケットに行って "プロパティ" -> "バージョニング" で 有効にすれば OK である。

「バージョニングを有効にするくらいならそもそも消さなければよくないか」という考え方もあるだろうが、使われている画像/使われていない画像が全てバケット内にあるようなカオスな状態にはしたくないケースでバージョニングが便利に使える。

これで、削除された画像もバージョン表示をすれば、取得が可能だ。S3 費用の節約よりも、安全に運用できる道を選ぶべきだろう。

そもそも、update はコールバックが呼ばれてしまうので、今回の場合は update_column を呼び出すのが正解ということになる。

終わりに

今回の根本である「Rails のコールバックがいけない」とは私は思わない。使い方次第ではコールバックは便利に使えるし、コードを短くできる。

本記事がより安全にサービス運用をするために考える きっかけになれば幸いである。