簡単な本番環境
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コンパイラのインストール~
$ 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 <ユーザー名> <ファイル名 (絶対パス)>
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で非同期処理をする
jQueryはRailsにおいて、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 テーブルはこんな感じ。
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