2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ActiveStorage CloudFront

Last updated at Posted at 2023-06-11
1 / 12

この記事を見てわかること

  • ファイルアップロードの共通基盤として導入したActiveStorageについて
  • CloudFrontの設定について

ActiveStorageとは(ざっくり)

  • Rails5.2からRailsの標準としてサポートされたファイル添付ライブラリ
  • ActiveRecordにファイルを一つまたは複数添付できる
  • クラウドストレージサービスにファイルを簡単にアップロードできる
  • on-demandでの画像の変換や動画、PDFのプレビューなども行える

ActiveStorage ~導入と構成~

↓を実行すると

rails active_storage:install
rails db:migrate

↓のスキーマが増えます

  create_table "active_storage_attachments", force: :cascade do |t|
    t.string "name", null: false
    t.string "record_type", null: false
    t.bigint "record_id", null: false
    t.bigint "blob_id", null: false
    t.datetime "created_at", null: false
    t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
    t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
  end

  create_table "active_storage_blobs", force: :cascade do |t|
    t.string "key", null: false
    t.string "filename", null: false
    t.string "content_type"
    t.text "metadata"
    t.bigint "byte_size", null: false
    t.string "checksum", null: false
    t.datetime "created_at", null: false
    t.index ["key"], name: "index_active_storage_blobs_on_key",  unique: true
  end

使用する時は
ActiveStorage::AttachmentActiveStorage::Blob
を理解するのが大事!

  • AttachmentはpolimorpicなN:Nの中間テーブル
  • Blobはリソースファイルのメタデータ

ex) Userにファイルを一つ添付する

class User < ApplicationRecord
  has_one_attached :avatar
end

↓こんなリレーションになる
image.png

公式のREADMEより

Active Storageの他のRailsのattachment solutionsとの重要な違いはActive Recordによるbuilt-inのBlobとAttachmentモデルを使用すること。
つまり、既存のモデルにファイルのための追加のカラムを用意する必要がない。
polymorphic associationによりmodelとAttachmentを関連付けし、Blobの実態と紐付ける。
Blobはファイル名やcontent-typeなどのメタデータを保持し、ストレージ内でのidentifyなkeyを持つ。(S3だとkeyがそのままファイルのパスになる)
Blobはバイナリデータそのものは持たない。Blob(と実態のファイル)はimmutableであることを想定している。一つのファイルに一つのBlob。BlobとAttachmentsはhas_many関係であり、Blobを変更したいなら、既存のものを変更するよりも新規に作った方がよい。


ActiveStorage ~Config S3を使う場合~

見れば大体わかる
Gemfile

gem 'aws-sdk-s3', require false

config/storage.yml

local:
  service: Disk
  root: <%= Rails.root.join('tmp/storage') %>

production:
  service: S3
  bucket: production-assets-bucket
  # AWS SDK documentationに記載されている認証オプションを使う場合以下は省略可
  access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
  secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
  region: <%= ENV['AWS_REGION'] %>

# あとはミラーリングとかも設定できる
s3_west_coast:
  servie: S3
  bucket: xxx
  access_key_id: ''
  secret_access_key: ''
  region: ''

s3_east_coast:
  service: S3
  ...

# ファイルはprimaryから提供される
# この機能はダイレクトアップロードと一緒には使えない
amazon:
  service Mirror
  primary: s3_east_coast
  mirrors:
    - s3_west_coast

config/environments/production.rb

  # ymlで定義したサービス名を指定
  config.active_storage.service = :production

あとはAWS側でバケットの作成と以下のポリシーをアタッチしてやる

  • s3:ListBucket
  • s3:PutObject
  • s3:GetObject
  • s3:DeleteObject

ActiveStorage ~使ってみよう~

  • ファイルをattachする
class User < ApplicationRecord
  has_one_attached :avatar
end

# userのparamsにして作成できる
user = User.create!(params.require(:user).permit(:avatar))
# 既存のuserにattachするには
user.update_attributes(params.require(:user).permit(:avatar))
# or
user.avatar = params[:avatar]
user.save  # rails6からsaveのタイミングでストレージに保存されるようになった
# or
user.avatar.attach(params[:avatar]

user.avatar.attached? # => true

# Fileオブジェクトからもattach可能
# content_typeはoptionalだが付けといた方がいいらしい
user.avatar.attach(io: File.open('path/to/example.jpg'), filename: 'example.jpg', content_type: 'image/jpg')
# csv形式のstringとかであればこうすれば良い
process.file.attach(io: StringIO.new(csv_text), filename: 'process.csv', content_type: 'text/csv')
  • has_many_attachedの場合
class Comment < ApplicationRecord
  has_many_attached :images
end

# createは大体おなじ
comment = Comment.create!(params.require(:comment).permit(images: []))
# updateするとファイルが置き換わる(Rails6.0から)
comment.update_attributes(params.require(:comment).permit(images: []))
# attachするとファイルが追加される
comment.images.attach(parmas[:images])

comment.images.attached? # => true

# Fileオブジェクトからもattach可能
comment.images.attach(io: File.open('path/to/example.jpg'), filename: 'example.jpg', content_type: 'image/jpg')

削除系


# avatarと実際のリソースファイルを同期的に破棄する
user.avatar.purge

# Active Jobを介して、関連付けられているモデルとリソースファイルを非同期で破棄
user.avatar.purge_later

ただし、purgeは若干問題があるらしい
https://tech.smarthr.jp/entry/2018/09/14/130139
なのでdetachを使おう

# ActiveStorage::Attachmentだけを削除している
user.avatar.detach

# 全てのimagesのAttachmentsが削除される
comment.images.detach

# 現状個別にAttachmentをdeleteしてやっている
image = comment.images.find_by(id: params[:image_id])
image.try(:delete)

# user.destroyではAttachmentは削除されるが
# Blobとリソースファイルは削除されないので問題ない
user.destroy

Blobとファイルはトランザクションの影響を受けない範囲で確実に削除した方が良さそう

ファイルへのアクセス

# at view
# S3がpublicアクセス可能ならばこれでいい
url_for(user.avatar)

# ActiveStorageは基本的に認証はサポートしないらしい
# 署名付きURLとか使いたい場合は自前の対応が必要
# 今回はcdn_signed_url_forというhelperメソッドを用意した
# has_one_attachedとhas_many_attachedを同列で引数で扱うのが苦しかったので
# attachmentを渡すようにした
cdn_signed_url_for(user.avatar.attachment)

# downloadしてバックエンド側で利用することもできる
# 但し、常にbinaryで取得する
binary = user.avatar.download

# UTF-8のstringに変換する
encode = NKF.guess(binary)
utf8_text = binary.force_encoding(encode).encode(Encoding::UTF_8)

ActiveStorageの微妙なところ

  • 画像をリサイズしてからアップロードみたいなことには対応してない
    • preview表示時に都度リサイズすることはできる
    • 100x100とかにリサイズしてからアップロードみたいがしたければviewかcontrollerで頑張ってやる必要がある
    • 今回はjqueryのdark powerを使った
  • 認証とか基本サポートしないスタイル
    • 今後もこのスタンスでいくのかは不明
  • S3のパスを自由に設定できない
    • PaperClipとかCarrierWaveとかはできるっぽい
    • 移行するのが面倒な原因のような...
  • トランザクションのロールバック時にバケットの状態とblobsがずれる危険性がある
    • しょうがない感はある

CloudFront


これを参考にやりました
https://dev.classmethod.jp/articles/cf-s3-deliveries-use-signurl/


実際に見てみよう!


特記事項

  • 署名付きurlを使う場合は以下のクエリ文字列パラメータを削除してからURLの残りをオリジンに転送する
    • Expires
    • Key-Pair-Id
    • Policy
    • Signature
  • クエリ文字列はCDNのキャッシュに影響するので上記以外のクエリを使う場合は順番を固定した方がキャッシュのヒット率が上がる
  • 独自ドメインを使用することもできるが今回は対応しなかった
  • ファイルダウンロードを許可するために独自のpolicyを設定している
  • policyをstagingとproductionで共有しているが、開発時は分けた方がいいかも
  • cloudfront用のWAFはGlobalに置いてやる必要がある

署名付きURLの発行

アプリを経由せずにCDNから直接ダウンロードするためにContent-Dispositionヘッダーを使用
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition

class UrlSigner
  include Singleton

  def cdn_file_url_for(file)
    root + file.key
  end

  def signed_url_for(file, expires_at: default_expires_at)
    signer.signed_url(cdn_file_url_for(file), expires: expires_at)
  end

  # 日本語のファイル名でのDLをCloudFront->S3経由で行うためには
  # CloudFrontで一度unescapeされたリクエストがS3に渡されるので
  # CGI.escapeをfilename(日本語)に対して2度行う必要がある

  def signed_download_url_for(file, filename: nil, expires_at: default_expires_at)
    name = CGI.escape(filename || file.filename.to_s)
    download_query = "?response-content-disposition=attachment#{CGI.escape(";filename=#{name};filename*=" + "utf-8''#{name}")}"
    signer.signed_url(cdn_file_url_for(file) + download_query, expires: expires_at)
  end

  # 以下略...
end

helper
local環境ではCDNの署名付きurlは使えないのでRailsのリダイレクトurlを生成している

  def cdn_signed_url_for(attachment, download: false, options: {})
    return '' if attachment.blank?

    if Rails.env.production? || Rails.env.staging?
      download ? UrlSigner.instance.signed_download_url_for(attachment) : UrlSigner.instance.signed_url_for(attachment)
    else
      url = rails_blob_url(attachment, options)
      download ? url + "?response-content-disposition=attachment&filename=#{attachment.filename}" : url
    end
  end

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?