AWS Ruby on Rails

RailsAPIでCarrierWaveからMinIO/S3に複数ファイルアップロードを行う方法

皆さんはRailsからS3にファイルアップロードする際なにをつかってますでしょうか?

Railsに標準で組み込まれているActiveStorageでしょうか?
それともプラグインが豊富でカスタマイズもしやすいCarrierWaveでしょうか?

私はシンプルな機能ならActiveStorage派なのですが、
今回はRailsのAPIモードかつ将来的に画像処理など
カスタマイズも求められる可能性があったためCarrierWaveを使用しました

今回の記事では、CarrierWaveをつかってS3に複数のファイルアップロードをする流れについてご紹介します
※なおRails APIモードですので、ファイルアップロードについてはapp/viewsなどフロントのフォームを用意せずPostmanを利用した方法の紹介になります

CarrierWaveの導入について

Gem のインストール

まずはGemfileに以下のGemを追加してbundle installをします

# Gemfile
gem 'carrierwave', '~> 3.0' // 画像のアップロード処理
gem 'fog-aws'               // s3と接続するアダプタ

gem 'fog'gem 'fog-aws' の違いですが、
gem 'fog'が多くのクラウドサービスプロバイダーをサポートする包括的なライブラリで
gem 'fog-aws''fog' から派生したものでAWSのサービスに特化した軽量なライブラリというイメージです

今回は本番環境でS3、開発環境ではS3互換のMinIOというストレージを使用するので'fog-aws' を使用します

Uploaderの生成

CarrierWaveのUploaderクラスを生成します

$ rails generate uploader File

これにより、app/uploaders/file_uploader.rbというファイルが生成されるので
ここでファイルのアップロード方法を定義します

Uploaderの設定

下記以外にも設定できる項目は色々ありますが、最低限以下があればいいかと思います

# app/uploaders/file_uploader.rb
class FileUploader < CarrierWave::Uploader::Base
  storage :fog

 // アップロード先パス
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  // 許可する拡張子のホワイトリストを設定
  def extension_allowlist
    %w[jpg jpeg gif png]
  end
end

storage は保存先がローカルなら storage :file 、外部サービスなら storage :fog とします
storage :file とした場合、デフォルトでpublic/配下に保存されます

もしも開発環境(ローカル)はMinIOじゃなくていいよ!
public/配下にとりあえず保存したいよ、という人は以下のようにもできます

if Rails.env.local?
  storage :file // ローカルファイルシステムに保存
else
  storage :fog  // 外部サービス(MinIOやS3)に保存
end

ここで Rails.env.local?Rails.env.development? || Rails.env.test? と同じ意味です
Rails7.1から追加された新メソッドのため、使えない方はRails.env.development? || Rails.env.test? としてください
https://github.com/rails/rails/pull/46786

CarrierWaveの設定

CarrierWaveとfog-awsを設定するための初期化ファイルを作成します

# config/initializers/carrierwave.rb

CarrierWave.configure do |config|
  config.storage = :fog
  config.cache_storage = :fog
  config.fog_provider = 'fog/aws'
  config.asset_host = Rails.application.credentials.dig(:uploader, :asset_host)
  config.fog_credentials = {
    provider: 'AWS',
    aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
    aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
    region: Rails.application.credentials.dig(:aws, :region) || 'ap-northeast-1',
    host: Rails.application.credentials.dig(:uploader, :host),
    endpoint: Rails.application.credentials.dig(:uploader, :endpoint),
    path_style: Rails.application.credentials.dig(:uploader, :path_style)
  }
  config.fog_directory = Rails.application.credentials.dig(:uploader, :bucket)
  config.fog_public = false
end

上記はMinIO/S3どちらにも対応できるようにまとめた書き方になってますが、MinIOとS3で設定を分けて書くと以下のようになります

# config/initializers/carrierwave.rb

CarrierWave.configure do |config|
  config.storage = :fog
  config.cache_storage = :fog
  config.fog_provider = 'fog/aws'
  region = Rails.application.credentials.dig(:aws, :region) || 'ap-northeast-1'

 // 開発環境ではMinIO、それ以外ではS3を利用
  if Rails.env.local?
    config.asset_host = 'http://localhost:9001/test-dummy-bucket' // ブラウザからのアクセスURL
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: 'minio_root',     // docker-compose.ymlの定義と合わせる
      aws_secret_access_key: 'minio_password',  // docker-compose.ymlの定義と合わせる
      region:,
      host: 's3',
      endpoint: 'http://s3:9000', // コンテナからのアクセスURL
      path_style: true
    }
    config.fog_directory = 'test-dummy-bucket' // minioのバケット名
  else
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
      aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
      region:,
      path_style: true
    }

    config.fog_directory = "rei-backend-api-#{Rails.env}" // S3のバケット名
    config.fog_public = false
  end
end

Rails.application.credentials.dig(:aws, :access_key_id)credentials という秘匿情報が記載されたファイルの内容を読み取るものです(Rails5.2から追加)

開発環境用の credentials にはMinIO用の情報を、
本番環境用の credentials には S3 用の情報を記載しておきます。

credentialsの編集には各ファイルに対応するkeyを用意したうえで、EDITOR=vim bin/rails credentials:edit -e development のようにエディタや環境を指定して開く必要があります。

keyは credentials ファイルの作成者が持ってるため、持っていない場合はgit管理せずに共有してもらいましょう

# config/credentials/development.yml.enc

aws:
  region: 'ap-northeast-1'
  access_key_id: 'minio_root'
  secret_access_key: 'minio_password'
uploader:
  asset_host: 'http://localhost:9001/test-dummy-bucket'
  host: 's3'
  endpoint: 'http://s3:9000'
  bucket: 'test-dummy-bucket'
  path_style: true
  
# config/credentials/production.yml.enc

aws:
  region: 'ap-northeast-1'
  access_key_id: 'xxx'     // IAMユーザーの認証情報
  secret_access_key: 'xxx' // IAMユーザーの認証情報
uploader:
  bucket: 'production-bucket'

Rails.application.credentials.dig(:uploader, :endpoint)などキーが存在しない場合には nil が返るので
最初に示したとおり、MinIO/S3どちらにも対応できるようにまとめた書き方が使えます

MinIOの各種情報については docker-compose.yml と合わせるようにご注意ください

# docker-compose.yml

services:
  s3:
    container_name: app-name
    image: minio/minio:latest
    volumes:
      - s3_data:/app-name/minio/data
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: "minio_root"
      MINIO_ROOT_PASSWORD: "minio_password"
    command: server --console-address ":9001" /app-name/minio/data

volumes:
  s3_data:

モデルにUploaderをマウント

次に、モデルにUploaderをマウントします。
まずはUploaderをマウントするための新規カラムを追加しましょう

$ rails generate migration add_file_to_users file:string
$ rails db:migrate

Userモデルにファイルをアップロードする場合は以下のようにします

# app/models/user.rb
class User < ApplicationRecord
  # CarrierWaveのファイルアップローダーを :file カラムにマウント
  mount_uploader :file, FileUploader

  # バリデーションやその他のロジック
end

ファイルを1点のみ登録する場合はこれでよいのですが、複数ファイルを files カラムに追加する場合は以下のようにします。

$ rails generate migration add_files_to_users files:json // json型にする
$ rails db:migrate
# app/models/user.rb
class User < ApplicationRecord
  # CarrierWaveのファイルアップローダーを :files カラムにマウント
  mount_uploaders :files, FileUploader // mount_uploadersと 's' がつくことに注意

  # バリデーションやその他のロジック
end

マウント設定については単一ファイルの場合と、複数ファイルの場合で細々した違いがあるので要注意です

mount_uploaders と 's' を付けることを忘れて私は少し詰まりました・・

コントローラの設定

登録APIの処理での注意点が1か所だけあります。

ストロングパラメーター(user_params)の files: [] です
filesカラムをjson型にしたのと、Postman などから配列で送ってくるためこのような指定にする必要があります。

# app/controllers/api/v1/users_controller.rb

module Api
  module V1
    class UsersController < ApplicationController
      def create
        user = User.new(user_params)
        if user.save
          render json: { message: 'User created successfully' }, status: :created
        else
          render json: { errors: user.errors.full_messages }, status: :bad_request
        end
      end

      private

      def user_params
        params.require(:user).permit(:name, files: []) // files: [] に注意
      end
    end
  end
end

ここまででMinIO/S3にCarrierwaveを使ってファイルをアップロードするための設定については終了です

複数ファイルを送るAPIをPostmanから実行する

では実際にPostmanからAPIをたたいてファイルアップロードを確かめてみましょう

POSTで登録用APIのURLを指定したら、ボディでは form-data を指定します。

①キーには files[]{カラム名}[] の形にしてください
②キーの種別は「ファイル」を選択し、
③値入力欄では「+ New file from local machine」をクリックしてローカルPCの画像などを複数選択
して「送信」をクリックします。

※Postmanですが、デスクトップアプリを使用してください!!
 以前はブラウザ版でもChrome拡張を入れれば複数ファイル送信ができていたようですが、現在Chrome拡張は廃止されデスクトップアプリ経由でしか送れないようですのでご注意ください

設定が問題なければ http://localhost:9000 などでMinIOにログインして

下記のようにPostmanから送信したファイルがMinIOの指定バケットのstore_dirパスに登録されているのが確認できます。

本番環境時のS3の場合も以下のように登録成功することが確認できると思います

最後に

いかがだったでしょうか

今回はRailsのAPIモードでCarrierwaveをつかったMinIOやS3への複数ファイルアップロードの流れについて見てきました

Carrierwaveの設定(config/initializers/carrierwave.rb)ではストレージのエンジン(MinIOかS3)かを意識しなくても済むように
環境ごとの credentials を利用する形にして設定を行いました

Postmanから複数ファイルをパラメーターに含める方法や
Carrierwaveでファイルアップローダーをmountする際のつまづきポイントについても紹介してきました。

これで少しでもCarrierwaveをもちいたファイルアップロードがより素早く実装できる人が増えればいいなと思います

-AWS, Ruby on Rails
-, , , , , ,