dely Tech Blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

画像管理をActiveStorageからCarrierWaveへ乗り換えた話

本記事は dely Advent Calendar 2019 22日目の記事です。

qiita.com
adventar.org


昨日はiOSエンジニアのknchstが「“ダーク“な2019年」という記事を書きました。
tech.dely.jp


こんにちは、delyでサーバーサイドエンジニアをやっているyamanoiです。

弊社のとあるプロダクトにて画像アップロード処理周りに、ActiveStorageを使用していたのですが、使いづらい点がいくつかあったため、採用実績があったCarrierWaveへ乗り換えました。
この記事ではなぜ乗り換えたのかと、乗り換える手順を書いていきたいと思います。

なぜActiveStorageから乗り換えたのか

1. CDNとの相性が悪い

ActiveStorageはアタッチされたモデルのurlメソッドを使用するとActiveStorageが定義した /rails/active_storage/blobs/* へのパスを生成します。
このパスへアクセスすると、各クラウドストレージ上のオブジェクトに対する一時的な認証コード付きURLへリダイレクトし、画像を取得することができます。
このURLをキャッシュしてしまうと、URLが期限切れになってしまうと画像が表示されなくなってしまいます。

ActiveStorageとCDNを併用するにはActiveStorageの機能を独自に拡張することで利用はできるのですが、拡張を行うことによってシステムが複雑になってしまい、アップデートの障壁になったりすることが容易に想像できるため今回は避けました。

2. 画像のリクエストがRailsに向いてしまう

ActiveStorageを有効にするとActiveStorage用のroutingが新しく追加されます。
ActiveStorageを用いて画像を取得する際はすべてこのルーティングを経由する必要があります。
静的ファイルはパフォーマンスの観点からアプリケーションサーバーを通さずnginxやs3等のバケットから直接配信したいですよね。

また追加されるルーティングは自分で定義しているconfig/routes.rbの後にロードされるため、以下の様なルーティングを定義しているとActiveStorage側のルーティングにマッチする前にルーティングが解決されてしまい、画像が正しく表示されない問題に直面しました。

  get "*path", controller: 'front', action: 'spa', via: :all

3. DBへのリクエストが頻繁に走る

ActiveStorageはactive_storage_attachmentsactive_storage_blobsの2つのテーブルを作成し、そこに画像のメタ情報やモデルとの関連を保持します。
ActiveStorageを使う場合は少なくともアタッチするモデルと1対1の関連が発生します。
何も考えず使用すると容易にN+1を誘発します。そのためActiveStorageではN+1を回避するためのメソッドが用意されています。
また上記の2つのテーブルですべてのモデルに対しての画像を扱うため、レコード数の多いテーブルが複数存在するとレコード数が増加し、パフォーマンスに影響が出てしまう可能性もありそうです。


CarrierWaveへ乗り換える手順

1. 設定ファイルを消す

config/application.rb
でrails/allしている場合は不要なファイルもロードしてしまうので必要なもののみをロードするように変更します

デフォルトだとconfig/application.rbに以下の様な記述があると思いますが、これだとActiveStorageもロード対象になってしまうので使うものだけをロードするように変更します。
Before

require 'rails/all'

After

require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "sprockets/railtie"
require "rails/test_unit/railtie"

2.CarrierWave gemの追加と書き換え

CarrierWave gemを入れて実際にコードを置き換えていきます。
置き換える点で厄介になりそうなところはActiveStorageのvariantを使用している場合です。

ActiveStorageではアタッチされている画像に対してvariantメソッドを使うことでリサイズ処理を手軽に実現することができます。

    <%= image_tag @post.thumbnail.variant(resize:'50x50').processed %>

様々なサイズを気軽に生成することができるので便利な機能なのですが、CarrierWaveではUploaderクラスに予めversionとして画像のパターンを定義しておく必要があります。
今回のプロダクトではこのvariantの機能をフル活用している場所はあまり無かったのでそこまで問題にはなりませんでした。

3. 移行スクリプト作る

弊社の場合、ActiveStorageで保存された画像はs3に置いてあり、移行するにあたってファイルの保存場所を変更する必要があったため移行するスクリプトを作りました。
ActiveStorageは1つのバケットにフラットに画像を保存するため、パスを特定し、置き換えていきます

↓サンプルコード

class FileDataStringIo < StringIO
  attr_accessor :original_filename, :cnotent_type

  def initialize(*args)
    super(*args[2..-1])
    @original_filename = args[0]
    @content_type = args[1]
  end
end

class ActiveStorageBlob < ActiveRecord::Base; end

class ActiveStorageAttachment < ActiveRecord::Base
  belongs_to :blob, class_name: 'ActiveStorageBlob'
  belongs_to :record, polymorphic: true
end

ActiveStorageAttachment.all.each do |attachment|
  blob = attachment.blob
  key = blob.key
  filename = blob.filename
  record = attachment.record
  name = attachment.name
  content_type = blob.content_type

  s3 = Aws::S3::Resource.new(region: 'ap-northeast-1')
  obj = s3.bucket(ENV["ACTIVE_STORAGE_S3_BUCKET"]).object(key).get
  data = obj.body.read

  io = FileDataStringIo.new(filename, content_type, data)
  record.send("#{name}=".to_sym, io)
  record.save!
end

4. テーブルの削除

最後にActiveStorageの有効時に生成されたテーブルを削除します。

class DestroyActiveStorageTables < ActiveRecord::Migration
  drop_table :active_storage_blobs
  drop_table :active_storage_attachments
end

まとめ

Rails備え付けの機能だからと言ってすぐ取っつかず、開発しているプロダクトの要件をしっかり満たせるかを検討しながら、Gemの選定を行いましょう

最後に

delyではサーバーサイドエンジニアを募集中です。ご興味ありましたらぜひこちらから!
note.com
www.wantedly.com


delyの開発について知りたい方はこちらもあわせてご覧ください!
speakerdeck.com