はじめに
CarrierWaveとMiniMagivkでHEIC
変換を実装したけどうまくいかなかったので別の方法を探していたときcloudinaryに出会って、cloudinaryを使って変換して投稿することを実装しました。
実装をする上でcloudinaryを無料枠で使いたい&画像の管理をAWSS3だけにしたいという思いがあります。
今回の実装でできること
- HEIC 画像をアップロードできる
- アップロードされた HEIC 画像は Cloudinary で JPEG に自動変換される
- 変換された JPEG 画像は AWS S3 に保存される
- Cloudinary 上の HEIC 画像は自動的に削除される (無料枠の節約)
- S3 に保存された画像の署名付き URL を生成し、有効期限付きでセキュアに画像を表示
- Cloudinary Webhook を利用し、非同期で処理を行う
前提条件
- Rails で基本的な投稿アプリケーションが作成済みであること
- AWS S3 バケットが作成済みであり、アクセスキーとシークレットキーが設定済みであること
- CarrierWave を使った画像アップロード機能が実装済みであること
- Cloudinary アカウントが作成済みであること
# AWS S3の設定
AWS_ACCESS_KEY_ID='アクセスキー'
AWS_SECRET_ACCESS_KEY='シークレットアクセスキー'
AWS_REGION='ap-northeast-1'
AWS_BUCKET_NAME='バケット名'
# Cloudinaryの設定
# Cloudinary のダッシュボードから取得し、環境変数に設定してください。
CLOUDINARY_CLOUD_NAME='クラウド名'
CLOUDINARY_API_KEY='APIキー'
CLOUDINARY_API_SECRET='APIシークレットキー'
実装手順
1. Gem のインストール
Gemfile
に以下の gem を追加し、bundle install
を実行します。
gem 'cloudinary'
gem 'aws-sdk-s3'
2.Cloudinary の設定
config/initializers/cloudinary.rb
を作成し、Cloudinary
の API キーなどを設定します。
Cloudinary.config do |config|
config.cloud_name = ENV['CLOUDINARY_CLOUD_NAME']
config.api_key = ENV['CLOUDINARY_API_KEY']
config.api_secret = ENV['CLOUDINARY_API_SECRET']
config.secure = true
end
3. CarrierWave の設定変更
app/uploaders/ooo_uploader.rb
を以下のように変更します。
class oooUploader < CarrierWave::Uploader::Base
# Cloudinary の機能を CarrierWave で使えるようにする
include Cloudinary::CarrierWave
# Cloudinary の public_id を保存 (Webhook で Post オブジェクトを特定するために使用)
after :store, :set_cloudinary_asset_id
# 保存先ディレクトリ
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# 許可する拡張子
def extension_allowlist
%w(jpg jpeg gif png heic)
end
# Cloudinary で JPEG に変換
process convert: "jpg"
private
# Cloudinary の public_id を Post モデルの cloudinary_asset_id カラムに保存
def set_cloudinary_asset_id(file)
Rails.logger.debug "set_cloudinary_asset_id called"
# Cloudinary へのアップロードが成功し、public_id が存在する場合のみ更新
if model && file.respond_to?(:public_id)
model.update_column(:cloudinary_asset_id, file.public_id)
end
end
end
4. Model の変更
Post モデルapp/models/post.rb
に、以下の変更を加えます。
class Post < ApplicationRecord
# CarrierWave を使って image カラムに PostImageUploader をマウント
mount_uploader :image, PostImageUploader
belongs_to :user
# Post 保存後に Cloudinary の asset_id を更新
after_save :update_cloudinary_asset_id
# Post 削除後に Cloudinary と S3 から画像を削除
after_destroy :delete_image_from_cloudinary_and_s3
private
# Cloudinary の asset_id を更新
def update_cloudinary_asset_id
if self.image.present? && self.image.file.respond_to?(:public_id) && self.cloudinary_asset_id.blank?
self.update_column(:cloudinary_asset_id, self.image.file.public_id)
end
end
# Cloudinary と S3 から画像を削除
def delete_image_from_cloudinary_and_s3
if self.cloudinary_asset_id.present?
begin
Cloudinary::Uploader.destroy(self.cloudinary_asset_id) # Cloudinary から画像を削除
rescue => e
Rails.logger.error "Failed to delete image from Cloudinary: #{e.message}"
end
end
if self.s3_image_url.present?
begin
s3 = Aws::S3::Resource.new( # AWS SDK for Ruby (v3) を使って S3 に接続
region: "ap-northeast-1",
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]
)
uri = URI.parse(self.s3_image_url)
bucket_name = uri.host.split(".").first # S3 のバケット名を取得
object_key = uri.path[1..-1] # S3 のオブジェクトキーを取得
bucket = s3.bucket(bucket_name)
obj = bucket.object(object_key)
obj.delete # S3 から画像を削除
rescue => e
Rails.logger.error "Failed to delete image from S3: #{e.message}"
end
end
end
5. Migration の追加
Post モデルにcloudinary_asset_id
とs3_image_url
カラムを追加するためのマイグレーションファイルを作成します。
% rails generate migration AddCloudinaryAssetIdAndS3ImageUrlToPosts cloudinary_asset_id:string s3_image_url:string
% rails db:migrate
6. Controller の変更PostsController
class PostsController < ApplicationController
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to posts_path
else
render :new
end
end
private
def post_params
params.require(:post).permit(:body, :image)
end
end
7. Webhook Controller の作成
Cloudinary
からのWebhook
を受け取るためのコントローラーを作成します。
rails generate controller CloudinaryWebhooks create --skip-routes
# AWS S3とのやり取り(オブジェクトのアップロード、署名付き URL の生成など)
require "aws-sdk-s3"
# Cloudinary API を使って、画像のアップロード、変換、削除などを行う
require "cloudinary"
# HTTP/HTTPS 経由でファイル(この場合は Cloudinary 上の画像)をダウンロードできます
require "open-uri"
class CloudinaryWebhooksController < ApplicationController
skip_before_action :verify_authenticity_token # CSRF トークンの検証をスキップ (Cloudinary からの Webhook のため)
def create
Rails.logger.debug "Cloudinary webhook received!"
Rails.logger.debug "Request body: #{request.body.read}"
payload = request.body.read # リクエストボディを取得
event = JSON.parse(payload) # JSON として解析
# Cloudinary からのレスポンスから必要な情報を取得
secure_url = event["secure_url"] # HTTPS の画像 URL
public_id = event["public_id"] # Cloudinary での画像の識別子
format = event["format"] # 画像のフォーマット (例: "jpg")
asset_id = event["asset_id"] # Cloudinary でのアセット ID (今回は未使用)
# public_id が nil の場合は処理を中断
if public_id.nil?
Rails.logger.error "public_id is nil. Aborting webhook processing."
render json: { error: "public_id is nil" }, status: :bad_request
return
end
begin
# Cloudinary の secure_url から画像データをダウンロード
image_data = URI.open(secure_url).read
# AWS SDK for Ruby (v3) を使って S3 に接続
s3 = Aws::S3::Resource.new(
region: ENV["AWS_REGION"],
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]
)
# S3 に保存するオブジェクトのキーを生成 (Cloudinary の public_id とフォーマットを使用)
object_key = "#{public_id}.#{format}"
# S3 バケットにオブジェクトをアップロード
obj = s3.bucket(ENV["AWS_S3_BUCKET_NAME"]).object(object_key)
obj.put(body: image_data, content_type: "image/#{format}", acl: "public-read") # 公開読み取り可能に設定
# Cloudinary から画像を削除
Cloudinary::Uploader.destroy(public_id)
# 対応する Post オブジェクトを検索
post = Post.find_by(cloudinary_asset_id: public_id)
if post
Rails.logger.debug "Post found: #{post.inspect}"
# S3 オブジェクトの署名付き URL を生成 (有効期限: 3600秒 = 1時間)
signer = Aws::S3::Presigner.new(client: s3.client)
presigned_url = signer.presigned_url(:get_object, bucket: ENV["AWS_S3_BUCKET_NAME"], key: object_key, expires_in: 3600)
Rails.logger.debug "Presigned URL: #{presigned_url}"
# Post オブジェクトの s3_image_url カラムに署名付き URL を保存
post.update_column(:s3_image_url, presigned_url.to_s)
post.reload
# Turbo Stream を使って、画像表示を動的に更新 (必要に応じて)
Turbo::StreamsChannel.broadcast_replace_to(
"posts", # ストリーム名
target: "post_image_#{post.id}", # 更新する要素の ID
partial: "posts/image", # 使用するパーシャル
locals: { post: post } # パーシャルに渡す変数
)
else
Rails.logger.warn "Post not found for public_id: #{public_id}"
end
head :ok # 成功レスポンスを返す
rescue => e
Rails.logger.error "Error processing Cloudinary webhook: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: { error: e.message }, status: :internal_server_error
end
end
end
8. ルーティングの設定
Rails.application.routes.draw do
post '/cloudinary_webhook', to: 'cloudinary_webhooks#create'
end
9. Cloudinary の Webhook 設定
Cloudinary
の管理画面でWebhook
を設定します。
-
Cloudinary
のダッシュボードにログインします -
Settings
>Webhook Notifications
を選択します -
Notification URL
を設定https://your-url/cloudinary_webhook
-
Notification Type(s)
をUpload
に設定
Webhook の URL は、HTTPS である必要があると思います。
ローカル環境でテストする場合は、ngrok などのツールを使って、ローカルサーバーを外部に公開する必要があります。
10. PostsControllerのアクションで、適切な画像 URL にリダイレクトする
display_image_post_path(post)
によって生成された URL にアクセスすると、PostsController#show_image
アクションが実行されます。
-
S3 の署名付き URL にリダイレクト
: post.s3_image_url が存在する場合 (Webhook 処理が完了し、S3 に画像が保存されている場合) -
Cloudinary の URL にリダイレクト
: post.s3_image_url が存在しない場合 (Webhook 処理が完了していない、または投稿に画像がない場合)
# 画像表示用のアクション
def show_image
if @post.s3_image_url.present?
# S3 の署名付き URL が存在する場合 (Webhook 処理が完了している場合)
s3 = Aws::S3::Resource.new(
region: "ap-northeast-1",
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]
)
signer = Aws::S3::Presigner.new(client: s3.client)
# S3 オブジェクトの署名付き URL を生成 (有効期限: 3600秒 = 1時間)
url = signer.presigned_url(:get_object, bucket: "walk-and", key: URI.parse(@post.s3_image_url).path[1..-1], expires_in: 3600)
redirect_to url, allow_other_host: true # 署名付き URL にリダイレクト
else
# S3 の署名付き URL が存在しない場合 (Webhook 処理が完了していない場合)
# Cloudinary の URL にリダイレクト
redirect_to @post.image.url, allow_other_host: true
end
end
11. ルーティング設定
resources :posts do
get "image", on: :member, to: "posts#show_image", as: :display_image
end
12. viewでの画像表示
<% if post.image.present? %>
<%= image_tag display_image_post_path(post), id: "post_image_#{post.id}" %>
<% else %>
<%= image_tag 'default_image.jpg', alt: t('posts.index.default_image_alt'), class: "rounded-lg shadow-lg w-full" %>
<% end %>
まとめ
この記事では、Rails アプリケーションで HEIC 画像を扱い、Cloudinary と AWS S3 を連携させて、以下の機能を実装する方法を解説しました。
- HEIC 画像のアップロード: ユーザーが HEIC 形式の画像をアップロードできる
- Cloudinary による JPEG 自動変換: CarrierWave と Cloudinary の連携により、アップロードされた HEIC 画像を自動的に JPEG 形式に変換する
- AWS S3 への保存: Cloudinary で変換された JPEG 画像を AWS S3 に保存する
- Cloudinary からの自動削除: Cloudinary の Webhook を利用して、S3 への保存が完了した画像を Cloudinary から自動的に削除する (無料枠の節約)
- 署名付き URL によるセキュアな画像配信: AWS SDK for Ruby を使用して、S3 に保存された画像への署名付き URL を生成し、有効期限付きでセキュアに画像を配信する
- Turbo Stream による動的な画像更新: Webhook 処理後に Turbo Stream を送信し、画像表示を動的に更新する (オプション)
これらの機能を実装することで、以下のメリットが得られます。
- HEIC 画像対応: 最新の iPhone などで撮影された HEIC 画像を、Web ブラウザで表示可能な JPEG 形式に自動変換できる
- ストレージコストの最適化: Cloudinary の無料枠を節約しつつ、AWS S3 の低コストなストレージを活用できる
- セキュリティの向上: S3 の署名付き URL を使用することで、画像の不正なアクセスを防ぎ、セキュアに配信できる
- パフォーマンスの向上: Cloudinary の Webhook を利用することで、画像変換・保存処理を非同期で行い、ユーザーの待ち時間を短縮できる
-
柔軟な画像表示:
PostsController#show_image
アクションにより、Webhook 処理の完了前 (Cloudinary の URL) でも、完了後 (S3 の署名付き URL) でも、常に適切な画像を表示できる