Docker + Google Cloud Storage + active_storageを導入したステップ

すでにできあがったRailsアプリケーションに対して、active_storageを導入してログです。アップロード先はGoogle Cloud Storageにしてみましたが、基本的にはAWSでも同様だと思います。

導入のキッカケ

すでにこのアプリケーションには自作のactive_storageもどきみたいなモジュールが存在していて、CarrierWaveを使いながらファイルアップロードができていた。

ただ、さすがにactive_storge使わないといけないよね?これから辛くなるよね?ということで途中から導入してみた感じです。

環境

gem 'rails', '~> 5.2.3'

environmentとして、以下があります。

  • development
  • test
  • staging-a
  • staging-b
  • staging-c
  • production  

installする

$ docker-compose run --rm app bundle exec rails active_storage:install
あるいは
$ rails active_storage:install

とすると、 active_storage_blobs と active_storage_attachments という2つのテーブルのmigrationファイルができあがります。

念のため解説すると、

  • active_storage_blobs:アップロードしたい対象のモデルにひもづくもの、imageとか
  • active_storage_attachments:実際のファイル

という違いがあります。

...続いて、できあがったmigrationファイルをdb:migrateします。

設定周り

続いては、アップロード先などの設定を書きます。一旦localで動くことを目標とします。とはいえ、localでは基本的にデフォルトで動くようになっているかと思います。以下のようになっていることを確認します。

config/storage.yml

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

localの場合は、Diskのstorage/に、testの場合はtmp/storageに画像がアップロードされていくイメージです。

そして、これを各環境ごとに指定します。local/testであれば...

config/environment/development.rb

config.active_storage.service = :local

config/environment/test.rb

config.active_storage.service = :test

この記述があればOKです。なければ追加して、サーバーを再起動します。

ざっくり説明すると、config/storage.yml はactive_storageそのものの設定で、:test :local のほかに :stage_hoge のように設定できたり各種サービスごとに分かりやすく :aws などの名前を振ることができます。そしてその内容は、yml形式で記述していきます。後ほど詳しく説明しますが、aws, gcpなどのサービスを利用する場合も必要なkey:valueを記述すれば設定がカンタンにできます。例えば...

awsを使う場合:

amazon:
  service: S3
  access_key_id: ""
  secret_access_key: ""

gcp(gcs)を使う場合:

google:
  service: GCS
  credentials: <%= Rails.root.join("path/to/keyfile.json") %>
  project: ""
  bucket: ""

のような感じです。

そして、各種環境でどれを使うのか、を環境ごとのファイルで指定します。いまの段階ではtestとlocalだけでしたが、productionではgoogleを使いたいとなったら、

config.active_storage.service = :google

とすればOKです。

念のため確認...

ここで2つ確認すべきことがあります。

1つは、適切なアップロード先のディレクトリが掘ってあるか。

上の例でいえば、

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

の2つ、つまり /storage/tmp/storage が存在するかどうか、です。もしなければ作成しておいたうえで、チーム開発であれば以下の点を追加で対応するといいと思います。

もう一つは .gitignore に storage周りのディレクトリを追加することです。おそらくすでにtmpディレクトリはignoreされているかもしれませんが、storageディレクトリも追加しておくと、localで検証した画像がそのままgitにのってしまうことを防げます。

モデルの修正

設定周りが終わったら、アップロードしたい画像にひもづくモデルに修正を加えます。例えば、userモデル(models/user.rb)に対して、avatar画像(user.avatar)をアップロードしたいとします。その場合、以下の1行を追加するだけです。

class User < ActiveRecord::Base

# これで :icon として画像をアップロード、取得ができるようになる
has_one_attached :avatar

また、例えばモデルと画像がhas_manyの関係にある場合は以下のようにします。

class User < ActiveRecord::Base

# これで :images として複数画像をアップロード、取得ができるようになる
has_many_attached :images

この場合、images と複数系になることに注意です。

モデルの変更は一旦これだけです。単にアップロード、取得するだけならこれでOKです。取得する場合は以下のようにできます。

# userに紐づいたavatarをとってくる
user.avatar

# userに紐づいたavatarに画像があるか?を確認する
user.avatar.attached?

画像をアップロードする

今回はslimでアップロードするためのインターフェースとそれに対応するコントローラーを作成します。

class Administration::UsersController < Administration::ApplicationController
  def new
    @user = User.new
  end
end
.field
    = f.label :avatar
    = f.file_field :avatar

細かいところは省略していますが、ここは普通にアップロード、保存ができればOKです。細かい記述は不要です。

他のパターンで添付する

インターフェースを用意せずに画像を添付することもできます。その場合は以下の通りです。

user.avatar.attach(params[:avatar])

attach() でモデルに画像を添付することができます。そして、添付できたかどうか?は先ほど記載したuser.avatar.attached? で確認することができます。rails consoleで調べるときも重宝します。

ここで注意すべきは、複数画像が紐づくモデルの場合( @message.images.attached? )は、「いずれかの画像が添付されているか」という判断になるので、どれかが足りない、ということはattach?ではチェックできません。

また、別パターンとして、HTTP経由で配信されない画像を添付したい場合は以下のようにします。一応Railsガイドではcontent_typeを指定することも推奨されています。

@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')

staging, productionにアップロードする

local, testではアップロードができた(はず)ので、続いてstagingからやっていきます。基本的には storage.yml と環境ごとに用意されているであろう environment/staging.rb をいじるだけです。

今回僕の環境ではstagingが3つ存在していて、仮にここではstaging_a, staging_b, staging_cとします。それぞれにアップロードできるようにしてみます。

ここで必要なもの

staging、productionのGCP(GCS)に入るためのアカウントで環境に入り、Service Accountを作っていきます。これはGCSに対するIAMで、いまなければ新規作成する必要があります。作り方は以下の記事が詳しいです。

Active StorageでGoogle Cloud Storageに画像を保存する - ツナワタリマイライフ

ここで用意すべき情報はこちらです。

hoge:
  service: GCS
  credentials:
    type: "service_account"
    project_id: "コレ"
    private_key_id: "コレ"
    private_key: "コレ"
    client_email: "コレ"
    client_id: "コレ"
    auth_uri: "https://accounts.google.com/o/oauth2/auth"
    token_uri: "https://accounts.google.com/o/oauth2/token"
    auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
    client_x509_cert_url: "コレ"
  project: "コレ"
  bucket: "コレ"

それ以外の項目(service, type, auth_url...etc)については固定でそのままで大丈夫です。

上記の項目をそれぞれ入力したら、storage.ymlは完成です。

credentialを使う場合

GCSのprivate_key_idなど秘匿性の高い情報はRails.credentialsに格納するのもアリだと思います。使い方は以下の記事がわかりやすかったです。

qiita.com

その場合、このようになります。(一部抜粋)

private_key_id: <%= Rails.application.credentials.dig(:production, :private_key_id) %>,
    private_key: <%= Rails.application.credentials.dig(:production, :private_key)&.dump %>,
    client_email: <%= Rails.application.credentials.dig(:production, :client_email) %>,

注意したい点

2019年12月時点ではGCSのprivate_keyRails.credentialsに入れている場合、取り出すときに一旦dumpしないといけませんでした。なので、以下のようにしています。

private_key: <%= Rails.application.credentials.dig(:production, :private_key)&.dump %>,

不要でしたらそのままcredentialsの中身を参照させちゃっていいと思います。

環境変数周りをしあげる

最後に、staging-a, staging-b, staging-c, productionそれぞれでアップロードができるようにします。今回はstaging-a,b,cはそれぞれ同じGCSのインスタンスを向いている前提です。

config.active_storage.service = :staging
Rails.application.routes.default_url_options[:protocol] = 'https'
Rails.application.routes.default_url_options[:host] = ENV['ADMIN_HOST']

運良くADMIN_HOST にa,b,cをいれているので、それをみるようにしました。これでそれぞれ正しくアップロードされるはず...。

一方productionはもっとシンプルで、

config.active_storage.service = :production
Rails.application.routes.default_url_options[:protocol] = 'https'
Rails.application.routes.default_url_options[:host] = 'hoge.net'

だけです。

まとめ

ご指摘があればいつでもくださいmm

twitter.com