[Rails5.2] Active Storageを使ってGCSに画像を保存・取得する

Kazuki Shibata
VISITS Tech Blog
Published in
19 min readDec 10, 2018

こんにちは。VISITS Technologiesでサーバーエンジニアをしています、@kshibata101です。

VISITSではインフラはAWS/GCPなどのクラウドを用いて開発しており、私が担当したプロジェクトではGCPを使って開発をしています。

今回フロントエンドからAPI経由で画像を保存しURLを返却する機能を用意することになったのですが、Active Storageを使うことでGoogle Cloud Storage上に簡単に画像を保存することができたため、その方法を共有したいと思います。

構成

今回実現したい構成を図にするとこんな感じです。

…図にするほど凝ったものはありませんでした!

基本的にはよくある構成だと思いますが、get/postをAPI経由でやるということで普段のviewがちょっと使いづらいというところがありました。

Active Storageとは

Active StorageはRails 5.2で導入された、画像や添付ファイルなどを簡単に保存して取得してくれる機能です。

アップロードしたファイルはサーバーのローカル上はもちろんのこと、S3やGoogle Cloud Storage(GCS)といったクラウドストレージにも保存することができます。

アプリ実装上はどこに保存されるかを気にしなくていいところもありがたいですね。

同様の機能を持つPaperclipがdeprecatedになり(https://robots.thoughtbot.com/closing-the-trombone)、代わりにActive Storageを使いましょうということになっているようです。

Active Storageの利点

先にActive Storageで実装した上で感じた利点を挙げておくと、

  • 機能が簡易的で実装が簡単
  • 既存のテーブルの構成を変える必要はない(alter table不要)
  • 保存後生成される閲覧用URLはトークンがついているのと、有効期間5分の制限もついているので、セキュリティを気にする場合にありがたい
  • かつ閲覧用URLはRailsのroutingを経由するので、認証をかけて特定ユーザーにのみupした画像を見せるなどの制御ができる

辺りになります。意外とセキュリティ面を気にした作りになってる模様です。既存テーブルのalterがいらないのも運用面では地味にありがたいですね。

Active Storage自体はフルスタックモード(非APIモード)で使われることが多いと思いますが、APIモードでも問題なく使えるようです。

手順

具体的な設定作業は以下の10個ほどの手順になります。

基本的には概要ページ(https://railsguides.jp/active_storage_overview.htm)に沿って設定を行っていきますので、そちらを見ていただいても構いません。

1. プロジェクト作成

Active Storageはrails 5.2以上で動作するので、5.2以上か確認して作成します。

[/path/to]$ rails new active-storage-sample -d mysql --api

2. setupコマンド実行

実行するとmigrationファイルが生成されます。

[/path/to/repo]$ rails active_storage:install
Copied migration yyyymmddHHMMSS_create_active_storage_tables.active_storage.rb from active_storage

3. migration実行

実行すると、テーブルが2つ作られます。

[/path/to/active-storage-sample]$ rails db:migrate
== yyyymmddHHMMSS CreateActiveStorageTables: migrating ========================
-- create_table(:active_storage_blobs)
-> 0.0739s
-- create_table(:active_storage_attachments)
-> 0.0258s
== yyyymmddHHMMSS CreateActiveStorageTables: migrated (0.0998s) ===============

アップした画像の情報はこの2つのテーブルで管理され、他のテーブルには影響しません。Active Storageでは画像はModelに紐づけることになりますが、has_one/has_manyの関係となるため画像情報自体は紐づけたModelには保存されません。

テーブル構成はこんな感じです(mysqlの場合)。

mysql> show create table active_storage_attachments\G
*************************** 1. row ***************************
Table: active_storage_attachments
Create Table: CREATE TABLE `active_storage_attachments` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`record_type` varchar(255) NOT NULL,
`record_id` bigint(20) NOT NULL,
`blob_id` bigint(20) NOT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `index_active_storage_attachments_uniqueness` (`record_type`,`record_id`,`name`,`blob_id`),
KEY `index_active_storage_attachments_on_blob_id` (`blob_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> show create table active_storage_blobs\G
*************************** 1. row ***************************
Table: active_storage_blobs
Create Table: CREATE TABLE `active_storage_blobs` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`key` varchar(255) NOT NULL,
`filename` varchar(255) NOT NULL,
`content_type` varchar(255) DEFAULT NULL,
`metadata` text,
`byte_size` bigint(20) NOT NULL,
`checksum` varchar(255) NOT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `index_active_storage_blobs_on_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.01 sec)

4. 画像連携のテーブル作成

今回はユーザーのavatar画像を作りたいので、userのテーブル(とmodel)を作成します。これもmigrationを実行しておきます。

[/path/to/active-storage-sample]$ rails g model User
Running via Spring preloader in process 78398
invoke active_record
create db/migrate/yyyymmddHHMMSS_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
[/path/to/active-storage-sample]$ rails db:migrate
== yyyymmddHHMMSS CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0994s
== yyyymmddHHMMSS CreateUsers: migrated (0.0995s) =============================

5. userモデルにActive Storageの連携を記述

ユーザー1人にavatar画像1つを設定するため、has_one連携としておきます。model設定はこれだけでOKです。

# app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar
end

6. ルーティング設定

UsersControllerを作り、そこに入出力処理を記載していきます。

[/path/to/active-storage-sample]$ rails g controller users
Running via Spring preloader in process 78631
create app/controllers/users_controller.rb
invoke test_unit
create test/controllers/users_controller_test.rb

avatar画像をpost(またはput)で送信しgetで画像URLで取得するため、その辺のURLが設定されていれば大丈夫です。

# config/routes.rb
Rails.application.routes.draw do
resources :users
end

設定されたroutingを確認すると以下のようになっています。

[/path/to/active-storage-sample]$ rails routes
Prefix Verb URI Pattern Controller#Action
users GET /users(.:format) users#index
POST /users(.:format) users#create
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create

よく見るとusers以外に/rails/active_storage/のルーティングも追加されています。

Active Storageの画像はtokenつきURLが発行されますが、そのURLは/rails/active_storage/を指すようになっており、ここからActive Storageのcontrollerが呼ばれるようです。

7. 登録処理

画像を保存するためpost処理を書きます。ここではユーザー作成時についでにavatar画像を保存する処理を書いています。

まず動作確認のためlocalに保存します。デフォルトではstorage/以下に保存されます。

# app/controllers/user_controller.rb
def create
user = User.create
image_match = params[:avatar].match(/^data:(.*?);(?:.*?),(.*)$/)
mime_type, encoded_image = image_match.captures
extension = mime_type.split('/').second
decoded_image = Base64.decode64(encoded_image)
filename = "avater#{user.id}.#{extension}" image_path = "#{Rails.root}/tmp/storage/#{filename}"
File.open(image_path, 'wb') do |f|
f.write(decoded_image)
end
user.avatar.attach({ io: File.open(image_path), filename: filename, content_type: mime_type })
end

まず、APIではbase64エンコードした以下の形式で状態で画像を送信します。xxxには画像をbase64した文字列をいれます。

data:image/{image_type};base64,xxxx...

処理の中では、mime_type(image/pngなど)と拡張子、そしてencodeされた画像の内容をそれぞれ変数にとっています。

画像はbase64デコードしてから、一度tmpのファイルとしてローカルに保存します。(直接保存する方法もあるかとは思いますが、詳しくないのでいったんローカルに保存してます。保存しない方法が分かれば更新します。)

最後にattachメソッドでファイル(や名前)を指定すれば完了です。

試しにrails serverでサーバーを起動した後、http://localhost:3000/users/に対して以下のデータをPOSTで送信します。ここでは1x1の空画像をbase64エンコードして送信しています。

$ curl 'http://localhost:3000/users/' -H 'Content-Type: application/json' -d '{"avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvIAAAAAXRSTlMAQObYZgAAAApJREFUeNpjYAAAAAIAAeUn3vwAAAAASUVORK5CYII="}'

成功するとstorage/以下にファイルが出来ます。

[/path/to/active-storage-sample]$ tree storage
storage
└── xG
└── mG
└── xGmGUHBnPPiXbNRYyxMRgMbf
2 directories, 1 file

8. 設定をGCSに切り替え

設定は二箇所行います。

  • config/storage.yml: ストレージ自体の設定を記載
  • config/environments/[env].yml: どのストレージを使うかを設定

まずstorage.ymlの設定ですが、デフォルトではtestやlocalなどの場合の設定が記載されており、amazon(S3)やgoogle(GCS)の設定の雛形はコメントアウトされて書かれています。

今回はGCSを使うので、GCSのコメントアウトを外して必要事項を記入します。

# config/storage.yml
google:
service: GCS
project: your_project
credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
bucket: your_own_bucket

projectにはGCPのproject名、bucketにはGCSのバケット名、credentialsにはGCSに保存できる権限をもったサービスアカウントのjsonファイルのパスを指定します。

(今回はGCPの各種設定は設定済みという前提で進めます。設定方法については後日別途記事を書くかもしれません。)

続いて[env].ymlを編集します。active_storageの設定で:localとなっているところを:googleに切り替えます。

# config/enviromnents/development.yml
config.active_storage.service = :google

9. Gemの追加

もう一点、GCP系のサービスを扱うにあたりGCPのAPIを叩く必要があります。ただし直接APIを叩くのではなく、gemにライブラリとして公開されているのでそれを利用します。

# Gemfile
gem 'google-cloud-storage'

Gemfileを編集した後、bundle installを実行してgemを取得します。

以上で設定が完了したため、もう一度サーバーを立ち上げてcurlを実行してみます。

成功するとDBに保存され、GCSのバケットにも画像が追加されます。

10. 取得処理

# app/controllers/user_controller.rb
def show
user = User.find(params[:id])
render :ok, { json: { avatar: polymorphic_url(user.avatar) } }
end

user.avatarに対してhelperメソッドをかませることでURLを取得することができます。発行されたURLは5分の時間制限がついています。

$ curl 'http://localhost:3000/users/2' -H 'Content-Type: application/json'
{"avatar":"http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--e1d6ff9a4053e1b68ae068aad756fa6ff5cdb8a6/avater2.png"}

長くなってしまいましたが、思ったより簡単にGCSへの保存ができました。

感想

その他にもフロントから直接クラウドのストレージに保存させることもできるので、APIモードでは出番はなさそうですが場面次第で使い勝手がよいかもしれません。自分は別途管理画面を用意した際に直接保存させる機能を追加しました。

一方トークンつきのURLかつRails(Web Server)を経由するので、この方式ではCDNとの相性は良くなさそうです。

画像をユーザー全体に公開するケースでは、通常通りGCSのURLを発行してCDN経由で配信した方がWeb Server的に優しいかもしれません。

実装中の感触としては

  • コーディングよりも設定多め
  • 機能も簡易的なので、やれることはそれほど多くない
  • GCPのサービスアカウントの権限周りで詰まるかも

といった辺りで、導入にあたってのハードルはそんなに高くないと思われます。

というわけでActive Storageのご紹介でした。

さいごに

VISITSではサーバーエンジニアを募集しています!

  • rubyに興味がある、実務でrubyを使ったことがある
  • GoやPythonなど新しい言語に興味がある
  • クラウドのいろんな機能を使ってサーバー開発したい
  • GCP触りたい

などなど、興味のある方は気軽にお声がけください!

--

--