簡単な本番環境

Amazon Linux2を用いて、アプリをデプロイします。
今回はNginxと連携せず、アプリを開発して、デプロイしながら確認できるような環境を作ります。

下準備

イメージ、セキュリティ等を設定した上で、インスタンスにログインします。

パッケージのインストール

$ sudo yum -y install git make gcc-c++ patch libyaml-devel libffi-devel libicu-devel zlib-devel readline-devel libxml2-devel libxslt-devel ImageMagick ImageMagick-devel openssl-devel mysql-devel

Railsアプリを起動するために

次に、Ruby, Bundler, Rails, Node, MySQLをインストールします。

Ruby

Rubyの環境構築1/4 ~rbenvのインストール~

rbenvをホームディレクトリにcloneして、PATHを通します。

$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

このままでは、rbenvコマンドが使用できないので、更新します。打ち間違えなければ、sourceコマンドでも大丈夫です。

$ rbenv -v
> rbenv: コマンドが見つかりません
$ . ~/.bash_profile
$ rbenv -v
> rbenv 1.1.2-11...

Rubyの環境構築2/4 ~ruby-buildのインストール~

デフォルトのrbenvには、rubyをインストールする機能がないため、ruby-buildをインストールします。

インストールする場所を指定します。

$ mkdir -p "$(rbenv root)"/plugins
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

Rubyの環境構築3/4 ~Rubyコンパイラのインストール~

Rubyコンパイラ等をインストールします。

$ sudo yum -y install gcc gcc-c++ make openssl-devel readline-devel gpg patch libyaml-devel libffi-devel libicu-devel zlib-devel libxml2-devel libxslt-devel bzip2

Rubyの環境構築4/4 ~バージョン指定してインストール~

ここでは、ruby 2.5.1をインストールします。

$ rbenv install 2.5.1

デフォルトに設定します。

$ rbenv global 2.5.1
$ rbenv rehash
$ ruby -v
> ruby 2.5.1p57

Railsの環境構築1/3 ~bundlerのインストール~

Rubyをインストールしたため、gemコマンドが使用できます。gemコマンドでbundlerをインストールします。-vオプションでバージョンを指定できます。

$ gem install bundler -v 2.1.4
$ bundler -v
> Bundler version 2.1.4

Railsの環境構築2/3 ~Railsのインストール~

Railsもインストールできます。

$ gem install -v 5.2.4 rails
$ rails -v
> Rails 5.2.4

Railsの環境構築3/3 ~Node.jsのインストール~

RailsでJSを使用する場合、Node.jsも必要です。公式ホームページでは、12.xが推奨とあるため、リポジトリを追加します。

$ sudo curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -

インストールを実行します。

$ sudo yum -y install nodejs
$ node -v
> v12.14.1

MySQL

MySQLの環境構築1 ~レポジトリ追加~

公式のガイド通りにすればできます。release-el6にすると、CentOS6用であるため、後々エラーが発生します。

$ sudo yum -y localinstall http://dev.mysql.com/get/mysql57-community-release-el7-7.noarch.rpm

MySQLの環境構築2 ~インストールするバージョンの指定~

デフォルトではMySQL5.7のインストールを優先する設定になっています。

$ yum repolist all | grep mysql

バージョン切り替え用のライブラリをインストールします。

$ sudo yum -y install yum-utils

以下のコマンドにより、/etc/yum.pores.dファイルの記述内容を変更します (enableの値を0...falseや1...trueに切り替え)。

$ sudo yum-config-manager --disable mysql57-community
$ sudo yum-config-manager --enable mysql56-community

インストールするバージョンの切り替えができたので、インストールします。

$ sudo yum -y install mysql-community-server
$ mysql --version
> mysql  Ver 14.14 Distrib 5.6.47

MySQLの環境構築3 ~常時起動~

MySQLを起動します。コマンド実行後、エンターを押します (-yオプションが使えます)。

$ sudo service mysqld start

MySQLを常時起動しておきます。

$ sudo chkconfig mysqld on

[参考]
https://weblabo.oscasierra.net/installing-mysql56-centos7-yum/
http://sig9.hatenablog.com/entry/2016/11/20/120000

MySQLの環境構築4 ~rootユーザー設定~

初期パスワードを確認します。MySQL5.6の場合は特に生成されないはずです。

$ sudo cat /var/log/mysqld.log | grep 'is generated'
> R,NDijGL;4Mj

rootユーザーでログインし、パスワードを変更します。

$ mysql -u root
> Welcome to the MySQL monitor.  Commands end with ; or \g.
....
....

   mysql> update mysql.user set password=password('<新しく設定するパスワード>') where user = 'root';
  または > set password for root@localhost=password('<新しく設定するパスワード>');
> Query OK, 4 rows affected (0.00 sec)

   mysql> flush privileges;
> Query OK, 0 rows affected (0.00 sec)

   mysql> exit

パスワードの確認もしておきます。

$ mysql -u root -p
> Enter password:

[参考]
https://webkaru.net/mysql/mysql-root-password/

このままではコンテナが壊れると、アプリ利用者のデータも消えてしまうため、MySQLのデータはRDSコンテナに分けておくと安心です。ただ、AWSの請求額が高額になるため、財布と要相談。

GitHubと連携

暗号キーの取得

初期では連携していないため、以下のエラーメッセージが出力されます。

$ ssh -T git@github.com
> The authenticity of host 'github.com (140.82.113.4)' can't be established.

以下を実行し、暗号化キーを取得します。

$ ssh-keygen -t rsa -b 4096 -C "<GitHubに登録したメールアドレス>"

/home/centos/.ssh/id_rsa.pub (パブリックキー) が作成されたので、内容を表示しコピーします。id_rsaファイルではありません。

$ cat /home/ec2-user/.ssh/id_rsa.pub
> ssh-rsa APMih....gI6M8yb6TS2Ig.....

GitHubのユーザーページ > setting > SSH and GCG keys > New SSH keyで先ほどのキーを登録します。

ssh-rsa
APMih....gI6M8yb6TS2Ig.....3NZByiOgAj2Xmpfev...EiXG/JjSoB1JZbqEGV3JCUCvgo1qUey93zZ7JzPzgOGtD7+IpE5hF/wyifcLBCF2eKlGTcGw== <GitHubに登録したメールアドレス>
$ ssh -T git@github.com
> The authenticity of host 'github.com (140.82.113.4)' can't be established.
> ...
> Are you sure you want to continue connecting (yes/no)? 
> ...
> Hi <ユーザー名>! You've successfully authenticated, but GitHub does not provide shell access.

[参考]
https://help.github.com/en/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
https://qiita.com/suthio/items/2760e4cff0e185fe2db9

クローン

アプリの置き場を作ります。

$ sudo mkdir -p /var/www
$ cd /var/www

ユーザーにファイル操作権限を与えます。

$ sudo chown ec2-user /var/www/

Gitからアプリをダウンロードします。

$ sudo git clone https://github.com/<GitHubのユーザー名>/<レポジトリ名>.git
$ sudo chown ec2-user /var/www/<レポジトリ名>/

環境変数を設定します。

/etc/environment以下にシークレットキーを書いておきます。

DATABASE_PASSWORD='<MySQLで設定したパスワード>'
SECRET_KEY_BASE='<rails secretで生成した文字列>'

インスタンスに入り直して以下を実行し、環境変数が設定できているか確認します。

$ env | grep DATABASE_PASSWORD
$ env | grep SECRET_KEY_BASE

Unicorn

Unicornはgem経由で導入します。
本番で使用したい場合は、Nginxと連携させます。

コンパイル

webpackerを使用する場合は、yarnをインストールします。
precompileしない場合、cssファイルがないというエラーが出て、本番環境の確認ができないことがあります。

$ sudo npm install yarn -g
$ yarn -v
> 1.22.4
$ yarn install --check-files
$ rails assets:precompile RAILS_ENV=production

バックグラウンドで起動させます。

$ bundle exec unicorn_rails -c config/unicorn.rb -E production -D

エラーが発生したら、以下ファイルより原因を追求します。

log/production.log
log/unicorn.stderr.log

It is likely that you need to grant write permissions for that path.

What

bundle install時に以下のエラーが発生しました。

There was an error while trying to write to `xxx`. 
It is likely that you need to grant write permissions for that path.

Solution

エラー文通り、権限関連のエラーです。
以下で権限を与えることで、解決できます。

$ sudo chown <ユーザー名> <ファイル名 (絶対パス)>

can't find gem bundler (>= 0.a) with executable bundle (Gem::GemNotFoundException)

What

/varディレクトリ直下に、/wwwディレクトリを作成し、GitHubからレポジトリをクローンしたディレクトリ内で、以下コマンドを実行したときに発生しました。

$ bundler -v

ちなみに、/var直下では問題なくバージョンが示されます。
chownコマンドも実行済みです。

Solution

以下コマンドを実行すると、解決しました。

$ gem update --system

DBを再構築して、既存の本番環境を更新する

What

表題の通りです。
機能を追加するために、後からDBを修正することがあると思います (初めの設計がよくなかったパターン)。
Railsでの対処法を解説します。

Solution

ローカル

まず、migrationファイルでは扱いづらいため、jalkoby/squasherを使います。
以下の記事で簡単に説明しています。
https://diary-study-by-takuya.hateblo.jp/entry/2020/01/20/225942

現状のmigrationファイルでは、ファイルを作成した順番でTableが作成されるため、外部キーを新たに作りたい場合、不便なことがあります。squasherで集約すると、同一のファイル内でテーブルを生成する順番を簡単に入れ替えることができます。

デプロイしよう

capistranoでデプロイします。
しかし、taskを設定していない場合、以下は実行されません。

$ rails db:migrate:reset

ただ、taskを設定して、デプロイ時に毎回実行されるのも不便です。capistrano-rails-collectionとかもありますが、インスタンスに入って直接railsコマンドを打った方が手間が少ないと感じます。

capistranoを使用している場合は以下のファイルに最新ファイルがあります。

/var/www/<アプリ名>/current/

そこで、以下を実行すれば、dbを再構築できます。

$ RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 rails db:migrate:reset

結果が反映されない場合は、インスタンスを再起動したり、capistranoを実行し直したりすれば、反映されるようになります。

Axios + RailsでのPOSTの非同期化

What

Railsにおいて、GET以外のメソッド (POST, UPDATE, DELETE) を非同期で扱う際は、CSRF対策が施されています。

本記事は、非同期処理とは何か、そしてまた何であったかを知るための備忘録です。

下準備

ルーティング、コントローラー、モデル等を作成しておきます。

Rails.application.routes.draw do
  root 'main#index'
  post '/post', to: 'main#create'
end
class MainController < ApplicationController
  def index
  end

  def create
    post = Post.create(post_params)
  end

  private
  def post_params
    params.permit(:title,:content)
  end
end
class Post < ApplicationRecord
end
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :content
      t.timestamps
    end
  end
end

view側は次のようにしておきます。Slimで書いています (cssとかは触りたくなかったため、brを多用していて汚い)。
ちなみに、formでsubmitを囲っておかないと、うまく発火しません。

# views/main/index.html.slim

form[id='myForm']
  | title:
  input[type='text' name='title' value='initial post']
  br
  br
  | content:
  input[type='text' name='content' value='hello world wide web!!!']
  br
  input[type='submit' value='submit data']

gemで非同期処理をする

jQueryRailsにおいて、gemとして利用することができます。
gemとして利用した場合は、CSRF対策が施された状態なので、普通にPOSTなどができます。

$(function(){
  $('#myForm').submit(function(e){
    e.preventDefault();

    var myData = {
      title: $('input[name="title"]').val(),
      content: $('input[name="content"]').val()
    }

    $.ajax({
      url: '/post',
      type: 'post',
      data: myData,
      success: function(data){
        console.log(data)
      },
      error: function(){
        console.log('error!!!')
      }
    })
  })
})

$.ajaxの代わりに以下で書いても動きます。

    var url='/post'
    $.post(url, myData).done(function(data){
      console.log(data);
    })

上記は、/postに向けてPOSTメソッドを実行しています。
まず、ルーティングでmainコントローラーのcreateアクションが実行されます。
そのアクションで、paramsで保存された値をDBへ追加します。
Sequel Proでレコードを確認すると、きちんと保存できていることがわかります。

Axiosの場合

同じように書いてやります。
ここでは、getElementByIdが認識されなかったため、jQueryのメソッドですが、$(function(){})で囲って対処しています。
以下の書き方では、CSRF対策に引っかかり、POSTメソッドが失敗します。

$(function(){
  var myForm = document.getElementById('myForm');
  myForm.addEventListener('submit', function(e){
    e.preventDefault();
    var url = '/post'
    var myData = {
      title: document.querySelector('input[name="title"]').value,
      content: document.querySelector('input[name="content"]').value
    }
    axios.post(url,myData)
    .then(function(data){
      console.log(data)
    })
  })
})

以下のように、CSRFトークンを付け加える設定をすれば、AxiosでもPOSTメソッドの非同期化ができます。

$(function(){
  // import axios from 'axios'
  axios.defaults.headers.common = {
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
  };

  var myForm = document.getElementById('myForm');
  myForm.addEventListener('submit', function(e){
    e.preventDefault();
    var url = '/post'
    var myData = {
      title: document.querySelector('input[name="title"]').value,
      content: document.querySelector('input[name="content"]').value
    }
    axios.post(url,myData)
    .then(function(data){
      console.log(data)
    })
  })
})

配列とハッシュ, 定数と変数, ==と===の違い

What

JavaScriptを用いて、アルゴリズムとデータ構造について学びます。
以下のサイトで打ちながら学ぶと理解が深まります。
https://stephengrider.github.io/JSPlaygrounds/

定数と変数

上書きできるか、できないかが異なります。

変数 (var, let) を使うと値を上書きできます。

> let hoge = 'Hello'
> hoge
>> Hello
> hoge = 'fuga'
> hoge
>> fuga

定数は上書き不可です。

> const hoge = 'Hello'
> hoge = 'fuga'
>> TypeError

配列とハッシュの違い

key: valueの仕組みが異なります。
配列では、配列の順番でバリューを指定します。
ハッシュでは、キーと対応するバリューが読み出されます

> const array = ['hoge', 'fuga', 'hogehoge'];
> const hash = {a: 'hoge', 3: 'fuga', 'state': 'hogehoge'}

> array[0] == hash['a']
>> true

つまり、以下は同じです。

> const array = ['hoge', 'fuga', 'hogehoge'];
> const arrayHash = {0: 'hoge', 1: 'fuga', 2: 'hogehoge'}

==と===

===を使うと、文字列と数列を区別して比較します。

> '1' == 1
>> true
> '1' === 1
>> false

タグ付け機能[バックエンド]

What

タグ付け機能の実装例を紹介します。

DB テーブルはこんな感じ。 f:id:hellow_takuya:20200225001141p:plain

Strategy

文字列 (タグ) をparamsで保管しておいて、mapメソッドで都度都度保存
その時に、中間テーブルにもデータを入れておく

このようにすれば、元からあるWorkテーブルと無関係にタグを実装できます。

Solution

モデル側の記述

中間テーブルを作成する関係上、以下のように定義しておきます。
バリデーションや依存関係 (dependent系) はよしなに。

# work.rb

class Work < ApplicationRecord
  has_many :tag_works
  has_many :tags, through: :tag_works
end
# tag.rb

class Tag < ApplicationRecord
  has_many :tag_works
  has_many :works, through: :tag_works
end
# tag_work.rb

class TagWork < ApplicationRecord
  belongs_to :work
  belongs_to :tag
end

view側の記述

私の場合は、works tableに新しくレコードを追加するときに、タグも保存しておきたかったので、form_with(model: @work)内に追記しました。

記事とか画像とかが元々投稿できていたら、form_with内に何か足したりする必要はありません。

= form_with(model: @work, url: '/new', local: true) do |f|
  ...
  = f.text_field :all_tags, class:'p-Post__box', required: true
  ...

コントローラー側の記述

コントローラーを触りますが、送信ボタンを押した時に、paramsに値が入っているか確かめましょう。

  def new
    @work = Work.new
  end

   def create
    binding.pry
    @work = Work.new(pile_create_params)
    if @work.save
    else
      ...
    end
  end

ターミナルで、paramsと打ちます。

> params
>> ... "work" => {"..." => "...", ..., "all_tags" => "..."}

params[:work][:all_tags]とかparams.require(:work).permit(:all_tags)などと打てば、入力したタグが取り出せると思います。 それをprivateメソッド以下にtag_paramsとして定義してやります。

これでタグ入力した値を取得できました。

  private
  def tag_params
    params.require(:work).permit(:all_tags)
  end

タグが1個だけの場合は、そのまま保存すれば完成です。 例えば、以下のように書いてやります。

  def create
    @work = Work.new(pile_create_params)
    if @work.save
      tag = Tag.find_or_create_by!(name: tag_params[:all_tags].name)
      TagWork.find_or_create_by!(tag_id: tag.id, work_id: @work.id)
    end
  end

タグが複数の場合は何で区切るかを決める必要があります。
例えば、#で区切ることを考えます。
splitメソッドで文字列を#毎に区切り配列にします。

tag_params[:all_tags].split("#")

あとはmapメソッドで同じ処理を繰り返せば、完成です。
合わせるとこんな感じになります。
stripメソッドで空白を消すのもありです。

   def create
    @work = Work.new(pile_create_params)
    if @work.save
      tag_params[:all_tags].split("#").map do |name|
        tag = Tag.find_or_create_by!(name: name.strip)
        TagWork.find_or_create_by!(tag_id: tag.id, work_id: @work.id)
      end
      redirect_to root_url
    else
      ...
    end
  end

大いに参考にしました
https://www.sitepoint.com/tagging-scratch-rails/