本記事は dely Advent Calendar 2019 22日目の記事です。
昨日は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_attachments
とactive_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