概要
Ruby on Rails 5.2でサポートされたActiveStorageを使ってみましたので、備忘録として残します。
ActiveStorageとダイレクトアップロード
ActiveStorageは従来Carrierwave等のGemで実現されていたファイルアップロードをRails標準でサポートした機能です。アップロード先サーバとしてS3などクラウドストレージが利用できます。
ただし、そのままローカルの代わりにクラウドストレージを使うと、ファイルの転送経路としてクライアント→Webサーバ(Rails)→クラウドストレージと二段転送になってしまい、無駄な(かつ大量な)Webサーバ負荷が発生することになります。
これを解消するため、ファイルをブラウザからクラウドストレージのRESTインターフェースに直接送信するのがダイレクトアップロードです。
構成
項目 | 内容 | 理由等 |
---|---|---|
Webサーバ | Heroku | 無料利用が可能なため |
Webフレームワーク | Rails 5.2.1 | |
クラウドストレージ | GCS (Google Cloud Storage) | Always Free利用が可能のため |
インフラ構成に関しては再利用性に考慮し、Terraformを使ったコードで定義する形で進めました。
完成形コードは下記を参照ください。
https://github.com/o-t-k-t/incremental_todo
また、今回は単純なCRUD程度の機能は実装済みの前提で記載します。
クラウドストレージのバケット作成
Google Cloud Platformプロジェクトの作成
GCSバケットを定義するためのプロジェクトを作成します。まだ行なっていない場合はユーザ登録から必要となります。ここでは空のプロジェクトを作成するのみであるため、説明は省略します。
GCPサービスアカウント作成
GCPプロジェクトに下記用途で外部APIから操作するためのアカウントを作成します。
それぞれ作成後に認証情報が入ったJSONファイルをダウンロードします。
- Terraform(バケット作成等の管理)
- Railsアプリ(バケットへのファイル読み書き)
Terraformアカウント
まずCompute Engine APIを有効化する必要があります。
(操作するのはGCSのみですが、バケット作成にCompute Engine APIが必要となります)
その上で、ストレージ管理者権限と、Compute管理者権限を与えたサービスアカウントを作成します。
このブラウザ操作の詳細はドキュメントを参照ください。
サービスアカウント作成
https://cloud.google.com/iam/docs/creating-managing-service-account-keys?hl=ja
API有効化
https://cloud.google.com/apis/docs/enable-disable-apis?visit_id=636838276690750507-878885287&rd=1
Railsアカウント
Terraformアカウントと同様の手順で、ストレージのオブジェクト管理者を設定
TFファイルの作成
下記の通りGCS構成を記述します。
# Heroku側の設定
...
variable "gcs_credential_file" {}
# GCP: object storage
provider "google" {
credentials = "${var.gcs_credential_file}"
project = "【GCP Project ID】"
region = "us-central1" # NOTE: 米国のみAlways Free対象
}
resource "google_storage_bucket" "【バケット名】" {
name = "【バケット名】"
location = "us-central1" # NOTE: 米国のみAlways Free対象
storage_class = "REGIONAL"
}
バケットのロケーションで米国以外を指定するとAlways Free対象外となる点に留意ください。
構成を適用するにはGCPのAPIを使用するため、当然認証情報が必要になるのですが、Terraformのコードでは、バージョン管理下におけない認証情報などを、上記のgcs_credential_file
のように値がない変数として宣言することで、別ファイル(.tfvars)から読み込ませられます。
デフォルトではterraform.tfvars
が読み込まれるため、ここにサービスアカウント認証情報を記述します。
gcs_credential_file = 【Terraform用サービスアカウントのJSONファイル格納パス】
これを下記コマンドで実行するとバケットが作成されます。
terraform apply
Rails側でGCS使用設定
GCSクライアントGemインストール
...
gem 'google-cloud-storage'
...
Gemfileに上記を追記し、インストール
bundle install
ActiveStorageのインストール
下記でActiveStorage用のファイルが生成されます。
bundle exec rails active_storage:install
ファイルストレージ向け設定ファイルがあるので、下記を追記します。
google:
service: GCS
project: 【GCP Project ID】
credentials: <%= ENV["GCS_CREDENTIAL"] %>
bucket: 【バケット名】
このファイルでは複数のファイルストレージの情報が記述できます。
これをRails実行環境ごとに指定することにより、Railsのアップロード先サーバを設定できます。
今回は実験用なので、develop環境でGCSを使用します。
config.active_storage.service = :local
上記を下記に変更。
config.active_storage.service = :google
また、ActiveStrageはレコードとファイルの関連を管理するために専用のテーブルを必要とします。インストール時にマイグレーションファイルが生成されているので、データベースに反映します。
bundle exec rails db:migrate
機能追加
モデル定義
ここではUserモデルに対する画像のアップロード機能を追加します。
まず、下記のようhas_one_attached
の記述を追記します。ActiveRecordのbelongs_to
などと似たスタイルとなっており、これはインスタンスメソッドなども同様です。
class User < ApplicationRecord
...
has_one_attached :avatar
...
end
これによりavatar
メソッドコールによりUserモデルのファイルにアクセスできるようになります。
また1レコード対多ファイルとする場合はhas_many_attached
で定義できます。
バリデーション
ActiveStorageには現状、類似機能のGemのような__バリデーションアシスト機能がありません。__
このため全てカスタムバリデーションで記述します。
一例ですが、下記します。
validate :requre_avatar_size_within_limit
validate :requre_avatar_is_image_file
...
private
def requre_avatar_size_within_limit
return unless avatar.attached?
return if avatar.byte_size <= 2.megabytes
avatar.purge
errors.add(:attachments, 'が大きすぎます')
end
def requre_avatar_is_image_file
return unless avatar.attached?
return if avatar.content_type.in?(%w[image/png image/jpg image/jpeg image/gif])
avatar.purge
errors.add(:attachments, 'がサポートされないファイル形式です')
end
end
purgeは対象ファイルを削除するメソッドです。通信を伴う処理になるので、別途Active Jobで非同期に処理するpurge_laterを用いるほうがバリデーションエラー時の応答は速くなります。
ビュー実装
フォーム実装はほぼActiveRecord属性と同様に記述できます。
= form_with(model: user, url: user_path, local: true) do |f|
.form-group
= f.label :name, class: 'label'
= f.text_field :name, class: 'form-control'
...
.form-group
= f.label :avatar, class: 'label'
= f.file_field :avatar, class: 'form-control'
表示も同様。
= user.avatar.variant(resize: '300x300')
ダイレクトアップロード設定
ここまででGCSアップロードは実装できているのですが、前述のとおりダイレクトアップロードを設定します。
CORS
ダイレクトアップロードではブラウザはWebサーバーが配信したHTMLから、Webサーバーと、それと異なるドメイン(バケット)に対しHTTPアクセスをする必要があります。しかし仮にこれを一律OKとしてしまうと、ユーザがログイン中に、何らかの方法で攻撃者が別サイト常にバケットへのフォームを入力させれば、自由に操作できていまいます。
これを防ぐため、そもそもブラウザには一度に同じドメイン以外にアクセスを行わない制限があります。これを今回のような用途で部分的に解除するのがCORS(Cross-Origin Resource Sharing)です。
GCSバケット設定
resource "google_storage_bucket" "【バケット名】" {
name = "【バケット名】"
location = "us-central1" # NOTE: 米国のみAlways Free対象
storage_class = "REGIONAL"
cors {
method = ["*"]
origin = ["【WebサーバのURL】"]
response_header = ["Content-Type", "Content-MD5"]
max_age_seconds = 3000
}
}
Rails設定
下記のようにフォームヘルパーのオプションdirect_upload
で指定します。
.form-group
= f.label :avatar, class: 'label'
= f.file_field :avatar, direct_upload: true, class: 'form-control'