20210225のRubyに関する記事は25件です。

productionモードでサーバーを立ち上げるとcssが反映されない。

producition用のデータベース作成

rails db:create RAILS_ENV=production

アセットのプリコンパイルを行います

rails assets:precompile RAILS_ENV=production

サーバー起動

rails s -e production -b 0.0.0.0

...cssが反映されない。。

解決策

config/enviroments/production.rbの

config.assets.compile = false
config.assets.compile = true

に変更したらうまくいきました。
アセットファイルがないときにファイルを探して自動コンパイルしてくれる設定のようです。

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

test

良い記事を書くためのガイドライン
このガイドラインについて
Qiitaは、プログラミングに関する知識を記録・共有するためのサービスです。Qiita上の記事は、投稿者以外の人にとっても価値のある記事であることが望まれます。

あなたの知識が他の誰かの役に立つようにするために、このガイドラインを参考にしてください。

役に立つ記事を書くために
Qiitaに投稿した記事はフィードに表示され、他のユーザーが目にすることになります。また検索から記事を見つける人もいるでしょう。あなたの記事が読んだ人の役に立つかどうかを意識してみましょう。

自分用のメモのつもりで書いたものでも、少し気をつけるだけでぐっと価値のある記事にすることができます。

意識すると望ましいもの
具体的で簡潔なタイトルをつける
記事のタイトルは、記事のページだけでなくQiitaのフィードやGoogleの検索結果にも表示されます。タイトルを見ただけで記事の中身を想像できるようにしましょう。

具体的なタイトルをつけることで、特定の情報を探している人が記事を見つけやすくなります。

例:

「Rubyメモ」→「Rubyの定数探索はスコープと継承関係のどちらが優先されるか」
「正規表現でハマった」→「RubyとJavaScriptの正規表現では複数行モードの意味が違う」
「OpenSSLのバグ」→「CentOS 6.5でOpenSSLのHeartbleedバグにパッチを当てた」
環境や前提条件を説明する
記事で取り上げたソフトウェアのバージョンは何か、記事中のコードを実行するためにはどんな手順が必要かを書いておくと、自分の環境に当てはまるかどうかを後から読む人が判断しやすくなります。

例:

この記事ではCentOS 6.5にインストールしたRuby 2.2を使っています。
コードを実行するためには http://... で紹介されている設定ファイルが必要です。
コマンドライン引数に与えるユーザー名は適宜読み替えてください。
また、適切なタグをつけることで記事を見つけやすくなります。ある言語やライブラリの特定のバージョンについて話をするときは、タグにバージョンをつけてみましょう。例: Ruby:2.2

引用元や参考元へのリンクを書く
他のwebページから文章を引用したらそのページへのリンクを張りましょう。そのほか、記事の内容を理解する助けになるページがあればリンクすると親切です。

例:

色々な言語で配列の重複を削除する方法

uniq.pl
sub uniq {
my %seen;
grep !$seen{$_}++, @_;
}
say uniq(1, 1, 2, 3, 3);

(uniq関数は http://stackoverflow.com/q/7651/454997 より引用)

uniq.rb
p [1, 1, 2, 3, 3].uniq

(uniqにブロックを与える書き方は http://ruby-doc.org/core-2.2.0/Array.html を参照)
記事を書くうえで気をつけること
どんなに役に立つ記事であっても、最低限守らなければならないルールがあります。

著作権に気をつける
自分のものではない文章を引用したり画像を掲載したりするときは、著作権で認められた引用の範囲を超えないように注意しましょう。

ライセンスや秘密保持契約に気をつける
利用にあたっての条件がライセンスで定められている著作物を記事に含めるときは、そのライセンスに従うようにしましょう。

プレリリース版や開発者向けプレビュー版のソフトウェアの中には、秘密保持契約 (NDA; Non-Disclosure Agreement) によって情報の公開が制限されているものがあります。正式公開前のソフトウェアについての記事を書くときは契約を確認しましょう。

利用規約を守る
その他にも、Qiitaの利用規約ではサービスを利用するうえで行ってはならないことを定めています。規約に目を通し、違反することのないようにしましょう。

分からないことがあったら
このガイドラインの内容について不明な点があれば support@qiita.com までお気軽にお問い合わせください。

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

【 Ruby on Rails 6.0 】AWS + Nginx + Unicornでデプロイ⑥

始めに

前回の内容でブラウザ上でRailsアプリを起動するところまで実装出来ました。
今回はNginxというWebサーバーをインストールしてリクエストとレスポンスをインタラクティブに可能にしていきたいと思います。

目次

目次 内容
セクション1 EC2インスタンス作成
セクション2 Linuxサーバー構築
セクション3 データベース設定
セクション4 EC2上でGemをインストールし環境変数を設定
セクション5 Railsアプリを起動
セクション6 Nginxの導入(今回の内容)

Nginxの導入

Nginx(エンジン・エックス)とは、Webサーバの一種です。
ユーザーのリクエストに対して静的コンテンツの取り出し処理を行い、そして動的コ ンテンツの生成をアプリケーションサーバに依頼するためのものになります。 早速インストールしていきましょう。

Nginxをインストール

ターミナル(EC2)
# Nginxをインストール
[ec2-user@ip-172-31-25-189 ~]$ sudo amazon-linux-extras install nginx1

Nginxの設定ファイルを編集

次に、Nginxが正しく動くように設定しましょう。
Nginxの設定は設定項目X 設定値x;という形式で入力していきます。
これも先ほどと同様に、vimコマンドを使ってターミナル上で編集していきます。 /etc以下のファイルなので、強い権限でないと書き込み、保存ができません。そのため、コマンドの頭にsudoをつけています。

ターミナル(EC2)
[ec2-user@ip-172-31-25-189 ~]$ sudo vim /etc/nginx/conf.d/rails.conf

開けたら次のように編集します。

rails.conf
upstream app_server {
  # Unicornと連携させるための設定。
  # アプリケーション名を自身のアプリ名に書き換えることに注意。今回であればおそらく
  server unix:/var/www/〇〇〇〇〇〇<アプリケーション名>/tmp/sockets/unicorn.sock;
}

# {}で囲った部分をブロックと呼ぶ。サーバの設定ができる
server {
  # このプログラムが接続を受け付けるポート番号
  listen 80;
  # 接続を受け付けるリクエストURL ここに書いていないURLではアクセスできない
  server_name XX.XXX.XXX.XXElastic IP;

  # クライアントからアップロードされてくるファイルの容量の上限を2ギガに設定。デフォルトは1メガなので大きめにしておく
  client_max_body_size 2g;

# 接続が来た際のrootディレクトリ
  root /var/www/〇〇〇〇〇<アプリケーション名>/public;

# assetsファイル(CSSやJavaScriptのファイルなど)にアクセスが来た際に適用される設定
  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }

  error_page 500 502 503 504 /500.html;
}
  • 3行目の<アプリケーション名> となっている箇所は、ご自身のものに変更してください。
  • 11行目のとなっている箇所も同様に、ご自身のものに変更してください。
  • 14行目の<アプリケーション名>となっている箇所は、ご自身のものに変更してくだ さい。

nginxの権限を変更

POSTメソッドでもエラーが出ないようにするために、下記のコマンドも実行

ターミナル(EC2)
[ec2-user@ip-172-31-25-189 ~]$ cd /var/lib
[ec2-user@ip-172-31-25-189 lib]$ sudo chmod -R 775 nginx

これで、Nginxの設定が完了しました。

Nginxを起動して、設定ファイルを再読み込み

[ec2-user@ip-172-31-25-189 lib]$ cd ~
[ec2-user@ip-172-31-25-189 ~]$ sudo systemctl start nginx
[ec2-user@ip-172-31-25-189 ~]$ sudo systemctl  reload nginx

# ステータス確認
[ec2-user@ip-172-31-25-189 ~]$ sudo systemctl  status nginx
● nginx.service - The nginx HTTP and reverse proxy server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
   Active: active (running) since 日 2021-02-21 04:20:53 UTC; 32s ago

unicorn.rbを修正

次にNginxを介した処理を行うためにunicornの設定を修正します。

unicorn.rb(ローカル)
listen 3000

以下のように修正

listen "#{app_path}/tmp/sockets/unicorn.sock"

修正をしたら忘れずに、commitとpushをし、サーバ側で以下のコマンドを実行して修正点を反映させておきます。

ローカルの変更点を本番環境へ反映

ターミナル(EC2)
[ec2-user@ip-172-31-25-189 ~]$ cd /var/www/アプリ名

# 本番環境に反映
[ec2-user@ip-172-31-23-189 <アプリ名>]$ git pull origin master

Unicornを再起動

Unicornのプロセスをkillして、再起動する作業を行います。

ターミナル(EC2)
# プロセスを確認
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ ps aux | grep unicorn

ec2-user 17877  0.4 18.1 588472 182840 ?       Sl   01:55   0:02 unicorn_rails master -c config/unicorn.rb -E production -D
ec2-user 17881  0.0 17.3 589088 175164 ?       Sl   01:55   0:00 unicorn_rails worker[0] -c config/unicorn.rb -E production -D
ec2-user 17911  0.0  0.2 110532  2180 pts/0    S+   02:05   0:00 grep --color=auto unicorn

# 続いて、unicorn_rails master(一番上)のプロセスをkillします。
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ kill <確認したunicorn rails masterのPID上のコードでは17877>

# unicornを起動します
[ec2-user@ip-172-31-23-189 <アプリ名>]$ RAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -D

ブラウザで確認してみましょう

ブラウザからElastic IPでアクセスすると、アプリケーションにアクセスできます(:3000 をつける必要はありません)。なお、この時もunicornが起動している必要があります。

ローカルの変更を本番環境に反映させる手順まとめ

  • 開発環境でgit push
  • EC2インスタンスにSSHログイン
ターミナル(ローカル)
# ssh.ディレクトリで行う
ssh. $ ssh -i ダウンロードしたキーペア名.pem ec2-user@Elastic IPアドレス
  • ローカルの変更を本番環境に反映
ターミナル(EC2)
# アプリのディレクトリでに移動
[ec2-user@ip-172-31-23-189 <アプリ名>]$ cd /var/www/リポジトリ名

# 本番環境に反映
[ec2-user@ip-172-31-23-189 <アプリ名>]$ git pull origin master
  • アセットのコンパイル
ターミナル(EC2)
[ec2-user@ip-172-31-23-189 <アプリ名>]$ rails assets:precompile RAILS_ENV=production
  • Unicorn起動
ターミナル(EC2)
# プロセスを確認
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ ps aux | grep unicorn

ec2-user 17877  0.4 18.1 588472 182840 ?       Sl   01:55   0:02 unicorn_rails master -c config/unicorn.rb -E production -D
ec2-user 17881  0.0 17.3 589088 175164 ?       Sl   01:55   0:00 unicorn_rails worker[0] -c config/unicorn.rb -E production -D
ec2-user 17911  0.0  0.2 110532  2180 pts/0    S+   02:05   0:00 grep --color=auto unicorn

# 続いて、unicorn_rails master(一番上)のプロセスをkillします。
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ kill <確認したunicorn rails masterのPID上のコードでは17877>

# unicornを起動します
[ec2-user@ip-172-31-23-189 <アプリ名>]$ RAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -D

IPアドレスをうってブラウザで確認

IPアドレスにアクセスしてもエラーが出る時

エラーが出る時は以下の項目確認してみてください。

  • 502 but gateway とエラーが出る場合は、nginxのlogの確認が必要になります。
  • /var/log/nginx/error.logをlessまたはcatコマンドで確認する。
  • サーバ側で、 /var/www/<レポジトリ名>/log/unicorn.stderr.log を less またはcatコマンドで確認し、エラーが出ていないか確認する(下に行くほど最新のログです。時刻表記がUTCであることに注意してください)
  • Railsを起動しているか
  • EC2インスタンスの再起動を行ってみる(※本番環境にてmysqlとnginxの起動が必要です。)

終わりに

以上で本番環境でRailsアプリを起動・操作できると思います!
しかし、現状だとローカル環境で変更点が発生した時に本番環境に反映させるのが工数が多く大変です。
なので次回の記事でこのデプロイ作業を自動化するためCapistranoというツールを導入したいと思います。
お疲れさまでした。。。

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

【Railsチュートリアル】第10章 ユーザーの更新・表示・削除 10.2まで

はじめに

これまで未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させる。

10.1 ユーザーを更新する

PATCHリクエストに応答するupdateアクションを作成する。

10.1.1 編集フォーム

まず、Usersコントローラにeditアクションを追加して、それに対応するeditビューを実装する。
ユーザー編集ページのURLは/users/1/edit。ユーザーidはparams[:id]変数で取り出すことができる。

app/controllers/users_controller.rb
def edit
  @user = User.find(params[:id])
end

アクションを作成したら、ビューを作成する。
editビューは見た目はapp/views/users/new.html.erbと似ているが、HTMLソースに少し違いがある。

<form accept-charset="UTF-8" action="/users/1" class="edit_user"
      id="edit_user_1" method="post">
  <input name="_method" type="hidden" value="patch" />
    # 注目すべきは1つ上のコード↑
  .
  .
  .
</form>
<input name="_method" type="hidden" value="patch" />

URL/users/1PATCHでリクエストをするとユーザーを、Railsが既存のユーザーである(すでにDBに存在する)ということを区別し、更新をしてくれる。

演習 1

先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング(Phising)サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。

app/views/users/edit.html.erb
.
.
.
<div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="https://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>
  </div>
</div>

演習 2

リスト 10.5のパーシャルを使って、new.html.erbビュー(リスト 10.6)とedit.html.erbビュー(リスト 10.7)をリファクタリングしてみましょう(コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます3 。

リストに沿ってリファクタリングする。

10.1.2 編集の失敗

updateアクションを作成する。

def update
  @user = User.find(params[:id])
    # DBからparams[:id]でuserを検索し、@userに代入
    if @user.update(user_params)
      # 更新に成功した場合を扱う。
    else
      render 'edit'
        # falseの場合はeditビューに再レンダリング
  end
end

演習 1

編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。

確認のみなので省略

10.1.3 編集失敗時のテスト

エラーを検知するための統合テストを実装する。

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    # 編集が失敗するときのテスト
    get edit_user_path(@user)
      # 編集ページにアクセス
    assert_template 'users/edit'
      # editビューがレンダリングされるかどうか検証
    patch user_path(@user), params: { user: { name:  "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } }
        # 無効な情報を送信
    assert_template 'users/edit'
      # editビューが再レンダリングされるか検証
      # updateアクションがfalseのときは「render 'edit'」が実行されるため
  end
end

演習 1

リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。

test/integration/users_edit_test.rb
.
.
.
test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } }
    assert_template 'users/edit'
    assert_select "div.alert", "The form contains 4 errors."
  end

10.1.4 TDDで編集を成功させる

編集の成功に対するテストを実装する。

test/integration/users_edit_test.rb
class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit" do
    # 編集が成功するときのテスト
    get edit_user_path(@user)
      # 編集ページにアクセス
    assert_template 'users/edit'
      # editビューがレンダリングされるか検証
    name  = "Foo Bar"
      # nameを更新
    email = "foo@bar.com"
      # emailを更新
    patch user_path(@user), params: { user: { name:  name, email: email, password: "", password_confirmation: "" } }
      # 有効な情報を送信
    assert_not flash.empty?
      # flashメッセージが空かどうか
    assert_redirected_to @user
      # プロフィールページにリダイレクト
    @user.reload
      # リロードする
    assert_equal name,  @user.name
      # nameと@user.nameが同じかどうか
    assert_equal email, @user.email
      #emailと@user.emailが同じかどうか
  end
end

パスワードが空でも更新できるようにしているが、バリデーションがかかっているため、まだエラーになる。

app/models/user.rb
class User < ApplicationRecord
.
.
.
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
**.
.
.**
end

has_secure_passwordがオブジェクト生成時に存在性を検証するため、新規ユーザー登録時に空のパスワードが有効になることは無い。

演習 1

実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。

確認のみなので省略。

演習 2

もしGravatarと紐付いていない適当なメールアドレス(foobar@example.comなど)に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみましょう。

初期設定のアイコン?が表示される。

10.2 認可

ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御する。

10.2.1 ユーザーにログインを要求する

Usersコントローラの中でbeforeフィルターを使う。beforeフィルターは、before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みのこと。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
    # editアクション、updateアクションが呼び出されたら、logged_in_userアクションを実行する。
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        # ログインしているか?
        flash[:danger] = "Please log in."
          # falseのときはflashを表示
        redirect_to login_url
          # ログインページへリダイレクト
      end
    end
end
unless 条件式
  条件式が偽の時に実行する処理
end

unless文は条件式が偽の場合の処理を記述するのに使われる。

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
      # editページにアクセス
    assert_not flash.empty?
      # flashが表示されていないか?
    assert_redirected_to login_url
      # ログインページにリダイレクトされたかどうか?
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
      # @user情報を送信
    assert_not flash.empty?
      # flashが表示されていないか?
    assert_redirected_to login_url
      # ログインページにリダイレクトされたかどうか?
  end
end

演習 1

デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです(結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか(テストが失敗するかどうか)確かめてみましょう。

確認のみなので省略。

10.2.2 正しいユーザーを要求する

ユーザーが自分の情報だけを編集できるようにする。

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user)
      # テストユーザー(:archer)としてログイン
    get edit_user_path(@user)
      # michaelのeditビューにアクセス
    assert flash.empty?
      # flashが表示されて
    assert_redirected_to root_url
      # root_urlにリダイレクトされる
  end

  test "should redirect update when logged in as wrong user" do
    log_in_as(@other_user)
      # テストユーザー(:archer)としてログイン
    patch user_path(@user), params: { user: { name: @user.name, email: @user.email } }
      # michaelの情報を更新しようとする
    assert flash.empty?
      # flashが表示されて
    assert_redirected_to root_url
      # root_urlにリダイレクトされる
  end
end

別のユーザーのプロフィールを編集しようとしたらリダイレクトさせたいので、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにする。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
    # editアクション、updateアクションが呼び出されたらcorrect_userアクションを実行する
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
        # 受け取ったログイン情報を@userに代入
      redirect_to(root_url) unless current_user?(@user)
        # @userと現在ログインしているユーザーが違う場合はroot_urlにリダイレクトさせる
    end
end

演習 1

何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。

他のユーザーの個人情報の表示、更新ができてしまうから。

演習 2

上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?

editアクション。viewが定義されているから。

10.2.3 フレンドリーフォワーディング

リダイレクト先をユーザーがアクセスしたかったページにする。

編集ページにアクセスし、ログインした後に、(デフォルトのプロフィールページではなく)編集ページにリダイレクトされているかどうかをチェックするテスト。

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit with friendly forwarding" do
    # フレンドリーフォワーディングのテスト
    get edit_user_path(@user)
      # ログインがまだの状態で、editビューにアクセス
    log_in_as(@user)
      # テストユーザー(michael)でログイン
    assert_redirected_to edit_user_url(@user)
      # editビューにリダイレクト
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要がある。

app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # 記憶したURL(もしくはデフォルト値)にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
      # 送られてきたURLがnilでなければ左側を評価する。
    session.delete(:forwarding_url)
      # 転送用のURLを削除
  end

  # アクセスしようとしたURLを覚えておく
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
      # もしGETリクエストが送られてきたらoriginal_urlをsession[:forwarding_url]に代入する
  end
end

演習 1

フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト(プロフィール画面)に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    assert_equal session[:forwarding_url], edit_user_url(@user)
    log_in_as(@user)
    assert_nil session[:forwarding_url]
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

演習 2

7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください(デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう(デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって(コラム 1.2)、落ち着いて対処してみましょう)。

[1, 10] in /home/vagrant/work/sample_app2/app/controllers/sessions_controller.rb
    1: class SessionsController < ApplicationController
    2: 
    3:   # GET /login
    4:   def new
    5:     debugger
=>  6:   end
    7: 
    8:   # POST /login
    9:   def create
   10:     @user = User.find_by(email: params[:session][:email].downcase)
(byebug) session[:forwarding_url]
"http://localhost:3000/users/1/edit"
(byebug) request.get?
true
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

第9章 発展的なログイン機構

はじめに

永続cookie(permanent cookies)を使って[remember me]を実装する。

9.1 Remember me 機能

ユーザーのログイン状態をブラウザを閉じた後でも有効にする[remember me]機能を実装していく。

9.1.1 記憶トークンと暗号化

Cookiesの場合

ブラウザ(cookie)に暗号化したパスワードとDBに入っているハッシュ化しているパスワードが一致するかRailsで認証する。
下記の方針で永続的セッションを作成する。
1. 記憶トークンを保存する場所を用意する。
2. 記憶トークンにはランダムな文字列を生成して用いる。
3. ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。
4. トークンはハッシュ値に変換してからデータベースに保存する。
5. ブラウザのcookiesに保存するユーザーIDは暗号化しておく。
6. 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。

db/migrate/[timestamp]_add_remember_digest_to_users.rb
class AddRememberDigestToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :remember_digest, :string
  end
end

記憶トークン用のカラムを用意。string(文字列)のremember_digest属性を追加する。

app/models/user.rb
# ランダムなトークンを返す
def User.new_token
  SecureRandom.urlsafe_base64
end

A–Z、a–z、0–9、"-"、""のいずれかの文字(64種類)からなる長さ22のランダムな文字列を返すクラスメソッド`User.newtoken`を作成する。

マイグレーションは実行済みなので、Userモデルには既にremember_digest属性が追加されているが、remember_token属性はまだ追加されていない。attr_accessorを使って「仮想の」属性を作成する。

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token

  .
  .
  .

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
      # 自分自身のremember_tokenに新しいtokenを代入する。保存されない
    update_attribute(:remember_digest, User.digest(remember_token))
      # :remember_digestにremember_tokenをハッシュ化したものを保存する。
      # 頭のselfが省略されている
  end
end

user.remember_tokenメソッドを使ってトークンにアクセスできるようにし、かつ、トークンをDBに保存せずに 実装する。

演習 1

コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。

>> user = User.first
   (1.9ms)  SELECT sqlite_version(*)
  User Load (1.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2021-02-13 18:10:29", updated_at: "2021-02-13 18:10:29", password_digest: [FILTERED], remember_digest: nil>

>> user.remember
   (0.1ms)  begin transaction
  User Update (8.2ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2021-02-23 08:22:41.315576"], ["remember_digest", "$2a$12$y6wygDQ4HwYF6smxv8E1Y.K6Nz.3tNhCHpJVEDKHOBFs9MBu/NuLe"], ["id", 1]]
   (14.1ms)  commit transaction
=> true

>> user.remember_token
=> "JTiObGhfDhCqI-mYx7jGrw"

>> user.remember_digest
=> "$2a$12$O6z7y1SMzDbXqIBa5OoYrOXv4nKB132fjA4WQ7MQxubweRSg/5nYa"

演習 2

リスト 9.3では、明示的にUserクラスを呼び出すことで、新しいトークンやダイジェスト用のクラスメソッドを定義しました。実際、User.new_tokenやUser.digestを使って呼び出せるようになったので、おそらく最も明確なクラスメソッドの定義方法であると言えるでしょう。しかし実は、より「Ruby的に正しい」クラスメソッドの定義方法が2通りあります。1つはややわかりにくく、もう1つは非常に混乱するでしょう。テストスイートを実行して、ややわかりにくいリスト 9.4の実装でも、非常に混乱しやすいリスト 9.5の実装でも、いずれも正しく動くことを確認してみてください。ヒント: selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、リスト 9.4やリスト 9.5の文脈では、selfはUser「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。

確認だけなので省略

9.1.2 ログイン状態の保持

cookiesメソッドを使い、永続セッションを作成する。

value(値)とオプションのexpires(有効期限)が必要。有効期限は省略できる。

cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }
cookies.permanent[:remember_token] = remember_token
 # Railsの20年で期限切れになるcookies設定
app/helpers/sessions_helper.rb
# ユーザーのセッションを永続的にする
  def remember(user)
    # 必ず引数を設定する
    user.remember
     # DBに書き込む
    cookies.permanent.signed[:user_id] = user.id
      # cookieに暗号化したuser.idを代入する(20年で期限切れになる)
    cookies.permanent[:remember_token] = user.remember_token
      # cookieにuser.remember_tokenを代入する(20年で期限切れになる)
  end

signed: 暗号化、復号化するときのメソッド

app/models/user.rb
def authenticated?(remember_token)
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
    # ハッシュ値remember_digestと平文remember_tokenがあっているかBCryptがチェック
end
app/helpers/sessions_helper.rb
def current_user
  if (user_id = session[:user_id])
    # session[:user_id]をuser_idに代入してnilかどうか確認
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    # cookies.signed[:user_id]をuser_idに代入してnilかどうか確認
    user = User.find_by(id: user_id)
      # 
    if user && user.authenticated?(cookies[:remember_token])
      # nilかどうか確認(左側)して、引数にcookies内の:remember_tokenを引数にuser.authenticatedする
      log_in user
      @current_user = user
    end
  end
end

session[:user_id]cookies.signed[:user_id]もnilの場合はnilを返す。

演習 1

ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう。

確認のみなので省略

演習 2

コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。

確認のみなので省略

9.1.3 ユーザーを忘れる

ユーザーがログアウトできるようにする。

user.forgetメソッドでuser.rememberが取り消される(nilで更新する)

app/models/user.rb
# ユーザーのログイン情報を破棄する
def forget
  self.update_attribute(:remember_digest, nil)
    # nilで更新する。削除する。
end
app/helpers/sessions_helper.rb
# 永続的セッションを破棄する
def forget(user)
  user.forget
    # remember_digestを削除
  cookies.delete(:user_id)
    # user_idを削除
  cookies.delete(:remember_token)
    # remember_tokenを削除
end

# 現在のユーザーをログアウトする
def log_out
  forget(current_user)
    # cookie情報を削除
  session.delete(:user_id)
  @current_user = nil
end

演習 1

ログアウトした後に、ブラウザの対応するcookiesが削除されていることを確認してみましょう。

確認のみなので省略

9.1.4 2つの目立たないバグ

ユーザーがタブを複数開いているとき、複数のブラウザでログインしているときにそれぞれバグが発生する。

前者はユーザーがログイン中の場合にのみログアウトさせる必要があり、後者はremember_digestが存在しないときはfalseを返す処理をauthenticated?に追加する必要がある。

# 記憶トークンcookieに対応するユーザーを返す
def current_user
  if (user_id = session[:user_id])
    # log_outメソッドによってfalseになる
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    # log_outメソッドによってfalseになる
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      # 左側の条件式でエラーが出る
      log_in user
      @current_user = user
    end
  end
end

#current_userメソッドの評価はnil
test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end

演習 1

リスト 9.16で修正した行をコメントアウトし、2つのログイン済みのタブによるバグを実際に確かめてみましょう。まず片方のタブでログアウトし、その後、もう1つのタブで再度ログアウトを試してみてください。

確認のみなので省略

演習 2

リスト 9.19で修正した行をコメントアウトし、2つのログイン済みのブラウザによるバグを実際に確かめてみましょう。まず片方のブラウザでログアウトし、もう一方のブラウザを再起動してサンプルアプリケーションにアクセスしてみてください。

確認のみなので省略

演習 3

上のコードでコメントアウトした部分を元に戻し、テストスイートが red から green になることを確認しましょう。

確認のみなので省略

9.2 [Remember me]チェックボック

params[:session][:remember_me] == '1' ? remember(user) : forget(user)

もし、params[:session][:remember_me]が「1」だったら(チェックボックスにチェックが入っていたら)、ログイン情報を記憶するためにrememberメソッドを呼び出す。「1」でなかったら記憶しないのでforgetメソッドを呼び出す。

三項演算子

if boolean?
  (true)var = foo
else
  (false)var = bar
end

三項演算子を使うと下記のようになる。

var = boolean? ? (true) : (false)

演習 1

ブラウザでcookies情報を調べ、[remember me]をチェックしたときに意図した結果になっているかどうかを確認してみましょう。

確認のみなので省略。

演習 2

コンソールを開き、三項演算子を使った実例を考えてみてください(コラム 9.2)。

> def first(type)
>   type = "fire" ? "ヒトカゲ" : "フシギダネ"
> end  
=> :type
> first("fire")
=> "ヒトカゲ"

# ゼニガメごめんね

9.3 [Remember me]のテスト

test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_not_empty cookies[:remember_token]
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '1')
      # cookieを保存してログイン
      # remember me チェックする
    delete logout_path
      # ログアウトする
    log_in_as(@user, remember_me: '0')
      # cookieを削除してログイン
      # remember me チェックしない
    assert_empty cookies[:remember_token]
      # cookiesには情報が入ってないはず
  end

演習 1

リスト 9.25の統合テストでは、仮想のremember_token属性にアクセスできないと説明しましたが、実は、assignsという特殊なテストメソッドを使うとアクセスできるようになります。コントローラで定義したインスタンス変数にテストの内部からアクセスするには、テスト内部でassignsメソッドを使います。このメソッドにはインスタンス変数に対応するシンボルを渡します。例えばcreateアクションで@userというインスタンス変数が定義されていれば、テスト内部ではassigns(:user)と書くことでインスタンス変数にアクセスできます。本チュートリアルのアプリケーションの場合、Sessionsコントローラのcreateアクションでは、userを(インスタンス変数ではない)通常のローカル変数として定義しましたが、これをインスタンス変数に変えてしまえば、cookiesにユーザーの記憶トークンが正しく含まれているかどうかをテストできるようになります。このアイデアに従ってリスト 9.27とリスト 9.28の不足分を埋め(ヒントとして?やFILL_INを目印に置いてあります)、[remember me]チェックボックスのテストを改良してみてください。17

app/controllers/sessions_controller.rb
def create
    @user = User.find_by(email: params[:session][:email].downcase)
    # if user && user.authenticate(params[:session][:password])
    if @user && @user.authenticate(params[:session][:password])
      log_in @user
      params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
      redirect_to @user
    else
      # alert-danger => 赤色のフラッシュ
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
      # GET /users/1 => show template
      #                 render 'new'(0回目)
    end
  end
test/integration/users_login_test.rb
test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal cookies[:remember_token], assigns(:user).remember_token
  end

9.3.2 [Remember me]をテストする

raiseメソッド: 例外(わざとバグ)を発生させるメソッド。きちんとテストがされているか確認する。

require 'test_helper'

class SessionsHelperTest < ActionView::TestCase

  def setup
    @user = users(:michael)
      # michaelを@userに代入
    remember(@user)
      # @userの情報をrememberに入れる
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
      # @userとcurrent_user(ログインしているuser)が一致しているかどうか
    assert is_logged_in?
      # ログインしているかどうか
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
      # @userのremember_digestを新しいものに書き換える
    assert_nil current_user
      # 新しいものに書き換えたのでcurrent_userはnilを返しているはず
  end
end

演習 3

リスト 9.33にあるauthenticated?の式を削除すると、リスト 9.31の2つ目のテストで失敗することを確かめてみましょう(このテストが正しい対象をテストしていることを確認してみましょう)。

確認のみなので省略。

さいごに

第9章からほぼ理解できないまま飛ばす箇所が出てきました。
Railsチュートリアルを最後まで進めたら戻ってこようと思います。

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

Rubyについて

本日はRubyについて。。。
志望している会社でRubyに力を入れていらっしゃったので、復習というより知識をつけようと。

Rubyって何?

・プログラミング言語
・Webアプリ開発に特化
・DBと繋がりやすい
・幅広く使われている
・文法が覚えやすい

ってな感じ。

そもそもプログラムって実行しなきゃメモ用紙なのよねえ。=Excel的な。

Ruby on Railsって何?

最初、「どっちもRubyやん。違いが分からん」という感じでした。

結論は、「Rubyのフレームワークの一つ」ということ。。
フレームワークとは、要は「頻度の高いツールのハッピーセット」的なものだと考えてる。
それぞれを単品で細かく頼むより楽だし、早いし。
つまり、Ruby(?)のハッピーセットがRuby on Railsって名前で売ってある的な。わかりずらいか。

もちろんRubyのフレームワークは他にも「Sinatra」や「HANAMI」なんて可愛い名前のものもある。
それぞれ特徴があるが、一番使われているのが「Ruby on Rails」とされているんだと。

参考はこちら
https://eng-entrance.com/ruby-framework

まとめ

Railsっていう、Webアプリが作りやすくてみんな使ってる言語の一つで、ツールがあらかじめセットになってるものがRuby on Rails。

つまり、Railsハンバーガーのハッピーセット(おもちゃがRails)を頼んでるってことね。

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

[Rails6] Sprockets::Rails::Helper::AssetNotPrecompiled in エラーが出た

結論

app/assets/config/manifest.jsファイルにコードを一行追加したら解決しました。

app/assets/config/manifest.js
//= link_tree ../images
//= link_directory ../stylesheets .css

#以下を追記
//= link application.css

環境

  • macOS Catalina
  • Ruby 2.7.2
  • Rails 6.0.3

実際には下記の記事通りに環境構築をしています

[Docker] Ruby2.7.2 / Rails6.0.3 / MySQL8.0の開発環境構築できたメモ

参考

このサイトで「rails 6」でページ検索して出てきた解決案のひとつがうまくいきました!

Stack Overflow】Rails: Sprockets::Rails::Helper::AssetNotPrecompiled in development

他に試したこと

  • config/initializers/assets.rbファイルに追記

    1. Rails.application.config.assets.precompile += %w( application.css )この一行を追記
    2. rails assets:precompile コマンド実行後サーバー再起動
  • 画像を再取り込み?(SVGリンク切れ?)

いくつか試しましたが、Railsのバージョン違いなのか、これではエラー解消できませんでした。
(前提としては、タイトルのエラー文が出て、画像表示がうまく動作していなかったようです)

さいごに

Railsガイド - アセットパイプライン

原因はこれかな?
このあたりを読めば、仮説検証を考えながら解決できそう。

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

[Rails6]Sprockets::Rails::Helper::AssetNotPrecompiled in エラーが出た

結論

app/assets/config/manifest.jsファイルにコードを一行追加したら解決しました。

app/assets/config/manifest.js
//= link_tree ../images
//= link_directory ../stylesheets .css

#以下を追記
//= link application.css

環境

  • macOS Catalina
  • Ruby 2.7.2
  • Rails 6.0.3

実際には下記の記事通りに環境構築をしています

[Docker] Ruby2.7.2 / Rails6.0.3 / MySQL8.0の開発環境構築できたメモ

参考

このサイトで「rails 6」でページ検索して出てきた解決案のひとつがうまくいきました!

Stack Overflow】Rails: Sprockets::Rails::Helper::AssetNotPrecompiled in development

他に試したこと

  • config/initializers/assets.rbファイルに追記

    1. Rails.application.config.assets.precompile += %w( application.css )この一行を追記
    2. rails assets:precompile コマンド実行後サーバー再起動
  • 画像を再取り込み?(SVGリンク切れ?)

いくつか試しましたが、Railsのバージョン違いなのか、これではエラー解消できませんでした。
(前提としては、タイトルのエラー文が出て、画像表示がうまく動作していなかったようです)

さいごに

Railsガイド - アセットパイプライン

原因はこれかな?
このあたりを読めば、仮説検証を考えながら解決できそう。

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

RubyでシンプルにAPIリクエストを行うための備忘録

Ruby の net/https にて、簡単に API のリクエストをしたいと思ったとき、
書き方がいくつもあるようで... 私が一番気に入っているコードを以下にメモしておきます。

GETリクエストの例

#! /usr/bin/env ruby
#-*- coding: utf-8 -*-

require 'net/https'
require 'json'

uri = URI.parse "https://qiita.com/api/v2/..."
req = Net::HTTP.new(uri.host, uri.port)
req.use_ssl = true

res = req.get(uri.request_uri)
arr = JSON.parse(res.body)

リクエストヘッダーを追加する

#! /usr/bin/env ruby
#-*- coding: utf-8 -*-

require 'net/https'
require 'json'

uri = URI.parse "https://qiita.com/api/v2/..."
req = Net::HTTP.new(uri.host, uri.port)
req.use_ssl = true

res = req.get(uri.request_uri, {
    "Accept-Language" => "ja",
    "Content-Type" => "application/json"
})
arr = JSON.parse(res.body)

クエリ文字列を追加する

#! /usr/bin/env ruby
#-*- coding: utf-8 -*-

require 'net/https'
require 'json'

uri = URI.parse "https://qiita.com/api/v2/..."
uri.query = URI.encode_www_form({
    "id" => "c686397e4a0f4f11683d"
})
req = Net::HTTP.new(uri.host, uri.port)
req.use_ssl = true

res = req.get(uri.request_uri)
arr = JSON.parse(res.body)

POSTリクエストの例

#! /usr/bin/env ruby
#-*- coding: utf-8 -*-

require 'net/https'
require 'json'

uri = URI.parse "https://qiita.com/api/v2/..."
req = Net::HTTP.new(uri.host, uri.port)
req.use_ssl = true

res = req.post(uri.request_uri, {
    "rendered_body" => "<h1>Example</h1>",
    "body" => "# Example"
}.to_json)
arr = JSON.parse(res.body)

リクエストヘッダーを追加する

#! /usr/bin/env ruby
#-*- coding: utf-8 -*-

require 'net/https'
require 'json'

uri = URI.parse "https://qiita.com/api/v2/..."
req = Net::HTTP.new(uri.host, uri.port)
req.use_ssl = true

res = req.post(uri.request_uri, {
    "rendered_body" => "<h1>Example</h1>",
    "body" => "# Example"
}.to_json, {
    "Accept-Language" => "ja",
    "Content-Type" => "application/json"
})
arr = JSON.parse(res.body)

クエリ文字列を追加する

#! /usr/bin/env ruby
#-*- coding: utf-8 -*-

require 'net/https'
require 'json'

uri = URI.parse "https://qiita.com/api/v2/..."
uri.query = URI.encode_www_form({
    "id" => "c686397e4a0f4f11683d"
})
req = Net::HTTP.new(uri.host, uri.port)
req.use_ssl = true

res = req.post(uri.request_uri, {
    "rendered_body" => "<h1>Example</h1>",
    "body" => "# Example"
}.to_json)
arr = JSON.parse(res.body)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_withのlocal: trueって必要なん?これ何なん?(Ruby on Rails)

はじめに:私が犯した罪と償い

社内研修の一環で、プチIoTシステムを開発しています。
そんな中、自分のエラーの解消に大先輩3人の60分を溶かしてしまったので、せめてもの償いのために、学んだことをメモしておきます。

おかげで、

  1. Railsのメソッドform_withの引数local: trueが何の役に立っているのか
  2. アプリ作成時のネットワーク通信のこと

が痛いほどわかりました。

環境
Rails6.0.3, macOS Catalina 10.15.7

何のエラー出してたん?

フォームでバリデーションが通らなかった時にエラーメッセージをビューで表示したい。
ただそれだけのことでした。しかし仕込んだそのエラーメッセージが表示されません!

うまくいっていなかったビューはこちら。1行目にご注目。

new.html.erb
<%= form_with model: @supplier do |form| %>

  <% if form.object.errors.any? %>
    <div id="error_explanation">
      <div class="alert alert-danger">
        エラーが <%= form.object.errors.count %> 個あります。 
      </div>
      <ul>
      <% form.object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="mb-3">
    <%= form.label :会社名, for: 'company', class: 'col-sm-2 col-form-label' %>
    <%= form.text_field :name, class: 'form-control', id: 'company', placeholder: '株式会社 喧嘩上等' %>
  </div>
  <div class="mb-3">
    <%= form.label :メールアドレス, for: 'email', class: 'col-sm-2 col-form-label' %>
    <%= form.text_field :email, class: 'form-control', id: 'email', placeholder: 'order@example.com' %>
    </div>

  <%= form.submit '送信', class: 'btn' %>
<% end %>

私は、Railsチュートリアル出身なのですが、そこではform_withlocal: trueという引数を渡していました。しかし、ネットでform_withの使い方を他で改めて調べてみたところ、この引数がなくても新規レコードの登録処理ができそうだったので、省いてみちゃえ!と出来心が全ての事の始まりでした。実際、これでデータベースまでの登録はきちんとできていて、なんだlocalパラメータいらないんじゃん。となっていました。

一方.....if form.object.errors.any?を入れても、エラー表示は一切できませんでした。

ちなみにコントローラはこんな感じ。問題なさそう。

supplier.controller.rb
class SuppliersController < ApplicationController

  def new
    @supplier = Supplier.new
  end

  def create
    @supplier = Supplier.new(supplier_params)
    if @supplier.save
      redirect_to suppliers_path
    else
      render 'new'
    end   
  end

end

binding.irbをビューやコントローラなど至る箇所に埋め込んでは、各変数に値が入っているかどうかをチェック。
form.objectには値もある。@supplierにしても同じ。errors.any?もtrueを返している。何故なんだ。タイポもなさそう。先輩たちの時間をどんどん溶かしてしまっています...。

解決:「クルクルしてない!」

クルクルしてない!
問題の原因が全くわからず途方にくれていた末に、ある先輩が。
フォームの送信ボタンを教えても画面がリロードしないことを変に思ったようで、そこからフォームの動きが怪しいとなりました。そこでlocal: trueを追記したのです。

new.html.erb
<%= form_with(model: @supplier, local: true) do |form| %>

以下略

学び1:local: trueとは?

form_withはRailsの比較的新しいメソッド。このメソッドではデフォルトがAjax通信で、非同期通信になってしまうのです。(これはRails 5.1〜6.0の仕様でRails 6.1からは同期通信がデフォルトに戻っているそうです。当記事はRails6.0.3です。→参考@jnchito さんありがとうございます!)それはつまり、必要な箇所だけページが更新され、その他の箇所はそのままになるという事。だからエラーメッセージのHTML部分が加えられなかったのです。
ここにlocal: trueと引数を渡す事で、これが通常のHTTPリクエストになり、ページ全体が返ってきてページがリロードされ、エラーメッセージも表示されるようになります。

学び2:アプリの開発でネットワークをチェックする

その後、ブラウザの開発者ツールでネットワークを確認しました。
スクリーンショット 2021-02-25 15.06.16.png
↑Typeがxhrになっとる。xhr=XMLHttpRequest。Ajaxのxの部分。つまりAjax通信になっているということ。

スクリーンショット 2021-02-25 15.06.04.png
↑suppliersが下の方に。他の要素がリロードされずにそのまま残っているということ。

local: trueにしておく(=通常のHTTP通信にする)とこのようなネットワーク動作になります。
suppliersがリストの一番先頭にいます。
ページ内の全ての要素が更新され、suppliersが一番最初に処理されたという事です。
スクリーンショット 2021-02-25 15.08.37.png

エンジニア中級者になるには、ネットワークのことを理解すること」と先輩。

最後に

エラーの解消の過程で、他にもbinding.irbの使い方、errorsやfull_messagesにどんなデータが格納されているのかなどいろんなことを勉強できました。

先輩方ありがとうございました!

@yuuu
@yukabeoka
@Junkins

宣伝ではないですが、周りがこんなエラーに親身に向き合ってくれる先輩ばかりです。
本当にFusicという会社でエンジニアキャリアを始められて幸せすぎる...。

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

【Rails】1日に記録できる数値の合計を制限するバリデーション

はじめに

投稿3回目です。
文章力が赤ちゃんレベルかつ遅筆なのでまだ慣れないです...
間違い等あればご指摘よろしくお願いします。

環境

  • Ruby 2.6.3
  • Rails 5.2.4

背景/目的

  • ポートフォリオで学習内容を記録するサイトを開発中。
  • 学習内容は1日に何回でも記録可能であり、項目には学習時間がある。
  • 学習時間の合計値が24時間/1日を超えないようにバリデーションを設定する。
  • 学習内容(learningsテーブル)の詳細は下記の通り。
カラム名 データ型 説明
date date 学習日
time float 学習時間

結論

app/models/learning.rb
class Learning < ApplicationRecord
  # 一つのユーザー(user)に対して、複数の学習記録(learning)が結びついている
  belongs_to :user

  # validateに定義したメソッドを設定
  validate :total_time_cannot_exceed_limit_time, on: :create
  validate :total_time_cannot_exceed_limit_time_for_edit, on: :update

  # 1日に記録できる学習時間の合計
  LIMIT_TIME_HOUR = 24

  # 入力された学習日に既に記録されている学習時間の合計値を取得する
  def one_day_time_sum(date)
    user.learnings.where(date: date).sum(:time)
  end

  # 入力された学習日に既に記録されている「編集対象の投稿以外の」学習時間の合計値を取得する
  def one_day_time_sum_unless_target_date(date)
    user.learnings.where(date: date).where.not(id: id).sum(:time)
  end

  # create時のカスタムバリデーション用のメソッドを定義
  def total_time_cannot_exceed_limit_time
    # [学習日が入力済]かつ[学習時間が入力済]かつ[学習日に既に記録された学習時間と入力した学習時間の合計が24時間を超える場合]
    if date.presence && time.presence && one_day_time_sum(date) + time > LIMIT_TIME_HOUR
      # エラーメッセージを表示する
      errors.add(:date, ":#{date.strftime("%Y年%m月%d日")}の学習時間の合計が#{LIMIT_TIME_HOUR}時間を超えています")
    end
  end

  # update時のカスタムバリデーション用のメソッドを定義
  def total_time_cannot_exceed_limit_time_for_edit
    # [学習日が入力済]かつ[学習時間が入力済]かつ[学習日に既に記録された「編集対象の投稿以外の」学習時間と入力した学習時間の合計が24時間を超える場合]
    if date.presence && time.presence && one_day_time_sum_unless_target_date(date) + time > LIMIT_TIME_HOUR
      errors.add(:date, ":#{date.strftime("%Y年%m月%d日")}の学習時間の合計が#{LIMIT_TIME_HOUR}時間を超えています")
    end
  end
end

createとupdateで処理を分けています。
コメントで処理の内容を記載しましたが、自分で読んでても正直よく分からないです。
例えば、下記のデータが既に保存されており、学習日2021-01-01・学習時間4の投稿を新たにする場合、

id 学習日 学習時間
1 2021-01-01 10.6
2 2021-01-01 11.4

入力された学習日2021-01-01に既に記録されている学習時間の合計値22時間を取得し、入力した学習時間4時間を足して、24時間と比較します。=> この場合26時間なのでエラーメッセージが表示されます。

これをupdateでも同じ処理にすると動作がおかしくなります。
例えば、id:1の学習時間を5時間に変更する場合、5 + 11.4 = 16.4時間になるのが理想です。
しかし、実際は10.6 + 11.4 + 5 = 27時間でNGになってしまいます。
user.learnings.where(date: date).sum(:time)だと編集前の学習時間も取得してしまうので、update時はwhere.not(id: id)を追記し、編集対象の投稿以外の学習時間の合計を取得するようにしています。

参考にさせていただきました

ここまで見て頂きありがとうございました。
ネーミングセンスがないのは許していただけると助かります?

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

f.collection_selectの使い方(超ざっくり&初学者向け)

f.collection_selectで別モデルのカラムを使って選択肢を作りたい時に役立つ情報を提供します!

まずf.collection_selectについて
書き方としては

<%= f.collection_select :保存先カラム, :選択肢に使いたいカラムを持った配列, :保存先カラムに保存したい値, :選択肢に表示したいカラム %>といった感じです。

例としては<%= f.collection_select :genre_id, @genres, :id, :name, :prompt => "選択してください" %>こんな感じです。

promptに関しては未選択の時に表示させておきたい文字を表示させることができます!!

ぜひ使ってみてください!!

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

Active Hashを使ってidを DBに保存する

ActiveHashを利用する

gem 'active_hash'

ActiveHashを導入する。

modelsディレクトリに新しいファイル作成後に、クラスを定義してActiveHash::Baseクラスを継承する。ActiveHashを用いるのに必要なもの。

class Genre < ActiveHash::Base
 self.data = [
   { id: 1, name: '----' },
   { id: 2, name: '経済' },
   { id: 3, name: '政治' },
   { id: 4, name: '地域' },
   { id: 5, name: '国際' },
   { id: 6, name: 'IT' },
   { id: 7, name: 'エンタメ' },
   { id: 8, name: 'スポーツ' },
   { id: 9, name: 'グルメ' },
   { id: 10, name: 'その他' }
 ]

 include ActiveHash::Associations
 has_many :examples
 end

ActiveHashを用いてアソシエーションの記述をするので、モジュールをincludeする。
また紐付いているモデルとのアソシエーションを記述する。

class Example < ApplicationRecord
 extend ActiveHash::Associations::ActiveRecordExtensions
 belongs_to :genre

 validates :genre_id, numericality: { other_than: 1 } 
end

同様に紐付いているモデル内に対応するアソシエーションを記述する。
こちらでもActiveHashを用いるのに使用するモジュールを取り込む。
また今回の場合はid1は {'---'}となったいるためDBに保存しないようにバリデーションをかけておく。

<%= f.collection_select(:genre_id, Genre.all, :id, :name, {}, {class:"genre-select"}) %>

プルダウンを生成するために上記の記述をする。

役割
:genre_id 保存先のカラム名
Genre.all 配列データ
:id 表示する際に参照するDBのカラム名
:name 実際に表示されるカラム名
{} オプションの指定
{class:"genre-select"} htmlオプション

扱うデータを簡単に保存できるために役立ちそうです。

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

ダミーデータを条件付きで表示/非表示にする方法

はじめに

本記事は製品情報などのダミーデータを、DBにデータが保存されているかいないかで表示/非表示にする方法を解説するものです。
考えてみれば難しいことではないのですが、とても悩んで時間を使ってしまったので、今後のために残しておくことと、同じようなエラーに遭遇した方の助けになれば良いと思い、記事にしました。

環境

  • 言語:Ruby
  • フレームワーク:Rails
  • DB:MySQL

やりたかったこと

DBに保存済みのレコードをトップページに表示し、DBに何も保存されていなければダミーデータの製品を表示させる。

該当箇所のコード(コントローラー)

class ItemsController < ApplicationController
  before_action :authenticate_user!, only: [:new, :create]

  def index
    @items = Item.all.order('created_at DESC')
  end

該当箇所のコード(ビューファイル)

  <% if @items == nil %>
    #製品を表示させるための処理(今回は割愛)
  <% end %>

結果・・・・・
エラーは発生しないものの、ダミーデータが表示されない。。。
製品情報をDBに保存してみたところ、保存した製品自体は表示されるので、DBのデータを取得してトップページに反映させる処理自体は問題ない様子。

原因

if文の条件式にnilを指定しましたが、if文がDB内を「空」と判断してくれなかった様子

解決策①

if文の条件式部分を下記のコードに変更 (配列の要素を指定する)

<% if @items[0] == nil %>

nilに対し配列の1つ目、すなわち[0]を指定することにより、if文が「配列の1つ目が空」=「DBにデータは保存されていない」という判断をしてくれます。
これでDBにデータが保存されている時は保存されたデータを表示し、DBにデータが保存されていなければダミーデータを表示することができました。

解決策②

if文の条件式部分を下記のコードに変更 (empty?メソッドを用いる)

<% if @items.empty %>

emptyメソッドは要素が0の時にtrueを返すメソッドです。
これを条件式に指定することにより、DBに解決策①と同様の挙動をしてくれました。

さいごに

nilは条件指定で頻繁に使いますが、今回はそこに頭が集中するあまり他の方法論を検討していなかったのが時間を使い過ぎた原因と思っています。
一つの書き方が完答、というわけではありませんし、柔軟に考えるためにも様々な書き方を試してみるのも大事だなと感じました。

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

RSpecの書き方で最近悩んだこと3つとその解決法について

RSpecの書き方について、最近躓いた3選です

検証環境

  • ruby: 2.6.5
  • rails: 6.0.3.2
  • rspec-rails: 3.9.0

module単体に対してテストを書きたい

moduleをいろんなクラスで使用することを想定するとき、テストを特定のクラスに依存させて書くのはよくなさそうです。

そんなとき、どうすればよいか悩んだのですが、こんなふうに書くと、特定のクラスに依存せず、moduleをinclude/extendしたクラスのテストができそうでした。

Class.new { include Module } して作成したダミーのクラスに対しテストする

# app/models/concerns/good_module.rb

module GoodModule
  def ok
    'ok!'
  end
end
# spec/models/concerns/good_module_spec.rb

describe GoodModule do
  context '特異メソッドとして使う場合' do
    let(:dummy_class) { Class.new { extend GoodModule } }
    subject { dummy_class.ok }

    it { expect(subject).to eq 'ok!' }
  end

  context 'インスタンスメソッドとして使う場合' do
    let(:dummy_class) { Class.new { include GoodModule } }
    let(:dummy_class_instance) { dummy_class.new }
    subject { dummy_class_instance.ok }

    it { expect(subject).to eq 'ok!' }
  end
end

こんな感じで書いてみます。

$ bundle exec rspec spec/models/concerns/good_module_spec.rb                         
..

Finished in 0.0046 seconds (files took 1.79 seconds to load)
2 examples, 0 failures

実行できました!

インスタンス変数の書き込みメソッドをモックしたい

モックを利用すると、テストしやすい場面があると思います。

# app/models/concerns/sendable.rb

module Sendable
  def send_mail(message, email)
    message.to = email
    puts 'send!'
  end
end

こんな感じの処理を作りましたが、messageクラスをまだ作っていません。
モックにして先にテストだけしてみたいと思います。

# spec/models/concerns/sendable_spec.rb

describe Sendable do
  let(:dummy_class) { Class.new { extend Sendable } }
  let(:message) { double('message') }

  subject { dummy_class.send_mail(message, 'example@example.com') }

  before do
    allow(message).to receive(:to)
  end

  it { subject }
end

いざ実行すると…

$ bundle exec rspec spec/models/concerns/sendable_spec.rb 
F

Failures:

  1) Sendable 
     Failure/Error: message.to = email
       #<Double "message"> received unexpected message :to= with ("example@example.com")
     # ./app/models/concerns/sendable.rb:6:in `send_mail'
     # ./spec/models/concerns/sendable_spec.rb:9:in `block (2 levels) in <top (required)>'
     # ./spec/models/concerns/sendable_spec.rb:15:in `block (2 levels) in <top (required)>'

Finished in 0.01396 seconds (files took 1.74 seconds to load)
1 example, 1 failure

received unexpected message となってしまいました。
これだと想定通りに動きません。

attr_accessorの対象となるインスタンス変数に=をつける

ただ、よく考えてみると、attr_accessorの定義は

https://docs.ruby-lang.org/ja/latest/method/Module/i/attr_accessor.html

def name
  @name
end
def name=(val)
  @name = val
end

こうなので、モックも

# spec/models/concerns/sendable_spec.rb

  before do
    # =を追加する
    allow(message).to receive(:to=)
  end

これが正しかったのでした。

メソッドチェーンをモックする

例えば、Time.zone.now.hourのようなメソッドチェーンが返す値をモックしたいときがあると思います。

# app/models/concerns/checkable.rb

module Checkable
  def ok
    Time.zone.now.hour == 10
  end
end

早速RSpecを書いてみます。

# spec/models/concerns/checkable_spec.rb

describe Checkable do
  let(:dummy_class) { Class.new { extend Checkable } }

  subject { dummy_class.ok }

  before do
    allow(Time).to receive('zone.now.hour').and_return(10)
  end

  it { expect(subject).to be true }
end
$ bundle exec rspec spec/models/concerns/checkable_spec.rb
F

Failures:

  1) Checkable 
     Failure/Error: allow(Time).to receive('zone.now.hour').and_return(10)
       Time does not implement: zone.now.hour
     # ./spec/models/concerns/checkable_spec.rb:11:in `block (2 levels) in <top (required)>'

勘で書いてみましたが、やっぱり無理でした。。

receive_message_chainを使う

そういう場合は、RSpec側にこれを想定したメソッドが用意されているのでそれを利用します。

# spec/models/concerns/checkable_spec.rb

  before do
    allow(Time).to receive_message_chain(:zone, :now, :hour).and_return(10)
  end
$ bundle exec rspec spec/models/concerns/checkable_spec.rb
.

Finished in 0.00916 seconds (files took 1.65 seconds to load)
1 example, 0 failures

これで、無事、メソッドチェーンでも、特定の値を返すことができました。

終わり

RSpecのドキュメントを読むと、こんな書き方できたんだという発見が多いです。
テストに助けられることが多いので、今後も転ばぬ先の杖として、需要にあったテストを書けるように、RSpec力を鍛えていきたいと思いました。

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

Railsのfind_eachやfind_in_batchesでorderにid以外を指定したい場合の解決方法

はじめに

大量のデータにアクセスして処理を行う場合、

  • メモリ不足で処理が中断されないよう、少しずつメモリに展開したい
  • 途中で処理が中断されても問題ないよう、一定件数ごとにコミットをしたい

と考えることがあると思います。

そんなときにRailsで役に立つのがfind_eachやfind_in_batchesですね。

ただしこの2つのメソッドには弱点があり、id(主キー/primary key)の昇順(ASC)でしかデータを扱うことができません

※Rails v6.1.0時点での情報です。

    # NOTE: Order can be ascending (:asc) or descending (:desc). It is automatically set to
    # ascending on the primary key ("id ASC").
    # This also means that this method only works when the primary key is
    # orderable (e.g. an integer or string).

https://github.com/rails/rails/blob/914caca2d31bd753f47f9168f2a375921d9e91cc/activerecord/lib/active_record/relation/batches.rb#L128:title

そこで、今回は自分が指定したorderで大量データを扱いたい場合の解決方法を紹介いたします。

解決方法

前提

Rankingモデルのrank(順位)順で処理をしたい、とします。

実装

@rank_offset = 0
@batch_size = 1000

def find_rankings_in_batches
  loop do
    rankings = Ranking.where('rank > ?', @rank_offset).order(rank: :asc).limit(@batch_size)
    break if rankings.blank?
    rankings.each do |ranking|
      yield(ranking)
    end
    @rank_offset = rankings.last.rank
  end
end

find_rankings_in_batches do |ranking|
  # ここに処理を書く
end

発行されたSQL

1ループ目

SELECT  `rankings`.* FROM `rankings` WHERE (rank > 0)  ORDER BY `rankings`.`rank` ASC LIMIT 1000

2ループ目

SELECT  `rankings`.* FROM `rankings` WHERE (rank > 1000)  ORDER BY `rankings`.`rank` ASC LIMIT 1000

rankの昇順かつbatch_size単位で取得できていることがわかります。

注意点

基本的には、orderに指定するカラムはUNIQUE制約が設定されているものにしてください。
batchの切れ目で同じ値が続く場合、処理されないレコードが存在してしまうためです。

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

【Rails】seed_fuまとめ

何がうれしいか

  • seedデータの一部を変更した時、変更したファイルだけを読み込み、データの更新や追加ができる
    • デフォルトのseeds.rbの場合、既存のデータを削除してから再度読み込まなきゃいけない
  • 環境ごとにseedデータを分けやすくなる

インストール

Gemfile
gem 'seed-fu'
$ bundle install
$ mkdir db/fixtures # 必須
# 以下、環境ごとにseedファイルを分けたい場合に作成
$ mkdir db/fixtures/development
$ mkdir db/fixtures/production

データを読み込む

$ touch db/fixtures/development/01_user.rb #ファイル名は自由。アルファベット順に読み込まれる。

以下の2通りの書き方がある。データが多い時は2つ目の書き方の方が良さそう。

基本の書き方
User.seed do |s|
  s.id = 1
  s.name = '茂野吾郎'
  s.email = 'shigeno@example.com'
  s.password  = 'password'
end

User.seed do |s|
  s.id = 2
  s.name = '佐藤寿也'
  s.email = 'sato@example.com'
  s.password  = 'password'
end
この書き方でも良い
User.seed(
  {
    name: '茂野吾郎',
    email: 'shigeno@example.com',
    password: 'password'
  },
 {
    name: '佐藤寿也',
    email: 'sato@example.com',
    password: 'password'
  },
)
$ rails db:seed_fu

読み込むseedファイルを明示的に指定する

$ rails db:seed_fu

開発環境で上記のコマンドを叩くと、db/fixtures/development以下のファイルだけが読み込まれる。
他のディレクトリのファイルを読みたい場合などには以下のようにディレクトリを指定することができる。

$ rails db:seed_fu FIXTURE_PATH=db/fixtures/hogehoge

【注意】データの同一性はidで判断される

既存のデータが更新されたのか、新規データが追加されたのかは、idを元に判断されている。seedファイル内のデータにidを書かないと、既存のデータを更新したつもりでも新規データが追加されてしまう。

id以外の(ユニークな)データを基準にデータの同一性を判断させたい場合は、以下のように書く。

emailがユニークな場合
User.seed(:email) do |s|
  s.name = 'foo'
  s.email = 'foo@example.com'
  s.password  = 'password'
end

# or

User.seed(:email,
  {
    name: 'foo',
    email: 'foo@example.com',
    password: 'password'
  }
)

既存のDBからseedファイルを作成するrakeタスク

SeedFu::Writer.writeを使ってseedファイルを作ることができる

$ rails g task seed-fu-gen-user
lib.tasks/seed_fu_gen_user.rake
namespace :seed_fu_gen_user do
  desc 'usersテーブルのデータを元にseedファイルを生成'
  task create_seed_by_db: :environment do
    SeedFu::Writer.write('./db/fixtures/user_gen.rb', class_name: 'User') do |w|
      User.all.each do |x|
        w << x.as_json(except: %i[created_at updated_at])
      end
    end
  end
end
$ ./bin/rake seed_fu_gen_user:create_seed_by_db

※公式のREADMEを読むと、SeedFu::Writerにはもろもろ変更が入ったと注意書きがありますが、少なくともver2.3.9では上記の方法で動作確認ができました。

参考

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

【 Ruby on Rails 6.0 】AWS + Nginx + Unicornでデプロイ⑤

始めに

前回まででEC2上に必要なGemをインストールし本番環境用に環境変数を設定しました。今回はHTTP通信を設定しRailsアプリを起動する工程までをまとめたいと思います。

目次

目次 内容
セクション1 EC2インスタンス作成
セクション2 Linuxサーバー構築
セクション3 データベース設定
セクション4 EC2上でGemをインストールし環境変数を設定
セクション5 Railsアプリを起動(今回の内容)
セクション6 Nginxの導入

ポート開放

立ち上げたばかりのEC2インスタンスはSSHでアクセスすることはできますが、HTTPなどの他の通信方法では一切つながらないようになっています。そのため、WEBサーバとして利用するEC2インスタンスは事前にHTTPがつながるように「ポート」を開放する必要があります。

セキュリティグループのポートを設定

AWSのEC2インスタンス一覧画面から、対象のインスタンスを選択し、「セキュリティグ
ループ」のリンクをクリックします。
スクリーンショット 2021-02-25 10.11.11.png
すると、インスタンスの属するセキュリティグループの設定画面に移動するので、「イン バウンド」タブの中の「編集」をクリックします。
スクリーンショット 2021-02-25 10.14.07.png
モーダルが開くので、「ルールの追加」をクリックします。

タイプ カスタムTCPルール」
プロトコル TCP
ポート範囲 3000
送信元 カスタム、0.0.0.0/0

「0.0.0.0」は「全てのアクセスを許可する」という意味です。

以上で、ポートの開放が完了です。

database.ymlの本番環境の設定を編集

本番環境のmysqlの設定に合わせるため、ローカルのdatabase.ymlを以下のように編集して下さい。

config/database.yml
production:
  <<: *default
  database: ここは編集しないこと
  username: root
  password: <%= ENV['DATABASE_PASSWORD']
  socket: /var/lib/mysql/mysql.sock

ローカルでの編集をコミットして、GitHubにプッシュ

リモートリポジトリが更新されたため、サーバ上のアプリケーションにも反映させましょう。今回はすでにEC2とGithubは接続できているため、git pullコマンドを利用します。
※別にブランチを切っている場合は、masterブランチにmergeしてから以下のコマンドを 実行しましょう。

ターミナル(EC2)
[ec2-user@ip-172-31-23-189 <app名>]$ git pull origin master

データベースを作成しマイグレーションを実行

ターミナル(EC2)
[ec2-user@ip-172-31-23-189 <app名>]$ rails db:create RAILS_ENV=production
[ec2-user@ip-172-31-23-189 <app名>]$ rails db:migrate RAILS_ENV=production

もしここでMysql2::Error: Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' というエラーが起こった場合、データベース が起動していない可能性があります。sudo systemctl start mariadbというコマン ドをターミナルから打ち込み、mysqlの起動を試してみましょう。

アセットファイルをコンパイル

続いて、アセットファイルをコンパイルします。
アセットファイルとは、画像・CSS・ JavaScript等を管理しているファイルです。
コンパイルとはアセットファイルを圧縮し、 そのデータを転送するということです。
この作業を行わないと、本番環境でCSSが反映されずにビューが崩れてしまったり、エラーでブラウザが表示されない、などの問題が生じてしまいます。

ターミナル(EC2)
[ec2-user@ip-172-31-23-189 <アプリ名>]$ rails assets:precompile RAILS_ENV=production

# 成功すると
Yarn executable was not detected in the system.
Download Yarn at https://yarnpkg.com/en/docs/install
I, [2020-01-18T12:51:01.4345644 #1265]  INFO -- : Writing /var/app/web-share/public/assets/member_photo_noimage_thumb-224a733c50d48aba6d9fdaded809788bbeb5ea5f6d6b8368adaebb95e58bcf53.png
I, [2020-01-18T12:51:02.2615123#1265]  INFO -- : Writing /var/app/appname/public/assets/application-bc071e28a78e2b63c9313afed5ad3476e00e3f0e5b12445c37214d1f1317be48.js
I, [2020-01-18T12:51:02.2626434 #1265]  INFO -- : Writing /var/app/appname/public/assets/application-bc071e28a78e2b63c9313afed5ad3476e00e3f0e5b12445c37214d1f1317be48.js.gz
I, [2020-01-18T12:51:08.484546 #1265]  INFO -- : Writing /var/app/appname/public/assets/application-8549fb9a804686e593d5c0f90a2412a39de85908e5fb58fdf6681d4b0073d891.css
I, [2020-01-18T12:51:08.485454 #1265]  INFO -- : Writing /var/app/appname/public/assets/application-8549fb9a804686e593d5c0f90a2412a39de85908e5fb58fdf6681d4b0073d891.css.gz

ここでyarnのエラーが発生したらnode.jsをupdateします。

Node.jsのアップデート

Node.jsのバージョン管理ツールnvmを使用してインストールします。

ターミナル(EC2)
# 現在のバージョン確認
[ec2-user@ip-172-31-23-189 <アプリ名>]$ node -v
v4.9.1

# Node.jsのバージョン管理ツールnvmをclone
[ec2-user@ip-172-31-23-189 <アプリ名>]$ git clone git://github.com/creationix/nvm.git ~/.nvm
[ec2-user@ip-172-31-23-189 <アプリ名>]$ echo . ~/.nvm/nvm.sh >> ~/.bashrc
[ec2-user@ip-172-31-23-189 <アプリ名>]$ . ~/.bashrc

# nvmバージョン確認
[ec2-user@ip-172-31-23-189 <アプリ名>]$ nvm --version
0.35.0

# インストールできるNode.jsの確認
[ec2-user@ip-172-31-23-189 <アプリ名>]$ nvm ls-remote

# 最新の安定版をインストール
[ec2-user@ip-172-31-23-189 <アプリ名>]$ nvm install stable

# バージョン確認
[ec2-user@ip-172-31-23-189 <アプリ名>]$ node -v
v12.12.0 # バージョンが最新になっていたら成功

もう一度コンパイルを実行してみましょう。

Unicornのプロセスを確認

コンパイルが成功したら反映を確認するため、Railsを再起動します。しかし、まずは今動 いているUnicornをストップします。そのために、Unicornのプロセスを確認し、プロセスを止めます。ターミナルからプロセスを確認するにはpsコマンドを利用します。

ターミナル
# unicorn起動
[ec2-user@ip-172-31-38-126 <リポジトリ名>]$ bundle exec unicorn_rails -c config/unicorn.rb -E production -D

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ ps aux | grep unicorn

ec2-user 17877  0.4 18.1 588472 182840 ?       Sl   01:55   0:02 unicorn_rails master -c config/unicorn.rb -E production -D
ec2-user 17881  0.0 17.3 589088 175164 ?       Sl   01:55   0:00 unicorn_rails worker[0] -c config/unicorn.rb -E production -D
ec2-user 17911  0.0  0.2 110532  2180 pts/0    S+   02:05   0:00 grep --color=auto unicorn

大事なのは左から2番目の列です。ここに表示されるのがプロセスのid、つまりPIDになります。
「unicorn_rails master」と表示されているプロセスがUnicornのプロセス本体です。この時のPIDは、17877となっています。

killコマンドを入力してUnicornのプロセスを停止

ターミナル
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ kill <確認したunicorn rails masterのPID>

# 再度確認する
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ ps aux | grep unicorn
...
ec2-user 17911  0.0  0.2 110532  2180 pts/0    S+   02:05   0:00 grep --color=auto unicorn

# できない場合は強制終了する
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ kill -9 [プロセスID]

先頭にRAILS_SERVE_STATIC_FILES=1をつけて、unicornを起動

続いて再びunicornを起動しましょう。このときRAILS_SERVE_STATIC_FILES=1という指定を先頭に追加してください。これは、コンパイルされたアセットをRailsが見つけられるような指定になります。以下のようにコマンドを実行してください。

ターミナル
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ RAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -D

ブラウザで確認

ブラウザで http://:3000/ にアクセスして、サイトが正常に表示されているか確認してみましょう。

Railsの起動がうまくできなかった時

上記のコマンドを実行してもRailsが起動しないときや、起動できてもIPアドレス:3000に
アクセスするとエラーが表示されていることがあります。

以下の項目をチェックしてみてください。

  • pushのし忘れ、またはEC2サーバ側でのpullのし忘れは無いか
  • ローカルでの編集のpushやEC2でのgit pullを忘れていないか
  • データベースの起動は正しく行えているか
  • EC2サーバ側のSECRET_KEY_BASE等は正しく設定できているか
  • EC2インスタンスの再起動を行ってみる

unicornのログを確認する

上記のポイントでも解決出来ない場合はunicornのログを確認してみましょう。
lessコマンドまたはcatコマンドを使えばログを見ることが出来ます。

ターミナル(EC2)
[ec2-user@ip-172-31-23-189 <app名>]$ less log/unicorn.stderr.log

# 実行結果
I, [2021-02-21T02:28:26.245480 #7257]  INFO -- : Refreshing Gem list
I, [2021-02-21T02:28:28.232692 #7257]  INFO -- : listening on addr=0.0.0.0:3000 fd=9
E, [2021-02-21T02:28:28.245079 #7257] ERROR -- : Unknown database 'app_production' (ActiveRecord::NoDatabaseError)

ログはEnterキーを押せば下にスクロールすることが出来ます。(下にいくほど最新のログです。)
上の例ではUnknown databaseとなっているためrails db:createをし忘れている可能生があります。
もしくはdatabase.ymlの設定に誤りがあるかもしれません。
このようにログを頼りに原因を特定していきます。

終わりに

今回の内容までで、Elastic IPアドレスを入力してブラウザ上でアプリが起動していることを確認出来ました。
次回はユーザーのリクエストに対してレスポンスを送れるようにWebサーバーを導入していきます。

次回
Nginxの導入

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

AWSにデプロイ後、よく使うコマンド

EC2にSSHで接続し、git pullしてからよく使うコマンドまとめました。

  • 使用頻度が多いものを自分用に簡単にまとめました。

環境

rails 5.2.4
ruby 2.6.3
Cloud9
MySQL

AWSの構成

スクリーンショット 2021-02-25 8.15.49.png

EC2にSSHで接続する方法
$ ssh -i ~/.ssh/キー名.pem ec2-user@xx.xx.xx.xx

Nginxの再起動する方法
$ sudo systemctl restart nginx

アプリ(puma)起動する方法
$ rails s -e production

アプリ(puma)停止する方法
$ kill プロセスID
どちらも同じです
$ kill $(cat tmp/pids/puma.pid)
本番環境でbundle installする時のコマンド
$ bundle install --path vendor/bundle --without test development

CSS・JS変更時に使うコマンド
$ bundle exec rails assets:precompile RAILS_ENV=production

本番環境でrails db:migrateする時のコマンド
$ bundle exec rails db:migrate RAILS_ENV=production

seeds.rb編集した場合は、本番環境のデータベースを削除してからやり直す方がいいです。
本番環境のデータはなくなるので使う際はご自身で判断してください。

$ RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bundle exec rails db:drop
$ mysql -u root -p -h エンドポイント
mysql> CREATE DATABASE 作成したいデータベース名;
$ bundle exec rails db:migrate RAILS_ENV=production
$ bundle exec rails db:seed RAILS_ENV=production
nginxエラーログ確認するコマンド
$ sudo tail -f /var/log/nginx/error.log

railsのエラーログ確認するコマンド
$ sudo tail -f log/production.log

全体の把握に時間がかかりたくさんのエラーを経験し心が折れそうになりました。
何度もEC2作り直ししましたのでAWSの知識もより深まりました。
今回は、S3を使っていないため今度はS3を使って構成することを考えてます?

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

Selenium Webdriver + Chrome DevTools Protocolを使って情報を取得してみる

はじめに 〜この記事でご紹介すること〜

本記事では「 Selenium Webdriver Chrome DevTools Protocol 」などでググるとパッとヒットするものがなかったので、それについてご紹介します。
Selenium Webdriverを使ってコードを書いたことがある方、Chrome開発者ツールやFirebugなどの解析ツールを使ったことがある方であれば
この記事を読んでいただくことで「な〜んだ!思ってたよりも簡単にいろいろなことできるようになるじゃん。」と思われるかもしれません。

Chrome DevTools Protocolとは

Chrome DevTools Protocol(以下、CDP)を使うと、WebDriverでは定義されていなかったDevToolsの各種機能(JavaScrioptデバッガ、プロファイラ、パフォーマンスツール、などなど...)が利用できます。
公式ドキュメントはこちら

CDPを使ってどんなことができるのかは、 @d-dai さんが書かれている記事 Chromeデベロッパーツールの使い方まとめにわかりやすく解説がされていましたのでここでは割愛します。

Selenium WebdriverでCDPを使ってみる

まず、CDPには非推奨、実験的なものも含めて以下のようにたくさんのドメインがあります。
これらすべてのリファレンスを書いてみようと思いましたが、各ドメイン毎にもメソッドが定義されており多すぎるため、
ドメインの中で情報を取得するものをいくつかピックアップしてサンプルコードを書いてみました。

Domains

Rubyでの実装例

使用したメソッド

ソースコード

Gemfile
source 'https://rubygems.org'

gem 'selenium-webdriver'
sample.rb
require 'selenium-webdriver'

driver = Selenium::WebDriver.for :chrome

puts '### Page.navigate ###'
driver.execute_cdp('Page.navigate', { url: 'https://google.com' })

puts "\n### Browser.getVersion ###"
puts driver.execute_cdp 'Browser.getVersion'

puts "\n### Network.getAllCookies ###"
cookies = driver.execute_cdp 'Network.getAllCookies'
puts cookies['cookies']

puts "\n### Performance.getMetrics ###"
driver.execute_cdp 'Performance.enable'
metrics = driver.execute_cdp 'Performance.getMetrics'
puts metrics['metrics']

実行結果

取得した一部情報はマスキングしています。

### Page.navigate ###

### Browser.getVersion ###
{"jsVersion"=>"8.8.278.17", "product"=>"Chrome/88.0.4324.192", "protocolVersion"=>"1.3", "revision"=>"@31b458a18f133db9203eb5a5dd6552de0716dda3", "userAgent"=>"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36"}

### Network.getAllCookies ###
{"domain"=>".google.com", "expires"=>1616770705.154825, "httpOnly"=>false, "name"=>"1P_JAR", "path"=>"/", "priority"=>"Medium", "sameSite"=>"None", "secure"=>true, "session"=>false, "size"=>19, "value"=>"2021-02-24-14"}
{"domain"=>".google.com", "expires"=>1629989904.81409, "httpOnly"=>true, "name"=>"NID", "path"=>"/", "priority"=>"Medium", "sameSite"=>"None", "secure"=>true, "session"=>false, "size"=>178, "value"=>"XXXXXX"}

### Performance.getMetrics ###
{"name"=>"Timestamp", "value"=>655734.877406}
{"name"=>"AudioHandlers", "value"=>0}
{"name"=>"Documents", "value"=>2}
{"name"=>"Frames", "value"=>2}
{"name"=>"JSEventListeners", "value"=>173}
{"name"=>"LayoutObjects", "value"=>92}
{"name"=>"MediaKeySessions", "value"=>0}
{"name"=>"MediaKeys", "value"=>0}
{"name"=>"Nodes", "value"=>325}
{"name"=>"Resources", "value"=>17}
{"name"=>"ContextLifecycleStateObservers", "value"=>6}
{"name"=>"V8PerContextDatas", "value"=>1}
{"name"=>"WorkerGlobalScopes", "value"=>0}
{"name"=>"UACSSResources", "value"=>0}
{"name"=>"RTCPeerConnections", "value"=>0}
{"name"=>"ResourceFetchers", "value"=>2}
{"name"=>"AdSubframes", "value"=>0}
{"name"=>"DetachedScriptStates", "value"=>0}
{"name"=>"ArrayBufferContents", "value"=>0}
{"name"=>"LayoutCount", "value"=>0}
{"name"=>"RecalcStyleCount", "value"=>0}
{"name"=>"LayoutDuration", "value"=>0}
{"name"=>"RecalcStyleDuration", "value"=>0}
{"name"=>"DevToolsCommandDuration", "value"=>0.000104}
{"name"=>"ScriptDuration", "value"=>0}
{"name"=>"V8CompileDuration", "value"=>0}
{"name"=>"TaskDuration", "value"=>0.000131}
{"name"=>"TaskOtherDuration", "value"=>2.7e-05}
{"name"=>"ThreadTime", "value"=>0.000256}
{"name"=>"ProcessTime", "value"=>0.36707}
{"name"=>"JSHeapUsedSize", "value"=>7745352}
{"name"=>"JSHeapTotalSize", "value"=>10215424}
{"name"=>"FirstMeaningfulPaint", "value"=>0}
{"name"=>"DomContentLoaded", "value"=>655734.046827}
{"name"=>"NavigationStart", "value"=>655733.006387}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerのコンテナ立ち上げで上手くいかなかった時にやったこと

はじめに

初学者で、今回が初めての投稿になります。
Docker超入門 #5
(動画内容としましては イメージ作成→コンテナ作成→コンテナ起動 の順になります)
こちらの動画の内容に沿って学習した際に起こったエラー、そしてどう対処したかの記事になります

エラー内容

イメージ作成するとこまでは上手く行ったのですがコンテナ作成時にエラー表示が出ました。
結論、今回のエラーはイメージ内の誤字にありました。

やって見たこと

まずは作成はできていたものの、「イメージ作成」が上手く行っていないのではと思いファイル内の誤字脱字チェックに入りました。やはり2箇所誤字発見できました。そして今回はイメージファイルにミスがあったものの、イメージが作成出来てしまったのだとわかりました

対処の流れとしては
「①間違って起動したコンテナの停止→そのコンテナの削除→誤字含むイメージの削除
続いて
「②修正後のイメージ作成→コンテナ立ち上げ→コンテナ起動」で上手く行きました

コンテナの稼働状況チェック→停止→削除
$ docker ps

$ docker container stop  対象のコンテナ名

$ docker container rm  対象のコンテナ名  
イメージのチェック→削除
$ docker images

$ docker image rm 対象のイメージ名

最後に

もし記事内容に誤り等ございましたら是非ともご指摘ください。
同じような初学者の方の参考になれば幸いです

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

【 Ruby on Rails 6.0 】AWS + Nginx + Unicornでデプロイ④

始めに

前回の記事ではEC2インスタンスにmariaDBというデータベースの設定を行いました。今回はインスタンス上でRailsアプリの起動に必要なGemをインストールし環境変数を設定する工程までをまとめたいと思います。

目次

目次 内容
セクション1 EC2インスタンス作成
セクション2 Linuxサーバー構築
セクション3 データベース設定
セクション4 EC2上でGemをインストールし環境変数を設定(今回の内容)
セクション5 Railsアプリを起動
セクション6 Nginxの導入

EC2のサーバにアプリのコードをクローン

全世界に公開できるIPアドレスを持ったEC2サーバ上でRailsアプリを動かすためにアプリケーションのコードをGithubからEC2サーバへクローンします。

GithubにSSH鍵を登録

現状、EC2サーバにアプリケーションのコードをクローンしようとしても permission deniedとエラーが出てしまいます。これは、Githubから見てこのEC2インスタンスが何者かわからないためです。

EC2インスタンスからGithubにアクセスするためには、作成したEC2インスタンスのSSH 公開鍵をGithubに登録する必要があります。 SSH鍵をGithubに登録すると、Githubはそれを認証に利用し、コードのクローンを許可してくれるようになります。

ターミナル
[ec2-user@ip-172-31-23-189 ~]$ ssh-keygen -t rsa -b 4096

# 三段階ほど認証を求められるが全てyesでenterを押す

Enter file in which to save the key (/home/ec2-user/.ssh/id_rsa):
Enter passphrase (empty for no passphrase): 
Enter same passphrase again:

# 以下の表示が出れば成功

Your identification has been saved in /home/ec2-user/.ssh/id_rsa.
Your public key has been saved in /home/ec2-user/.ssh/id_rsa.pub.
The key fingerprint is:
3a:8c:1d:d1:a9:22:c7:6e:6b:43:22:31:0f:ca:63:fa ec2-user@ip-172-31-23-189
The key's randomart image is:
+--[ RSA 4096]----+
|    +            |
| . . =           |
|  = . o .        |
| * o . o         |
|= *     S        |
|.* +     .       |
|  * +            |
| .E+ .           |
| .o              |
+-----------------+

# .SSH公開鍵を表示し、値をコピーするため、下記コマンドを実装
[ec2-user@ip-172-31-23-189 ~]$ cat ~/.ssh/id_rsa.pub

# ssh~から最後の文字列までをコピーする
ssh-rsa AAAAB3Nza・・・・・

以下のURLからGithubにアクセスします。
https://github.com/settings/keys
アクセスしたら以下の画像のように操作します。
スクリーンショット 2021-02-25 4.06.19.png

スクリーンショット 2021-02-25 4.12.11.png
Githubに鍵を登録できたら、SSH接続できるか以下のコマンドで確認してみましょう。

ターミナル
[ec2-user@ip-172-31-23-189 ~]$ ssh -T git@github.com

# 下記の表示が出たらyesを選択

The authenticity of host 'github.com (IP ADDRESS)' can't be established.
RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
Are you sure you want to continue connecting (yes/no)?

# 成功したら下記の表示が出る

[ec2-user@ip-172-31-23-189 ~]$ ssh -T git@github.com
Hi <Githubユーザー名>! You've successfully authenticated, but GitHub does not provide shell access.

# エラーが出たらログインし直してもう一度ssh接続する

Warning: Permanently added the RSA host key for IP address 'XXXXXXXXX' to the list of known hosts.

アプリケーションサーバの設定

アプリケーションサーバとは、ブラウザからの「リクエスト」を受け付けRailsアプリケ ーションを実際に動作させるソフトウェアのことです。
アプリケーションサーバが動いていれば、ブラウザからのリクエストを受け付けてRails アプリケーションが動作します。
という訳で、全世界に公開するEC2サーバ上でもアプリケーションサーバを動かす必要が あるのです。今回はUnicornというアプリケーションサーバーを使用します。

Unicorn

全世界に公開されるサーバ上で良く利用されるアプリケーションサーバです。rails sコマンドの代わりに unicorn_rails コマンドで起動することができます。
この後、EC2サーバにSSH接続しUnicornを起動することで全世界からアクセスできるようにしていきます。

Unicornをインストール

UnicornはRubyで作成されており、gem化されています。なのでローカルのGemfileを編集しましょう。

Gemfile
# 追記(本番環境)
group :production do
  gem 'unicorn', '5.4.1' # バージョン指定
end
ターミナル(ローカル)
$ bundle install

config/unicorn.rbを作成し、内容を以下のように編集

Unicornの設定ファイルとして、次の内容でファイルを作成しましょう。最初からは存在していないので、自分でconfig ディレクトリ以下に作成します。
この後すぐ、ファイルの中身がそれぞれ何を行なっているか説明しますので、まずはコピ ー&ペーストで作成したファイルに貼り付けましょう。

config/unicorn.rb
app_path = File.expand_path('../../', __FILE__)

#アプリケーションサーバの性能を決定する
worker_processes 1

#アプリケーションの設置されているディレクトリを指定
working_directory app_path

#Unicornの起動に必要なファイルの設置場所を指定
pid "#{app_path}/tmp/pids/unicorn.pid"

#ポート番号を指定
listen 3000

#エラーのログを記録するファイルを指定
stderr_path "#{app_path}/log/unicorn.stderr.log"

#通常のログを記録するファイルを指定
stdout_path "#{app_path}/log/unicorn.stdout.log"

#Railsアプリケーションの応答を待つ上限時間を設定
timeout 60

#以下は応用的な設定なので説明は割愛

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

変更修正をリモートリポジトリに反映

ここまで、ローカルのフォルダ内で変更修正を行ったので、こちらをリモートリポジトリへ反映します。変更修正をコミットしてプッシュしましょう。
この時必ず、masterブランチで行うようにしてください。もし、別ブランチでコミット&プッシュした場合は、リモートリポジトリでプルリクエストを作成し、ブランチをmasterへマージしてください。

Githubからコードをクローン

続いて、Unicornの設定を済ませたコードをEC2インスタンスにクローンします。

/var/wwwディレクトリを作成し、権限をec2-userに変更

ターミナル(EC2)
#/var/wwwディレクトリを作成(後述するCapistranoの初期値がwwwなので、ディレクトリをwwwに設定しています)
[ec2-user@ip-172-31-23-189 ~]$ sudo mkdir /var/www/

#作成したwwwディレクトリの権限をec2-userに変更
[ec2-user@ip-172-31-23-189 ~]$ sudo chown ec2-user /var/www/

Githubから「リポジトリURL」を取得

スクリーンショット 2021-02-25 4.29.50.png
取得した「リポジトリURL」を使って、コードをクローンします。

ターミナル(EC2)
[ec2-user@ip-172-31-23-189 ~]$ cd /var/www/
[ec2-user@ip-172-31-23-189 www]$ git clone https://github.com/リポジトリURL

# 成功したら以下の表示が出る

remote: Enumerating objects: 298, done.
remote: Counting objects: 100% (298/298), done.
remote: Compressing objects: 100% (190/190), done.
remote: Total 298 (delta 109), reused 274 (delta 86), pack-reused 0
Receiving objects: 100% (298/298), 58.53 KiB | 365.00 KiB/s, done.
Resolving deltas: 100% (109/109), done.

EC2の能力を拡張

現状動かしているEC2のインスタンスではコンピューターの能力が足りず、Gemのインストール時などにエラーが発生する可能性があります。具体的には、コンピューターの処理能力に関係するメモリというものが足りません。これは、無料で動かせるインスタンスの限界であるため仕方ありません。
そこで、今後の設定を行う前にメモリを増強する処理を行います。

ターミナル
# ホームディレクトリに移行
[ec2-user@ip-172-31-25-189 ~]$ cd ~
[ec2-user@ip-172-31-25-189 ~]$ sudo dd if=/dev/zero of=/swapfile1 bs=1M count=512

# 成功すると以下の表示が出る

512+0 レコード入力
512+0 レコード出力
536870912 バイト (537 MB) コピーされました 5.19011  103 MB/

# 次は権限に制限をかけましょう(chmodコマンド)
[ec2-user@ip-172-31-25-189 ~]$ sudo chmod 600 /swapfile1

# スワップ(swap)領域を作成する - mkswap
[ec2-user@ip-172-31-25-189 ~]$ sudo mkswap /swapfile1

#成功すると下記の表示が出ます
スワップ空間バージョン1を設定しますサイズ = 524284 KiB
ラベルはありません, UUID=74a961ba-7a33-4c18-b1cd-9779bcda8ab1

# スワップ(swap)領域を有効化する - swapon
[ec2-user@ip-172-31-25-189 ~]$ sudo swapon /swapfile1
[ec2-user@ip-172-31-25-189 ~]$ sudo sh -c 'echo "/swapfile1  none        swap    sw              0   0" >> /etc/fstab'

gemをインストール

ターミナル(EC2)
# まずは、EC2にダウンロードしたWEB Appを開く
[ec2-user@ip-172-31-23-189 www]$ cd  /var/www/アプリ名

# Rubyのバージョンを確認する
[ec2-user@ip-172-31-23-189 <アプリ名>]$ ruby -v

# 指定したrubyのバージョンが表示されれば成功です。
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]

本番環境でgemを管理するための bundlerをインストール

まず開発環境(ローカル)で開発してきたアプリにおいて、どのバージョンの bundlerが使われていたのか確認します。

ターミナル(ローカル)
$ bundler -v

# するとバージョンが表示されます
Bundler version 2.1.4 # 人によってバージョンが違う可能性があります

開発環境で仕様しているbundlerのバージョンがわかったので、同じバージョンのものを EC2サーバ側にも導入します。上記の場合では、bundler 2.1.4のバージョンを導入してbundle installを実行します。

ターミナル(EC2)
# ローカルで確認したbundlerのバージョンを導入する
[ec2-user@ip-172-31-23-189 <アプリ名>]$ gem install bundler -v 2.1.4
Fetching bundler-2.1.4.gem
Successfully installed bundler-2.1.4
Parsing documentation for bundler-2.1.4
Installing ri documentation for bundler-2.1.4
Done installing documentation for bundler after 3 seconds
1 gem installed

# 上記コマンドは、数分以上かかる場合もあります。
[ec2-user@ip-172-31-23-189 <アプリ名>]$ bundle install

環境変数の設定

データベースのパスワードなどセキュリティのためにGithubにアップロードすることがで きない情報は、環境変数というものを利用して設定します。
環境変数は、Railsからは ENV['<環境変数名>'] という記述でその値を利用することができます。今回は、SECRET_KEY_BASE という環境変数を指定していきます。

secret_key_baseを作成

ターミナル(EC2)
[ec2-user@ip-172-31-23-189 <アプリ名>]$ rake secret

# うまくいくと、
cdfasdfadgfsadfdgc314751a8dadfadf7c8b9a1dc888e...
という感じで表示されます
これをコピーしておきましょう

環境変数は /etc/environment というファイルに保存することで、サーバ全体に適用さ れます。環境変数の書き込みはvimコマンドを使用して行います。

ターミナル
[ec2-user@ip-172-31-23-189 <アプリ名>]$ sudo vim /etc/environment

スクリーンショット 2021-02-25 4.43.33.png
上の様な画面になれば、iと打ち込んで入力モードに切り替えた後、下記の記述を打ち込みます。 = の前後にスペースは入れません。

/etc/environment
DATABASE_PASSWORD='データベースのrootユーザーのパスワード'
SECRET_KEY_BASE='先程コピーしたsecret_key_base'

書き込みができたら esc(エスケープキー)を押下後、:wq と入力して内容を保存します。 保存できたら環境変数を適用するために一旦ログアウトします。

ターミナル(EC2)
[ec2-user@ip-172-31-23-189 <アプリ名>]$ exit
logout
Connection to XX.XXX.XXX.XX closed.

# もう一度ログイン
$ ssh -i ダウンロードした鍵の名前.pem ec2-user@Elastic IPアドレス

# 設定した環境変数が適用されているか確認
$ env | grep SECRET_KEY_BASE
SECRET_KEY_BASE='XXXXXXXXXXXXXXXXXXXXXXXXX'

$ env | grep DATABASE_PASSWORD
DATABASE_PASSWORD='XXXXXXXXXXXXX'

これで環境変数が設定出来ました!

終わりに

ここまでで、EC2インスタンスにSSHログインしてアクセスすることが出来ました。
次回でHTTP通信を可能にしてRailsアプリを起動させる工程まで進めます。
お疲れさまでした。。。

次回
Railsアプリを起動

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

railsを使ってアプリを作る 登録編

データベースにデータを登録

ルーティングの作成

ルーティングに登録する時はpostで登録する
post "animals/create" => "animals#create"

postで登録する時は、フォームで入力した値をコントローラーで受け取る時に使うらしい

viewの作成

送りたい内容を
<%= form_tag "/animals/create" do %>

<% end %>
で囲む

コントローラーの作成

viewからのデータを取得する
view = Animal.new(name: params[:name])
view.save

登録が完了したら、別ページに遷移させる(リダイレクト)
redirect_to "/animals/index"

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

railsを使ってアプリを作る3

詳細ページ等のid付きのページの作成と表示

mvcの作成

urlが/details/1みたいにする場合は、ルーティングの書き方を
get "details/:id" => "details#show
にする

detailsコントローラーにshowメソッドを作成する

show.html.erbのファイルを作成する

idの取得と利用方法

コントローラーでparams[:id] で取得する
取得した値を
@id = params[:id]とし、viewで<%= @id %>と書いて使う

画面に置くリンクはこんな感じ
<% @details.each do |detail| %>
  <%=link_to (animal.content,"/animal/#{detail.id}")%>
<% end %>

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

ポートフォリオ制作録(Day1:環境構築~デプロイ)

はじめに

  • 今日は,railsの開発環境をvscodeで構築し,localhostとheroku上でhello worldが表示されるまで進めました

発生したエラーと解決策

1.herokuへデプロイ出来ない①

デプロイ時にweb pack not foundとエラーがでました。warningの文でnode.jsが古いよとあったので、node.jsをインストール(同時にnpmも)したら解決。

2.herokuへデプロイできない②

チュートリアルのままbundle installをしていたが、その際にインストールされるbundlerのバージョンがherokuに対応していないらしく,デプロイができなかった。bundleのバージョンをherokuに対応しているものにして再インストールして解決

3.application errorが出るよ問題

herokuにデプロイできたがapplicationエラーが発生。エラー文を読むと,postgreのバージョンが変だよとのことなので、指定し直したらできました

参考

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