<Ruby on Rails> Active Storageを利用して画像ホスティングサービスを実装する

KV画像
こんにちは! エンジニアインターンの田中です。株式会社CyberOwlは画像を多用するサービスを提供しており、それらで使用される画像をホスティングできるサービスを開発しました。サーバーサイドの言語にRuby、そのフレームワークにRuby on Railsを使用し、開発を行ったので、ここではこのホスティングサービスの設計方法について説明させていただきたいと思います。
  1. 画像ホスティングとは?
  2. Ruby on RailsのActive Storage ダイレクトアップロード機能を利用して画像をアップロードする
    1. Active Storage とは?
    2. Amazon S3に画像をダイレクトアップロード
    3. Active Storage ダイレクトアップローダーの問題点とその解決法 
  3. MiniMagickを用いた画像の編集作業
    1. URLクエリの変数を元に画像を編集しVariantレコードを作成する
    2. Variantレコードの追加時の問題点と解決策
  4. 画像取得
  5. その他の機能

画像ホスティングとは?

Webページで画像を使用するとき、ページの読み込み速度を上げる目的で画像のホスティングサービスが利用されます。このサービスは特定のURLにアクセスすると保存している画像を返す仕組みとなっており、imgタグにそのURLを指定することで、Webサイト内で画像を使用することができます。また、画像のサイズや解像度、フォーマットを簡単に変更できる利点もあります。本プロジェクトではクエリパラメータを使用して、画像のサイズや解像度を指定することができるようにしました。

Ruby on RailsのActive Storage ダイレクトアップロード機能を利用して画像をアップロードする

Active Storage とは?

Active Storageとはウェブアプリケーションで使用されるファイルをクラウドストレージサービスへアップロードし、Ruby on RailsのActive Recordモデルに紐付ける機能です。加えて、付属のJavaScriptライブラリを用いて、Ruby on Railsのサーバーからではなく、クライアントからクラウドストレージサービスへのダイレクトアップロードをサポートしています。

Amazon S3に画像をダイレクトアップロード

Active Storageのダイレクトアップロード機能を使用し画像をAmazon S3(以下S3)などのクラウドストレージに保存する場合、Ruby on Railsは以下のような処理を行います。

1. クライアントからサーバーに画像のメタ情報(ファイルサイズやContent-Typeなど)を送信
2. アップロード用の署名付きURLを発行し、1で受け取ったメタ情報と共にS3に送信
3. データベースのActive Storage blobテーブルにアップロードされる画像のレコードを追加
4. 署名付きURLとActive Storageデータベースに保存した画像のレコードid (blob_id)などをクライアントに返信
5. クライアントサイドは受け取った署名付きURLにイメージをアップロード
6. S3は2で受け取ったメタ情報などを使用し、正しいファイルかどうかをチェック  

以下が画像アップロード時のシーケンス図です。シーケンス図内の番号は上の箇条書きとは番号が一致しておりませんのでご注意ください。

画像アップロード図

Active Storage ダイレクトアップローダーの問題点とその解決法 

問題:S3のフォルダー分けができない

Ruby on RailsのActiveStorage DirectUploadsControllerをそのまま使用すると、S3内でフォルダー分けされずに保存されることになります。この問題点は複数サービスが画像ホスティングサービスを使用した場合に、全ての画像がS3バケット内に混在してしまう点です。仮に一つのサービスが停止した場合、そのサービスで利用された画像をS3バケットで特定、一括削除する状況が考えられますが、S3バケットにフォルダー分けせずに画像を保存していると、それが困難となります。

DirectUploadsControllerを継承して問題を解決

ダイレクトアップロードを利用してS3バケットに画像をアップロードする場合、まず、Railsサーバーはactive_storage_blobsテーブルにレコードを作成し、そのblobレコードのkeyを”1ym4o2y9mauvjgincwfcsolx4hul” のようなランダムに生成されたユニークな文字列とします。このkey名はS3での画像保存時のオブジェクト名となり、仮にblobレコードのkey(S3のオブジェクト名)が ”blog/1ym4o2y9mauvjgincwfcsolx4hul” となっていた場合、画像オブジェクトはS3内のblogフォルダ内に格納されます。よって、blobレコードのkeyを
”サービス名+ / +ユニークな文字列” とすることでサービスによるフォルダー分けを実現できます。これを実装するためDirectUploadsControllerを継承し、新しいコントローラーを作成し、その中でblobレコードの生成を担っている関数 create をオーバーライドしました。 


こちらがActiveStorage::DirectUploadsController内部のcreate関数です。
 
def create{
  blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
  render json: direct_upload_json(blob)
 }


下に示すコードがS3バケットのフォルダー分けに該当する部分です。本システムで画像はS3バケット内で二層のフォルダに入っており、メディアによるフォルダー分けとカテゴリーによるフォルダー分けが行われています。

ActiveStorage::Blobのcreate_before_direct_uploadメソッドでblobレコードが作成されるため、blob.keyをアップデートすることでS3アップロードのキーを変更できます。

def create{
  #-----------------------------
  #↓本ブログの内容と直接関係する 部分のみ記述
  #-----------------------------
  prefix = File.join(media, category)
  blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args) 
  blob.key = File.join(prefix, blob.id)
  blob.save
  render json: direct_upload_json(blob)
}

MiniMagickを用いた画像の編集作業

画像取得用URLにblob_id、解像度、サイズ、画像フォーマットなどのパラメーターを持たせて叩くと指定された形に編集された画像が返却されます。これを実現するため、画像サイズを編集したり、時にはファイル形式を変更する必要があります。Rubyでは画像編集を行うコマンドラインツールであるImageMagickをプログラミング的に記述できるMiniMagickと呼ばれるgemを使用することが可能で、これを利用して画像の編集を行いました。

この際、S3からイメージをダウンロードし、そのサイズや解像度を編集しVariant(編集された画像)を作成します。そして、active_storage_blobsテーブルにvariantレコードを追加し、S3にアップロードします。

URLクエリの変数を元に画像を編集しVariantレコードを作成する

 画像取得URLは以下のようになります。
“/image_hosting/blob_ID.jpg?quality=80&resize=2048”
詳細説明:
blob_ID: blob_id(データベースで保存される画像のID)
.jpg: 画像フォーマット指定
quality: 解像度指定
resize: サイズ指定

クエリパラメータを使用し画像を編集し送信しているコードが以下になります。

image_info = params[:id];
id = image_info.split('.')[0]
image = ActiveStorage::Blob.find(id)
image_format = image_info.split('.')[1] || original_format
quality = params.fetch(:quality, 80).to_i
resize = params[:resize] || 2048
resize = [2048, resize.to_i].min
variant = image.variant( 
  resize:  resize,
  gravity: "center", 
  quality: quality,
  format: image_format
 ).processed
variant_image = variant.image.service.download(variant.key)
send_data variant_image, type: variant.image.blob.content_type, disposition: 'inline'

Variantレコードの追加時の問題点と解決策

Variantレコードの作成でも画像保存時と同じ問題が発生します。Ruby on Railsで用意されているActive StorageのVariant作成をそのまま行うと、編集された画像がS3でフォルダー分けされずに保存されます。これでは画像を保存するときにフォルダー分けを行った意味がありません。そこで、variant作成時にしてされるS3のkeyを編集する必要があり、variantを作成するprocessed 関数で呼び出されているtransform_blob関数でその処理を行いました。


編集前 transform_blob関数 (models/active_storage/variant_record.rb)

def transform_blob
    blob.open do |input|
      variation.transform(input) do |output|
        yield io: output, filename: "#{blob.filename.base}.#{variation.format.downcase}",
          content_type: variation.content_type, service_name: blob.service.name
      end
    end
end


編集後 transform_blob関数 (models/active_storage/variant_record.rb)

def transform_blob
    blob.open do |input|
      variation.transform(input) do |output|
        prefix = blob.key.split('/')[0...-1].join("/") + "/variant" || "Unknown_Variant"
        v_key = File.join(prefix, extract_blob_id(blob), ActiveStorage::Blob.generate_unique_secure_token)
        yield key: v_key, io: output, filename: "#{blob.filename.base}.#{variation.format.downcase}",
             content_type: variation.content_type, service_name: blob.service.name                                                                            
      end
    end
end


yieldではcreate_or_find_record関数が呼び出されており、その引数にblobレコードをとっているため、このkeyに編集を行います。 オリジナルの画像の格納されているフォルダー内にvariantフォルダーを作り、そこにvariant画像を保存しています。 

画像取得

URLを指定し画像を取得しますが、サーバーが画像を返却する方法には以下の二パターンがあります。

1. 変換したことない画像を、MiniMagickで変換して返却する
2. 1度変換したことのある画像をS3から取得して返却する

まだ、作成したことのない画像の取得リクエストが来た場合、サーバーはオリジナルの画像をMiniMagickで編集して返却します。しかし、2回以上同じパラメーターのリクエストが届いた場合、S3に保存されいる一回目に編集した画像を返却します。これを可能とするためにRailsはvariant_digestという変数を画像取得URLのパラメータなどから作成します。データベースに保存されたvariant_digestをチェックすることで使用して過去に指定された画像が過去に作成されたかを確認します。

画像取得URLを使用して編集された画像を取得する場合のフローが以下の図になります。

画像取得図

その他の機能

画像のアップロードとホスティングに加えて以下の機能も実装しています。

・blob_idのハッシュ化
 ユーザーが容易に他の画像にアクセスできることを防ぐ
・画像レコードにaltテキストの追加
 スクリーンリーダー使用者、SEO対策用
・画像の一括削除機能
 複数画像を使用したページを削除した場合に使用画像を一括削除
・アクセストークンの導入
 画像の不正アップロードなどを防ぐ
・pdf管理機能
 pdfを保存、プレビューする用途に対応
・セマフォによる並列処理制御
 複数画像を処理した場合にメモリーオーバーフローを起こさないための対策

最後まで読んでいただき、ありがとうございました!

※2023年5月26日時点

Techブログ 新着記事一覧