rails+carrierwave+cloudFront+s3+画像サムネイル生成(nginx)な話

ども、マイケル(日本人)です。 調子がいいのでブログ連投してみます。 carrierwaveでめっちゃはまったのでその話を書きます。 carrierwave便利なんですがちょっといじるとすぐはまりますね。。。

やりたいこと

  • carrierwaveのキャッシュファイルはlocalに、実画像はS3に置く(その際画像はjpgに変換)
  • nginxで動的にサムネイル生成してcloudFrontでキャッシュさせたものをアプリ側で読み込む

ちなみにnginx等のインフラ周りの設定については書きません。 なぜならインフラ構築したのは僕じゃないから(`・ω・´) うちのスーパーインフラエンジニア様が3秒でやってくれました、もしかしたらブログに書いてくれるかもしれません。

画像UPLOAD実装

んでは早速実装していきましょう。 まずはドキュメントみてS3に上げる設定をしてください。 あと画像のリサイズ等にminimagick使うのでこの辺読んで設定してください

// avatar_uploader.rb(ファイル名はドキュメントに合わせてあります)
class AvatarUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  process :resize_to_limit => [600, 600] # ファイルのサイズを600*600に
  process :convert => "jpg" # jpgに変換してたもれ

  version :original do # 一応オリジナルも1200*1200で保存しておく
    process :resize_to_limit => [1200, 1200]
    process :convert => "jpg"
  end

  # 拡張子はjpgに変換してるからjpg固定
  def filename
     "#{secure_token}.jpg" if original_filename.present?
  end

  protected
  # cloudfrontでキャッシュするのでファイル名を毎回動的にユニークな物で生成
  def secure_token
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.uuid)
  end
end

この設定を書くことで以下のことを実現できます。

  • carrierwaveのキャッシュファイルはlocalに保存(これがデフォなので特に設定必要なし)
  • 600x600と1200x1200の画像をjpgに変換してS3にUP
  • ファイル名は毎回動的に生成することでcloudfrontのキャッシュ更新(同じ名前の画像とか来るとcloudfrontのキャッシュを見て画像更新されてないやないか(´Д`)ってなるからね)

ちなみにcarrierwaveのキャッシュファイルはvalidationエラーに引っかかった時などにフォームに表示する画像に使われます。 validationを通って無事にレコードが保存されるとS3に画像がUPされます。 carrierwaveのキャッシュファイルもS3に置くことが出来ますが、今回UPする画像が多く通信コストが馬鹿にならないので、あえてlocalに保存してます。 carrierwaveのキャッシュファイルもS3に置く方法はこちらが参考になります。 CarrierwaveでS3にアップロードさせるとき、キャッシュもS3に置く

画像読み込み実装

今回nginxで画像を動的にリサイズします、リサイズした画像のurl仕様はこんな感じ http://cloudfront_domain/resize/size/ファイル名 sizeには120x120等の画像サイズが入ります。

// carrierwave.rb
CarrierWave.configure do |config|
  config.asset_host = "cloudfront_domain"
end

// avatar_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base

  # urlをoverrideしてsize渡せるようにする
  def url(size = nil)
    if size.nil?
      super
    elsif current_path.present?
     # sizeが渡ってきた場合はそこのサイズの画像見る
      return asset_host + "/resize/" + size + "/" + current_path
    end
  end
end

これで画像参照先はcloudfrontになります。 またリサイズした画像を参照するときは@model.image.url("120x120")とかでそのサイズの画像を読んでくれます。 いやーcarrierwaveさん便利ですわ。 しかし、ここまで来て僕の場合いくつか問題が発生しました。

  • 画像UPLOADが異常に重い
  • validationエラー発生した時に画像表示されない
  • キャッシュフォルダが消えずに残ってる

画像UPLOADが異常に重い

今回UPする画像が多いのですが(MAX20枚)それでもUPLOADに異常に時間がかかりました。10枚で50秒とか。 newrelicで見るとs3との通信にもそれなりに時間かかっているのですが何よりrubyでの処理に異様に時間を取られていました。 インフラエンジニア様にも見てもらったんですがimageMagickが異常に呼ばれているとのこと。 なんでやーと思って、最小構成のrailsプロジェクト作って試したりしましたが解決せず。 もしかしてMiniMagickが怪しいんじゃないかと思い、試しにRMagickに変えたら直りました。 MiniMagickは最小機能で動作が軽いとか書いてあったので採用したのですがまさかの裏目に。。 とりあえずMiniMagick使ってて画像処理に異常に時間取られてる人はRMagickへの変更試してみてください。

validationエラー発生した時に画像表示されない

これ原因は単純でvalidationエラー出た時もcloudfront見に行ってるからですね。 validationエラーが発生した時はlocalのキャッシュを見に行くようにしてあげます。

// avatar_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base
  # キャッシュ先のフォルダを指定
  def cache_dir
    "public/uploads/tmp"
  end

  # キャッシュファイルがある場合はキャッシュファイルのURLを返すメソッドを作成
  def cdn_or_cache_url
    if cached?
      "/uploads/tmp/" + cache_name
    else
      url
    end
  end
end

これで後はhtml側に image_tag @model.image.cdn_or_cache_url とか書いておけばキャッシュがあるときはlocalのキャッシュファイルを見に行くようになります。

キャッシュフォルダが消えずに残ってる

// avatar_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base
  before :store, :remember_cache_id
  after :store, :delete_tmp_dir

  #キャッシュIDを保存しとく
  def remember_cache_id(new_file)
    @cache_id_was = cache_id
  end

  def delete_tmp_dir(new_file)
    if @cache_id_was.present? && @cache_id_was =~ /\A[\d]{10}\-[\d]{5}\-[\d]{4}\z/
      # ディレクトリ消すでー
      FileUtils.rm_rf(File.join(root, cache_dir, @cache_id_was))
    end
  end
end

これはここに書いてあったのほぼコピペですね。 キャッシュファイル自体は消してくれるんですがなぜかディレクトリが残って気持ちわるい状態だったので collback使って消してあげてます。