20200421のRailsに関する記事は20件です。

インクリメンタルサーチを噛み砕く

近況報告

やあ。テックキャンプのリモートワーク化がはじまって四週間目に突入しました。我がチームはついに最終課題をパスし間も無く就職活動に入ります。そこで大きな壁,履歴書の自己開示。わたしが一番苦手とする場所です。「わたしはプログラミングでご飯食べていくってきめたの!」って叫んで内定決まればいいのに笑

さて,今日の記事は個人アプリにインクリメンタルサーチを実装のついでの忘備録です。実況感覚で書いていきます。

インクリメンタルサーチ

なにそれ

 英語でincremental search,直訳は増加検索ですね。通常の検索は検索ボタンを押すとページが更新されて検索結果が出てきますが,この検索は一文字入れるごとに非同期通信で検索結果がぽんぽん出てきます。youtubeやGoogleとか検索の予測がでますね。あれです。文字を打つごとに予測される結果が減るからデクリメンタルサーチでもいいんじゃね。ちなみに,予測の変換に履歴が出てくるのはcookieがかかわるので少々異なります。

非同期通信・・・ユーザーの望むタイミングとは別で行われる通信です。通信自体は行われています 。railsなら,ユーザーのリクエストをMVCで逐一処理してそれに応じたビューをユーザーに届けています。ただ,例えばgoogle検索で「ツイッ」まで書いたところでリダイレクトして予測変換「ツイッター」が記載されたビューに更新されたとします。検索フォームの部分だけ変わればいいのに,変化のないGoogleのロゴとかも更新されるの無駄な気がしません?痒いところに届くのが非同期通信です。私のイメージは,必要な情報だけをビューに上書きする通信です。

なんで必要?

 いちいち検索ボタンを押して読み込むより早く検索結果をユーザーに届けることができます。個人アプリのような小さい規模ではそこまで破壊力があるわけではありません。ただ,ロゴが舌なめずりしている某サイトだと通信が0.1秒遅れると売り上げが1%落ちると聞きました。1兆円の売り上げなら100億円の損失です。まさに時は金なりですね!

環境(前提)

・rails 2.5.2
・ruby 5.2.3
gem 'jquery-rails'
DBに検索対象のレコードが入っている
検索機能の実装完了前提

今回はjqueryのgemを用いるから

gem 'jquery-rails'

からのbundle install。
application.jsでjqueryが使えるように,

//= require jquery

の記述の有無を確認。

これで準備おっけー

実装(実況感覚)

検索フォームに入力するとテーブルのnameカラムを検索する機能を実装していきます。

インデックスの実装

インデックスってなんだっけ?笑

カリキュラムを見直すと,検索の際にどれを検索すればいいのかをピックアップしてくれる機能なんだってさ。
UFOキャッチャーで例えると,UFOが検索するための端末としたらインデックスは商品(レコード)が引っかかりやすくなるために付いているリングみたいな役割なのかな。 ワカリニクイタトエデース

ただ,インデックスは検索に用いるカラムのみを複製して1つのテーブルみたいにしてるから,作りすぎるとデータベースの容量の圧迫になるみたい。

さて,インデックスを追加していきます

$ rails g migration AddIndexTo検索したいテーブルs

実行したら,マイグレーションファイルに飛んで

class AddIndexTo検索したいテーブル名 < ActiveRecord::Migration
  def change
    add_index :検索したいテーブルs, :検索したいカラム, length: 32(最大文字数)
  end
end

を記述したらマイグレート。ミスったらDB:rollbackなれdb:dropとなれ。

APIの準備

インクリメンタルサーチの実装の順番はrailsの実装とあんまり変わりません。
コントローラーに条件分岐の記入

reviews_controller.rb
  def search
    @reviews = Reviews.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end

formatごとのレスポンスで場合分けします。
format.htmlはhtml全部読み込みますよ,format.jsonはJS形式のみ読み込みますよって感じ。この分岐のおかげで非同期通信が可能になります。

コントローラーで条件分岐したら,DBから該当のでーたをビューまで運ぶ箱,jbuilder(パラムスに似てるね)を作成する。検索しているテーブルのビューファイルにフォルダsearch.json.jbuilderを作成。jbuilderを用いると,そのままJSでも用いることができる。

search.json.jbuilder
json.array! @reviews do |review| ←検索に引っかかった各インスタンスを配列でバラすぜ!中身は以下の通りだ!
  json.id review.id
  json.content review.content 
  json.image review.image 
  json.taste review.taste
  json.fragrance review.fragrance
  json.individuality review.individuality
  json.fruity review.fruity
  json.smorky review.smorky
  json.age review.age
  json.user_id review.user_id 
  json.user_sign_in current_user
end

テキストフィールドの実装

次はビューの作成をしていきます。検索フォームはこんな感じ↓

index.html.erb
<%= form_with(url: review_tweets_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>

ではJSの記述に入ります。記述量が多いので少しずつ刻んでいきます。

search.js
$(function() {
  $(".search-input").on("keyup", function() { ←フォームに何か打ち込まれたら作動するよ
    var input = $(".search-input").val(); ←inputを打ちこまれた文字と定義するよ
    console.log(input); ←JSが動いているか確認。うまく表示されていたら消す。
  });
});

フォームの情報をJSON形式でコントローラーに送る

今までやったことの確認をすると,
・respond_toで行き先決定
・jbuilderでデータベースの情報の引き出し準備
・ビューでフォームの情報をJSで抽出する
うん,MVCだね。railsは非同期通信でも大まかなやることは同じです。もしこれ行こうエラーが発生したらMVCに則ってエラーを解消していきます。

search.js
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
//以下を追加
    $.ajax({ ←ajaxは非同期通信するよ!っていう合図
      type: 'GET', ←HTTPメソッドはDBを得るからGET
      url: '/review/search', ←非同期通信が行われるurl
      data: { keyword: input }, ←さーばーに送信するキー。これを元にインデックスから引っ張ってくる。
      dataType: 'json'←データの形式
    })
  });
});

これでフォームの情報はJSON形式で送られ,コントローラーのrespond_toでJSONに振り分けられてモデルに進みます。多分。

非同期通信がビューまで届いているか確認します。メソッドdoneを用いて届いた情報をみてみる。

search.js
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/review/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(reviews) {
      console.log(reviews);
    })
  });
});

事故発生。読み込めてない。。。順番に確認していこう。
JSは読み込めていたから,クラスのミスはない。
ajaxの読み込みミスは、、、url間違えていました、、、
分岐は誤字なし,jbuilderは、、、nameを書き忘れる痛恨のミス!何調べようとしてたんだ

も一回検索フォームにキーワードを打ち込む、、、うん、うまくいった!!!

スクリーンショット 2020-04-21 19.21.46.png

さて,次に進みます。次はJSを利用してHTMLを上書きします。
まずdone以下を以下のように編集します。

search.js
~省略~
    .done(function(reviews) { ←reviewを引張ってきたから以下の実行するよ
      $(".showZone").empty(); ←該当クラスの中の子要素全て消すね
      if (reviews.length !== 0) { ←もし1でもあったら
        reviews.forEach(function(review){ ←1つずつ並べて
          appendReview(review); ←そのレコードを出現させるよ
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
~省略~

次はappendReview(review)に該当する定義を作成してクラスshowZoneに上書きするHTMLを作成します。

search.js

$(function() {

  var search_list = $(".showZone"); ←こういうやつは先に定義にしとく

  function appendReview(review) {
    var html = `<div class="reviewWhisky">
            <a href="/reviews/${review.id}">
            <div class="topZone">
              <div class="topZone__image">
                <img id="goShowPage" src="${review.image.url}" width="180" height="180">
              </div>
             <div class="bottomZone">
             <p>${review.name}</p>
            </div></div></a></div>`
   //htmlは検証からコピーが最速。データベースからの読み込み表示は上の書き方参考にしてね
    search_list.append(html);
  }
  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`
    search_list.append(html);
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/reviews/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(reviews) {
      search_list.empty();
      if (reviews.length !== 0) {
        reviews.forEach(function(review){
          appendReview(review); ←appendHTML召喚のメソッドと覚えておけばOK
        });
      }
      else {
        appendErrMsgToHTML("一致するのがありません");
      }
    })
  });
});

では,もし何も投稿が該当しなかった時用のを用意しましょ。いま一枚もない場合,何も写っていない状態だから,バグじゃないよーてわかるような記述を書きますappendErrMsgToHTMLに意味を持たせます)。

search.js
~省略~
    search_list.append(html);
  }
  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`←新しい要素なので必要ならCSS
    search_list.append(html);
~省略~
    .fail(function() {←うまく行かなかった時用にエラー表示を準備
      alert('error');
    });
  });
});

これで一通り完成かな。ちなみに中の人はキータ執筆と同時進行で行なっていたのですが,search.jsファイルが読み込まれなくなる不具合に遭遇し2時間詰みました笑

解決策としては別のファイルを作成して作り直したらうまくいきまっした、、よかった、、、ただ,何が原因のエラーだったんだろう。。。

 おわりに

 まあなんとか実装できました。非同期通信のjsはコード1つ1つがわかりやすいので比較的サクッといけますね。非同期通信で重要なのは,通信の根本はMVCであること!

余談

 非同期通信とturbolinks(以下TB)は仲が悪いみたいです。TBってなんやねんって話ですよね。カリキュラムでも無下に扱われていました。この子は名前の通り,リンク先に早くたどり着くため手助けをしてくれます。ただ,早くする方法というのは,余計な動きを削ること,ここではJSを起動しないことです。非同期通信はJSを用いた通信なので,turbolinksが邪魔をしてしまう可能性がある(実際邪魔する)訳ですねー。

ただ悪い子ではないので見つけ次第gemをコメントアウト!とかしないであげてください。
$(document).on('turbolinks:load', function(){
とJSの初めに打ってあげたり,link_toにつづけて
data:{"turbolinks": :false}
と打ってあげると,JSが動いてくれます

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

【Rails Docker Git】よく使うコマンド一覧

【Rails Docker Git】よく使うコマンド一覧

Dockerでサーバー立ち上げ

terminal
> docker-compose up

Dockerでサーバーダウン

terminal
> docker-compose down

DockerでDB確認

terminal
> docker-compose ps

rails db:create

terminal
> docker-compose exec イメージ名 ./bin/rails db:create

railsタスク一覧(コマンド一覧)

terminal
> docker-compose exec web ./bin/rails -T

gitのカレントブランチを確認

terminal
> git branch

gitのブランチを新規作成してテェックアウト

terminal
> git checkout -b ブランチ名

gitのステージングされテイルファイルのキャシュをみる

terminal
> git diff -cashed
terminal
> docker-compose exec イメージ名 ./bin/rails db:create
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails Tutorial12章】パスワード再設定機能の実装手順を整理してみました【後編】

はじめに

Railsチュートリアル12章の内容を、少しでも理解の助けとなればと思い、割としっかり目に整理しました!
備忘録です。

前提

Railsチュートリアル1〜11章までの内容が完了していること。
特に、アカウント有効化機能が実装されている事。

内容

Railsチュートリアル12章のパスワード再設定機能の実装手順を、前中後半の3回に分けて整理おります。
後編である今回は、パスワード再設定機能の本実装を行っていきます!

前編→パスワード再設定用のリソース作成
中編→パスワード再設定メール送信機能の実装

3.パスワードを再設定する。

  ●送信用メールの作成が完了したので、次はPasswordResetsコントローラのeditアクションの実装を行っていく。
  
  ●その次に統合テストを使って上手く動作しているかを確認するためのテストを書いていく。

3-1.パスワード再設定機能実装のための準備を行っていく。

  1.まずはパスワード再設定用メールに添付してあるリンクを動作させるために、パスワード再設定フォームのビューを作成する必要がある。
    ➡︎app/views/password_resets/edit.html.erbファイルで行う。

    ➡︎フォームに関してはパスワード入力フィールドと、パスワード確認入力フィールドを作成すれば十分。

  2.パスワードを再設定を行おうとしているユーザーを取得する必要がある。
    ➡︎ユーザーがメールに貼り付けられているリンクをクリックしてパスワード再設定ページへ移動してくる時、そしてパスワードを実際に再設定する時にユーザーを取得する必要がある。

    ➡︎メールアドレスをキーとして取得するのだが、そのためにはeditアクションとupdateアクションの両方で、メールアドレスが必要となる。
      →editアクションではリンクに含まれているメアドから検索してそのまま取得できる。
      →updateアクションでは、フォームを送信するとメールアドレスの情報は消えてしまうので、上手くユーザーを取得できるように準備をしなければならない。

    ➡︎updateアクションでメールアドレスからユーザーを取得するための準備。
      →どうすれば良いかというと、リンクに含まれているメアドを一時的に保持してあげればいい。そのために隠しフィールドを用意してページ内に一旦保持させる手法をとることにする。
      →隠しフィールドを用意することで、パスワード再設定の情報と一緒にメールアドレスも送信されるようになる。
      →パスワード再設定用のフォームにhidden_field_tagメソッドを使う。キーに:email属性、値に@user.emailでユーザーのアドレスがparams[:email]の値として送信されるようにする。
        →<%= hidden_field_tag :email, @user.email %>
        →注意。f.hidden_field_tagとしないこと。こうしてしまうと、form_for(@user)のブロック引数を受け取って、params[:user][:email]にメールアドレスが保存されてしまう。
      →しかし、これにてメールアドレスからユーザーを検索して取得する準備が整いました。

  3.次に上記のフォームを描画するための処理を、PasswordResetsコントローラのeditアクションへ記述してあげる。
    ➡︎app/controllers/password_resets_controller/rbファイルへ移動する。
      →まずprivateキーワード内に、メールアドレスと一致するユーザーの検索を行うメソッドを定義する。
        →メソッド名をget_userとする。
        →params[:email]のメールアドレスを持つユーザーを取得して、@userインスタンス変数へ代入する処理を記述する。
      →同じくprivateキーワード内で、取得したユーザーが有効なユーザーかどうかを確認するメソッドを定義する。
        →メソッド名をvalid_userとする。
        →そのユーザーが存在する事、有効化されている事、認証済みである事をunless文により判定する条件式を記述する。
          →@user(ユーザーの存在) && @user.activated?(有効化の確認) && @user.authenticated?(:reset, params[:id])(ダイジェストとトークンの一致を確認)とする。
        →正しいユーザーでなければ、ルートURLへダイレクトさせる処理を記述。
      →有効なユーザーかどうかの確認は、editアクションで必要なのはもちろんupdateアクションでも必要になるので、beforeフィルターを使ってバリデーションを行ってあげる。

    ➡︎以上でパスワード再設定用のフォームが描画されるようになったので、あとはupdateアクションにパスワードを実際に更新するための処理を記述していく。

app/views/password_resets/edit.html.erb
  #3-1.2
  <% provide(:title, 'パスワードの再設定') %>
 
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages' %>
 
      <%= hidden_field_tag :email, @user.email %>
 
      <%= f.label :password %>
      <%= f.password_field :password %>
 
      <%= f.label :password_confirmation %>
      <%= f.password_field :password_confirmation %>
 
      <%= f.submit "パスワードを更新する" %>
    <% end %>
app/controllers/password_resets_controller.rb
  #3-1.3
  class PasswordResetsController < ApplicationController
    before_action :get_user,   only: [:edit, :update]
    before_action :valid_user, only: [:edit, :update]
    .
    .
    .
    private
     #メールアドレスと一致するユーザーの検索を行う
      def get_user
        @user = User.find_by(email: params[:email])
      end
 
      #取得したユーザーが有効なユーザーかどうかを確認する
      def valid_user
        unless (@user && @user.activated? &&
          @user.authenticated?(:reset, params[:id]))
          redirect_to root_url
        end
      end
end

3-2.実際にパスワードを更新する機能を実装していく。

  ➡︎上記で作成したフォームからの送信に対応するupdateアクションを作成していく。

  ➡︎updateアクションの記述を行っていく上で、4つ考慮しなければならない事項がある。

    1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
      ➡︎パスワード再設定の期限を設定して、その期限の間に再設定されなかった場合は期限切れとする処理を行うメソッドを定義する。
        →app/models/user.rbファイルで記述する。
          →メソッド名をpassword_reset_expired?とすることにする。
          →期限は2時間とし、現在時刻から2時間以上経っている場合にはtrueを返す処理を記述する。
            →reset_sent_at < 2.hours.ago
            →注意。「2時間より少ない」という意味ではなく、「2時間以上前」と読むのが正しい。
            →つまり、「パスワード再設定メールの送信時刻が、現在時刻よりも2時間以上早い場合」と読むのが正しい。
          →準備が完了したので、パスワード再設定の有効期限が切れているかどうかをチェックするメソッドを定義していく。

      ➡︎パスワード再設定の有効期限が切れていた場合にユーザーを弾くメソッドを定義する。
        →app/controllers/password_reset_controller/rbファイルへ移動する。
          →メソッド名をcheck_expirationとする。
          →取得したユーザーのパスワード再設定有効期限が切れてるかどうかを判定する条件式を記述する。
            →上記で準備した「パスワード再設定のメール送信時刻が、現在時刻よりも2時間以上前ならtrueを返す」メソッドを利用する。
          →trueならフラッシュで警告メッセージを表示させ、パスワード再設定フォームへリダイレクトさせる。
          →このメソッドはprivateキーワード内で定義してあげる。

      ➡︎edit、updateアクションに対しbeforeフィルターで、check_expirationメソッドを適用してあげる。

    2つ目:無効なパスワードであれば失敗させる。そして、失敗した理由も表示させる。
      ➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
        →updateアクション内のどの条件式にも当てはまらない場合はeditのビューを再描画させ、パーシャルにエラーメッセージが表示されるようにする(パスワード再設定フォームに記述済み)。

    3つ目:新しいパスワードが空文字になっていないかを確認する。
      ➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
        →フォームから送信されたパスワードが空かどうかを判定する条件式を記述する。
        →空ならerrors.addを使って@userオブジェクトを直接指定して、空の文字列に対してデフォルトのメッセージを表示させるようにする。
          →@user.errors.add(:password, :blank)
        →空でなければ4の処理へ移行させる。

    4つ目:新しいパスワードが正しければ、更新する。
      ➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
        →パスワードが更新・保存された場合の条件式を記述する。
        →ユーザーをログインさせる。
        →ユーザーのリセットダイジェストを、nilで更新・保存する。
          →再設定が成功しても最低2時間は再設定が有効になってしまっているので、ダイジェストを空にすることで強制的に無効にさせる。
        →フラッシュメッセージを表示させる。
        →ユーザー詳細ページへリダイレクトさせる処理を記述する。

      ➡︎上記の条件式ではupdate_attributesメソッドを使用しているが、引数にuser_paramsメソッドを渡すことでより厳密な属性検索をかけさせるようにしたい。
        →なのでprivateキーワード内で、パスワードとパスワード確認のみを許可するためのuser_paramsメソッドを定義してあげる。
        →params.require(:user).permit(:password, :password_confirmation)

      ➡︎これで、updateアクションが動作するようになった。

app/controllers/password_resets_controller.rb
  class PasswordResetsController < ApplicationController
    .
    .
    #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
    before_action :check_expiration, only: [:edit, :update]
    .
    .
    .
    def update
      #3つ目:新しいパスワードが空文字になっていないかを確認する。
      if params[:user][:password].empty?
        @user.errors.add(:password, :blank)
        render 'edit'
      #4つ目:新しいパスワードが正しければ、更新する。
      elsif @user.update_attributes(user_params)
        log_in @user
        flash[:success] = "パスワードが更新されました"
        redirect_to @user
      #2つ目:無効なパスワードであれば失敗させる。
      else
        render 'edit'
      end
    end
 
    private
      def user_params
        params.require(:user).permit(:password, :password_confirmation)
      end
      .
      .
      .
      #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
      def check_expiration
        if @user.password_reset_expired?
          flash[:danger] = "パスワード再設定の有効期限が切れてます"
          redirect_to new_password_reset_url
        end
      end
  end
app/models/user.rb
  class User < ApplicationRecord
    .
    .
    .
    #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
    def password_reset_expired?
      reset_sent_at < 2.hours.ago
    end
    .
    .
    .
  end

3-3.パスワードの再設定をテストする。

  1.新しいパスワードの送信に成功した場合と、失敗した場合の統合テストを書いていく。
    ➡︎まずはファイルを作成する。
      →$ rails g integration_test password_resets

    ➡︎test/integration/password_resets_test.rbファイルへ移動して、パスワード再設定の統合テストを記述していく。
      →(1)setupメソッドを定義する。
        →メイラーの中身が空の配列を作成する。
        →テスト用のログインユーザーを取得する。
      →(2)パスワード再設定のテストを行う。
        →パスワード再設定用メール送信フォームに対してGETリクエストを送信させる記述を行う。
        →パスワード再設定用メール送信フォームのビューテンプレートが描画されていることを検証。
      →(3)まずは無効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。
        →フラッシュが存在することを検証。
        →パスワード再設定用のメールを送信するビューテンプレートが表示されていることを検証。
      →(4)今度は有効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。
        →更新後のリセットダイジェストが、更新前のダイジェストと(ダイジェストが空になったことで)イコールでないことを検証する。
        →メイラーの配列の値の個数が1になっていることを検証する。
        →フラッシュが表示されていることを検証する。
        →ルートURLへリダイレクトされていることを検証する。
      →(5)パスワード再設定フォームのテストを行う。
        →assignsメソッドでPasswordResetsコントローラから直接@userインスタンス変数を参照し、ユーザーを取得しておく。
      →(6)無効なメールアドレスで、パスワード再設定フォームへGETリクエストを送信させる記述を行う。
        →ルートURLへリダイレクトされることを検証する。
      →(7)ここでユーザーを無効なユーザーに切り替える処理を記述(攻撃者によるアカウント乗っ取りのテスト)。
        →メールアドレスは有効だが、無効なユーザーでパスワード再設定フォームヘGETリクエストを送信させる記述を行う。
        →ルートURLへリダイレクトされることを検証する。
      →(8)そして今度は有効なユーザーへ切り替える。
        →メールアドレスは有効だがトークンが無効なまま、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。
        →ルートURLへリダイレクトされることを検証する。
      →(9)有効なメールアドレスとトークンで、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。
        →ちゃんとパスワード再設定ビューテンプレートが描画されていることを検証する。
        →メールアドレス用の隠しフィールドセレクタのvalue値に、取得したユーザーのメールアドレスが表示されていることを検証する。
      →(10)無効なパスワードとパスワード確認を、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
        →エラーメッセージセレクタが表示されていることを検証する。
      →(11)パスワードとパスワード確認入力が空のまま、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
        →この場合もエラーメッセージセレクタが表示されていることを検証する。
      →(12)有効なパスワードとパスワード確認入力で、PasswordResetsコントローラのupdateアクションヘPATCHリクエストが送信された場合の記述をする。
        →ログインユーザーが存在することを検証する。
        →フラッシュが表示されていることを確認する。
        →そのユーザーのページへ、ちゃんとリダイレクトされているかどうかを検証する。

      ➡︎テストスイートGREEN。

test/integration/password_resets_test.rb
  require 'test_helper'
  class PasswordResetsTest < ActionDispatch::IntegrationTest
  #(1)setupメソッドを定義する。
    def setup
      ActionMailer::Base.deliveries.clear
      @user = users(:テスト用のユーザー)
    end
 
  #(2)パスワード再設定のテストを行う。
    test "password resets" do
      get new_password_reset_path
      assert_template 'password_resets/new'
    #(3)まずは無効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う
      post password_resets_path, params: { password_reset: { email: "無効なメアド" } }
      assert_not flash.empty?
      assert_template 'password_resets/new'
    #(4)今度は有効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。
      post password_resets_path,
         params: { password_reset: { email: @user.email } }
      assert_not_equal @user.reset_digest, @user.reload.reset_digest
      assert_equal 1, ActionMailer::Base.deliveries.size
      assert_not flash.empty?
      assert_redirected_to root_url
    #(5)パスワード再設定フォームのテストを行う。
      user = assigns(:user)
    #(6)無効なメールアドレスで、パスワード再設定フォームへGETリクエストを送信させる記述を行う。
      get edit_password_reset_path(user.reset_token, email: "無効なメアド")
      assert_redirected_to root_url
    #(7)ここでユーザーを無効なユーザーに切り替える処理を記述(攻撃者によるアカウント乗っ取りのテスト)。
      user.toggle!(:activated)
      get edit_password_reset_path(user.reset_token, email: user.email)
      assert_redirected_to root_url
    #(8)そして今度は有効なユーザーで(トークンは無効)。
      get edit_password_reset_path('無効なトークン', email: user.email)
      assert_redirected_to root_url
    #(9)有効なメールアドレスとトークンで、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。
      get edit_password_reset_path(user.reset_token, email: user.email)
      assert_template 'password_resets/edit'
      assert_select "input[name=email][type=hidden][value=?]", user.email
    #(10)無効なパスワードとパスワード確認を、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
      patch password_reset_path(user.reset_token),
        params: { email: user.email,
                  user: { password:              "無効なパス",
                          password_confirmation: "無効なパス確認" } }
      assert_select 'div#error_explanation'
    #(11)パスワードとパスワード確認入力が空のまま、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
      patch password_reset_path(user.reset_token),
        params: { email: user.email,
                  user: { password:              "",
                          password_confirmation: "" } }
      assert_select 'div#error_explanation'
    #(12)有効なパスワードとパスワード確認入力で、PasswordResetsコントローラのupdateアクションヘPATCHリクエストが送信された場合の記述をする。
      patch password_reset_path(user.reset_token),
        params: { email: user.email,
                  user: { password:              "有効なパス",
                          password_confirmation: "有効なパス確認" } }
      assert is_logged_in?
      assert_not flash.empty?
      assert_redirected_to user
    end
  end


最後に

 以上で後編のパスワード再設定機能の本実装が完了しました。
 前編→パスワード再設定用のリソース作成
 中編→パスワード再設定メール送信機能の実装

参考

 Railsチュートリアル 第12章パスワードの再設定

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

mkcertを使いRailsのローカル開発環境をSSL化する方法(for Mac)

mkcertとは?

Railsの開発環境でHTTPSを有効にするためのツール。
Github( https://github.com/FiloSottile/mkcert#installation )には以下のように書かれています。

mkcert is a simple tool for making locally-trusted development certificates. It requires no configuration.

訳:mkcertは、ローカルに信頼された開発証明書を作成するためのシンプルなツールです。設定は不要です。

mkcertをインストールする

homebrewを使っている場合のインストールコマンド

$ brew install mkcert

# ブラウザにFirefoxを使う場合は以下のコマンドも追加
$ brew install nss
# ローカルCA(認証局)を作成
$ mkcert -install
# 証明書を作成
$ mkcert localhost

スクリーンショット 2020-04-21 21.59.58.png
上から順に説明すると
■"/Users/ユーザーの名前/library/Application Support/mkcert"にあるローカル認証局を使っています
■"localhost"という名前で新しい証明書を作成しました
■証明書は".localhost.pem"に、鍵は".localhost-key.pem"にあります
ということです。

puma.rbの設定を変更

ポートと、証明書と鍵を追記しましょう。

(中略)

if ENV.fetch('RAILS_ENV') { 'development' } == 'development'
  ssl_bind '0.0.0.0', '9292', key: './localhost-key.pem', cert: './localhost.pem'
end

(中略)

以上でHTTPS接続の設定は完了

https://localhost:9292/ にアクセスして、接続が可能か確認します。
これだけで接続可能になるとは非常に簡単ですね。

今回はMacのみのやり方について書きましたので、Mac以外の方はGithubにやり方が載っているのでそちらを参照ください。

参考

mkcertのGithub
Railsの開発環境でHTTPSを有効にする
Railsの開発環境でhttps接続する!

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

初心者には謎が多いrails db:migrateとは?

rails tutorialを25日間かけてやり終えた和己です。
かなり挫折しかけましたがなんとかやり終えました。
そこでわからない用語が複数あったので復習をかねてまとめていきたいと思います。

今回はrailsチュートリアルで頻出する

rails db:migrate

についてです

そもそもmigrationって?

migration(rails db:migrateコマンド)は、railsで使用するデータベースの構造(テーブル、カラム)を変更するときに利用する機能です。

データベースを変更するときの大まかな流れ

1.$ rails g migrationコマンドでマイグレーションファイルを作成
2.マイグレーションファイルの中身を書き換える
3.$ rails db:migrateコマンドでマイグレーションを順番に実行してデータベースに変更を加える

基本

1. 空のマイグレーションファイルを作成する

# 空のmigrationファイルを作成する
$ rails g migration create_user 
Running via Spring preloader in process 2198
      invoke  active_record
      create    db/migrate/20200419035049_create_user.rb # 20200419035049というのがmigrationファイルが作られた日時                  

# マイグレーションの実行状況を確かめる
$ rails db:migrate:status

database: hogehoge/your_project

 Status   Migration ID    Migration Name
--------------------------------------------------
 down     20200419035049  CreateUser # statusが'down'となっているのはマイグレーションがまだ実行されていないことを示している

↓マイグレーションと呼ばれる新しいファイルが生成されます。

# db/migrate/20200419035049_create_user.rbの中身
class CreateUser < ActiveRecord::Migration[5.2]
  def change
  end
end

マイグレーションファイル名の先頭には、それが生成された時間のタイムスタンプが追加されます。以前はインクリメンタルな整数が追加されましたが、複数の開発者によるチームでは、複数のプログラマが同じ整数を持つマイグレーションを生成してしまい、コンフリクトを引き起こしていました。現在のタイムスタンプによる方法であれば、まったく同時にマイグレーションが生成されるという通常ではありえないことが起きない限り、そのようなコンフリクトは避けられます。

Railsチュートリアルより

2.migrationファイルを書いていく

class CreateUser < ActiveRecord::Migration[5.2]
  def change # rails db:migrateで実行されるメソッド
    create_table :users do |t| #usersという名前のテーブルを新たに作成             
    t.string :name #string型のnameカラムを作成
      t.text :profile #text型のprofileカラムを作成
      t.timestamps #created_at, updated_atを自動で作成
    end
  end
end

3.マイグレーションファイルを実行してデータベースに変更を加える

$ rails db:migrate
== 20200419035118 CreateUsers: migrating ======================================
-- create_table(:user)
   -> 0.0024s
== 20200419035118 CreateUsers: migrated (0.0026s) =============================

$ rails db:migrate:status # マイグレーションの実行状況を確かめる

database: hogehoge/your_project

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200419035049  CreateUser # statusが'up'に変わり、マイグレーションが正常に実行されたことを示している

1→2→3の流れで以下のようなデータ構造を持つUserテーブルが作成される

カラム名 データ型
id integer
name string
profile text
created_at datetime
updated_at datetime

migrationのメリット、デメリット

メリット
・SQL文を書かずにデータベースの構造を変更できる
・migrationファイルを共有することで、複数の開発環境でデータベースの構造を簡単に共有できる

デメリット
SQL書かずにデータベース作るから内容がわからないまま進めてしまう。

db:migrateが実行されると、db/development.sqlite3という名前のファイルが生成されます。これはSQLiteデータベースの実体です。

ほぼすべてのマイグレーションは、元に戻すことが可能

元に戻すことを「ロールバック (rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。

$ rails db:rollback

rails db:rollbackによってchangeメソッドに定義された処理が逆順に実行される。

$ rails db:rollback
== 20200419036118 CreateUsers: reverting ======================================
-- drop_table(:users) 
   -> 0.0014s
== 20200419036118 CreateUsers: reverted (0.0034s) =============================

$ rails db:migrate:status # マイグレーションの実行状況を確認

database: hogehoge/your_project

 Status   Migration ID    Migration Name
--------------------------------------------------
 down     20200419035049  CreateUser

参考にした記事

Railsチュートリアル第6章
RailsのMigrationに関する基本まとめ
【Rails入門】データベースを設定するrails db:migrateを説明!

余談

今回は記事を読んで頂きありがとうございました。
毎日Twitterも更新していますのでフォローして頂けると嬉しいです。
和己のTwitter

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

アップデートによりシェルスクリプトがbashからzshに変わった時の対処方法

まえがき

2020/3末、macをアップデートしてからrails sでサーバを立ち上げると、

Your Ruby version is 2.6.3, but your Gemfile specified 2.6.5

となるので、

bundle install

で、一旦はサーバーが立ち上がるようになるのですが、翌日などに立ち上げると、表示が出て立ち上がらなくなった

状況確認

① Rubyのバージョンの確認

ruby -v

すると、

ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]

2.6.5はインストールしているのですが、2.6.3はインストールした記憶がない…(したのかな?)

② rbenvの確認

which ruby

すると、

ユーザー名@mba desktop % which ruby
/usr/bin/ruby

このように表示された場合、rbenvが反映されていません。システムのRubyを参照したままになっています。

③ シェルスクリプトの確認

echo $SHELL
# 結果がbin/bashの場合
cat ~/.bash_profile
# 結果がbin/zshの場合
cat ~/.zshrc

すると、結果が、

bin/zsh

で、

cat ~/.zshrc

したところ、

cat: /Users/ユーザー名/.zshrc: No such file or directory

となりました。

次に、

echo 'eval "$(rbenv init -)"' >>  ~/.zshrc

を実行後、再度、

cat ~/.zshrc

を実行、結果は、

eval "$(rbenv init -)"

原因

アップデートした際、シェルスクリプトがbashからzshに変わった事で異常が起きている。

対処法

①open ~/.zshrc
でファイルを開き、以下に置き換える。

# Homebrew
export PATH=/usr/local/bin:$PATH
# Homebrew rbenv
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

その上で、次を実行。

source ~/.zshrc
rbenv global 2.6.5

ターミナルを再起動して、

ruby -v

を実行。結果は、

ruby 2.6.5p114 (2019-10-01 revision 67812)

と表示されました。次に、

which ruby 

を実行。

/Users/ユーザー名/.rbenv/shims/ruby

と表示されました。これでとりあえず大丈夫です。

あとがき

神里よしとさんの主宰するプログラミングのオンラインサロン「人生逆転サロン」のメンターのたけさんに質問して教えて頂いた流れを忘れないように、ここに書き留めておきます。

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

【Rails】seedから画像をdbに保存する方法

seedデータからdbに画像を保存するやり方です。
本番環境でもできます。

(db/seeds.rb)

20.times do |n|
 name = Faker::Games::Pokemon.name
 email = Faker::Internet.email
 password = 'password'
 User.create!(
  name: name,
  email: email,
  password: password,
  password_confirmation: password,
  icon: open("./db/fixtures/icon#{n}.jpg")
 )
end

こんな感じで
icon: open("./db/fixtures/icon#{n}.jpg")
で呼び出しています。
自分は(db/fixtures)の中にicon0.jpgicon1.jpgみたいな感じで20個jpgファイルを置きました。
ご参考までに

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

Rails で PostgreSQL のパーテイションを利用したい

なにこれ?

Ruby on Rails で PostgreSQL のハッシュパーティションを使いたいよねー、という方!こうすればできました。たぶん、rails db:migrate or db:rollback したら DDL が発行されてよしなに設定されるはず。

サンプルコード

class CreateStaffs < ActiveRecord::Migration[6.0]
  def change
    reversible do |dir|
      dir.up do
        execute <<~"SQL"
                    CREATE TABLE staffs (id uuid PRIMARY KEY UNIQUE, email varchar NOT NULL, cname varchar NOT NULL, created_at timestamp(6) NOT NULL, updated_at timestamp(6) NOT NULL) PARTITION BY HASH (id);
                    #{(0..99).map{|num| "CREATE TABLE staffs_partition_#{num} PARTITION OF staffs FOR VALUES WITH(MODULUS 100,REMAINDER #{num});"}.join}
        SQL
      end
      dir.down do
        execute <<-SQL
                   DROP TABLE staffs;
        SQL
      end
    end
  end
end

おわりに

PostgreSQL 11 からの機能なので、使えない環境の方もいらっしゃると思われます。が、あとで楽できそうなので必要に応じてパーテイションを作っておくことが良さそうですね。

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

Rails で PostgreSQL のパーティションを利用したい

なにこれ?

Ruby on Rails で PostgreSQL のハッシュパーティションを使いたいよねー、という方!こうすればできました。たぶん、rails db:migrate or db:rollback したら DDL が発行されてよしなに設定されるはず。

サンプルコード

class CreateStaffs < ActiveRecord::Migration[6.0]
  def change
    reversible do |dir|
      dir.up do
        execute <<~"SQL"
                    CREATE TABLE staffs (id uuid PRIMARY KEY UNIQUE, email varchar NOT NULL, cname varchar NOT NULL, created_at timestamp(6) NOT NULL, updated_at timestamp(6) NOT NULL) PARTITION BY HASH (id);
                    #{(0..99).map{|num| "CREATE TABLE staffs_partition_#{num} PARTITION OF staffs FOR VALUES WITH(MODULUS 100,REMAINDER #{num});"}.join}
        SQL
      end
      dir.down do
        execute <<-SQL
                   DROP TABLE staffs;
        SQL
      end
    end
  end
end

おわりに

PostgreSQL 11 からの機能なので、使えない環境の方もいらっしゃると思われます。が、あとで楽できそうなので必要に応じてパーテイションを作っておくことが良さそうですね。

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

rails-erd 設定

rails-erdの導入

Gemfile
group :development do
  gem "rails-erd"
end

Rails6.0以降の場合は以下を設定

config/environments/development.rb
config.eager_load = true

実行

bundle exec erd

おすすめ実行オプション

bundle exec erd --attributes=foreign_keys,content #外部キーを明らかにし、他のカラムも表示

参考

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

[Rails]TwitterAPI認証+deviseでのログインの実装方法

経緯

railsにdeviseをインストールした状態でTwitterAPI認証でのログインを実装しました。
色んなサイトを参考にした結果、迷走し2日以上かけてしまいました。
今後も使うかもしれない自分用の備忘録的サムシングとして、またみなさんにササッと実装いただけたらと思い投稿しました。

開発環境

ruby 2.7.0
rails 5.2.4
AWS EC2/RDS

前提条件

・Twitterアカウントがある。
・deviseインストール済のアプリがある。
・環境変数の設定ができている。
・本番環境でアプリの動作ができている状態。

TwitterのAPI認証取得および設定

a. API取得

https://qiita.com/kngsym2018/items/2524d21455aac111cdee
私はこちらのサイトを参考にしました。

callbackURLには以下のように入れてください。
URL : https://[自分のサイトのリンク]/users/auth/twitter/callback
(https化されてない場合は、うまく動作しないかもしれません。)

Terms of service URLとPrivacy policy URL何か埋めてください。次の項目で必須となってます。
こんな感じになります。
スクリーンショット 2020-04-20 20.45.44.png

b. 取得できたAPI KeyとAPI Secretはメモ
スクリーンショット 2020-04-20 20.36.28.png

c. Permissionsの設定
read onlyなどは用途に合わせて選んでください。
ユーザー登録にメルアドが必要なので「Request email addresses from users」チェック
スクリーンショット 2020-04-20 20.48.48.png

Gemの設定

Gemfile
gem 'omniauth'
gem 'omniauth-twitter'

忘れずに

$ bundle

API keyとAPI secretの設定

a. メモしたAPI keyとAPI secretを.envファイルに記述します。
ここではそれぞれの名前はTWITTER_CONSUMER_KEYとTWITTER_CONSUMER_SECRETにしています。
ドメインネーム(アプリのドメインorIPアドレス)も記述します

.env
TWITTER_CONSUMER_KEY = "****************************"
TWITTER_CONSUMER_SECRET = "****************************"

DOMAIN_NAME = "*****************"

b. .gitignoreに記述

.gitignore
.
.
.env
.
.

deviseの設定

記述した認証用のキーをdeviseが識別できるように記述します。

config/initializers/devise.rb
config.omniauth :twitter, ENV['TWITTER_CONSUMER_KEY'], ENV['TWITTER_CONSUMER_SECRET'], scope: 'email', oauth_callback: "#{ENV['DOMAIN_NAME']}/users/auth/twitter/callback"
OmniAuth.config.logger = Rails.logger if Rails.env.development? # debug用

UserモデルとTwitter認証モデルを紐付ける

a. deviseに:omniauthable, omniauth_providers: [:facebook, :twitter, :google_oauth2]を追加
facebookとグーグル認証も将来的に使うかもしないので一応付けておきます。

app/models/user.rb
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :validatable,
       # 外部API認証用に以下追加
       :omniauthable, omniauth_providers: [:facebook, :twitter, :google_oauth2]

b. マイグレーションファイルにカラムの追加

$ rails g migration AddColumnsToUsers uid:string provider:string
$ rails db:migrate

c. Userモデルにfindメソッドを実装

app/models/user.rb
# Twitter認証ログイン用
# ユーザーの情報があれば探し、無ければ作成する
def self.find_for_oauth(auth)
  user = User.find_by(uid: auth.uid, provider: auth.provider)

  user ||= User.create!(
    uid: auth.uid,
    provider: auth.provider,
    name: auth[:info][:name],
    email: User.dummy_email(auth),
    password: Devise.friendly_token[0, 20]
  )

  user
end

def self.dummy_email(auth)
  "#{Time.now.strftime('%Y%m%d%H%M%S').to_i}-#{auth.uid}-#{auth.provider}@example.com"
end

メールアドレスでの認証も実装している場合は、OAuthでの認証時もメールアドレスを保存する必要があります。
emailはダミー用のアドレスを作成して代用します。
email中に時間まで記載しているのは、認証後に退会などで論理削除された後も再度認証ログインできるように今回は設定しています。

d. omniauthコントローラーの設定
deviseのusers配下にあります。
無い方は$ rails g devise:controllers usersを実行すると見えるようになります。

app/controllers/users/omniauth_callbacks_controller.rb
def twitter
  callback_from :twitter
end

private
  # コールバック時に行う処理
  def callback_from(provider)
    provider = provider.to_s

    @user = User.find_for_oauth(request.env['omniauth.auth'])

    # persisted?でDBに保存済みかどうか判断
    if @user.persisted?
      # サインアップ時に行いたい処理があればここに書きます。
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end

ルーティングの設定

omniauth_callbacks_pathの追加

config/routes.rb
devise_for :users, :controllers => {
    :registrations => 'users/registrations',
    :sessions => 'users/sessions',
    :passwords => 'users/passwords',
    # このpathを通して外部API認証が行われる。
    :omniauth_callbacks => 'users/omniauth_callbacks'
   }

ビューに認証用リンクの設定

書き方はなんでもありだと思いますが一例として
a. loginページにリンク追加

app/views/devise/registrations/new.html.erb
<%= link_to user_twitter_omniauth_authorize_path do %>
  <i class="fab fa-twitter-square fa-2x"></i>
<% end %>

b. signupページにリンク追加

app/views/devise/sessions/new.html.erb
<%= link_to user_twitter_omniauth_authorize_path do %>
  <i class="fab fa-twitter-square fa-2x"></i>
<% end %>

c. cssにtwitterアイコンのカラーを追加

app/assets/stylesheets/application.css.scss
.fa-twitter-square {
  color: #1da1f2;
}

本番環境で動作確認

ここまでの操作でできているはずです。

a. アップロードの準備

.envのアップロード
ローカル環境でアプリケーション直下に移動して

$ scp -i ~/.ssh/practice-aws.pem .env ec2-user@××.××.×××.×××:[アプリ名]/

本番環境に入って、

// プリコンパイルを実行
$ bundle exec rails assets:precompile RAILS_ENV=production
// マイグレートを実行
$ bundle exec rails db:migrate RAILS_ENV=production
// サーバー起動
$ rails s -e production

b. ビューの確認
スクリーンショット 2020-04-21 14.16.42.png
ツイッターのアイコンが表示出来ています。

c. 認証ログイン後確認
スクリーンショット 2020-04-21 14.19.30.png
ツイッターでの表示名でログインできていることが確認できました。

まとめ

TwitterAPIを使ってワンクリックでサインアップすることができるようになったため、よりユーザービリティが上がったと思います。
はじめてAPI認証でのログインに挑戦してみて、WEBのつながりが見えてきて非常に勉強になりました。

今回時間がかかった要因

a. ローカルでの試験運用ができなかったこと
TwitterDeveloperのwebsiteURLに他サイトで推奨されているようなhttp://127.0.0.1:3000 などを入れても「invalid URL」といったメッセージが返ってくるため、(他のやりようがあったみたいですが少々面倒だったので)ローカル→本番→試験→NGを繰り返していました。

結果的にはめちゃめちゃ時間くっちゃいましたので、面倒でもローカルでの有効化方法を模索すればよかったと反省です。良い方法があれば教えていただけると嬉しいです。

b. deviseでの認証の場合、やり方が通常と異なること
通常では/config/initializers下にomniauth.rbというファイルを新規作成してコントローラーも別で作ってってやり方になると思いますが、そちらでやっていたためdeviseが入ってるとログイン認証はデバイスが絡んでいるため、エラーになっていました。
deviseの便利さに甘えて、大事なことに気づくことが遅れてしまいました。反省です。

参考

https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
https://qiita.com/zenizh/items/94aec2d94a2b4e9a1d0b
https://qiita.com/puremoru0315/items/f1d459b663fd3b715dee
https://kurose.me/twitter-omniauth/#Twitter

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

メモランダム Rails tutorial 第7章

Rails tutorial 第8章

ユーザー登録

  • いよいよユーザー登録機能を追加しましょう
    • 7.2ではHTML フォームを使ってWebアプリケーションに登録情報を送信
    • 7.4ではユーザーを新規作成して情報をデータベースに保存
  • ユーザーを表示するためのページを作成し、ユーザー用のRESTアーキテクチャを実装する第一歩を踏み出します

1.ユーザーを表示する

  • モックアップ

デバッグとRails環境

  • ビルトインのdebugメソッドとparams変数を使って、各プロフィールページにデバッグ用の情報が表示されるようになります
    • <%= debug(params) if Rails.env.development? %>
  • デバッグ情報は開発環境以外で表示させないべき

2.Usersリソース

  • resources :usersという行は、ユーザー情報を表示するURL (/users/1) を追加するためだけのものではありません。

    • サンプルアプリケーションにこの1行を追加すると、ユーザーのURLを生成するための多数の名前付きルート (5.3.3) と共に、RESTfulなUsersリソースで必要となるすべてのアクションが利用できるようになるのです
  • ーザーのid読み出しにはparamsを使いました。

    • Usersコントローラにリクエストが正常に送信されると、params[:id]の部分はユーザーidの1に置き換わります

debuggerメソッド

  • 直接的にデバッグする方法もあります。それがbyebug gemによるdebuggerメソッドです

Gravatar画像とサイドバー

  • 今度は各ユーザーのプロフィール写真のあたりをもう少し肉付けし、サイドバーも作り始めましょう
  • Gravatarは無料のサービスで、プロフィール写真をアップロードして、指定したメールアドレスと関連付けることができます

  • gravatar_forヘルパーメソッドを使ってGravatarの画像を利用できるようにします

  • Rubyでは、Digestライブラリのhexdigestメソッドを使うと、MD5のハッシュ化が実現できます。

2.ユーザー登録フォーム

  • モックアップ確認

form_tagを使用する

  • Railsでform_forヘルパーメソッドを使います
    • このメソッドはActive Recordのオブジェクトを取り込み、そのオブジェクトの属性を使ってフォームを構築します。
  • ユーザーオブジェクトの作成
  • 登録フォーム

フォームHTML

  • doキーワードは、 form_forが1つの変数を持つブロックを取ることを表します。この変数fは “form” のfです。
  • Railsヘルパーを使っている場合、実装の詳細について知っておく必要はありません。
    • ただしfというオブジェクトが何をするのかは知っておく必要があります。
    • このfオブジェクトは、HTMLフォーム要素 (テキストフィールド、ラジオボタン、パスワードフィールドなど) に対応するメソッドが呼び出されると、@userの属性を設定するために特別に設計されたHTMLを返します

3.ユーザー登録失敗

  • フォームを理解するにはユーザー登録の失敗のときが最も参考になります

正しいフォーム

  • createアクションでフォーム送信を受け取り、User.newを使って新しいユーザーオブジェクトを作成し、ユーザーを保存 (または保存に失敗) し、再度の送信用のユーザー登録ページを表示するという方法で機能を実装しようと思います
  • render メソッドを再度使いまわし

Strong Parameters

  • user_paramsという外部メソッドを使うのが慣習になっています
  • @user = User.new(user_params)

エラーメッセージ

  • Railsは、このようなメッセージをUserモデルの検証時に自動的に生成してくれます
  • errors.full_messagesオブジェクトは、 エラーメッセージの配列を持っています
  • 'shared/error_messages'というパーシャルをrender (描画) している点に注目

  • any?メソッドはちょうどempty?と逆の動作で、要素が1つでもある場合はtrue、ない場合はfalseを返します

  • pluralizeの最初の引数に整数が与えられると、それに基づいて2番目の引数の英単語を複数形に変更したものを返します。このメソッドの背後には強力なインフレクター (活用形生成) があり、不規則活用を含むさまざまな単語を複数形にすることができます。

    • これにより、"1 errors" のような英語の文法に合わない文字列を避けることができます (これはWeb上でどうしようもないほどよく見かけるエラーです)。

失敗時のテスト

  • 有効な送信をしたときの正しい振る舞いについてテストを書いていきます
  • 色々(パス)

4.ユーザー登録成功

  • まずは、ユーザーを保存できるようにします

登録フォームの完成

  • Railsの一般的な慣習に倣って、ユーザー登録に成功した場合はページを描画せずに別のページにリダイレクト (Redirect) するようにしてみましょう
  • redirect_to メソッド

flash

  • 登録完了後に表示されるページにメッセージを表示し (この場合は新規ユーザーへのウェルカムメッセージ)、2度目以降にはそのページにメッセージを表示しないようにするというものです
  • flash[:success] = "Welcome to the Sample App!"

実際のユーザー登録

  • データベースの内容を一旦リセット

成功時のテスト

  • 今回はassert_differenceというメソッドを使ってテストを書きます
    • このメソッドは第一引数に文字列 ('User.count') を取り、assert_differenceブロック内の処理を実行する直前と、実行した直後のUser.countの値を比較します。第二引数はオプションですが、ここには比較した結果の差異 (今回の場合は1) を渡します。

5.プロのデプロイ

  • このアプリケーションをデプロイして、本番環境でも動かせるようにしてみましょう

    • 実際にデータを操作できるようにするデプロイは初めて
  • デプロイの下準備として、まずはこの時点までの変更をmasterブランチにマージしておいてください。

本番環境でのSSL

  • このようなネットワークに流れるデータは途中で捕捉できるため、扱いには注意が必要です。これはサンプルアプリケーションの本質的なセキュリティ上の欠陥です。

    • そしてこれを修正するためにSecure Sockets Layer (SSL)を使います 12。これはローカルのサーバからネットワークに流れる前に、大事な情報を暗号化する技術です
  • production.rbという本番環境の設定ファイルの1行を修正するだけで済みます。具体的には、configに「本番環境ではSSLを使うようにする」という設定をするだけです

本番環境用のWebサーバー

  • WEBrickは本番環境として適切なWebサーバではありません。よって、今回はWEBrickをPumaというWebサーバに置き換えてみます
  • 新しいWebサーバを追加するために、Heroku内のPumaドキュメント (英語) に従ってセットアップしていきます。

本番環境へのデプロイ

  • 変更をコミットし、デプロイしてみましょう
  • ** 次の点は頭の片隅に置いておいてください。それは、仕事でHerokuを使ったアプリケーションを動かす場合はGemfileでRubyのバージョンを明示しておいた方が賢明である、という点です**

6.最後に

  • 第8章と第9章では、認証 (authentication) システムを導入し、ユーザーがログインとログアウトをできるようにします ([remember me] という発展的な機能も実装します)。

本章のまとめ

  • debugメソッドを使うことで、役立つデバッグ情報を表示できる
  • Sassのmixin機能を使うと、CSSのルールをまとめたり他の場所で再利用できるようになる
  • Railsには標準で3つ環境が備わっており、それぞれ開発環境 (development)、テスト環境 (test)、本番環境 (production)と呼ぶ
  • 標準的なRESTfulなURLを通して、ユーザー情報をリソースとして扱えるようになった
  • Gravatarを使うと、ユーザーのプロフィール画像を簡単に表示できるようになる
  • form_forヘルパーは、Active Recordのオブジェクトに対応したフォームを生成する
  • ユーザー登録に失敗した場合はnewビューを再描画するようにした。その際、Active Recordが自動的に検知したエラーメッセージを表示できるようにした
  • flash変数を使うと、一時的なメッセージを表示できるようになる
  • ユーザー登録に成功すると、データベース上にユーザーが追加、プロフィールページにリダイレクト、ウェルカムメッセージの表示といった順で処理が進む
  • 統合テストを使うことで送信フォームの振る舞いを検証したり、バグの発生を検知したりできる
  • セキュアな通信と高いパフォーマンスを確保するために、本番環境ではSSLとPumaを導入した
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでstubを使う

よく使う構文をメモ

post = Post.new

# 引数なし
allow(post).to receive(:title).and_return('title')

# 引数あり
allow(post).to receive(:title).with(arg1, arg2).and_return('title')

# エラーを起こさせる
allow(post).to receive(:subtitle).and_raise(StandardError.new)

# クラスに対してインスタンスメソッドのスタブ化
allow_any_instance_of(Post).to receive(:title).and_return('title')

参考

開発
Rails tips: RSpecでシンプルなスタブを使う(翻訳)

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

メモランダム Rails tutorial 第6章

Rails tutorial 第6章

ユーザーのモデルを作成する

  • 認証 (authentication) と認可 (authorization) のシステムの例だと、Clearance、Authlogic、Devise、CanCanなど

1. Userモデル

  • 情報を保存するためのデータ構造を作成する
  • Railsでは、データモデルとして扱うデフォルトのデータ構造のことをモデル (Model) と呼びます
  • データベースとやりとりをするデフォルトのRailsライブラリはActive Recordと呼ばれます

データベースの移行

  • この節での目的は、簡単に消えることのないユーザーのモデルを構築すること
  • nameとemailの2つの属性からなるユーザーをモデリングするところから始める
    • attr_accessor :name, :emailは必要ない
    • generate modelというコマンドを使います -$ rails generate model User name:string email:string

modelファイル

  • この節では、以後このモデル用ファイルを理解することに専念します

ユーザーオブジェクトを作成する。

  • Active Recordを理解する上で、「有効性 (Validity)」という概念も重要
    • データベースにデータがあるかどうかは有効性には関係ありません
  • Userモデルのインスタンスはドット記法を用いてその属性にアクセスすることができます
  • create, destroy

ユーザーオブジェクトを検索する

  • User.find(3)
  • User.find_by(email: "mhartl@example.com")
  • User.first
  • User.all

ユーザーオブジェクトを更新する。

  • 基本的な更新の方法は2つ
    • 属性を個別に代入する方法
    • update_attributesを使うケース
    • user.update_attribute(:name, "El Duderino")

2.ユーザーを検証する。

  • nameとemailにあらゆる文字列を許すのは避けるべき
    • 存在性 (presence)の検証、
    • 長さ (length)の検証、
    • フォーマット (format)の検証、
    • 一意性 (uniqueness)の検証

有効性を検証する。

  • 有効なUserかどうかをテストする
    • assert @user.valid?

    - $ rails test:models

存在性を検証する

  • 「存在性 (Presence)」。これは単に、渡された属性が存在することを検証する。
    • name属性にバリデーションに対するテスト
    • assert_not @user.valid?
    • name属性の存在性を検証する
    • validates :name, presence: true
    • validates(:name, presence: true)
    • user.errors.full_messages

長さを検証する

  • nameの長さの検証に対するテスト
    • user.name = "a" * 51
  • validates :name, presence: true, length: { maximum: 50 }
  • validates :email, presence: true, length: { maximum: 255 }

フォーマットを検証する

  • ここではメールアドレスにおなじみのパターンuser@example.comに合っているかどうかも確認することを要求します
     - %w[]という便利なテクニック
     - assert @user.valid?, "#{valid_address.inspect} should be valid"

  • このオプションは引数に正規表現 (Regular Expression) (regexとも呼ばれます) を取ります。正規表現は一見謎めいて見えますが、文字列のパターンマッチングにおいては非常に強力な言語です。つまり、有効なメールアドレスだけにマッチして、無効なメールアドレスにはマッチしない正規表現を組み立てる必要があります

    • VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    • format: { with: VALID_EMAIL_REGEX }

一意性を検証する

  • validatesメソッドの:uniqueオプションを使います
  • test "email addresses should be unique" do duplicate_user = @user.dup @user.save assert_not duplicate_user.valid? end
    • @userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています
  • emailのバリデーションにuniqueness: trueというオプションを追加

3.セキュアなパスワードを追加する

  • 、ハッシュ関数を使って、入力されたデータを元に戻せない (不可逆な) データにして保存する。

ハッシュ化されたパスワード

  • セキュアなパスワードの実装は、has_secure_passwordというRailsのメソッドを呼び出すだけでほとんど終わってしまいます
  • 今回の用途ではハッシュ化されたパスワードと暗号化されたパスワードは類義語となります
  • has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になります。パスワードを適切にハッシュ化することで、たとえ攻撃者によってデータベースからパスワードが漏れてしまった場合でも、Webサイトにログインされないようにできます。サンプルアプリケーションでbcryptを使うために、bcrypt gemをGemfileに追加します

パスワードの最小文字数

  • test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
    end

  • test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
    end

  • validates :password, length: { minimum: 6 }

  • validates :password, presence: true, length: { minimum: 6 }

ユーザーの作成と認証

  • has_secure_passwordをUserモデルに追加したことで、そのオブジェクト内でauthenticateメソッドが使えるようになっています

4.最後に

  • たった12行でここまでの機能が実装できたことは、Railsの注目に値する特長でもあります

本章のまとめ

  • マイグレーションを使うことで、アプリケーションのデータモデルを修正することができる
  • Active Recordを使うと、データモデルを作成したり操作したりするための多数のメソッドが使えるようになる
  • Active Recordのバリデーションを使うと、モデルに対して制限を追加することができる
  • よくあるバリデーションには、存在性・長さ・フォーマットなどがある
  • 正規表現は謎めいて見えるが非常に強力である
  • データベースにインデックスを追加することで検索効率が向上する。また、データベースレベルでの一意性を保証するためにも使われる
  • has_secure_passwordメソッドを使うことで、モデルに対してセキュアなパスワードを追加することができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メモランダム Rails tutorial 第5章

Rails tutorial 第5章

レイアウトを作成する

  • apurike-syonnnibootstrapフレームワークを組み込み、カスタムスタイルを追加する。
  • home, aboutへのリンクをレイアウトに追加。
    • パーシャル、 railsのルーティング、Asset pipelineについて学ぶ。+ Sass。ユーザーログインへの第一歩も。

1.構造を追加する

  • webアプリケーション = ユーザインターフェース
  • モックアップをスケッチ。
  • この時点でブランチを作成。

ナビゲーション

  • サイトのレイアウトがいるに HTMLを追加。
  • internetExplorerのHTML5サポート不備を回避するJavaScriptのHTML5shimというコード。
  • header

    • navbar、navbar-fixed-top、navbar-inverse
  • classラベルは何度も使える。

  • idラベルは一度しか使えない。

  • container

  • link_to

  • オプションハッシュ#

  • navタグには「その内側がナビゲーションリンクである」という意図を明示的に伝える役割

  • yieldメソッドはWebサイトのレイアウトにページごとの内容を挿入する。

  • link_to の image_tag

    • app/assets/images/ディレクトリの中から
  • alt属性は、画像がない場合に代わりに表示される文字列

BootstrapとカスタムCSS

Bootstrapを使うことでアプリケーションをレスポンシブデザイン (Responsive Design) にできる

  • どの端末でアプリケーションを閲覧しても、ある程度見栄えをよくすることができ

    • Gemfileにbootstrap-sassを追加する
    • bundle installを実行
    • カスタムCSSを動かすための最初の一歩は、カスタムCSSファイルを作ること
  • Sassは5.2.2まで登場しませんが、bootstrap-sass gemが動作するためのおまじないとして必要

    • Webサーバを再起動させると、アプリケーションに反映させる
  • Ctrl-Cを押してWebサーバを停止させた後、rails serverコマンドを打ってWebサーバを起動してください

    • navbar-fixed-top
    • navbar-inverse
    • text-align: center
    • color: #fff

パーシャル partial

一箇所にまとめた方が便利

  • Railsではパーシャル (partial) という機能でこのような課題を解決できる

- renderと呼ばれるRailsヘルパー

2.Sassとアセットパイプライン

  • 最近のRailsに追加された機能の中で最も特筆すべき機能の1つは、CSS、JavaScript、画像などの静的コンテンツの生産性と管理を大幅に強化する「アセットパイプライン (Asset Pipeline)」

アセットパイプライン

アセットディレクトリ

  • 3種 app, lib , vendor

マニフェストファイル

  • ニフェストファイルを使って、アセットをどのように1つのファイルにまとめるのかをRailsに指示する

プリプロセッサエンジン

  • プリプロセッサエンジンは、繋げて実行する (chain) ことができます

Sass-素晴らしい構文を備えたスタイルシート

  • ネスト = ルールを継承

    • :hoverなど
  • bootstrap-sassというgemを使えば、SCSSでも同様に$gray-lightという変数が使える

3.レイアウトのリンク

Contactページ

'test "should get contact" do
rails test`
- Contact用ページのルートとアクション,ビューを追加する。
- 全てのテストがグリーンであることを確認。

railsのルートURL

  • 一般的な規約に従い、基本的にはpath書式を使い、リダイレクトの場合のみurl書式を使うようにします

名前付きルート

  • link_toメソッドの2番目の引数で、適切な名前付きルートを使ってみましょう

リンクのテスト

  • これらのリンクが正しく動いているかどうかチェックするテストを書いてみましょう
  • 「統合テスト (Integration Test)」を使って一連の作業を自動化

4.ユーザー登録:最初のステップ

  • ユーザーのモデリングは第6章、ユーザ登録の完成は第7章。

Usersコントローラ

  • Usersコントローラを作成
  • $ rails generate controller Users new

ユーザー登録用URL

  • ユーザー登録URL用にget '/signup'のルートを追加

5.最後に

  • 以後サンプルアプリケーションを肉付けすることに専念します。最初に、ユーザー登録、サインイン、サインアウトできるユーザーを追加します。次に、マイクロポストを追加します。最後に、他のユーザーをフォローできるようにします。
  • Gitを使っている方は、この時点でmasterブランチに変更をマージしてください。

本章のまとめ

  • HTML5を使ってheaderやfooter、logoやbodyといったコンテンツのレイアウトを定義
  • Railsのパーシャルは効率化のために使われ、別ファイルにマークアップを切り出すことができます
  • CSSは、CSSクラスとidを使ってレイアウトやデザインを調整します
  • Bootstrapフレームワークを使うと、いい感じのデザインを素早く実装できる
  • SassとAsset Pipelineは、(開発効率のために切り分けられた) CSSの冗長な部分を圧縮し、本番環境に最適化した結果を出力する
  • Railsのルーティングでは自由にルールを定義することができ、また、その際に名前付きルートも使えるようになる
  • 統合テストは、ブラウザによるページ間の遷移を効率的にシミュレートする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsで一覧ページに[gem kaminari]を導入する流れについてまとめてみた。

kaminariの導入の仕方

1.Gemfileにkaminariを追加して保存。

Gemfile.
gem 'kaminari','~> 1.1.1'

2.kaminariをインストールする

$ bundle install

3.kaminariの設定ファイルを作成する

$ rails g kaminari:config

4.kaminariがページャで利用するテンプレートを作成する

$ rails g kaminari:views default

5.ページャを実装する

ページネーションさせたい場所に

app/views/books/index.html.erb
    <%= @books.each do |book| %>
        :
        :
    <% end %>
    <%= paginate @books %> <"←これを追加">
app/controllers/books_controller.rb
@books = Books.all.page(params[:page]).per(10) <"←.page以降追加">

この場合1ページの表示件数が10件まで表示される。

10件以上投稿するとこのように次のページへと分かれる。
スクリーンショット 2020-04-10 14.31.07.png

kaminariにBootstrapを適用させる

Bootstrapをすでに導入していれば簡単にkaminariに適用させることができる。

$ rails g kaminari:views bootstrap3

これでapp/views/kaminariフォルダにBootstrap用のViewが生成され、Bootstrap用のテンプレートに適用される。

参考サイト

https://qiita.com/ayies128/items/f5fec87df31b39d84f23

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

YouTubeっぽいコメント欄を作ってみよう!(返信機能追加)

はじめに

こんにちは!今回はYouTubeっぽいコメント欄を作ってみようと思います!
色々調べていたのですが、似たような記事がなかったので自力で実装した部分が多いです(><)
もっと良い方法があるかもしれないので、今後アップグレードしていければと思います。

それではいきましょう!

完成イメージ

exhoop_200420_reply.gif

ユーザーアイコンの実装については過去記事を参照ください。
https://qiita.com/naoki00m/items/6430ab0b62766b582c9a

開発環境

  • ruby 2.5.1
  • Rails 5.2.3
  • mysql
  • Haml 5.1.2
  • Ruby Sass 3.7.4

前提条件

  • 投稿に紐づくコメント機能が実装できていること(TECH CAMPでいうところのPictweetコメント機能)

実装内容

  • どのコメントに対する返信かを保存するカラムを追加
    (Commentテーブルにreplyカラムを追加)

  • コメントに対する返信フォームを作成
    (form_withを使用)

  • 返信フォームを表示するためのボタンを作成
    (FontAwesomeを使用)

  • 返信数をコメントの下に表示
    (replyカラム内の値を計算)

  • 返信数をクリックして返信を閲覧表示

テーブル

Comment テーブル

Column Type Options
user_id integer null: false
video_id integer null: false
text text null: false
reply integer

Assosiation

  • belongs_to :user
  • belongs_to :video

実装手順

1. どのコメントに対する返信かを保存するカラムを追加

Commentテーブルにreplyカラムを追加します。これはどのコメントに対する返信かを表すカラムです。
id:1のコメントに対してid:2で返信した場合、idが2のレコードにあるreplyカラムに1が保存されるといった具合です。

ターミナル.
$ rails g migration AddReplyToComment reply:integer

生成されたマイグレーションファイルを反映させましょう。

ターミナル.
$ rails db:migrate

2. コメントに対する返信フォームを作成 + 返信フォームを表示するためのボタンを作成

まずはビューの実装から!

今回はVideoのshowアクション内でコメントを表示しています。一般的にはPostのshowアクションと同義です。

show.html.haml
(省略)
- @comments.each do |comment|
  -# replyカラムがnull(空)の場合(返信ではない場合)
  - if comment.reply.blank?
    .content__show__comment__bottom__top
      .content__show__comment__bottom__top__user
        -# ユーザーアイコンを表示
        .content__show__comment__bottom__top__user__icon
          = link_to "/users/#{comment.user.id}" do
            - if comment.user.image.present?
              = image_tag comment.user.image.url, width: '100%'
            - else
              = image_tag('/images/default_user.jpg', width: '100%')
        -# ユーザー名を表示
        .content__show__comment__bottom__top__user__name
          = link_to "/users/#{comment.user.id}" do
            = comment.user.name
        -# 返信ボタン、削除ボタンを表示
        .content__show__comment__bottom__top__icons
          -# コメントした本人であれば返信ボタンと削除ボタンを表示
          - if user_signed_in? && current_user.id == comment.user.id
            .content__show__comment__bottom__top__icons__reply
              = link_to '#' do
                = icon('fa', 'reply', id: "reply-btn#{comment.id}")
            .content__show__comment__bottom__top__icons__delete
              = link_to video_comment_path(comment.video_id, comment.id), method: :delete do
                = icon('fa', 'trash-alt')
          -# ログインしているがコメントした人でない場合返信ボタンのみ表示
          - elsif user_signed_in? && current_user.id != comment.user.id
            .content__show__comment__bottom__top__icons__reply
              = link_to '#' do
                = icon('fa', 'reply', id: "reply-btn#{comment.id}")
          -# それ以外は何も表示しない
          - else
              = ''
  -# ユーザーがログインしている場合のみ返信できるようにする
  - if user_signed_in?
    .content__show__comment__bottom__reply
      -# idをcomment.idに応じて変化させる
      = form_with(model: [@video, @new_comment], local: true, id: "reply-form#{comment.id}") do |f|
        = f.text_area :text, placeholder: "Add a public reply...", id: "reply-text"
        -# 画面上には表示しないが、裏でreplyカラムに保存させるためにhidden_fieldを用いる!
        = f.hidden_field :reply, value: comment.id
        .content__show__comment__bottom__reply__btn
          .content__show__comment__bottom__reply__btn__cansel
            = 'Cansel'
          = f.submit "Reply", class: 'content__show__comment__bottom__reply__btn__send'

そしたらJqueryでコメントのフォームに動きをつけます!

  • 入力フォーム選択時にCansel, Replyボタンを表示する
  • Canselボタン選択時に入力フォームを非表示にする

ファイル名は◯◯.jsになっていればなんでもOKです!

video-comment.js
// 返信フォームの表示・非表示切り替え

$(function() {
  // replyという配列に1から1000までを順番に格納していく
  // 本当はビューやコントローラーからcomment.idを取ってきて格納できるのが理想
  var reply = [];
  for ( var i = 1 ; i <= 1000 ; i++ ) {
    reply.push(i);
  }
  // eachでreplyの中から1つずつ値を取り出しビューで定義したid名に付与
  $.each(reply, function(index, value) {
    // 入力フォームは初め隠しておく
    $("#reply-form" + value).hide();
    // 返信ボタンをクリックしたら入力フォームを表示
    $("#reply-btn" + value).on("click", function() {
      $("#reply-form" + value).show();
    });
    // Canselボタンをクリックしたら入力フォームを非表示に
    $(".content__show__comment__bottom__reply__btn__cansel").on("click", function() {
      $("#reply-form" + value).hide();
    });
  });
});

3. 返信数をコメントの下に表示

返信ができるようになったので試しに返信してみましょう!

テーブルのreplyカラムにidが保存されているか確認してください。
ビューに記載した「if comment.reply.blank?」を外すと返信も表示されますが、
コメントの下に紐づくような感じで表示はまだできていません。

replyカラム内の値を計算しよう!

さて、replyカラムに保存できるようになりましたが、コメントに対して何件の返信があるかを表示するには
コントローラーでこんな記述をする必要があります。

videos_controller.rb
class VideosController < ApplicationController
  (省略)

  def show
    (省略)
    @new_comment = Comment.new
    @comments = @video.comments.includes(:user)
    # Commentテーブルのreplyカラムの数をカウント
    @replies = Comment.group(:reply).count
  end
end

試しにビューファイルで下記のように記載してみるとこのように表示されます。

show.html.haml
= @replies

左側がreplyカラムの中身、右側がその合計数になります。
今回の例でいくとコメントが2件あり、コメント1に対する返信が3件、コメント2に対する返信が1件あるといった状況です。

{nil=>2, 1=>3, 2=>1}

この@repliesは配列の形になっているので下記のように書いてあげると合計数だけを取り出すことができます。

show.html.haml
= @replies[1]
3

これを用いて返信数を実装していきます!

show.html.haml
(省略)
.content__show__comment__bottom__view-btn
  -# 返信がなければ
  - if @replies[comment.id] == nil
    = ""
  -# 返信が1件なら
  - elsif @replies[comment.id] == 1
    = "View #{ @replies[comment.id] } reply"
  -# 返信が2件以上なら
  - else
    = "View #{ @replies[comment.id] } replies"

4. 返信数をクリックして返信を閲覧表示

最後に返信をそれぞれのコメントの下に表示できるようにしよう!

ここで重要となるのはコメントをeach doで繰り返し表示している中で
更にreplyを繰り返し表示させるということ。
こうすることでそれぞれのコメントの下に返信が紐づいているように見えます。

show.html.haml
-# 前の実装
- @comments.each do |comment|
  - if comment.reply.blank?
  (省略)
  -# ここから実装
  -# replyという変数を用意して、再度繰り返し表示
  - @comments.each do |reply|
    -# (重要)replyカラムがcomment.idと同じ場合のみ表示する
    - if reply.reply == comment.id
      .content__show__comment__bottom__view-reply
        .content__show__comment__bottom__view-reply__top
          .content__show__comment__bottom__view-reply__top__user
            .content__show__comment__bottom__view-reply__top__user__icon
              = link_to "/users/#{reply.user.id}" do
                - if reply.user.image.present?
                  = image_tag reply.user.image.url, width: '100%'
                - else
                  = image_tag('/images/default_user.jpg', width: '100%')
            .content__show__comment__bottom__view-reply__top__user__name
              = link_to "/users/#{reply.user.id}" do
                = reply.user.name
            .content__show__comment__bottom__view-reply__top__icons
              - if user_signed_in? && current_user.id == reply.user.id
                .content__show__comment__bottom__view-reply__top__icons__delete
                  = link_to video_comment_path(reply.video_id, reply.id), method: :delete do
                    = icon('fa', 'trash-alt')
              - else
                = ''
        .content__show__comment__bottom__view-reply__text
          // 改行を含んだテキストも表示できるようにsimple_formatを使用
          = simple_format(reply.text)

Jqueryで動きを加えて完了です!

video-comment.js
// 返信の表示・非表示切り替え
$(function() {
  $(".content__show__comment__bottom__view-reply").hide();
  $(".content__show__comment__bottom__view-btn").on("click", function() {
    // toggleにすると同じボタンを押したときに表示・非表示を切り替えてくれる
    $(".content__show__comment__bottom__view-reply").toggle();
  });
});

コード量は少し長いですが、ポイントを掴めれば実装できると思うので是非真似してみてください!

今後の追加実装予定

  • 返信を非表示にするときは「View」ではなく「Hide」にする
  • メンションをつけられるようにする
  • コメント・返信した際に通知としてストックされるようにする

参考

https://qiita.com/rockguitar67/items/4fd28cba0c243a8d0ba5
https://pikawaka.com/rails/count

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

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.16 - Deploy to EKS -

はじめに

こんにちは!今回でラストです!

今回はAWSのマネージドKubernetesサービスであるElastic Kubernetes Service(EKS)にデプロイしてみたいと思います。
今まで作ってきたRailsアプリコンテナをEKSで動かし、DBは同じくAWSのマネージドRDBサービスのRelational Database Service(RDS)を使います。
インフラ構築にはInfrastructure as CodeツールのTerraformを使ってみます。

あくまで「こいつ、動くぞ!」を目的にしているので、今回のハンズオンだけでこれらの全てを伝えるわけではありませんし、使いこなせるわけではありません。あくまでとっかかりとして捉えてみてください。
気になる方はどんどん調査して使ってみてください!

AWSを使うので、各自AWSアカウントは取得しておいてくださいね。

では、最後のハンズオンを始めます!

前回のソースコード

ここまでのソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

Kubernetes

Kubernetes(k8s)はコンテナオーケストレーションツールに位置付けられるOSSです。
コンテナオーケストレーションツールは、Dockerなどのコンテナ技術を使って作られたアプリケーションのデプロイ、スケーリング、サービスディスカバリー、負荷分散などなどを管理したり自動化したりできるもので、Docker単体だけでは難しかったコンテナの本番環境稼働を可能にしてくれます。(Docker単体ではボリュームやネットワークがサーバーと1対1だったり、サーバーとコンテナの関連の管理がおよそ人ではできなかったり困難がありました。)

コンテナオーケストレーションツールとしては、Kubernetesの他にもDocker SwarmMesosphereなどがありましたが、2020年現在、事実上Kubernetesがデファクトスタンダードとなっています。(参考:国内でDockerコンテナを本番利用している企業は9.2%、コンテナオーケストレーションツールはKubernetesがデファクト - ITmedia NEWS

マネージドKubernetes

KubernetesはOSSなので誰でも自分のサーバーで環境構築することができます。
ただ、すごいことをやってくれるので仕組みもけっこう複雑です。「Kubernetesがデファクトか!じゃあ構築するぞ!」みたいなノリではできないんじゃないかと思います。
Kubernetesでは、全体をクラスターと呼び管理しています。クラスターを管理する部分を「マスターコンポーネント」、アプリコンテナが稼働する部分を「ノードコンポーネント」と分けたりします。(参考:Kubernetesのコンポーネント - Kubernetes
特に「マスターコンポーネント」はクラスターのコントロールプレーンを担っておりとても重要かつ要素も複雑です。運用したくないです。

そこで大手クラウドベンダーはマネージドKubernetesサービスとして、利用者は主にノードコンポーネントの一部(インスタンス数とかボリュームとか)だけに関心を持っていればKubernetesを使えるサービスを展開しています。
AWSであればElastic Kubernetes Service(EKS)、GCPであればGoogle Kubernetes Engine(GKE)、Microsoft AzureであればAzure Kubernetes Service(AKS)です。

今回はEKSを使ってKubernetes上に作ってきたRailsアプリケーションをデプロイしてみましょう。

システム構成

network_diagram.png
Diagramsで描いてみました。(参考:Diagrams on Dockerでシステム構成図を書いてみた - Qiita

VPCの中でEKS(ノードコンポーネント)とRDSはPrivate Subnetに配置します。今回はあまり使う機会なしですが、ノードコンポーネントをPrivate Subnetにおいているのでインターネットと通信するためにPublic SubnetにNAT Gatewayをおきます。
また、Dockerイメージを管理するためにElastic Container Registry(ECR)を使います。

このシステム構成を構築し、EKSでPodをデプロイしていくために、Infrastructure as CodeツールのTerraform、AWSリソースをコマンドラインで操作するためのAWS CLI、Kubernetesを操作するためのkubectlを使います。
まずは、これらのツールを使うためのコンテナを作成して、その中でシステム構成を実現していきます。

システム構築、Kubernetes操作のためのコンテナを作る

これから、Railsアプリを公開するまでにやることは大まかに以下の流れです。

  1. Terraformでシステム構成図の通り必要なリソースを作成する
  2. ECRにRailsアプリのDockerイメージを登録する
  3. EKSにECRに登録したRailsアプリをデプロイする

今回はこの一連のデプロイ作業をするためのdeployコンテナを作って、その中でデプロイ作業を進めていこうと思います。

ディレクトリ構造を見直す

現状のホームディレクトリはRailsアプリのソースコードが置かれていますが、新たにrails/ディレクトリを作成し一階層したで管理するようにします。そしてrails/ディレクトリと同じ階層にdeploy/ディレクトリを作成し、そちらにRailsアプリ以外のデプロイに必要なファイルを作っていくことにします。

|-- rails_on_docker_handson
  |-- app/
  |-- bin/
  |-- ...
  |-- Gemfile
  |-- Gemfile.lock
  |-- Dockerfile
  |-- docker-compose.yml
  |-- .git/
  |-- .gitignore
  |-- ...

|-- rails_on_docker_handson
  |-- docker-compose.yml
  |-- .git/
  |-- .gitignore
  |-- rails/
  | |-- app/
  | |-- bin/
  | |-- ...
  | |-- Gemfile
  | |-- Gemfile.lock
  | |-- Dockerfile
  |
  |-- deploy/
    |-- Dockerfile
    |-- k8s/
    |-- terraform/

まずはこのディレクトリ・ファイル作成とファイル移動をやっていきましょう。

$ mkdir -p rails/ deploy/k8s/ deploy/terraform/
$ touch deploy/Dockerfile
$ mv `ls -a | egrep -v ".git|README.md|docker-compose.yml|rails|deploy"` rails

少しmvコマンドで複雑なことしてます。「``」のコマンドの実行結果を使ってmv [実行結果] railsが実行されます。
「``」で囲まれたコマンドは|が間に入っていますが、これは左側の結果に対して右側のコマンドを実行する時に使います。ls -aはカレントディレクトリのディレクトリ、ファイルを隠しファイル含めて表示するコマンドです。この結果からegrep-vオプションでその後に指定した文字列にマッチしない文字列を実行結果として返しています。
ま、そんなこんなでカレントディレクトリから.gitREADME.mddocker-compose.ymlrailsdeployにマッチしないディレクトリやファイルをrailsディレクトリに移動しました。

ディレクトリ構造を変更したので、docker-compose.ymlbuildvolumesの位置を更新します。

docker-compose.yml
  version: "3"

  services:
    db:
      image: postgres:12.1-alpine
      environment:
        - TZ=Asia/Tokyo
      volumes:
-       - ./tmp/db:/var/lib/postgresql/data
+       - ./rails/tmp/db:/var/lib/postgresql/data

    web:
-     build: .
+     build: rails/
      volumes:
-       - .:/app
+       - ./rails:/app
      ports:
        - 3000:3000
      depends_on:
        - db
      environment:
        - RAILS_SYSTEM_TESTING_SCREENSHOT=inline

これでファイルの移動は完了です。念のため、イメージをビルドしなおしてコンテナを起動させてみるといいかもしれません。

$ docker-compose build --no-cache web
$ docker-compose up -d

エラーなくサイトにアクセスできていたらOKです!

$ docker-compose down

デプロイ作業用のコンテナを作る

先にのべたデプロイ手順から、デプロイ作業用のコンテナは以下のことができなければなりません。

  1. terraformコマンドが使える
  2. awsコマンドが使える
  3. kubectlコマンドが使える
  4. dockerコマンドが使える(DockerイメージのビルドとECRへのpushのため)

Dockerコンテナ上でdockerコマンドを使うため、今回はDocker in Docker(dind)のDockerイメージをベースに各コマンドをインストールすることにします。

deploy/Dockerfile
FROM docker:dind

ENV HOME="/workspace"

WORKDIR ${HOME}

RUN apk update && \
    apk upgrade && \
    # Install terraform
    apk add --no-cache -q terraform && \
    # Install aws cli
    apk add --no-cache -q curl unzip python3 groff && \
    curl -sO https://bootstrap.pypa.io/get-pip.py && \
    python3 get-pip.py && \
    pip3 install awscli --upgrade && \
    rm get-pip.py && \
    # Install kubectl
    curl -s https://amazon-eks.s3-us-west-2.amazonaws.com/1.14.6/2019-08-22/bin/linux/amd64/kubectl -o kubectl && \
    chmod +x ./kubectl && \
    mv kubectl /usr/local/bin

terraformapk addでインストールしました。
awskubectlはそれぞれの公式の手順に従ってインストールしています。

docker-compose.ymlも更新します。

docker-compose.yml
- version: "3"
+ version: "3.7"

  services:
    ...
+   deploy:
+     build: deploy/
+     environment:
+       - AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
+       - AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
+       - AWS_DEFAULT_REGION=ap-northeast-1
+       - KUBECONFIG=/workspace/k8s/.kube/config
+     volumes:
+       - ./rails:/workspace/rails
+       - ./deploy/k8s:/workspace/k8s
+       - ./deploy/terraform:/workspace/terraform
+     privileged: true

privileged: trueオプションをつけることでDockerコンテナの中からローカルのDockerデーモンを使ってdockerコマンドを操作できるようになります。
このprivilegedはdocker-composeのversionが3.4以上じゃないと使えないので、最新の3.7を指定しています。

また、AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYはそれぞれ$AWS_ACCESS_KEY_IDRAWS_SECRET_ACCESS_KEYから取得するようにしています。$は環境変数を表していて同じディレクトリの.envファイルで定義することができます。
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYはセンシティブな値なので、docker-compose.ymlとは別で管理して間違ってGitで公開したりしないようにするのがオススメです。

$ touch .env
.env
AWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY_IDを記述する]
AWS_SECRET_ACCESS_KEY=[AWS_SECRET_ACCESS_KEYを記述する]

とりあえずAdministratorAccess権限を持っているIAMがあるといいです。まだ発行していない方は公式ドキュメントを参考に作成してください!

それではデプロイ操作コンテナをビルドして、コマンドが使えるようになっているかチェックしておきましょう。

$ docker-compose build deploy
$ docker-compose up -d deploy
$ docker-compose exec deploy ash
# docker -v
Docker version 19.03.8, build afacb8b7f0

# terraform version
Terraform v0.12.17

# aws --version
aws-cli/1.18.36 Python/3.8.2 Linux/4.19.76-linuxkit botocore/1.15.36

# kubectl version
Client Version: version.Info{Major:"1", Minor:"14+", GitVersion:"v1.14.7-eks-1861c5", GitCommit:"1861c597586f84f1498a9f2151c78d8a6bf47814", GitTreeState:"clean", BuildDate:"2019-09-24T22:12:08Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"}
The connection to the server localhost:8080 was refused - did you specify the right host or port?

# exit

ちょっとkubectlが怪しい感じですが今の段階ではクラスターと接続できていないのでエラーっぽい感じの表示がなされます。コマンド自体は使えているので問題ないです。

これでデプロイ作業コンテナの準備ができました。

Terraformでシステム構築する

最初に提示したシステム構成図をTerraformで実現していきます。

これにはいろいろな記事を参考にさせていただきました。

そして何よりもTerraformの公式ドキュメント(AWS)を読みました。

ではではTerraformの定義ファイルを作っていきましょう!

VPC周りを作る

まずはVPCなどの骨格となるネットワーク構成を作っていきます。

最初に、いろいろなところで共通的に使うことになる変数を定義してみます。

$ touch deploy/terraform/variables.tf
deploy/terraform/variables.tf
variable "project" {
  default = "handson"
}

variable "num_subnets" {
  default = 3
}

variable "eks_name" {
  default = "handson-eks"
}

このように定義しておくことで他のファイルからvar.projectといった感じで変数を呼び出すことができます。

あと、providerを定義しておく必要がある。今回はAWS。

$ touch deploy/terraform/provider.tf
deploy/terraform/provider.tf
provider "aws" {
  version = "~> 2.0"
}

では、VPC周りの定義ファイルを作成していきます。ここではVPCPublic SubnetPrivate SubnetInternet GatewayNAT Gateway(with Elastic IP)、Route TableRoute Table Associationを定義していきます。

$ touch deploy/terraform/vpc.tf
deploy/terraform/vpc.tf
data "aws_availability_zones" "available" {
  state = "available"
}

##############################
# VPC
##############################
resource "aws_vpc" "vpc" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "${var.project}-vpc"
  }
}

##############################
# Subnet
##############################
resource "aws_subnet" "public_subnet" {
  count                   = var.num_subnets
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = data.aws_availability_zones.available.names[ count.index % var.num_subnets ]
  cidr_block              = cidrsubnet(aws_vpc.vpc.cidr_block, 8, count.index)
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project}-public-subnet-${count.index+1}"
    "kubernetes.io/cluster/${var.eks_name}" = "shared"
  }
}

resource "aws_subnet" "private_subnet" {
  count                   = var.num_subnets
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = data.aws_availability_zones.available.names[ count.index % var.num_subnets ]
  cidr_block              = cidrsubnet(aws_vpc.vpc.cidr_block, 8, var.num_subnets + count.index)
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project}-private-subnet-${count.index+1}"
    "kubernetes.io/cluster/${var.eks_name}" = "shared"
  }
}

##############################
# Internet Gateway
##############################
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.project}-igw"
  }
}

##############################
# Elastic IP
##############################
resource "aws_eip" "nat" {
  vpc = true

  tags = {
    Name = "${var.project}-nat"
  }
}

##############################
# NAT Gateway
##############################
resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public_subnet.0.id

  tags = {
    Name = "${var.project}-nat"
  }
}

##############################
# Route table
##############################
resource "aws_route_table" "public_rtb" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${var.project}-public-rtb"
  }
}

resource "aws_route_table" "private_rtb" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_nat_gateway.nat.id
  }

  tags = {
    Name = "${var.project}-private-rtb"
  }
}

##############################
# Route table association
##############################
resource "aws_route_table_association" "rtba_public" {
  count           = var.num_subnets
  subnet_id       = element(aws_subnet.public_subnet.*.id, count.index)
  route_table_id  = aws_route_table.public_rtb.id
}

resource "aws_route_table_association" "rtba_private" {
  count           = var.num_subnets
  subnet_id       = element(aws_subnet.private_subnet.*.id, count.index)
  route_table_id  = aws_route_table.private_rtb.id
}

今回は「こいつ、動くぞ!」を目的にしているので、詳細は公式ドキュメントと見比べてみてください。
1点、aws_subnet.public_subnetaws_subnet.private_subnetのタグに"kubernetes.io/cluster/${var.eks_name}" = "shared"を入れています。これはEKSの公式ユーザーガイドに記載があるのですが、EKSがターゲットのサブネットをディスカバリーするために必須のタグです。お忘れなきよう!

これで環境の骨格ができましたので、次にEKSを定義してみます。

EKSを作る

EKSではマスターコンポーネントが動作するEKSクラスターとノードコンポーネントが動作するノードグループを作る必要があります。
それぞれ、Terraformのドキュメント(EKS ClusterEKS Node Group)に従えばさほど難しくありません。

$ touch deploy/terraform/eks.tf
deploy/terraform/eks.tf
##############################
# IAM Role for EKS Cluster
##############################
resource "aws_iam_role" "eks_iam_role" {
  name = "eks-iam-role"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "eks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "eks-AmazonEKSClusterPolicy" {
  policy_arn  = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role        = aws_iam_role.eks_iam_role.name
}

resource "aws_iam_role_policy_attachment" "eks-AmazonEKSServicePolicy" {
  policy_arn  = "arn:aws:iam::aws:policy/AmazonEKSServicePolicy"
  role        = aws_iam_role.eks_iam_role.name
}

##############################
# EKS Cluster
##############################
resource "aws_eks_cluster" "eks" {
  name      = var.eks_name
  role_arn  = aws_iam_role.eks_iam_role.arn

  vpc_config {
    subnet_ids  = concat(aws_subnet.public_subnet.*.id, aws_subnet.private_subnet.*.id)
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks-AmazonEKSClusterPolicy,
    aws_iam_role_policy_attachment.eks-AmazonEKSServicePolicy
  ]
}

##############################
# IAM Role for EKS Node Group
##############################
resource "aws_iam_role" "eks_node_group_iam_role" {
  name = "eks-node-group-iam-role"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "eks_node_group_AmazonEKSWorkerNodePolicy" {
  policy_arn  = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role        = aws_iam_role.eks_node_group_iam_role.name
}

resource "aws_iam_role_policy_attachment" "eks_node_group_AmazonEKS_CNI_Policy" {
  policy_arn  = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role        = aws_iam_role.eks_node_group_iam_role.name
}

resource "aws_iam_role_policy_attachment" "eks_node_group_AmazonEC2ContainerRegistryReadOnly" {
  policy_arn  = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role        = aws_iam_role.eks_node_group_iam_role.name
}

##############################
# EKS Node Group
##############################
resource "aws_eks_node_group" "eks_ng" {
  cluster_name    = aws_eks_cluster.eks.name
  node_group_name = "eks-ng"
  node_role_arn   = aws_iam_role.eks_node_group_iam_role.arn
  subnet_ids      = aws_subnet.private_subnet.*.id
  instance_types  = ["t2.small"]

  scaling_config {
    desired_size  = 3
    max_size      = 4
    min_size      = 2
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks_node_group_AmazonEKSWorkerNodePolicy,
    aws_iam_role_policy_attachment.eks_node_group_AmazonEKS_CNI_Policy,
    aws_iam_role_policy_attachment.eks_node_group_AmazonEC2ContainerRegistryReadOnly
  ]
}

EKSではクラスター、ノードグループそれぞれにIAM Roleを付与してあげる必要があるので少し複雑に見えるかもしれませんが、Terraformに沿って書けばさほど難しくありません。
aws_eks_node_group.eks_ng.instance_typesでノードグループのインスタンスタイプを指定してます。今回はサンプルですし小さめのt2.smallaws_eks_node_group.eks_ng.scaling_configではノードの最小数、最大数を定義しています。これに合わせてEKSがスケーリングしてくれるわけです。ちょっとdesired_sizemin_sizeの関係がわかっていないのですが...基本的にdesired_sizeでノードが展開されます。もしリソースが足りなくなったらmax_sizeまでオートスケールしてくれます。

RDSを作る

次はRDSです。

$ touch deploy/terraform/rds.tf
deploy/terraform/rds.tf
##############################
# Security Group for RDS
##############################
resource "aws_security_group" "rds" {
  vpc_id      = aws_vpc.vpc.id

  ingress {
    protocol        = "tcp"
    from_port       = 5432
    to_port         = 5432
    security_groups = [aws_eks_cluster.eks.vpc_config[0].cluster_security_group_id]
  }

  tags = {
    Name = "sg-${var.project}-rds"
  }
}

##############################
# DB Subnet for RDS
##############################
resource "aws_db_subnet_group" "default" {
  name  = "${var.project}-db-subnet-group"
  subnet_ids = aws_subnet.private_subnet.*.id

  tags = {
    Name = "${var.project}-db-subnet-group"
  }
}

##############################
# RDS
##############################
resource "aws_db_instance" "rds" {
  allocated_storage       = 20
  db_subnet_group_name    = aws_db_subnet_group.default.name
  engine                  = "postgres"
  engine_version          = "12.2"
  instance_class          = "db.t2.micro"
  username                = "handson_user"
  password                = "handson2020"
  port                    = 5432
  vpc_security_group_ids  = [aws_security_group.rds.id]
  skip_final_snapshot     = true

  tags = {
    Name = "${var.project}-db"
  }
}

Security GroupDB SubnetDB Instanceを定義しています。

Security Groupではaws_security_group.rds.ingress.security_groupsでEKSに設定されたセキュリティグループをIngressで許可しており、これをaws_db_instance.rds.vpc_security_groups_idでRDSに付与しています。こうすることでEKSのノードグループの上のPodからRDSにアクセスできるようになります。

そう言えばここまでPostgreSQLはversion12.1を使っていましたが、RDSでは12.2しか使えないようです...(参考:PostgreSQL on Amazon RDS - Amazon Relational Database Service
問題はないとは思いますが、念のためテストを回しておきましょう。

docker-compose.yml
  ...
  db:
-   image: postgres:12.1-alpine
+   image: postgres:12.2-alpine
    ...
$ docker-compose run web rspec

Finished in 2 minutes 11.5 seconds (files took 18.92 seconds to load)
85 examples, 0 failures

OKOK。

ECRを作る

最後にECRも作成しておきます。

$ touch deploy/terraform/ecr.tf
deploy/terraform/ecr.tf
##############################
# ECR for Rails app
##############################
resource "aws_ecr_repository" "ecr" {
  name  = "${var.project}_app"
}

これはレポジトリの名前をつけてあげてるだけですね。

TerraformでAWSリソースを構築する

ここまでで定義ファイルの準備が整いましたので、TerraformでAWSリソースを作成・構築していきます。
まず、再びデプロイ作業用のコンテナに入ります。

$ docker-compose exec deploy ash
# cd terraform
# terraform init
# terraform plan
# terraform apply

これだけです。planでファイルから設定するべき項目をプランニングし、applyで適用するという感じです。
applyの時に「Do you want to perform these actions?」と聞かれますがyesと答えましょう。

少し時間がかかりますが、Apply Complete!となれば環境構築は完了です!

RailsアプリをECRに登録する

次に、ECRにRailsアプリのDockerイメージを登録しようと思います。
EKSではproduction環境として動かしますし、RDSに接続できるように設定をできるようにしないといけません。

DBの接続情報はconfig/database.ymlで設定していましたね。
ということで、そのファイルでRAILS_ENVproductionの場合は接続情報を環境変数から設定できるようにしてみます。

rails/config/database.yml
  ...
  production:
    <<: *default
-   database: app_production
-   username: app
+   host: <%= ENV['APP_DATABASE_HOST'] %>
+   database: <%= ENV['APP_DATABASE_DATABASE'] %>
+   username: <%= ENV['APP_DATABASE_USERNAME'] %>
    password: <%= ENV['APP_DATABASE_PASSWORD'] %>

これでそれぞれの環境変数から接続情報が設定されるようになります。環境変数の指定はKubernetesのConfigMapを使ってやりますので、また後ほど。

また、Dockerfileもdevelopment環境とproduction環境では実行したいことが異なります。
例えば、development環境ではChromeブラウザがテスト用に必要ですが、production環境にはいりません。また、bundle installでインストールしたいgemにも差があります。
このような差分を同じDockerfileでできるように、docker buildコマンドのオプションで--build-argsを使って変数を送り込むことで動作を変えることができます。
具体的には、Dockerfileを以下のように更新します。(結構大きく変わるのでコピペしてください。)(参考:DockerFileにif文(条件分岐) - Qiita

rails/Dockerfile
ARG BUILD_MODE="dev"
FROM ruby:2.6.5-alpine3.11

ARG BUILD_MODE
ARG PROD_MODE="prod"
ARG RUNTIME_PACKAGES="gcc \
                      g++ \
                      less \
                      libc-dev \
                      libxml2-dev \
                      linux-headers \
                      make \
                      nodejs \
                      postgresql \
                      postgresql-dev \
                      tzdata \
                      yarn"
ARG BUILD_PACKAGES="build-base \
                    curl-dev"
ARG CHROME_PACKAGES="chromium \
                     chromium-chromedriver \
                     dbus \
                     mesa-dri-swrast \
                     ttf-freefont \
                     udev \
                     wait4ports \
                     xorg-server \
                     xvfb \
                     zlib-dev"

ENV HOME="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR $HOME

RUN apk update && \
    apk upgrade && \
    apk add --no-cache ${RUNTIME_PACKAGES} && \
    apk add --virtual build-packs --no-cache ${BUILD_PACKAGES} && \
    if [ "${BUILD_MODE}" != "${PROD_MODE}" ]; then \
        apk add --no-cache ${CHROME_PACKAGES}; \
    fi

COPY Gemfile ${HOME}
COPY Gemfile.lock ${HOME}

RUN if [ "${BUILD_MODE}" = "${PROD_MODE}" ]; then \
        bundle install --without development test -j4; \
    else \
        bundle install --without production -j4; \
    fi && \
    apk del build-packs

COPY . ${HOME}

RUN if [ "${BUILD_MODE}" = "${PROD_MODE}" ]; then \
        bundle exec rails assets:precompile RAILS_ENV=production; \
    else \
        yarn install; \
    fi

EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

BUILD_MODEprodかどうかによって挙動を分けています(デフォルトはdev)。ARGは変数で、docker buildの時に--build-arg [ARG_NAME]=[ARG_VALUE]で変数を外部から引き渡すこともできるやつです。なのでprodでビルドしたいときだけ--build-arg BUILD_MODE=prodと指定すればproduction用のビルドができるようになります。

さらに、productionには不要なファイルもあります。テストシナリオとか今までのログファイルとかは不要です。これをビルドする時に無視するためにdockerigonoreファイルを作成して不要なファイルを定義しておきます。

$ touch rails/.dockerignore
rails/.dockerignore
.local
.pki
log
node_modules
spec
tmp
.rspec
yarn-error.log

ざっとみた感じ、この辺りが今のところ不要かなー。

またここまででGitからダウンロードしたりした場合はconfig/master.keyがない状態だと思います。これだとproductionでビルドできないので生成します。

$ rm rails/config/credentials.yml.enc
$ docker-compose run -e EDITOR="mate --wait" web rails credentials:edit

こうすることでmaster.keyを再生成することができる。今回は活用していませんが、credentialsについて詳しくはいろいろと参照してください。(参考:Rails5.2から追加された credentials.yml.enc のキホン - Qiita

これで準備が整いました。イメージをビルドしていきます。
イメージをビルドするに際して、イメージ名を決める必要があります。これはECRに作成したリポジトリ名と同一でなくてはなりません。まず、awsコマンドを使って名前を確認しておきます。

# aws ecr describe-repositories

JSON形式で情報が表示されますが、このうちrepositoryUriがDockerイメージに名付けるべき名前です。またタグは1.0.0としておきます。

# cd /workspace/rails
# docker build -t [repositoryUri]:1.0.0 --build-arg BUILD_MODE=prod .

これでrepositoryUriの名前をつけて、production環境用にDockerイメージをビルドできました。

次にECRにログインします。

# aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [repositoryUri]

Login Succeeded

ログインができたらECRにDockerイメージを登録できるようになるので、今作成したDockerイメージをプッシュします。

# docker push [repositoryUri]:1.0.0

これでイメージの登録も完了です。

あとはEKS上にこのイメージをベースとしたPodをデプロイしていきます!

EKSにデプロイする

環境変数用のConfigMapを生成する

最初にデプロイするPod(コンテナ)に渡す環境変数をConfigMapリソースに保存します。

$ mkdir deploy/k8s/config
$ touch deploy/k8s/config/rails_config.yaml
deploy/k8s/rails_config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: rails-config
data:
  RAILS_ENV: production
  RAILS_SERVE_STATIC_FILES: "true"
  APP_DATABASE_HOST: [RDSのエンドポイント名]
  APP_DATABASE_DATABASE: app_production
  APP_DATABASE_USERNAME: handson_user
  APP_DATABASE_PASSWORD: handson2020

ConfigMapではこんな感じでデータを保存できるんですね。

RAILS_ENVはRailsアプリケーションをproductionモードで起動するための環境変数です。

RAILS_SERVE_STATIC_FILESassets:precompileしたCSS/JSファイルをpublicディレクトリから提供するための環境変数。何かしら設定されていれば有効になるので、今回はtrueの文字列を指定。(RAILS_SERVE_STATIC_FILESconfig/environments/production.rbに記述があります。)

rails/config/environments/production.rb
...
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
...

APP_DATABASE_*は先ほど、config/database.ymlにて指定した環境変数です。USERNAMEPASSWORDは先ほどterraform/rds.tfで定義したものです。DATABASEはこれからrails db:createで作成するものなので、好きな文字列で問題ありません。
HOSTは先ほどTerraformで作成したRDSのエンドポイント名を設定する必要があります。これもawsコマンドで確認してみましょう。

# aws rds describe-db-instances

またJSON形式で情報がアウトプットされたかと思いますが、このうちEndpoint.Addressがエンドポイント名ですので、これを設定します。

ConfigMapの宣言は以上です。

Jobリソースを宣言する

Railsアプリケーションを起動させる前にDBの作成とマイグレーションファイルの適用が必要です。
そのために1度だけ起動してコマンド発行されたら落ちるJobリソースを宣言して、Podをデプロイする前に実行しようと思います。

$ mkdir deploy/k8s/settings
$ touch deploy/k8s/settings/set_db_job.yaml
deploy/k8s/settings/set_db_job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: rails-db-setup
spec:
  template:
    metadata:
      name: rails-db-setup
    spec:
      containers:
      - name: rails-db-setup
        image: [RepositoryUri]:1.0.0
        imagePullPolicy: Always
        command: ["ash"]
        args: ["-c", "bundle exec rails db:create && bundle exec rails db:migrate"]
        envFrom:
        - configMapRef:
            name: rails-config
      restartPolicy: Never
  backoffLimit: 1

ファイルの書き方は公式ドキュメントなどを参考に。
spec.template.spec.containers.imageに先ほどECRにプッシュしたRailsアプリのDockerイメージを宣言しています。そのイメージを使ってbundle exec rails db:create && bundle exec rails db:migrateを実行することでDBの作成とマイグレーションの適用を行おうとしています。
また、envFormで先ほど宣言したConfigMap(rails-config)から環境変数を読み取っています。そこにはRDSのエンドポイントがAPP_DATABASE_HOSTとして定義されているので、RDSに対してDBの作成とマイグレーションの適用がなされることがわかりますね。
kindJobを宣言しているので、このリソースはコマンド実行が終わったら自動的に停止状態になります。

PodをDeploymentする

最後に実際に動作し続けるPodリソースの宣言ファイルを作ります。Podの宣言と言いましたが、KubernetesではPodリソースの数を宣言するReplicaSetリソースのデプロイ戦略を宣言するDeploymentリソースのファイルを適用することでPodをデプロイすることが一般的です。
また、外部と通信するためのServiceリソースも一緒に宣言しちゃいましょう。

$ touch deploy/k8s/deployment.yaml
deploy/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rails-deployment
  labels:
    app: rails
spec:
  replicas: 3
  selector:
    matchLabels:
      app: rails
  template:
    metadata:
      labels:
        app: rails
    spec:
      containers:
      - name: rails
        image: [RepositoryUri]:1.0.0
        imagePullPolicy: Always
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef:
            name: rails-config


---
apiVersion: v1
kind: Service
metadata:
  name: rails-service
spec:
  type: LoadBalancer
  selector:
    app: rails
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

---を挟んでDeploymentリソースとServiceリソースが宣言されているのがわかりますね。

Deploymentリソースの方ではJobリソースと似ていますが、spec.template.spec.containers.imageでECRのRailsアプリイメージをベースにし、envFormrails-configのConfigMapから環境変数を読み取り起動しようとしていることがわかります。
また、spec.replicas3にしているので、Podは3つ起動され、起動され続けるようにKubernetesに管理してもらいます。

Serviceリソースの方では、spec.typeLoadBalancerを宣言しています。これによってKubernetesのノードコンポーネントで外からの通信を許可することができます。
spec.selectorapp: railsを定義していますが、これはDeploymentリソースのlabelを指定しているものです。ポートは80ポートを受け取り、3000ポートに流していることが読み取れ、Deploymentリソースの方でcontainerPortとして3000を開けていることも読み取れます。

EKSではServiceリソースでLoadBalancerを作成すると、AWSのNLB(Network Load Balancer)が生成されます。これによって、NLBに付与されるドメインを通じてEKSで稼働しているRailsコンテナのPodにアクセスできるようになるわけです。

ここまででファイルの準備は揃いましたので、EKSに適用していきましょう。

デプロイ

デプロイするためにはまずデプロイするクラスターを指定する必要があります。awsコマンドを使ってこれをやってみます。
Terraformのファイルを見返していただきたいのですが、今EKSクラスターにはhandson-eksという名前がついています。

deploy/terraform/eks.tf
resource "aws_eks_cluster" "eks" {
  name      = var.eks_name
  ...
}
deploy/terraform/variables.tf
variable "eks_name" {
  default = "handson-eks"
}

デプロイするクラスターを指定するためにconfigを更新します。

# aws eks update-kubeconfig --name handson-eks

これでdocker-compose.ymlで定義したKUBECONFIGの場所にconfigファイルが生成されているはずです。

また、これでEKSクラスターを特定できるようになったので、例えばkubectl get nodesコマンドでノードグループのインスタンスの状態を確認することができるようになっているはずです。

# kubectl get nodes
NAME                                                 STATUS   ROLES    AGE     VERSION
ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal   Ready    <none>   HhMMm   v1.15.10-eks-bac369
ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal   Ready    <none>   3h10m   v1.15.10-eks-bac369
ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal   Ready    <none>   3h10m   v1.15.10-eks-bac369

では、ConfigMap -> Job -> Deployment/Serviceの順番で適用していきます。

Kubernetesではkubectl apply -f [file_name]でリソースを適用していくことができます。これはとても統一的でめちゃくちゃ便利です。変更があった場合も同様のコマンドでアップデートをかけることができます(一部をのぞき)。

ちなみに似たような感じでkubectl delete -f [file_name]でそのファイルで適用していたリソース(もっと言えばそのファイル内でラベル付されてるリソース)を削除することができます。

# kubectl apply -f k8s/config/rails_config.yaml
configmap/rails-config created

# kubectl get configmap
NAME           DATA   AGE
rails-config   6      81s

ConfigMapが作成されていることがわかります。もっと詳しく中身をみたい場合はkubectl describe cm rails-configでみれます。

次はJobです。

# kubectl apply -f k8s/settings/set_db_job.yaml
job.batch/rails-db-setup created

# kubectl get jobs -w
NAME             COMPLETIONS   DURATION   AGE
rails-db-setup   0/1           29s        30s
rails-db-setup   1/1           18s        36s

-wオプションは変化があった時に表示が更新されるモードです。Ctrl+Cで抜け出せます。
Jobもコンプリートしたことがわかりますね。

最後はDeploymentとServiceです。

# kubectl apply -f k8s/deployment.yaml
deployment.apps/rails-deployment created
service/rails-service created

# kubectl get pods -w
NAME                                READY   STATUS      RESTARTS   AGE
rails-db-setup-lb9cs                0/1     Completed   0          4m27s
rails-deployment-56b4695fdf-2jdgl   1/1     Running     0          2m17s
rails-deployment-56b4695fdf-5dxsf   1/1     Running     0          2m17s
rails-deployment-56b4695fdf-npf7b   1/1     Running     0          2m17s

先ほどのJobもPodとして動いていたのでStatus: Completedで残っていますね。他の3つがRailsアプリのPodです。Deploymentで宣言した通り3つ起動していますね。

# kubectl get service -w
NAME            TYPE           CLUSTER-IP       EXTERNAL-IP                PORT(S)        AGE
kubernetes      ClusterIP      xxx.xxx.xxx.xxx  <none>                     443/TCP        3h42m
rails-service   LoadBalancer   xxx.xxx.xxx.xxx  xxxxxxxxxx.amazonaws.com   80:31230/TCP   3m47s

今回生成したのはrails-serviceの方ですね。EXTERNAL-IPに書かれているドメインがNLBに付与されているドメインです。これがRailsアプリのPodに流してくれるはず。アクセスしてみましょう。

image.png

ドメインが反映されるまで少し時間がかかりますが、ちょっと待てば今まで作ってきたサイトが表示されました。
独自ドメインやHTTPS化を考えると、他にもいろいろとやらねばならぬことはある(例えば)のですが、ひとまずこれでEKS+RDSでRailsアプリを公開することができました!!!!

まとめ

最後の最後は少し難しめなお題、EKSでアプリをデプロイしてみように挑戦してみましたがいかがだったでしょうか?
実際にサービスとして本番環境で運用をしようとすると、いろいろと足りないところは多いのですが、ひとまず「こいつ、動くぞ!」というところには到達できたんじゃないかと思います。

今回でハンズオンは終わりです。Dockerから始まり、Railsアプリ、TDD、そしてHerokuやEKSでのサービス公開。
一通りのサービス開発の流れを体験して、「あれ、意外と調べながらやったりすればできそうだな。」というような感覚を持っていただけたなら幸いです。そうです。開発は選ばれた物にしかできない魔法ではない。学びです。

直接コーディングやデプロイに関わらないロールだとしても、サービス開発に携わっているならばこういったことを知っていることは確実に優位性になるでしょう!
もしさらなる興味が湧いてきたら、個人開発とかにも挑戦してみましょう!

ここまでお付き合いいただきありがとうございました!

後片付け

あ、今日はAWSを使っていろいろやりました。放置しとくとちゃりんちゃりんなのでちゃんと後片付けをしておきましょう。

まず確実にDeployment、SVCは落としておきましょう。ずっと公開されっぱなしになっちゃうので。

# kubectl delete -f k8s/deployment.yaml
deployment.apps "rails-deployment" deleted
service "rails-service" deleted

# kubectl delete -f k8s/settings/set_db_job.yaml
job.batch "rails-db-setup" deleted

# kubectl delete -f k8s/config/rails_config.yaml
configmap "rails-config" deleted

今回の環境を全部無かったことにする場合はTerraformで削除しちゃいましょう。

# cd /workspace/terraform
# terraform destroy
Do you really want to destroy all resources?

...

Destroy complete! Resources: 31 destroyed.

これまたyesと答えてあげれば削除が始まります。これまた結構時間がかかりますが、跡形もなく消してくれている様子がみて取れます。

本日のソースコード

Other Hands-on Links

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

【rails】Active Storage

Active Storage

active_storage_attachments

id name record_type record_id blob_id
1 avatar Taxonomy 3 1
10 eye_catch Article 13 10
14 og_image Site 1 14
17 favicon Site 1 17

record_type model名
name has_one_attachedの関連付け
record_id articlesテーブルのid
blob_id active_storage_blobsテーブルのid


record_type = Article
name = eye_catch

article.rb
has_one_attached :eye_catch

active_storage_blobs

説明部分のみ抜粋

id filename content_type
1 IMG_4837.jpg image/jpeg
10 サイバスター.jpg image/jpeg
14 ジャスティス.jpg image/jpeg
17 SSLプロトコルスタック.png image/png

filename ファイルの実体
content_type 拡張子のタイプ

site.rb
validates :og_image, attachment: { purge: true, content_type: %r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 }
validates :favicon, attachment: { purge: true, content_type: %r{\Aimage/png\Z}, maximum: 524_288_000 }

許可するcontent_typeを指定できる。

attached?

リファレンス

avatar.attached?で特定のuserがavatarを持っているかどうかを調べられる。

Current.user.avatar.attached?

variantメソッド

  • Variantを使うと新しい画像サイズが欲しくなった時に、オンデマンドで画像が生成される
# 200px * 200pxの画像にリサイズされた画像を表示
images.variant(resize: '200x200').processed

processedをつけることで、すでにそのサイズで保存されて画像があれば、変換処理は行われず、即時にURLが返される。

複数枚アップロード

レコードとファイルの間に1対多の関係を設定。
各レコードには、多数の添付ファイルをアタッチできる。

site.rb
has_many_attached :images

multiple: trueを設定

任意のview
= form_with model: [:admin, @site], url: admin_site_path do |f|
  = f.label :images
  = f.file_field :images, multiple: true

simple_form_for使用時は書式が変わるため気をつける
(検証ツールでmultipleが付与されているか確認すればこの点に気づける)

= simple_form_for [:admin, @site], url: admin_site_path do |f|
  = f.input :images, as: :file, input_html: { multiple: true }
def site_params
    params.require(:site).permit(:name, :favicon, :og_image, main_images: [])
end

空の配列にimagesが入る受け入れ準備。
multiple: trueにするとインターフェースが変わるため注意。

?‍♀️ params.require(:site).permit(:main_images)
?‍♂️ params.require(:site).permit(main_images: [])

active_storage_attachmentsからインスタンス取得

ActiveStorage.png

active_storage_attachmentsの主キーがわかっていれば
ActiveStorage::Attachment.find(params[:id])
attachmentsテーブルからインスタンスを取得できる。

site/edit.html.slim
- if @site.og_image.attached?
  = image_tag @site.og_image_url(:ogp), class: 'img-responsive'
  = link_to '削除', admin_site_attachment_path(@site.og_image.id), class: 'btn btn-danger',
    method: :delete

@site.og_image.idでog_imageのidを引数としてコントローラに渡すことで

sites_controller.rb
ActiveStorage::Attachment.find(params[:id])

そのIDを使用してAttachment全体から合致するインスタンスを取得することができる。
(渡ってきたIDはAttachmentテーブルのものでなければならない)

ActiveStorage_加工.png

今回@site.og_image.idで取ってきたIDは赤丸のID。
このIDと紐づいたblobsから情報を取得できる。

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

rbenvを使用したRubyのバージョン管理

この記事は、rbenvがインストールされている事を前提に書いてあります。
インストールに関しては他記事をご参照してください。

目的

Rubyのバージョンを変えずに作業していると、簡単なrbenvのバージョン管理方法を忘れてしまいがちなので、すぐに思い出せるように簡潔にまとめました。

Rubyのインストール

今回はバーション2.5.7のインストールします。
他のバーションをインストールするときは「2.5.7」の部分を書き換えてください。

ターミナル
# インストールできるRubyのバージョンを一覧表示
$ rbenv install -l

# Ruby 2.5.7をインストール
$ rbenv install 2.5.7

# インストールしたRubyのコマンドを使えるようにする
$ rbenv rehash

# 既にインストールされているバージョンの確認
$ rbenv versions

バージョンの変更

PCの全体で使うバージョンの設定

ターミナル
$ rbenv global 2.5.7

カレントディレクトリだけのバーションの設定

ターミナル
$ rbenv local 2.5.7

不要になったバージョンの削除

ターミナル
$ rbenv uninstall 2.5.7

Ruby インストール後にrailsコマンドが使えないとき

Rubyのインストール後に、Railsを使おうとすると「rails: command not found」と表示されると思います。
なので、変更したバージョンにRailsをインストールしましょう。

ターミナル
# gem のupdate
$ gem update --system
# bundler のinstall
$ gem install bundler
# rails のinstall
$ gem install rails

Railsインストール部分はこちらの記事を引用させていただきました。こちらの方が詳しく書いてあるのでおすすめです。

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