【MinIO】RailsアプリケーションにS3互換ストレージ MinIO を導入してみる

# はじめに
プライベートでの共同開発において、「開発環境と本番環境の差異をできるだけ少なくしたい」という思いからローカルで動かせるS3互換ストレージを調べていました。業務で`fake-s3`というサービスを使用したことがありますが、MinIOというサービスも現場で使われている例を発見したのでそちらを導入してみました。

# MinIOについて
MinIOとはAWS S3互換のオブジェクトストレージのことです。

https://min.io/

* 特徴
    * ローカルに仮想的な S3 環境を構築できる
    * ローカルの仮想環境なので不用意にAPIを叩いてもお金も掛からない

# シナリオ
簡単なアプリを作成した後、MinIOを導入し、投稿した画像がMinIOに格納されるかを確認してみます。

### 1. アプリの土台を作成する
以下の機能をもつ簡易的なブログアプリを作成する。

* 記事一覧機能
* 記事詳細機能
* 記事投稿機能

#### 動作確認
MinIOを用いない状態で画像を投稿し、storageフォルダにファイルが格納されることを確認する

### 2. MinIOの導入
提供されているMinIOのdockerイメージを使用してMinIOを導入します。

#### 動作確認
MinIOを導入した状態で画像を投稿し、MinIOのバケットににファイルが格納されることを確認する

# 1. アプリの土台を作成する
**1. [こちら](https://zenn.dev/tmasuyama1114/articles/rails-docker-6x-how-to)の記事を参考にDockerでrailsのアプリケーションを新規作成します。**
**2. 必要モデルの生成**

  ```bash
$ rails g model article
  ```
**3. マイグレーションファイルの編集**
一応タイトルと内容を追加しておきます。

  ```ruby:db/migrate/20..._create_articles.rb
class CreateArticles < ActiveRecord::Migration[6.0]
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end
  ```
**4. データベースに反映**

  ```bash
$ rails db:migrate
  ```

**5. 必要コントローラーの生成**

  ```bash
$ rails g controller articles
  ```
**6. ActiveStorageの導入**

  ```bash
$ rails active_storage:install
$ rails db:migrate
  ```

  ```ruby:app/models/article.rb
class Article < ApplicationRecord
  has_one_attached :image # 追加
end
  ```

**7. 必要アクションの定義**

  ```ruby:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  # 一覧
  def index
    @articles = Article.all
  end

  # 詳細
  def show
    @article = Article.find(params[:id])
  end
  
  # 新規投稿画面
  def new
    @article = Article.new
  end

  # 新規投稿
  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new
    end
  end

  private
    def article_params
      params.require(:article).permit(:title, :body, :image)
    end
end
  ```
**8. ルーティングの設定**

  ```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :articles
end
  ```
**9. viewの作成**

  ```erb:app/views/articles/index.html.erb
<h1>Articles</h1>
<%= link_to '新規登録', new_article_path %>

<div>
  <ul>
    <% @articles.each do |article| %>
        <li id="article_<%= article.id %>">
          <%= link_to article.title, article %>
        </li>
    <% end %>
  </ul>
</div>
  ```

  ```erb:app/views/articles/show.html.erb
<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>

<p>
  <strong>Body:</strong>
  <%= @article.body %>
</p>

<p>
  <% if @article.image.attached? %>
    <%= image_tag @article.image %>
  <% end %>
</p>

<%= link_to 'Back to Articles', articles_path %>
  ```

  ```erb:app/views/articles/new.html.erb
<h1>New Article</h1>

<%= form_with model: @article, local: true do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %><br>
  </div>

  <div>
    <%= form.label :image %><br>
    <%= form.file_field :image %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>


<%= link_to 'Back to Articles', articles_path %>
  ```

## 動作確認
**1. 新規投稿フォームから画像を投稿してみます。**
URL: http://localhost:3000/articles/new  
<img width="400" alt="スクリーンショット 2022-01-21 6.11.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/02bac9dc-c7f7-9c9d-82ee-36bfffcb9c3a.png">

**2. 投稿詳細画面を確認**
URL: http://localhost:3000/articles/1
<img width="400" alt="スクリーンショット 2022-01-21 6.13.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/fa1cc704-d2cd-fb4f-1746-57773a9bea2b.png">

**3. ファイルの格納場所を確認**
以下の記述により`storage`ディレクトリにバイナリファイルが保存される。

```ruby:config/environments/development.rb
...
config.active_storage.service = :local
...
```

```ruby:config/storage.yml
...
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>
```

![スクリーンショット 2022-01-21 6.15.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/7b1e2e27-1ca6-aff9-3eb1-00395d3a0a3c.png)

# 2. MinIOの導入
**1. docker-composeにMinIOの設定を追加する**
MinIOのバージョンアップにより指定する環境変数が`MINIO_ACCESS_KEY`と`MINIO_SECRET_KEY`から`MINIO_ROOT_USER`と`MINIO_ROOT_PASSWORD`に変更となりました。

  ```yaml:docker-compose.yml
version: "3"

services:
  db:
    (省略)

  web:
    (省略)
  minio:
    image: minio/minio:latest
    command: ["server", "/data", "--console-address", ":9001"] # --console-address オプションの追加
    ports:
      - 9000:9000
      - 9001:9001 # コンソール用のポートフォワード設定追加
    volumes:
      - minio:/data
    environment:
      MINIO_ROOT_USER: minio # 元 MINIO_ACCESS_KEY
      MINIO_ROOT_PASSWORD: miniosecret # 元 MINIO_SECRET_KEY

volumes:
  mysql-data:
  minio:
    driver: local
  ```

**2. storage.ymlを編集**

  ```yml:config/storage.yml
local:
  # service: Disk
  # root: <%= Rails.root.join("storage") %>

  service: S3
  access_key_id: "minio"
  secret_access_key: "miniosecret"
  region: "us-east-1"
  endpoint: "http://minio:9000"
  bucket: rails-blog-minio
  force_path_style: true
  ```

**3. docker-compose.ymlを編集したのでbuildして再度コンテナ立ち上げる**

  ```bash
$ docker-compose up -d --build
  ```

**4. http://localhost:9001 にアクセスする**
![スクリーンショット 2022-01-21 7.08.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/ce9c13f7-ee9c-b0b2-16e2-db6ef7d56b05.png)

**5. ログイン**
`docker-compose.yml`に記述したUsername、Passwordを入力すると以下のような画面になります。
この時点ではまだBucketは作成されていません。
![minioのコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/dff629b4-8b6f-9b8b-d254-360667d73f6c.png)

**6. Bucketの作成**
サイドメニューの「Buckets」 → 「Create Bucket」でバケットを作成します。
名前は`config/storage.yml`の`bucket`で指定したバケット名にします。
![スクリーンショット 2022-01-21 7.17.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/80c958b6-fb96-cf3f-d5d1-194f69b081c6.png)

## 動作確認
**1. 画像を投稿する**
この状態で画像を投稿すると以下のようなエラーが発生します.

  ```bash
RuntimeError (Cannot load `Rails.config.active_storage.service`:
Missing service adapter for "S3"):
  ```
S3のためのアダプターがないと言われるので以下をGemfileに`aws-sdk-s3`を追加してinstallします。

  ```ruby:Gemfile
gem "aws-sdk-s3", require: false
  ```

**2. 再度画像を投稿して投稿詳細画面を確認してみます.**
正しく画像が表示されていることが分かります。
<img width="400" alt="スクリーンショット 2022-01-21 7.47.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/8b95bf40-fb0c-8450-5269-3fbc2fa06a3d.png">

**3. MinIOコンソール画面Bucketの中身の確認**
以下のようにオブジェクトが一つ格納されていることが確認できます。
![スクリーンショット 2022-01-21 7.48.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646903/56bfb8e9-c04b-ce03-677a-461af13d3251.png)

**4. storageディレクトリを確認**
ファイルの格納先をMinIOに切り替えたため追加で記事を投稿しても`storage`ディレクトリに新しくファイルが格納されていません。

**※画像の取得ができない場合は/etc/hostsにminioを追加してみてください。**

```text:etc/host
...
127.0.0.1       minio
...
```

## おまけ
* 先ほどはconsole画面から手動でBucketを作成しましたが、dockerを立ち上げた時点で作成されているとより運用しやすいと思うのでdocker-composeを再度編集していきます。
* `{}`で示した、MINIO_ROOT_USER, MINIO_PASSWORD, bucket-nameを置き換えて指定してください

```yaml:docker-compose.yml
version: "3"

services:
  db:
    (省略)

  web:
    (省略)

  minio:
    (省略)

  # ↓ 追加
  createbuckets:
    image: minio/mc
    depends_on:
      - minio
    entrypoint: >
      /bin/sh -c "
      until (/usr/bin/mc config host add myminio http://minio:9000 {MINIO_ROOT_USER} {MINIO_ROOT_PASSWORD} miniosecret) do echo '...waiting...' && sleep 1; done;
      /usr/bin/mc mb myminio/{bucket-name};
      /usr/bin/mc policy download myminio/{bucket-name};
      exit 0;
      "

volumes:
  mysql-data:
  minio:
    driver: local
  ```

# 終わりに
以上で開発環境（ローカル）にいながら本番環境を意識した開発ができるようになりました。

# 参考

https://min.io/

https://hub.docker.com/r/minio/mc/dockerfile

https://buildersbox.corp-sansan.com/entry/2020/04/13/110026

https://qiita.com/p-baleine/items/e0eb7ea64b6cf34677dd

https://qiita.com/potdig/items/601aa883d87efbbbdc65

https://qiita.com/aru12/items/e9420c95418c80bb334e