20200720のRailsに関する記事は12件です。

railsで簡単に複数画像をアップロードする rails+carrierwave+cloudinary

1.概要

railsで画像投稿を利用したアプリを作りたいという時に、どうせなら複数枚を一度に投稿したいという人は多いのではないでしょうか。
今回は簡単なrails投稿アプリを開発しながら、複数画像投稿ができるように実装して行きます。
※今回の画像投稿は、cloudinaryを利用して行います。

2.はじめに

開発環境

環境
ruby 2.6.3
Rails 6.0.3

完成予定

ezgif.com-video-to-gif (2).gif

はじめる前に:Cloudinaryに登録

このリンクから登録をはじめましょう。
https://cloudinary.com/

右上のsign up for freeを押すと、登録画面に飛びます。
諸々の情報を登録し仮登録を終えてから、最後にメールアドレス宛に送信されている本登録用のメッセージを開いて、本登録の完了までお願いします。

3.複数画像投稿機能の実装

3.1.事前準備

まず複数画像投稿機能を実装するために、投稿するだけのサンプルアプリを作ります。

ターミナル
$ cd
$ cd desktop
$ rails new ImageSample

3.2.投稿周りの機能

投稿周りの機能はscaffoldを利用し作成します。その際、imageカラムを複数作成します。
今回は同時にimage1,image2の2枚投稿できるようにします。3枚以上同時に投稿したい場合は、image3,image4とカラムを増やしてみてください。

ターミナル
$ cd ImageSample
$ rails g scaffold post body:text image1:string image2:string
$ rails db:migrate

3.3.viewの変更(新規投稿ページ)

新規投稿ページで画像を選択できるように、_form.html.erbを編集します。

posts/_form.html.erb
  # 変更前
<%= form.text_field :image1 %>

  # 変更後 
<%= form.file_field :image1 %>

image2についても同様の変更を行います。

3.4.viewの変更(投稿一覧ページ)

posts/index.html.erb
  # 変更前
<td><%= post.image1 %></td>

  # 変更後
<td><%= image_tag post.image1_url ,size: '200x150' %></td>

image2についても同様の変更を行います。
,size: '200x150'に関しては、記述をしなくても問題ありませんが、画面いっぱいに画像が広がるのを防ぐために記述しました。

3.5.viewの変更(投稿詳細ページ)

こちらも投稿一覧ページと基本的には同じです。

posts/show.html.erb
  # 変更前
<%= @post.image1 %>

  # 変更後
<%= image_tag @post.image1_url ,size: '200x150' %>

image2についても同様の変更を行います。

3.6.gemの追加

Gemfile
gem 'carrierwave'
gem 'cloudinary'
gem 'dotenv-rails'

追加したら、

ターミナル
$ bundle install

をして反映させます。

3.7.アップローダー

アップローダーの作成

CarrierWaveのジェネレーターでアップローダーを作成します。

ターミナル
$ rails g uploader Image

モデルの修正

app/models/post.rbを以下のように修正します。

app/models/post.rb
class Post < ApplicationRecord
    mount_uploader :image1, ImageUploader
    mount_uploader :image2, ImageUploader
end

アップローダの設定

app/uploaders/image_uploader.rbの6~8行目を変更します。

app/uploaders/image_uploader.rb
  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

上記を下図のように変えます。

app/uploader/image_uploader.rb
  # Choose what kind of storage to use for this uploader:
  if Rails.env.production?
    include Cloudinary::CarrierWave
    CarrierWave.configure do |config|
      config.cache_storage = :file
    end
  else
    storage :file
  end
  # storage :fog

3.8.cloudinaryのAPIキー

.envファイル

.envというファイルをアプリケーションディレクトリ(appやdbやGemfileがあるディレクトリ)に自分で作成します。
次に作成した.envファイルに以下を入力します。

.env
CLOUD_NAME=q0w9e8r7t6yu5  #←この値は人によって違います!!
CLOUDINARY_API_KEY=123456789012345 #←この値は人によって違います!!
CLOUDINARY_API_SECRET=1a2s3d4f5g6h7j8k9l0a1s2d4f5g6h1q #←この値は人によって違います!!

取得したCloudinaryのアカウントの、「Cloud name」、「API Key」、「API Secret」を利用します。
ここで「=」の後のそれぞれの値は先ほどのCloudinaryのマイページで取得したキーに書き換えてください(数字は個々人によって変わります)

.gitignore

アプリケーションディレクトリにある.gitignoreに下記を追加します。

.gitignore
/.env

cloudinary.yml

configフォルダにcloudinary.ymlファイルを作成してください。
config/cloudinary.ymlに以下のようにそのままコピペしてください。

config/cloudinary.yml
development:
  cloud_name: <%= ENV['CLOUD_NAME'] %>
  api_key: <%= ENV['CLOUDINARY_API_KEY'] %>
  api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %>
  enhance_image_tag: true
  static_file_support: false
production:
  cloud_name: <%= ENV['CLOUD_NAME'] %>
  api_key: <%= ENV['CLOUDINARY_API_KEY'] %>
  api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %>
  enhance_image_tag: true
  static_file_support: false
test:
  cloud_name: <%= ENV['CLOUD_NAME'] %>
  api_key: <%= ENV['CLOUDINARY_API_KEY'] %>
  api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %>
  enhance_image_tag: true
  static_file_support: false

4.終わりに

以上で複数画像投稿ができるようになったと思います。
説明が最小限で申し訳ないですが、もし良かったら参考にしてみてください!
最後まで読んでくださってありがとうございました!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsで簡単に複数画像投稿を実装する rails+carrierwave+cloudinary

1.概要

railsで画像投稿を利用したアプリを作りたいという時に、どうせなら複数枚を一度に投稿したいという人は多いのではないでしょうか。
今回は簡単なrails投稿アプリを開発しながら、複数画像投稿ができるように実装して行きます。
※今回の画像投稿は、cloudinaryを利用して行います。

2.はじめに

開発環境

環境
ruby 2.6.3
Rails 6.0.3

完成予定

ezgif.com-video-to-gif (2).gif

はじめる前に:Cloudinaryに登録

このリンクから登録をはじめましょう。
https://cloudinary.com/

右上のsign up for freeを押すと、登録画面に飛びます。
諸々の情報を登録し仮登録を終えてから、最後にメールアドレス宛に送信されている本登録用のメッセージを開いて、本登録の完了までお願いします。

3.複数画像投稿機能の実装

3.1.事前準備

まず複数画像投稿機能を実装するために、投稿するだけのサンプルアプリを作ります。

ターミナル
$ cd
$ cd desktop
$ rails new ImageSample

3.2.投稿周りの機能

投稿周りの機能はscaffoldを利用し作成します。その際、imageカラムを複数作成します。
今回は同時にimage1,image2の2枚投稿できるようにします。3枚以上同時に投稿したい場合は、image3,image4とカラムを増やしてみてください。

ターミナル
$ cd ImageSample
$ rails g scaffold post body:text image1:string image2:string
$ rails db:migrate

3.3.viewの変更(新規投稿ページ)

新規投稿ページで画像を選択できるように、_form.html.erbを編集します。

posts/_form.html.erb
  # 変更前
<%= form.text_field :image1 %>

  # 変更後 
<%= form.file_field :image1 %>

image2についても同様の変更を行います。

3.4.viewの変更(投稿一覧ページ)

posts/index.html.erb
  # 変更前
<td><%= post.image1 %></td>

  # 変更後
<td><%= image_tag post.image1_url ,size: '200x150' %></td>

image2についても同様の変更を行います。
,size: '200x150'に関しては、記述をしなくても問題ありませんが、画面いっぱいに画像が広がるのを防ぐために記述しました。

3.5.viewの変更(投稿詳細ページ)

こちらも投稿一覧ページと基本的には同じです。

posts/show.html.erb
  # 変更前
<%= @post.image1 %>

  # 変更後
<%= image_tag @post.image1_url ,size: '200x150' %>

image2についても同様の変更を行います。

3.6.gemの追加

Gemfile
gem 'carrierwave'
gem 'cloudinary'
gem 'dotenv-rails'

追加したら、

ターミナル
$ bundle install

をして反映させます。

3.7.アップローダー

アップローダーの作成

CarrierWaveのジェネレーターでアップローダーを作成します。

ターミナル
$ rails g uploader Image

モデルの修正

app/models/post.rbを以下のように修正します。

app/models/post.rb
class Post < ApplicationRecord
    mount_uploader :image1, ImageUploader
    mount_uploader :image2, ImageUploader
end

アップローダの設定

app/uploaders/image_uploader.rbの6~8行目を変更します。

app/uploaders/image_uploader.rb
  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

上記を下図のように変えます。

app/uploader/image_uploader.rb
  # Choose what kind of storage to use for this uploader:
  if Rails.env.production?
    include Cloudinary::CarrierWave
    CarrierWave.configure do |config|
      config.cache_storage = :file
    end
  else
    storage :file
  end
  # storage :fog

3.8.cloudinaryのAPIキー

.envファイル

.envというファイルをアプリケーションディレクトリ(appやdbやGemfileがあるディレクトリ)に自分で作成します。
次に作成した.envファイルに以下を入力します。

.env
CLOUD_NAME=q0w9e8r7t6yu5  #←この値は人によって違います!!
CLOUDINARY_API_KEY=123456789012345 #←この値は人によって違います!!
CLOUDINARY_API_SECRET=1a2s3d4f5g6h7j8k9l0a1s2d4f5g6h1q #←この値は人によって違います!!

取得したCloudinaryのアカウントの、「Cloud name」、「API Key」、「API Secret」を利用します。
ここで「=」の後のそれぞれの値は先ほどのCloudinaryのマイページで取得したキーに書き換えてください(数字は個々人によって変わります)

.gitignore

アプリケーションディレクトリにある.gitignoreに下記を追加します。

.gitignore
/.env

cloudinary.yml

configフォルダにcloudinary.ymlファイルを作成してください。
config/cloudinary.ymlに以下のようにそのままコピペしてください。

config/cloudinary.yml
development:
  cloud_name: <%= ENV['CLOUD_NAME'] %>
  api_key: <%= ENV['CLOUDINARY_API_KEY'] %>
  api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %>
  enhance_image_tag: true
  static_file_support: false
production:
  cloud_name: <%= ENV['CLOUD_NAME'] %>
  api_key: <%= ENV['CLOUDINARY_API_KEY'] %>
  api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %>
  enhance_image_tag: true
  static_file_support: false
test:
  cloud_name: <%= ENV['CLOUD_NAME'] %>
  api_key: <%= ENV['CLOUDINARY_API_KEY'] %>
  api_secret: <%= ENV['CLOUDINARY_API_SECRET'] %>
  enhance_image_tag: true
  static_file_support: false

4.終わりに

以上で複数画像投稿ができるようになったと思います。
説明が最小限で申し訳ないですが、もし良かったら参考にしてみてください!
最後まで読んでくださってありがとうございました!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Digdagを用いてRubyOnRails環境でバッチ実装

Digdagについて

Getting started
Architecture
Concepts
Workflow definition
Scheduling workflow
Operators
Command reference
Language API -Ruby
Digdagで環境毎に設定値を変える(RubyOnRails)
Digdagを用いてRubyOnRails環境でバッチ実装

準備

Ruby on Rails チュートリアルのサンプルアプリケーションダウンロード
https://github.com/yasslab/sample_app

Ruby on Rails チュートリアルのサンプルアプリケーションを実行して、ユーザーとPostを登録。
ぼくはtestユーザーを作って4つの投稿を登録しました。
スクリーンショット 2020-07-20 19.01.38.png

今回は、ユーザー名をパラメーターにして該当するユーザーの投稿数を出力する簡単なバッチを作ります。
バッチの実装・起動は二つの方法でやってみます。

Railsでバッチ処理を書く際によく使われている以下の二つの方法をやってみます。
①rails runner:スクリプトとしてバッチを書く
②rake task:ビルドタスクとしてバッチを書く

rails runnerで実行

パラメーター取得はOptionParserを使いました。

バッチ処理スクリプトを追加

/lib/scripts/ の配下にバッチ処理スクリプトを追加

lib/scripts/post_batch.rb
require 'optparse'

module Scripts
  class PostBatch
    def initialize
      @option = {}
      OptionParser.new do |opt|
        opt.on('-n VALUE', 'user name') { |v| @option[:name] = v}
        opt.parse!(ARGV)
      end
    end

    def count
      user = User.find_by(name: @option[:name])
      puts "ID: #{user.id} 名前: #{user.name}"
      puts "投稿数: #{Micropost.where(user_id: user.id).count}"
    end
  end
end

lib配下のrubyファイルを自動ロードする

config/application.rb
config.autoload_paths += %W(#{config.root}/lib)

workflowsの配下にrails_runner.digを追加して以下の内容を追加

rails_runner.dig
+task:
  sh>: bundle exe rails runner Scripts::PostBatch.new.count -n 'test'

実行

testユーザーのID,名前、投稿数が出力されています。

実行結果
$ digdag run rails_runner.dig --rerun
2020-07-20 19:38:39 +0900 [INFO] (0017@[0:default]+rails_runner+task): sh>: bundle exec rails runner Scripts::PostBatch.count 'test'
ID: 5 名前: test
投稿数: 4

rakeで実行

rakeタスク生成

$ rails g task task_post
Running via Spring preloader in process 4255
      create  lib/tasks/tast_post.rake

/lib/tasks/task_post.rake ファイルが生成されるので開いて以下のソースを追加

lib/tasks/task_post.rake
namespace :task_post do
  desc "ユーザー投稿数を取得"
  task :count, ['name'] => :environment do |task, args|
    user = User.find_by(name: args.name)
    puts "ID: #{user.id} 名前: #{args.name}"
    puts "投稿数: #{Micropost.where(user_id: user.id).count}"
  end
end

Workflow追加

rake.dig
+task:
  sh>: bundle exec rake task_post:count[test4]

実行

testユーザーのID,名前、投稿数が出力されています。

実行結果
$ digdag run rake.dig --rerun
2020-07-20 20:04:16 +0900 [INFO] (0017@[0:default]+rake+task): sh>: bundle exec rake task_post:count[test]
ID: 5 名前: test
投稿数: 4


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Windows10 Home にDocker をインストールする方法

Windows10 HomeにDockerを使用

Windows10 Proを買えば、Docker Desktop for Windowsをインストールするだけで済むことを。。。
- PC購入時に予算節約するため、
- Windows10 Home と Proの違いが分からないので、"ま、Homeで良いよ"
と思って、
Windows10 Homeを使用してDocker Desktop for Windowsインストールできず、困っている方、
Windows10 HomeにDockerインストールで無駄な時間を費やしている方
のために、身をもって経験したWindows10 HomeにDockerをインストールする方法をまとめましたので、共有します。

環境の整理

PCスペック:

  • Windowsのエディション:Windows10 Home
  • バージョン:2004  (初期状態1909でしたが、Windowsアップデートしたことで、2004となりました。)
  • システムの種類:64ビットオペレーティングシステム、x64ベースプロセッサ

手順

  1. VirtualBox をダウンロードする。
    VirtualBoxインストール
    「Windows hosts」をクリック、VirtualBox-6.1.12-139181-Win.exeをダウンロード

  2. DockerToolbox をダウンロードする。
    DockerToolboxインストール
    「DockerToolbox-19.03.1.exe」をクリック、DockerToolbox-19.03.1.exeをダウンロード

    *1.2.で以下の状況
    1_1.png

  3. VirtualBoxからインストールする。

    *DockerToolboxからインストールすると、DockerToolboxインストールファイルがPCにVirtualBoxがないことを自動検出し、強制チェック入った状態(=外すことができない状態)でDockerToolboxインストールファイルからVirtualBoxをインストールさせてしまう様となる。これじゃ、うまく動作しなかった。VirtualBoxから先にインストールする手順を踏もう。)

    *何もカスタマイズすることなく、「Yes」, 「Next」, 「Finish」クリックし進める。
    2.png
    3.png
    4.png
    5.png
    6.png
    7.png

  4. DockerToolbox をインストールする。

    8.png
    9.png
    10.png
    *ここで「VirtualBox」のチェックを外す

    11.png
    12.png
    13.png

    *3.4.で以下の状況
    14.png

  5. Docker Quickstart Terminalをダブルクリックする。
    15.png
    *ここでError creating machine...E_FAIL(0x80004005)エラーとなる。落着きPCを再起動する。

  6. Docker Quickstart Terminalを再度ダブルクリックする。
    16.png
    *ここでWaiting for an IP...で起動できそうだが、30分、1時間、半日過ぎても進展なかった。

    19.png
    *解決方法はPCの「Windowsの機能」を起動し「仮想マシンプラットフォーム」にチェックが入っていた。これを外す。PCを再起動する。

  7. Docker Quickstart Terminalを再度ダブルクリックする。
    17.png
    18_1.png

    クジラが現れた!!!成功。

念のため、docker versionコマンドを打ち込んでdockerコマンドが効くか確認しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tailwind on Rails

なぜTailwind on Rails?

  • クラス名を決める必要がなくなる
  • クラス名の衝突がなくなり、BEMやCSS設計から開放される
  • デザインの修正により不要になったCSSが残ってしまうことがなくなる
  • どの要素にどんなスタイルが当たっているかがすぐにわかる
  • カラーコードやフォントサイズ、ブレイクポイント等の統一性を保ちやすい
  • ネット上に転がっているサンプルコードを気軽に取り入れやすい(他の人が書いたコードでもカスタマイズが楽)
  • スタイルの修正のたびにapp/assets/stylesheets/任意のフォルダ/任意のファイルを開く必要がなくなる

環境

Rails 6.0.3

導入

$ yarn add tailwindcss
$ yarn tailwindcss init
$ mkdir app/javascript/css
$ touch app/javascript/css/tailwind.css
app/javascript/css/tailwind.css
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
app/javascript/packs/application.js
import '../css/tailwind.css';
postcss.config.js
module.exports = {
  plugins: [
    //...
    require("tailwindcss"), //追加
    require("autoprefixer"), //追加
    require("postcss-preset-env")({
      autoprefixer: {
        flexbox: "no-2009",
      },
      stage: 3,
    }),
  ],
};

動作確認

$ rails g controller test index
config/routes.rb
root to: 'tests#index'
app/views/tests/index.html.erb
<div class="max-w-sm mx-auto bg-white shadow-lg rounded-lg overflow-hidden">
  <div class="sm:flex sm:items-center px-6 py-4">
    <img class="block mx-auto sm:mx-0 sm:flex-shrink-0 h-16 sm:h-24 rounded-full" src="https://randomuser.me/api/portraits/women/17.jpg" alt="Woman's Face">
    <div class="mt-4 sm:mt-0 sm:ml-4 text-center sm:text-left">
      <p class="text-xl leading-tight">Erin Lindford</p>
      <p class="text-sm leading-tight text-gray-600">Customer Support Specialist</p>
      <div class="mt-4">
        <button class="text-purple-500 hover:text-white hover:bg-purple-500 border border-purple-500 text-xs font-semibold rounded-full px-4 py-1 leading-normal">Message</button>
      </div>
    </div>
  </div>
</div>

applyを使う

Tailwind CSSにはapplyという機能があり、複数のクラスをまとめて適用することができます。例えば同じボタンがあらゆる箇所に出現する場合、毎回font-bold py-2 px-4 rounded bg-red-500 text-white hover:bg-red-700などと書くのは大変なので、btnというクラスを指定するだけで上記のクラスを適用するためにapplyを使います。

TailwindをRailsで利用する場合applyを使用するのが難しいので、helper関数で対応することにします。

(こちらの記事ではRailsでapplyを使っているようですが、この通り設定するとapplyを適用した箇所以外のスタイルが効かなくなってしまいました)

$ rails g helper tailwind
app/helpers/tailwind_helper.rb
module TailwindHelper
  def btn
    'fosnt-bold py-2 px-4 rounded bg-red-500 text-white hover:bg-red-700'
  end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  helper TailwindHelper
end
app/views/tests/index.html.erb
<a href='#' class='<%= btn %>'>ボタン</btn>

懸念点

TailwindCSSでは、想定されうるあらゆるユーティリティークラスが用意されているので、他のCSSフレームワークよりファイルサイズが大きいです。
この問題を、PurgeCSSという機能を使いビルド時に実際に使われているクラスに関するスタイルだけを抽出する方法で解決しています。

しかしRails上でTailwindを使う場合、PurgeCSSが使えません。(正確には設定方法がわかりません。分かる方がいたら教えてください。)
そのため通常よりファイルサイズが大きくなってしまいます。

当初この点を懸念して、Tailwind on Railsは無理ではないかと考えていました。

Tailwindの公式サイトを確認したところ

Using the default configuration, the development build of Tailwind CSS is 1996kb uncompressed, 144.6kb minified and compressed with Gzip, and 37.kb when compressed with Brotli.

とあり、要はminify&gzip済で144.6kbとのこと。Bootstrapが22.1kbってことを考えるとまあ重いですが、許容範囲なんじゃないかと思っています。

gzipの設定、ブラウザにキャッシュさせる期間の設定、CDNの活用とかをちゃんとやっていればクリティカルではないでしょう。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ビューファイルにbinding.pryを使う方法

binding.pryをビューファイルで使用する方法

自分がbinding.pryをビューファイルで使う時に[- binding.pry]を動作を確認したい行の下に記述すれば良いだけと認識していたが、なぜかsyntaxerrorとで出てしまい、それだけではbinding.pryを行えなかったため。記載しました。

使い方

至って単純で、ビューファイルの下の行にインデントを合わせる事でbinding.pryを行う事ができました。

ビューファイル
            - binding.pry
            =@sending_destinaton.post_code

#下の行のインデントを合わせて記述する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSでデプロイして放置してたらアクセス拒否された件

初デプロイから2週間、スプリントレビューでメンターさんに動作確認してもらおうとしたら・・・

そんな不思議なトラブルのお話です。

TECH CAMP最終課題カリキュラム中、私はデプロイ担当をしております。

そして迎える初スプリントレビュー、本番環境見せようとしたら・・・

アクセスが拒否されました(確かこんな文面)
スクショ撮っておけばよかった。

???

おそらく私含め、チームメンバーもそんな顔してたような気がします。
ハナからできてなかったんならまだしも、一度は成功してたんですから。

よくわからないのでインスタンスの再起動やら一度停止、起動を経てもういいやと再デプロイ、すると・・・

1eeca22305ed9d2611dc51d814045929.png

ググったところ、NGINXのホーム画面のようで、これが出るということは、
Elastic IP設定するファイルにElastic IPがちゃんと書けてない(誤字ってる)
という記述を発見、ならイージーモード。秒で直せばいい。

いやElastic IP合ってるんですけど?

デプロイの勉強会用のQiitaをいくら見ても何が間違ってるのかわからない。
ググってもわからん。

チームメンバーもあれこれ助言してくれますが、解決に至らず...
メンターさんへの相談も視野に入れつつ、3度目の正直の再デプロイ。

作りかけのほぼ白紙のwebページが見事表示できましたとさ。

ここで仮説。
デプロイ後、一定時間放置してたら、AWS側で「これいらないのかな?」と判断してアクセス弾く説。
大穴の考えとしてはアクセス集中で弾かれるということもありますが、Elastic IPなんざそんな誰も彼も知ってるわけじゃないんだから。この線は薄いと思います。
仮に総当たり攻撃的な感じで打って入ったんだとしても、Basic認証もあるので、一定の硬さはあるはず。

体感で思っただけなので、むしろその辺り知ってる人いたら教えてください。
お願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでPostgreSQLのRow Level Security Policyを使ったマルチテナント

Clipkit(クリップキット) というSaaSを作っています。マルチスキーマ方式のマルチテナントシステムなのですが、テナント数が1,000近くなってきて辛さも出てきているので他の方式を検証中です。

LD;TR

PostgreSQLのRow Level Security Policyを利用したマルチテナントの実装を試してみました。

問題なく実装できてうまく動きそうでした。が、結局今回は採用を見送りました。RDBのマルチテナントの手法は一長一短で難しい。

個人的には最初はRLSではなくマルチスキーマ方式で始めれば良いのではないかと思いました。

はじめに

SaaS型のWebサービスでは、顧客ごとの独立したアプリケーションを、1つのシステムに同居させる方式があります。これをマルチテナントといいます。

RDBのマルチテナント方式

まず普通に考えると、テーブルに複数のテナントのデータを混在させる設計を思いつきます。しかしそれだとプログラムにバグがあった場合、他のテナントのデータが見えてしまうなど非常に大きなセキュリティ上の問題を起こしてしまう可能性があります。

なので、絶対に混線が起こらないようにテナントごとにデータをしっかり分離させる方法を考える必要があります。

RDBでマルチテナントを実現するには、ざっくり以下の3つの方法があります。

マルチインスタンス(サイロ)

テナントごとに独立したDBインスタンス(仮想マシンなど)を使用する。独立性が高いがコストや保守性のメリットが小さい。

シングルインスタンス・マルチスキーマ(ブリッジ)

単一のDBインスタンス内にテナントごとのスキーマを用意する。テナントごとに独立したテーブルを持つのでテーブル定義の管理が煩雑。

シングルスキーマ(プール)

単一のスキーマ内のテーブルにすべてのテナントのデータを混在させる。最もリソースの効率が良いがプログラムにバグが入ると他のテナントのデータが混線するなどの大きなリスクがある。

簡単に実現できるのはマルチスキーマ方式

RailsだとApartmentというgemがある。これで全テナントへの一斉マイグレーションなども勝手にやってくれます。

マルチスキーマ方式の欠点

テナントごとにスキーマを分けるということで、テーブルの構造を変更する際には、すべてのスキーマに対して同じようにマイグレーションを実行する必要があります。2〜3秒のマイグレーション処理だったとしても数千以上のテナント数になるとそれなりに厳しくなってくるでしょう。すべてのテナントでマイグレーションが確実に完了できるようにする管理コストも大きくなります。

なのでアクセス制御さえ確実にできれば、シングルスキーマ方式が理想のような気がしてきます。

Row Level Security Policyを利用したシングルスキーマ方式

概要

PostgeeSQL 9.5以降には「行セキュリティポリシー」(Row Level Security Policy :RLS)という機能があります。これはユーザーのロールや実行時パラメータに応じてあらかじめ指定された条件の行以外にはアクセスできないようにする機能です。

設定方法

具体的には次のように設定します。

例)usersテーブルのtenant_idカラムが特定の値のレコード以外は見えないようにしたい。

RLSを設定。(これは実行時パラメータに応じて制御する設定)

ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_isolation_policy ON users FOR ALL USING (tenant_id = current_setting('tenant.id')::BIGINT);

あとは実行時パラメータを次のように設定すると、以降、tenant_id=999のレコード以外にはアクセスできなくなります。

SET tenant.id = 999;

Railsでの実装(案)

テナントを管理するtenantsテーブル(Tenantモデル)を作っておきます。(※ 説明用の例なのでテーブルの定義とかは省略します)

Tenant#switchメソッドでテナントを切り替えられるように実装します。さらにTenant.currentで現在のテナントを取得できるようにしておくと便利です。

class Tenant < ApplicationRecord
  def switch
    ActiveRecord::Base.connection.execute("SET tenant.id = #{id}")
  end
  def self.current
    find(ActiveRecord::Base.connection.execute('SHOW tenant.id').getvalue(0, 0))
  end
end

ApplicationControllerbefore_actionで、リクエストのドメインに応じてテナントが切り替わるようにします。

class ApplicationController < ActionController::API
  before_action :switch_tenant

  def switch_tenant
    Tenant.find_by(domain: request.host).switch
  end
end

以上で自分のテナントのデータだけに触れるようになります。

ただしデータを追加するときはtenant_idを自分で入れなくてはいけません。これが面倒なので自動的に入るようにModelの基底クラス(ApplicationRecord)に実装します。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  after_initialize :set_tenant_id

  def set_tenant_id
    if new_record?
      if has_attribute?(:tenant_id)
        self.tenant_id = Tenant.current.id
      end
    end
  end
end

これでほぼテナントを意識せず透過的にデータアクセスできるようになりました。

注意点

RLSは一般ユーザーにしか効かない

CREATE TABLEしたユーザーやSUPERUSERに対してはRLSの制限は無効となります。なので、migrationはSUPERUSERで実行、アプリは一般ユーザーで起動。などとする必要があります。

一般ユーザーには次のように必要な権限を与えておきましょう。

GRANT SELECT, UPDATE, INSERT, DELETE ON ALL TABLES IN SCHEMA public TO PUBLIC;
GRANT SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO PUBLIC;

UNIQUE制約に注意

UNIQUE制約はtenant_idとの複合インデックスにする必要があります。(アプリケーションからは見えないのでバリデーションについては複合条件にする必要はない)

INSERT時の注意

SELECTは透過的に制約されたレコードしか見えませんが、INSERTするときはtenant_idを自分でセットする必要があります。(前述の実装案ではModelの基底クラスApplicationRecordを使って自動的に入るようにした)

マイグレーションの注意

テーブルを追加するときに必要になるCREATE POLICYは、マイグレーションでやりたくなりますが、その場合schema.rbに反映されないので、db:reset/db:setupは使えません。(db:migrate:resetはok)

デメリット

今回は実際にこの実装で運用したわけではないのですが、考えられるデメリットを上げてみます。

テーブルが肥大化する

RDBはレコード数が膨大になると取り扱いが大変になります。インデックスを設定していてもメモリに乗らず急激に重くなったり。

そこでパーティショニング(テーブル分割)機能の利用を検討することになります。カラムの値に応じて分割するリストパーティションという方法があるので、それを使うことになるでしょう。

tenant_idごとにテーブル分割する。という戦略を最初に思いつきますが、一般的にパーティショニングで100を超えるような子テーブルを作るのは想定されていないようで、パフォーマンスに問題がでるという報告も見られます(実際に試してはいませんが)。このアプローチはあまり現実的ではなさそうです。

データが増えたら臨機応変に手動で分割していく、といった戦略になりそう。めんどくさいですね。

テナントの削除が面倒

すべてのテーブルのレコードを消して回らないといけないので大変そう。マルチスキーマ方式の場合はスキーマを削除するだけなので簡単でした。

他の環境からのデータ移行が難しい

SaaS型のサービスだけどオンプレミスでも提供する。といった場合、オンプレミスからSaaSへのデータ移行が必要になったときに大変そう。各テーブルのidが変わってしまうためです。マルチスキーマ方式の場合はダンプ&リストアするだけで済みました。

RLSは見送ることにした

やはりテーブルの肥大化がつらそう。という懸念が払拭できませんでした。

マルチスキーマ方式でマイグレーションがつらい、というのはデプロイのときだけの問題であり、日常的にパフォーマンスを気にするよりずっとマシな気がします。

そのマイグレーションも数百程度のテナント数ならほとんど問題はないので、テナント数の想定にもよりますがスタートアップ段階ではマルチスキーマ方式でも良いのではないか? と思いました。

Apartmentはテナントに応じてDBサーバを変えられる機能などもあり、性能に関してはこちらのほうが安心感が大きいです。

他のソリューション

Citus
https://www.citusdata.com/

マルチテナントをいい感じに実現してくれるPostgreSQLの拡張機能。

OSSなのでEC2にはインストールできますが、RDSでは使えないですね……

2016年〜 にAWS上でマネージドサービスを提供するCitus Cloudというサービスがあったようです。

ところが、2019年にMicrosoftがCitusを買収。Citus Cloudは終了。代わりにAzureで利用できるようなったようです。あ"〜

AWSはマストなので厳しい……

Apartmentの開発が停滞してる

よし、やっぱりこれからもApartmentで行くぞ! と思って新しいプロジェクト(Rails 6)で使おうとしたら動かなくてあれれとなりました。

結構メジャーなGemだと思うのですが、なんと今時点(2020年7月)でまだRails 6に対応していないのでした。

活発にメンテされているFork版があったのでとりあえずこちらを使えば大丈夫そうですが。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Deviseでログイン前のルーティングを設定する

やりたいこと

Devise導入した状態で、
ログイン前後のルーティングをカスタマイズする。

前提

Deviseをインストールすると、デフォルトではルートパスが/users/sign_inとなっている。
そのため、ログインしていない状態でtopページへ行こうとするとdevise側で勝手にサインインページに飛んでしまう。

方法

ログインしていない状態で特定のページへ遷移させたい場合は
before_action :authenticate_user!

を該当コントローラーに記述する必要があります。
全てのアクションについてログイン認証を必要とする場合はapplication_controllerに記述することで
各コントローラーに記述する手間を省けます。

before_action :authenticate_user!とは

before_action :authenticate_user!はdeviseのヘルパーメソッドです。
これを記述することで認証ユーザーのみ各アクションを実行するようになります。

特定のアクションのみ、ログインしていない状態で実行したい場合

例えば「topページとaboutページだけはログインしていなくても表示させたい…」という時は
下記のように、該当アクションを除外する形で指定します。

before_action :authenticate_user!, except: [:top, about]

こうすることでtopアクションとaboutアクションのみログアウト状態でも表示することができます。
その他のアクションはログインしていないとURL直打ちしても表示されなくなります。

▼参考Devise authentication_user!
https://skillhub.jp/courses/137/lessons/978

又、今回私はapplication_controllerに記述すると記載しましたが、
userコントローラー以外の他のコントローラーにログイン認証設定を加えたくない場合は
ディレクトリに階層を作り、そのディレクトリのみ適応されるコントローラーを作成する方法もあります。

下記を参考にしてください。
▼参考
https://qiita.com/ryuuuuuuuuuu/items/bf7e2ea18ef29254b3dd

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubocop Airbnb

はじめに

作成したポートフォリオにRubocop Airbnbを導入したのでメモします。

Rubocopとは

Rubocopは書かれたコードがRubyのコーディング規約に沿った書かれ方をしているかを自動的に確認してくれるgemです。

Rubocopではなく、Rubocop Airbnbを導入する理由

Rubocopはデフォルト設定だと自分で設定を変更する必要があるとのことなので、airbnbの開発で使用されているrubocopの設定をインストールできるRubocop Airbnbを導入することにしました。

Rubocop Airbnbの導入手順

基本的にRubocop Airbnbの手順に従いました。

Gemfileにgemを追加

gemを追加したら、bundle installします。

Gemfile
group :development do
  gem 'rubocop-airbnb'
end

Rubocop-airbnbを適用させるアプリファイルに、rubocop.ymlとrubocop-airbnb.ymlを作成し、それぞれ内容を記載

rubocop.ymlの内容は必要に応じて適宜変更してください。

rubocop.yml
 inherit_from:
  - .rubocop_airbnb.yml

 # Rails用に最適化
 Rails:
   Enabled: true

# 文字数の上限を80文字から変更
LineLength:
  Max: 130

 #rubocopで検証したくないフォルダを指定
 AllCops:
   Exclude:
     - 以下省略
rubocop-airbnb.yml
 require:
   - rubocop-airbnb

実行

設定が完了したら、bundle exec rubocop --require rubocop-airbnb

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】docker-composeの速度を上げる方法

開発環境

・Docker: 19.03.8
・Docker-Compose: 1.25.5
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Dockerで環境構築する方法

原因

docker-compose実行時に、templogvendor.git等の余計なファイルをマウントしてしまっている。

解決方法

マウントする必要のないディレクトリのマウントを、別のvolumeで上書きします。

docker-compose.ymlを編集

docker-compose.yml
# 変更前
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

# 変更後
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app:cached
      - /app/vendor
      - /app/tmp
      - /app/log
      - /app/.git
    ports:
      - '3000:3000'
    depends_on:
      - db
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[herokuデプロイ手順②]新しいアプリの作成からindexアクション表示まで

はじめに

今回は前回の続きでアプリを作成し、indexアクションをローカルで表示するとこまでのアウトプットを行います。

前回の[herokuデプロイ手順①]heroku CLIのインストールからログインまでをまだ読まれていない方はそちらを先に確認して下さい。(下記URLから)
https://qiita.com/nkekisasa222/items/52c872957b30f4e8de1e

環境

Rails 5系
macOS catalina 10.15

Railsアプリの新規作成

今回はRails5系で新規作成を行って下さい。そちらの方がスムーズに実装できます。
バージョンの確認方法はターミナルにてrails -vを入力して下さい。

必要に応じて次のコマンドで新しいRailsのバージョンを入手して下さい。

ターミナル.
$ gem install rails --no-document
Successfully installed rails-5.2.2
1 gem installed

次に新しいアプリを作成し、作成したアプリのディレクトリに移動します。

ターミナル.
$ rails _バージョン指定_ new アプリ名 --database=postgresql
ターミナル.
$ cd アプリ名

続いてローカルにデータベースを作成します。

ターミナル.
$ rails db:create
Database 'アプリ名_development' already exists
Database 'アプリ名_test' already exists

gemを追加する

gemファイルを次のように変更して下さい。

ターミナル.
gem 'sqlite3' #このgemを
gem 'pg' #このgemに変更

gemを変更しないで開発を進めるとデプロイ実行の際にエラーが発生します。

開発時にはPostgreSQLを使用することを強くお勧めします。開発環境とデプロイメント環境の間の同等性を維持することで、環境の違いによる微妙なバグの発生を防ぐことができます。システムにPostgresがまだインストールされていない場合は、下記URLからローカルにインストールします。
https://devcenter.heroku.com/articles/heroku-postgresql#local-setup

gemを保存するので下記コマンドを入力して下さい。

ターミナル.
$ bundle install

PostgreSQLアダプターの確認

config/database.ymlファイルがpostgresqlアダプターを使用していることを確認してください。

ターミナル.
$ cat config/database.yml
# PostgreSQL. Versions 9.1 and up are supported.
#
# Install the pg driver:
#   gem install pg
# On OS X with Homebrew:
#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
# On OS X with MacPorts:
#   gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
# On Windows:
#   gem install pg
#       Choose the win32 build.
#       Install PostgreSQL and put its /bin directory on your path.
#
# Configure Using Gemfile
# gem 'pg'
#
default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # http://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: myapp_development

  # The specified database role being used to connect to postgres.
  # To create additional roles in postgres see `$ createuser --help`.
  # When left blank, postgres will use the default role. This is
  # the same name as the operating system user that initialized the database.
  #username: myapp

  # The password associated with the postgres role (username).
  #password:

  # Connect on a TCP socket. Omitted by default since the client uses a
  # domain socket that doesn't need configuration. Windows does not have
  # domain sockets, so uncomment these lines.
  #host: localhost

  # The TCP port the server listens on. Defaults to 5432.
  # If your server runs on a different port number, change accordingly.
  #port: 5432

  # Schema search path. The server defaults to $user,public
  #schema_search_path: myapp,sharedapp,public

  # Minimum log levels, in increasing order:
  #   debug5, debug4, debug3, debug2, debug1,
  #   log, notice, warning, error, fatal, and panic
  # Defaults to warning.
  #min_messages: notice

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: myapp_test

# As with config/secrets.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password as a unix environment variable when you boot
# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full rundown on how to provide these environment variables in a
# production deployment.
#
# On Heroku and other platform providers, you may have a full connection URL
# available as an environment variable. For example:
#
#   DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
#
# You can use this database configuration with:
#
#   production:
#     url: <%= ENV['DATABASE_URL'] %>
#
production:
  <<: *default
  database: myapp_production
  username: myapp
  password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>

indexアクションの作成

indexアクションを作成していない場合ローカル環境ででエラーが発生するので作成して下さい。

まずコントローラーを作成します。

ターミナル.
$ rails g controller コントローラー名

次にindexアクションを設定します。
app/views/コントローラー名/index.html.erbを編集。

index.html.erb
<h2>Hello World</h2>
<p>
  The time is now: <%= Time.now %>
</p>

続いてルーティングの設定でconfig/routes.rb2行目に、以下を追加します。

routes.rb
 root 'コントローラー名#index'

サーバーを立ち上げてページが存在するか確認します。

ターミナル.
$ rails s

app/views/コントローラー名/index.html.erbの内容が表示されれば成功です。

最後に

次回はアプリをgitに連携からherokuデプロイまでをアウトプットします。
参考にして下さい!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む