RailsとHeroku、AWSで画像アップロードからのサムネイル作成（実践編）

## はじめに
1年前に[RailsとHeroku、AWSでAwesomeな画像アップロードからのサムネイル作成](http://qiita.com/anoworl/items/35e75a20e70d2ad0f658)という記事を書きました。

そこから1年間ほど手を変え品を変え運用したので、改めて現在実際に運用しているプラクティスをご紹介します。ここでは実際に画像にコメントを付加出来る掲示板を作成しながら説明していきます。

### この方法のメリット
- 画像アップロードが早い
    - 画像をブラウザ側でリサイズして軽量化
    - HerokuではなくS3へ直接アップロード
    - 非同期
- 投稿が早い
    - 画像を事前にアップロードするため、投稿時にはそのパスを渡すだけ
    - サムネイルはアクセスされた時に同期的に作成するため、投稿のレスポンスは即時
- サムネイル表示が早い
    - 作成したサムネイルはS3へ保存するため重複が発生しない
    - 既にサムネイルを作成している場合は直接S3のURLを返すため、Herokuへのアクセスが発生しない
    - サムネイルとS3パスの対応はmemcacheでキャッシュしているため引くのが早い
    - S3の画像はCloudFrontにキャッシュするので早い
 
## 仕組み概要
ここではこの仕組みの概要を説明します。心ではなく身体で学びたい方は、この節は飛ばしてしまって構いません。

### 環境構築方法
1. AWS S3のUS Standard リージョンに画像 (オリジナル, サムネイル) 保存用バケットを作成
2. 上記バケットに対応したCloudFront ディストリビューションを作成
3. Dragonfly gem, Dragonfly::S3DataStore gem を導入
4. 画像を持たせるモデルのテーブルに画像のパスを保存するカラムを追加
5. 上記モデルにDragonflyアクセサの設定を追加
5. S3へのアクセスはCloudFrontを通すようDragonflyを設定

### 画像アップロードプロセス
1. ユーザが画像選択ダイアログで画像を選択した時点で非同期で以下を行う
    1. 画像の軽量化 (アスペクト比保持したまま幅を縮小)
        - アップロード時間の短縮のため
    2. S3へのダイレクトアップロード
        - Herokuを通した大容量ファイルのアップロードは無駄が多いため
    3. アップロード先のURLをフォーム内に挿入
2. ユーザがフォームを送信したらサーバ側で以下を行う
    1. 送信されたアップロード先のURLを分解しS3上でのパスを抽出
    2. 抽出したパスをモデルに存在するDragonflyのカラムへ挿入

### 画像表示プロセス
1. ビュー内では上記モデルを通してDragonflyを使用し、画像フォーマットを指定してサムネイルのURLを生成
    - （事前にDBとmemcachedへ、S3に既に保存したサムネイル画像のパスとサムネイル生成情報のハッシュの組を保存しておき）既に指定されたフォーマットのサムネイル画像が存在する場合は、S3に作成したサムネイル画像のURLを返す
        - この際、CloudFrontを通すURLを返す
    - 存在しない場合はサムネイル生成情報を付加したHerokuのURLを生成
2. ユーザが上記URLをimgタグで表示する際に同期的に以下を行う
    1. 元画像をS3から読込、サムネイル画像をHeroku上で作成
    2. 上記サムネイル画像をブラウザへ返す
    3. 生成したサムネイル画像をS3へ保存
    4. サムネイル生成情報のハッシュとサムネイル画像のパスの組み合わせをDBに保存

## テストアプリの作成例
ガンガンガン速（震え声）。コマンドの出力は省略します。

```zsh
anoworl@crow ~ $ rails -v
Rails 4.1.8
anoworl@crow ~ $ rails new ImageBoard
anoworl@crow ~ $ cd ImageBoard
anoworl@crow ~/ImageBoard $ vi Gemfile
```

```ruby:Gemfile
gem 'dragonfly'
gem 'dragonfly-s3_data_store'
gem 's3_file_field'
gem 'dotenv-rails', group: :development
```

```zsh
anoworl@crow ~/ImageBoard $ bundle install
anoworl@crow ~/ImageBoard $ bundle exec rails g scaffold post title body:text image_uid
anoworl@crow ~/ImageBoard $ bundle exec rake db:create
anoworl@crow ~/ImageBoard $ bundle exec rake db:migrate
anoworl@crow ~/ImageBoard $ bundle exec rails s
```

AWS S3でdevelopment用のバケット作成。RegionはHerokuのデフォルトと同じUS Standard。

![バケット作成](https://qiita-image-store.s3.amazonaws.com/0/18606/36f6c996-2787-6a4e-9b53-649911a3c947.png)

JavaScriptからダイレクトアップロードを行うためCORSの設定。

![S3 Management Console](https://qiita-image-store.s3.amazonaws.com/0/18606/623c85a3-1764-0b26-4039-9e12c263d5d9.png)

以下を入力してSave。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>POST</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>
```

ゴリゴリと設定ファイルを作成。（[]は実際の値に置き換えてね！）

```zsh:.env
S3_BUCKET=[Bucket Name]
S3_KEY=[Access Key ID]
S3_SECRET=[Secret Access Key]
```

```zsh
anoworl@crow ~/ImageBoard $ rails g dragonfly
```

DataStoreをFileからS3に変更。

```ruby:config/initializers/dragonfly.rb
  datastore :s3,
    bucket_name: ENV['S3_BUCKET'],
    access_key_id: ENV['S3_KEY'],
    secret_access_key: ENV['S3_SECRET'],
    url_host: ENV['S3_CDN_HOST']
```

ダイレクトアップロードに使用するs3_file_field gemも設定。

```ruby:config/initializers/s3_file_field.rb
S3FileField.config do |c|
  c.access_key_id = ENV['S3_KEY']
  c.secret_access_key = ENV['S3_SECRET']
  c.bucket = ENV['S3_BUCKET']
end
```

モデルを設定。

```ruby:app/models/post.rb
class Post < ActiveRecord::Base
  dragonfly_accessor :image
end
```

フロント側を設定。

```js:app/assets/javascripts/application.js
//= require s3_file_field
//= require_tree .
```

```erb:app/views/posts/_form.html.erb
    <%= f.s3_file_field :image_uid %>
```

```coffeescript:app/assets/javascripts/posts.js.coffee
$(document).on 'ready page:load', ->
  $this = -> $("#post_image_uid")
  $this().S3FileField
    done: (e, data) ->
      $this().attr(type: 'text', value: data.result.filepath.slice(1), readonly: true)
```

```erb:app/views/posts/index.html.erb
        <td><%= image_tag post.image.thumb('100x100#').url %></td>
```

取り敢えず動く。

![スクリーンショット 2014-12-03 18.22.12.png](https://qiita-image-store.s3.amazonaws.com/0/18606/33f7ffe2-8f6e-5dfb-f105-ab62f189c753.png)

ファイルを選択すると、パスが入る。

![スクリーンショット 2014-12-03 18.23.28.png](https://qiita-image-store.s3.amazonaws.com/0/18606/2f585929-fbb1-e0d7-81e5-0dc0d8d21e43.png)

一応動く。

![スクリーンショット 2014-12-03 18.48.00.png](https://qiita-image-store.s3.amazonaws.com/0/18606/5bc1aa11-3140-d9b0-cbc4-6979174ef6d5.png)

### 宿題
改めて実装すると意外に結構かかるので残りは今後書き足していきます。

- ファイル名対応
    - URLエンコードされたパスのデコード
    - パスをS3に合わせてNFCへ正規化
- ブラウザでの画像軽量化
- 生成したサムネイルの保存
    - S3への保存
    - サムネイルのパスをmemcachedにキャッシュ
- CloudFront対応
- Herokuへのデプロイ

## おわりに
### 今後の展望
- CloudFront経由で画像をアップロード
    - 早くなる可能性を検証したい

### [Heroku Advent Calendar 2014](http://qiita.com/advent-calendar/2014/heroku)
まだまだ空きがあります！明日どうしよう……。