RMagick使ってlocalstackのS3に画像をアップしてブラウザで参照する

かなり時間かかっちゃった上に
有力な情報がなかなか得られず結構苦しんだのでメモ

# local開発でS3が使いたいんだけど！
そもそものスタートはこちら。
AWS上で動かし知ているRoRのシステムで
諸事情によりレールに乗らずに開発をしていたのですが
ローカルの開発環境であるdocker上でS3のモックが欲しくなり調べて実装してみました。

## それ「localstack」があるよ
githubはこちら
https://github.com/localstack/localstack

AWSのモックを提供してくれるとても優秀なプロジェクトです。
一つのインスタンスでサービス毎にポートを割り当てて稼働させているようです。
詳細はドキュメントで。

## localstackを立ち上げる
今回はdocker-composeで立ち上げるので `docker-compose.yml` はこんな感じ

```yaml:docker-compose.yml
version: '2'
services:
  aws:
    image: localstack/localstack
    ports:
      - 8080:8080
      - 4567-4578:4567-4578
    environment:
      - DEFAULT_REGION=ap-northeast-1
      - HOSTNAME=localstack.test
      - DATA_DIR=/tmp/localstack
    privileged: true
```

`ports` でポートを外部に開放してあげる事でブラウザからも参照できます。
環境変数に関しては
`DEFAULT_REGION` で東京に指定
`HOSTNAME` は内部でドメインに割り当てるのに必要
`DATA_DIR` はデータの永続化のために割り当てています。
たぶんこれであっているはず・・・
他にも大量に設定はあるので詳しくは[githubのREADME](https://github.com/localstack/localstack/blob/master/README.md)でも読んでみてください。

設定が終わったら `docker-compose up -d` で立ち上げます

## プロジェクトからアップをしてみる
### 環境設定とか
ローカルと本番で向き先を変えたいので設定ファイルと設定の読み込みを作ります

```yaml:config/s3.yml
default: &default
  id: <%= ENV.fetch("AWS_S3_ACCESS_KEY_ID") %>
  access_key: <%= ENV.fetch("AWS_S3_SECRET_ACCESS_KEY") %>
  region: ap-northeast-1
  endpoint: https://s3.ap-northeast-1.amazonaws.com
  bucket_name: oreno-bucket

development:
  <<: *default
  endpoint: http://localstack.test:4572

test:
  <<: *default

staging:
  <<: *default

production:
  <<: *default
  bucket_name: honban-no-bucket
```

こんな感じで設定ファイルを作って読み込みのスクリプトを置きます

```ruby:config/initializers/s3.rb
Rails.application.config.s3 = YAML.load(ERB.new(File.read("#{Rails.root}/config/s3.yml")).result)[Rails.env]
```

環境情報は `Rails.env` に入っているのでここを参照
ymlの中でrubyのscriptを使いたいので `ERB` を使って読み込ませています。

### アップロードの処理
コードで書くとこんな感じです

```ruby:lib/s3/handler.rb
require 'aws-sdk-s3'

class S3::Handle
    def initialize
        @s3 = Aws::S3::Resource.new(
          region:      Rails.application.config.s3["region"],
          endpoint:    Rails.application.config.s3["endpoint"],
          force_path_style: Rails.env.development?,
          credentials: Aws::Credentials.new(
            Rails.application.config.s3["id"],
            Rails.application.config.s3["access_key"]
          )
        )
    end

    def upload_image(bucket_name, key_name, image)
        begin
            @s3.bucket(bucket_name)
                # ここがファイル名とかファイルパスになる
                .object(key_name)
                # `acl` でアクセス制御を追加しておく
                .put(body: image, acl: 'public-read')
            return true
        rescue => err
            return false
        end
    end
end
```

あんまりrails的な書き方では無いんだろうなと思いつつ・・・
`Aws::S3::Resource` の引数として設定している中で
`endpoint` はドメインの向き先の指定を
`force_path_style` はbucket_nameがサブドメインに含ませるかどうかの設定になります。
localstackを使う際には少なくともこの2点が本番稼働の時と違う設定になるのでお気をつけください。


## ブラウザで参照出来なくて困ったにゃん・・・
適当に放り投げて適当に名前付けて保存してブラウザでimgタグで表示しようとしたら出来なかったので
片っ端から `jpeg` に変換するという暴挙に出ました。

### ImageMagickのインストール
`yum` で入れるなら1行で終わりです。

`RUN yum -y install ImageMagick ImageMagick-devel`

もしDebian系とかうぶんちゅとかであればググって下さい。
情報はたくさんあります。

### RMagickのインストール
これもgemなんで一瞬ですね。
gemファイルに以下の行を追加して `gem install` でOKです。

`gem "rmagick"`

### jpeg変換をかけてみる
formからPOSTされてきたファイルをjpegに変換するのでcontroller側はこんな感じです。

```ruby:app/controller/images_controller.rb
class ImagesController
    # ~~~~ 中略 ~~~~
    # 画像のアップロードの処理
    def upload_image
        s3 = S3::Handle.new

        # file名は固定なので今回はこんな感じ
        file_name = 'suteki_na_image.jpg'

        # pathの指定とか出来る
        path = 'oreno/gazou/' + file_name

        # fileの読み込みと変換
        blob = s3.convert_image(params[:file].tempfile, file_name, 'JPEG')
        if s3.upload_image(path, blob)
            render json: {
                # ドメインの設定とか読み込むためにメソッドを噛ましている
                path: s3.make_image_url(path)
            }
        else
            head 500
        end
    end
end
```

jpqgの実際の変換はこんな感じでやっています。

```ruby:lib/s3/handler.rb
require 'RMagick'
class S3::Handle
    # ~~~~ 中略 ~~~~
    def convert_image(image, name, extension)
        # `from_blob` じゃないとうまく読み込めなかったりとかするみたい
        img = Magick::Image::from_blob(image.read).first

        # ここで変換
        img.format = extension

        # ここで書き出し
        blob = img.write(name).to_blob

        # オブジェクトを破棄してメモリ開放
        img.destroy!
        return blob
    end
end
```

### ブラウザからアクセスしてみる
画像をアップロードしたらブラウザからアクセスしてみます。
pathは
`http(s)://{domain}/{bucket_name}/{file_path}`
の構成になるので、今回の場合は
`http://localstack.test:4572/oreno-bucket/oreno/gazou/suteki_na_image.jpg`
にアクセスすれば画像が見られるはずです。

## 「bucketが無い」ってエラーが出たんだけど！
`aws-sdk-s3` の `create_bucket` メソッドがうまく動かずに苦労しましたが
通常運用で動的にbucketを作らないといけない事態は想定していなかったので
普通にaws-cliで作りました。
localstackのidとkeyは適当で大丈夫みたいです。
aws-cliについてはまた別の機会に・・・

# まとめ
便利な道具があるのはありがたいけどドキュメントが疲れた(語彙力