Railsの画像管理を途中からCarrierwaveに切り替える方法

## はじめに

![pomeru.png](https://qiita-image-store.s3.amazonaws.com/0/106711/9514a0ff-2e94-56b9-9e8a-3acea856a0bc.png)

- pomeruというサービスを開発しているエンジニアです。
- 最初はユーザーのアイコン画像を、**Twitterのアイコン画像へのURL（[こんなの](https://pbs.twimg.com/profile_images/464615059017048064/jwUhotje_400x400.jpeg)）をそのまま所持する形でDBに格納**していたのですが、それだと画像を自分で変えることができないので、途中からCarrierwaveで管理するという方式に切り替えることになりました。
- 今回はその時の珍しい経験が少しでも同じような境遇の人のお役に立てればと思い、まとめました。

## 環境
```
$ ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]

$ rails -v
Rails 5.2.1
```

## 最初の状態
- **Devise**で作成した`User`モデルに`usericon`というフィールドを持たせていて、そこにtwitterアイコンのURLを格納していました。

```rb
class AddColumnsToUsers < ActiveRecord::Migration[5.2]
  add_column :users, :usericon, :string
end
```
```rb
# サインアップ時に、URLをDBに保存
def find_for_twitter_oauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    # 中略
    user.usericon = auth.info.image # アイコンへのURLが入っている
  end
end
```

```erb
<% # ビューから呼び出すとき %>
<img src="#{@user.usericon}" />
```

## 目指す最終形
- `Carrierwave`と`fog`を使い、画像をS3へ格納する形にします。

```rb
# Gemfile
gem 'carrierwave'
gem 'mini_magick'
gem 'fog-aws'
```

- uploaderモデルをUserモデルにマウントさせます。

```rb
# uploaderモデルを作成する
# rails g uploader usericon

# app/uploaders/usericon_uploader.rb

class UsericonUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  if Rails.env.development? || Rails.env.test?
    storage :file
  else
    storage :fog
  end

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  def default_url(*args)
    '/images/fallback/profile_icon.png'
  end

  process resize_to_fill: [300, 300]
  process convert: 'png'

  def extension_whitelist
    %w[jpg jpeg gif png]
  end

  def filename
    super.chomp(File.extname(super)) + '.jpg' if original_filename
  end
end
```

```rb
# user.rb
mount_uploader :usericon, UsericonUploader
```

```rb
# サインアップ時の所作をCarrierwave対応
def find_for_twitter_oauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    # 中略
    user.remote_usericon_url = auth.info.image
  end
end
```

```erb
<% # ビューから呼び出すときもCarrierwave対応 %>
<img src="#{@user.usericon_url}" />
```

## 切り替えに必要な処理
- 切り替えるに当たって、以下のことをクリアする必要があります。

> 1. 現状のユーザー全員の画像をS3に保存する。
2. それぞれの画像の保存先のURLをCarrierwaveを介してDBに保存する。
3. これ以降、サインアップした人は自動的にTwitterのアイコン画像をS3に保存されるようにする。
4. Carrierwaveに対応したコードを本番環境に反映する。

- 厄介なのはこの順番で、**最初からいきなりCarrierwaveをマウントしてしまうと、現在格納されているアイコン画像へのURLがおかしくなってしまいます。**
- なので、上をクリアするために、具体的な動作として以下の道筋を踏みました。

> 1. `UsericonUploader`を作成して、`User`モデルへマウントする。
2. ログイン時や表示する時の画像へのアクションを、Carrierwave対応する。
3. 上のコードを`feature/image_to_s3`ブランチとして切っておき、いつでも`master`ブランチから切り替えられるようにしておく。
4. 本番環境をメンテナンスモードへ移行。
5. 本番環境のDBデータのバックアップを取り、ローカルへダウンロード・反映させる。
6. ローカルで全てのユーザーの「ID」と「画像へのURL」をCSVに書き出すタスクを走らせる。（ブランチは`master`）
7. ローカルでブランチを`feature/image_to_s3`に切り替え、CSVを順次読み込んでS3へ画像データを保存するタスクを走らせる。（**carrierwaveのストレージをこの時だけ`fog`に変えておくこと!!**）
8. ローカルの書き換わったDBデータをdumpし、本番環境のDBに反映する。
9. `master`ブランチに`feature/image_to_s3`をマージして、本番環境へpushする。

- 実際に使ったタスクファイルは以下のようなものです。

```rb
# 最初に全てのUserのIDと画像URLをCSVに書き出すタスク
namespace :get_twitter_image_urls do
  desc 'get and save twitter_image_urls'
  task run: :environment do
    f = File.open('twitter_image_url.csv', 'w')
    User.all.each do |u|
      # Carrierwaveをマウントする前に実施すること（マウントしたあとだと、URLが変わってしまうため）
      f.puts "#{u.id},#{u.usericon}"
    end
  end
end
```

```rb
# 書き出したCSVを元に、画像をS3へ保存するタスク

# Carrierwaveのストレージを、development環境でもfogを使うように設定しておくこと（fileだとローカルに保存されてしまうため。）
namespace :save_images_to_s3 do
  desc 'save twitter_image_urls to s3'
  task run: :environment do
    require 'csv'

    ActiveRecord::Base.transaction do
      CSV.foreach('twitter_image_url.csv') do |row|
        begin
          u = User.find(row.first)
          u.remote_usericon_url = row.last
          if u.save
            Rails.logger.debug("#{u.id}:#{u.nickname} saved")
          else
            # NOTE: URLに画像が存在しなかった場合は、デフォルトの画像を使うために画像を削除する。
            u.remove_usericon!
            # HACK: どういうわけかvalidationを切らないと保存してもらえない（私だけだろうか？）
            u.save(validate: false)
          end
        rescue StandardError => e
          Rails.logger.debug(e.message)
          next
        end
      end
    end
  end
end
```

## 注意点
### DBのバックアップとメンテナンス
- 言わずもがなですが、**切り替え中はDBへの更新を防ぐため、メンテナンスモードにしましょう。**
- そして**データをいつでもリストアできるよう、バックアップも取っておいてください。**
- なお今回は`Heroku`で動かしていたため、メンテナンス・バックアップ・リモートとローカル間でのデータのやり取りがとても楽でした！

```sh
# メンテナンスモード移行
$ heroku maintenance:on

# バックアップをとる
$ heroku pg:backups capture

# 最新のバックアップファイルをローカルへ（デフォルトで`latest.dump`という名前になります）
heroku pg:backups:download

# ローカルへダウンロードしたDBデータを反映する
$ pg_restore --verbose --clean -d app_development latest.dump

# 加工したローカルのDBデータをダンプする
$ pg_dump app_development > dump.sql

# heroku環境に反映する（向こうのデータは一旦リセットしないと入らないこともあるので注意）
$ heroku pg:psql DATABASE_URL < dump.sql

# 万が一前のデータをバックアップを使ってリストアしたくなった場合
heroku pg:backups:restore b101(バックアップの識別子) DATABASE_URL

# メンテナンスモード解除
$ heroku maintenance:off
```

### Carrierwaveをマウントするタイミング
- 度々書いていますが、**Carrierwaveをマウントする前と後で、`usericon`を呼び出した時の返り値が変わってしまう**ため、意図したデータを取得・保存させるためには適切なタイミングで切り替える必要があります。
- 今回の場合は、最初`usericon`に画像へのURLが入っていて、その一覧を取得する必要があったので、Carrierwaveをマウントする前にURL一覧をCSVに書き出しました。
- そして然る後、Carrierwaveをマウントして、ストレージを`fog`にした状態でタスクを回し、CSVの情報を元に画像をS3へ保存する処理を行いました。
- この順番をしっかりと守るようにしてください。

### ネット環境の確保
- 今回は3000を超えるユーザーデータを処理したため、時間的には30分以上かかってしまいました。
- やはりS3との通信を確保するために安定したネット環境は必須です。（今回の場合はローカルで処理しているので）

## おわりに
- 今回はイレギュラー中のイレギュラーで大変でしたがなんとかなりました。
- が、なんにせよ一番大切なのは**最初からCarrierwaveを使うか使わないかをしっかり議論して、使うのなら最初から使うのがベスト**なんだなぁと再認識しました。