capistrano3を使ってRailsアプリをさくらVPSにデプロイする

前回の記事の続き。 capistrano3を使って、VPS上にRailsアプリをデプロイする。

↓の記事を参考にした。 Rails5+Puma+Nginxな環境をCapistrano3でEC2にデプロイする(前編)

1. Gemの追加

デプロイに使うGemを設定する。 Gemfileに下記追加。

# Use Puma as the app server
gem 'puma', '~> 3.7'

group :development do
  gem 'capistrano', '3.7.0'
  gem 'capistrano-rails'
  gem 'capistrano-bundler'
  gem 'capistrano-rbenv'
  gem 'capistrano-rbenv-vars'
end
$ bundle install

2. capistranoの設定を追加

下記コマンドを実行することで、設定ファイルが自動で作成される。

$ bundle exec cap install
mkdir -p config/deploy
create config/deploy.rb
create config/deploy/staging.rb
create config/deploy/production.rb
mkdir -p lib/capistrano/tasks
create Capfile

まず、Capfileを以下のように編集。

require "capistrano/setup"
require "capistrano/deploy"
require "capistrano/scm/git"

install_plugin Capistrano::SCM::Git

Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

次に、config/deploy.rbを編集。

lock "3.7.0"

set :application, "アプリ名"
set :repo_url, "git@github.com:ユーザー名/アプリ名.git"

namespace :deploy do
  desc "Make sure local git is in sync with remote."
  task :confirm do
    on roles(:app) do
      puts "This stage is '#{fetch(:stage)}'. Deploying branch is '#{fetch(:branch)}'."
      puts 'Are you sure? [y/n]'
      ask :answer, 'n'
      if fetch(:answer) != 'y'
        puts 'deploy stopped'
        exit
      end
    end
  end

  desc 'Initial Deploy'
  task :initial do
    on roles(:app) do
      invoke 'deploy'
    end
  end

  before :starting, :confirm
end

最後に、config/deploy/production.rbを編集。

server "IPアドレス", user: "deploy", roles: %w{app db web}

set :ssh_options, {
  keys: %w(~/.ssh/id_rsa),
  forward_agent: true,
  auth_methods: %w(publickey)
}

2. デプロイ用ユーザーの作成

$ sudo su -
$ useradd deploy
$ passwd deploy
$ sudo visudo

## Allow root to run any commands anywhere
root   ALL=(ALL) ALL
deploy ALL=(ALL) ALL ← この1行を追加

SSH接続のための鍵の設定をする。

$ su - deploy
=> 先程作成したdeployユーザーに切り替え
$ mkdir ~/.ssh
$ chmod 700 ~/.ssh
$ vi ~/.ssh/authorized_keys
=> ローカルで作成した公開鍵を貼り付ける
$ chmod 600 ~/.ssh/authorized_keys

3. アプリケーションのデプロイ先のディレクトリを作成

$ pwd
/home/deploy

$ sudo mkdir /var/www && cd /var/www
$ sudo mkdir アプリ名
$ sudo chown deploy:deploy アプリ名

4. GitHubの設定

サーバーからGitHubに接続するために、公開鍵を登録する。

$ ssh-keygen -t rsa -v
=> まずは公開鍵と秘密鍵のペアを作成

作成した公開鍵を下記のページからGitHubに登録。

https://github.com/settings/ssh/new

SSHで接続確認。

$ ssh -T git@github.com
Hi ユーザー名! You've successfully authenticated, but GitHub does not provide shell access.
=> このメッセージが表示されればOK.

5. 試しデプロイ実行

ここまで出来たら、一旦デプロイを実行してみる。

$ bundle exec cap production deploy
=> deploy:log_revision まで実行出来たらOK

6. 必要なパッケージのインストール

yum で必要なパッケージをインストールする。

$ pwd
/home/deploy

$ sudo yum update
$ sudo yum install git
$ sudo yum install gcc
$ sudo yum install gcc-c++
$ sudo yum install openssl-devel
$ sudo yum install readline-devel
$ sudo yum install mysql-devel
$ sudo yum -y install nodejs

ruby-build のインストール

$ git clone git://github.com/sstephenson/ruby-build.git
$ sudo ruby-build/install.sh

rbenv のインストール

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

$ rbenv --version
rbenv 1.1.1

rbenv-varsのインストール

$ git clone https://github.com/rbenv/rbenv-vars.git $(rbenv root)/plugins/rbenv-vars

ruby2.4.1のインストール

$ rbenv install 2.4.1
$ rbenv global 2.4.1
$ rbenv rehash

$ ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]

bundler のインストール

$ gem install bundler

rubyracer のインストール

$ gem install therubyracer

Rails 5.1.6 のインストール

$ gem install rails -v 5.1.6

$ rails -v
Rails 5.1.6

puma のインストール

$ gem install puma

MySQL をインストール

参考記事:MySQL8をCentOS7にインスール(開発環境用)

$ rpm -qa | grep maria
mariadb-libs-5.5.60-1.el7_5.x86_64
=> mariadb が入っていないか確認
$ sudo yum remove mariadb-libs
=> mariadbのパッケージをアンインストール
$ sudo yum -y localinstall https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm
$ sudo yum -y install mysql mysql-devel mysql-server mysql-utilities
$ sudo systemctl start mysqld

7. nginxの設定を修正

/etc/nginx/nginx.conf を下記のように編集する。

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    include /etc/nginx/conf.d/*.conf;

    index   index.html index.htm;

    upstream puma {
        server unix:///var/www/アプリ名/shared/tmp/sockets/puma.sock;
    }
    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  localhost;
        root         /var/www/アプリ名/current/public;

        include /etc/nginx/default.d/*.conf;

        location / {
            try_files $uri $uri/index.html $uri.html @webapp;
        }

        location @webapp {
            proxy_read_timeout 300;
            proxy_connect_timeout 300;
            proxy_redirect off;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://puma;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}

8. デプロイ設定の修正

本番環境で、下記のディレクトリが /var/www/アプリ名/shared/ の下に配置されるように設定を編集する。

ローカルに戻り以下の設定を編集する。

config/puma.rbに下記追加。

app_dir = File.expand_path("../..", __FILE__)
bind "unix://#{app_dir}/tmp/sockets/puma.sock"
pidfile "#{app_dir}/tmp/pids/puma.pid"
state_path "#{app_dir}/tmp/pids/puma.state"
stdout_redirect "#{app_dir}/log/puma.stdout.log", "#{app_dir}/log/puma.stderr.log", true

config/deploy.rb に下記追加

append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"

デプロイを実行

$ bundle exec cap production deploy

9. SECRET_KEY_BASE の設定

以下のコマンドで本番環境の SECRET_KEY_BASE を環境変数に設定する。

$ export SECRET_KEY_BASE=`bundle exec rake secret`

10. MySQL の初期設定

Rootユーザーの初期パスワードを変更する。

参考:(MySQL8をCentOS7にインスール(開発環境用))https://d-ebi.hatenablog.com/entry/2018/11/26/210000

$ sudo grep 'temporary password' /var/log/mysqld.log
2018-11-26T01:55:34.881029Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: Rw.gaqQMp2>R
=> 初期パスワードを確認する
$ mysql_secure_installation
Enter password for user root:
=> 初期パスワードを入力して、パスワード設定などを行う。

アプリケーションで使用する(config/database.ymlに記載されている)本番用のDBとユーザーの作成。

$ mysql -u root -p
mysql> create database DB名;
Query OK, 1 row affected (0.10 sec)
mysql> create user ユーザー名@localhost identified by パスワード;
Query OK, 0 rows affected (0.01 sec)
mysql> grant all on DB名.* to ユーザー名@localhost; 
Query OK, 0 rows affected (0.03 sec)

11. MySQLのSocketファイルの設定

MySQLのSocketファイルの出力先パスを調べて config/database.yml に設定する。

$ mysqladmin -u root version -p
..
UNIX socket     /var/lib/mysql/mysql.sock
...
=> このパスを `config/database.yml` に追記する。

12. asset compile

$ bundle exec rake assets:precompile RAILS_ENV=production

12. Pumaの再起動

サーバーに接続して、Pumaを再起動する。

$ cd /var/www/アプリ名/current
$ bundle exec puma -t 5:5 -e production -C config/puma.rb
=> Pumaを起動

起動に成功すれば、socketファイルやpidファイルが生成される。

どんなプログラマになりたいか考えたのである

どんなプログラマになりたいか考えてみたのだ。 一言で言えば。「金を作れるプログラマ」だなぁ。 せせりさんとかマジリスペクト。 技術的にはまずは、RubyRailsを極めるしかないっしょ。 でも極めるってかなりぼやっとした目標。漂う達成不可能感。 どうすりゃいいの。 じゃあもっと具体的な目標にすればええんでないの? ということで、とりあえず、今年中にWebサービスを最低3個作る。ってのはどうでしょう。 いいんじゃないでしょうか。すごく具体的。 あと、個人的には数学も勉強したい。数学検定一級合格!とかいい目標でない? あとTOEIC900点!とかね。かっこいいじゃない。 でもその前になりよりもプログラマとして一人前になりたいのですね。アタシ。 インタプリタとかOSとか作ってみたいじゃない、プログラマなら。

さくらVPSの初期設定〜nginxの起動まで

ドットインストールの「(さくらのVPS入門)https://dotinstall.com/lessons/basic_sakura_vps」をやったので簡単に内容をメモしておく。 OSはCentOS7。

1. VPSに接続する

$ ssh root@[IPアドレス]
Are you sure you want to continue connecting (yes/no)? yes
root@IPアドレス's password: 
=> 仮登録完了メールに記載されているパスワードを入力

$ yum update
=> インストール済みのパッケージをアップデート

2. 作業用ユーザーの設定

$ useradd admin
=> adminという作業用ユーザーを追加
$ passwd admin
=> 任意のpasswordを設定

次に作成した作業用ユーザーで sudo コマンドを実行できるようにする。

$ usermod -G wheel admin
=> wheel グループに作業用ユーザーを追加する。wheelというのは、sudoを実行できるユーザーグループの名前。大物とか実行者という意味らしい。
$ visudo
=> sudo権限を変更するためのコマンド。sudoersというファイルが開く。

以下の行を探して、2行目の「#」を消すことでこの一文を有効化する。 これで wheel グループのユーザーが sudo を実行できるようになる。

## Allows people in group wheel to run all commands
# %wheel  ALL=(ALL)       ALL

3. 鍵認証の設定

パスワード認証だとセキュリティの強度が低いので、公開鍵を使った認証に切り替える。

まず、VPS側に公開鍵をいれるディレクトリを作る。

$ pwd
/home/admin
$ mkdir ~/.ssh
$ chmod 700 ~/.ssh
=> 所有者のみ操作可能とする。

次に、SSHの接続元(ローカル)側で接続に使う鍵のペアを作成する。

$ ssh-keygen -t rsa -v
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/***/.ssh/id_rsa):
=> 保存するファイル名を指定。基本はデフォルトでよい。
$ chmod 600 ~/.ssh/id_rsa.pub
=> 作成された公開鍵のファイルのパーミッションを変更。
$ scp ~/.ssh/id_rsa.pub admin@IPアドレス:~/.ssh/authorized_keys
=> 公開鍵をVPSに転送する。

鍵のアップロードが完了したら、以下のコマンドで秘密鍵を使ってローカルからVPSに接続ができる。

$ ssh -i ~/.ssh/id_rsa admin@IPアドレス
# デフォルトで ~/.ssh/id_rsa を見に行くようになっているので、下記のように省略してもOK。
$ ssh admin@IPアドレス

4. SSHの設定

以上で鍵を使ってSSH接続するところまで完了。だが、パスワードログインやルートログインが有効になっていて、このままではセキュリティ上良くないので、それらを無効化する

VPSに接続して、以下のコマンドを実行。

$ sudo -s
=> ここからは、ルート権限で実効するコマンドが多いので、ルート権限に変更する。
# cp /etc/ssh/sshd_config /etc/ssh/sshd_config.org
=> SSHの設定ファイルを編集する前にバックアップをとっておく
# vim /etc/ssh/sshd_config
=> vimで設定ファイルを開く

vimで、/etc/ssh/sshd_config を開いて以下の設定を編集する。

編集前:PasswordAuthentication yes
編集後:PasswordAuthentication no
編集前:# PermitRootLogin yes
編集後:PermitRootLogin no

下記コマンドを実行して編集したSSHの設定を読み込む。

# systemctl restart sshd

5. ファイアウォールの設定

CentOS6までは、iptablesというコマンドだったけど、7からはfirewalldというのに変わったらしい。 firewalldについては↓の記事がわかりやすかった。 ネコでもわかる!さくらのVPS講座 ~第七回「ファイアウォール”firewalld”について理解しよう」

あとで、Webサーバーを立ち上げるので、あらかじめHTTP通信を許可しておく。

# firewall-cmd --add-service=http --zone=public --permanent
# firewall-cmd --add-service=https --zone=public --permanent
# systemctl restart firewalld

6. nginxのインストール

$ yum install nginx
=> nginx をインストール
$ systemctl start nginx
=> 起動
$ systemctl enable nginx
=> OS起動時に自動で起動させる

割り振られたIPアドレスにアクセスしたらnginxのトップページが表示されるはず。

うつのようなものになって2ヶ月が過ぎた

うつ病になって会社を休職してから2ヶ月ちょっと経った。 一次は布団から起きるのもままらならかったけれど、いまではいくぶんかマシになった。

今日は浜松町にある心療内科に行ったのだけれど、休診日だということを忘れていた。 病院に向かう京浜東北線の車内で気づいた。

しょうがないので、そのまま乗り過ごして、秋葉原と神保町で時間を潰す。 惰性で過ごした学生時代を思い返して少しだけ後悔。

初めて神田の近江屋 に入った。 アップルケーキとドリンクセットを頼む。1,080円。 休職中の身には少し高いけどまぁいいや。

なぜ、ああたの仕事は終わらないのか? 中島聡

著者はマイクロソフト本社で働いていたこともある日本人プログラマの中島さん。
中島さんが培ってきた仕事術を紹介している。

・(仕事が終わらない理由は)大きくまとめると、次の3点に集約されます。(p.48)
 ① 安請け合いをしてしまう
 ② ギリギリまでやらない
 ③ 計画の見積もりをしない

・言葉で説明するのが難しいときは形にして見せてしまうのが一番いい(p.123)
・どんな仕事でも、企画をアイデアのままではなく形にした人がその企画の推進者になることができます。
 私のような日本から渡米したばかりのいちエンジニアがマイクロソフトで活躍できたのは、
 まさに限られた時間を濃密に使いこなし、プロトタイプを先に作ったからなのです。(p.134)

・まずは「締切は絶対に守るもの」と考える(p.152)
 ① 「まずはどのくらいかかるかやってみるので、スケジュールの割り出しのために2日ください」と
   答えて仕事に取り掛かる(見積もりをするための調査期間をもらう)。
 ② その2日をロケットスタート期間として使い、2日で「ほぼ完成」まで持っていく
 ③ 万が一、その2日で「ほぼ完成」まで持っていけなかった場合、これを「危機的な状況」と認識して
  スケジュールの見直しを交渉する

・期間が10日だとしたら、最初の2日は「ロケットスタート」の期間。ここで、「ほぼ完成」に持っていく。残り8日は「流し」の期間。(p.165)
ロケットスタート期間のまま全て終わらせないこと。
 ・いつも全力を出していると、真の実力を発揮できなくなる(p.164)

Bundlerとは

Bundlerとは

Bundlerとはアプリケーションで利用するgemパッケージを定義し、依存関係を解決するための仕組み。
Bundlerはアプリケーションごとにgemパッケージの依存関係を閉じ込めることで、特定のアプリケーションだけで利用するgemパッケージを簡単に管理することができる。

Gemfile

Bundlerはアプリケーションで利用するgemパッケージをGemfileというファイルに列挙する。Gemfileは「bundle init」コマンドを実行することで生成される。
Gemfileの中身は以下のようになっている。

# frozen_string_literal: true
# A sample Gemfile
source "https://rubygems.org"

# gem "rails"

sourceがgemパッケージの取得先、デフォルトではrubygems.orgが指定されているが、ローカルネット内に独自に立てているリポジトリサーバーに向けることも出来る。

インストール

Gemfileに使用するgemパッケージを記載して「bundle install」コマンドを実行するとgemパッケージがインストールされる。またインストール対象のパッケージ内で使われているgemパッケージも芋づる式にインストールされる。
「bundle install」を実行すると、Gemfileと同じディレクトリにGemfile.lockファイルが作成される。このファイルには、Gemfilleに記述されたgemパッケージの依存関係を解決した結果が書き出される。
Gemfile.lock内にはインストールされたgemパッケージの具体的なバージョンも記載される。
Gemfile.lockも更新したい場合は、「bundle update」コマンドを実効する。

「bundle install」でインストールしたコマンドの実行

「bundle install」でインストールしたGemは「bundle exec コマンド名」で実行する。

$ bundle exec [コマンド]

開発用、テスト用

Gemfileは開発環境の場合とテスト環境の場合にインストールするものを設定可能。

source "http://rubygems.org"

gem 'sinatra'

# 開発用のGem
group :development do
    gem 'awesome_print'
end

# テスト用のGem

group :test do
    gem 'rspec'
end

特定の環境のGemだけインストールしない場合は、--withoutオプションを使う。

# developmentとtest以外のGemをインストールする
% bundle install --without development test

binstubsオプション

「bundle install」でインストールしたコマンドは、通常「bundle exec コマンド名」でなければ実行できない。
しかし、「bundle install」時に「--binstubs」オプションを指定すれば、直下にbinディレクトリが作成されその中にGemのコマンドが配置されるので、コマンド名だけでコマンドが実行可能になる。

RSpecのまとめ

概要

BDDを行うためのテストフレームワーク

準備

gemのインストール。

$ gem install rspec

初期化

$ rspec -init

これを実行すると spec というディレクトリが直下に出来て、その中に spec_helper.rb ファイルが出来る。rspecのテストファイルを作成する際は必ずこれを require する必要がある。

チュートリアル

(1) テスト対象を決める
今回は、/lib/dog.rb を対象とする。

(2) 失敗するテストを書く
下記のテストファイルを作る。
spec/lib/dog_spec.rb

require "spec_helper"
require "dog"

describe Dog do
  it "is named 'Pochi'" do
    dog = Dog.new
    expect(dog.name).to eq 'Pochi'
  end
end

またテスト対象である Dog クラスもファイルだけ作成しておく。 (でないとテスト実行時にエラーがとなってしまうので)

テストを実行してみます。実行結果は・・

$ rspec spec/lib/dog_spec.rb
F

Failures:

  1) Dog is named 'Pochi'
     Failure/Error: expect(dog.name).to eq 'Pochi'

     NoMethodError:
       undefined method `name' for #<Dog:0x007fcc4f97a8d8>
     # ./spec/lib/dog_spec.rb:7:in `block (2 levels) in <top (required)>'

Finished in 0.00061 seconds (files took 0.55266 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/lib/dog_spec.rb:5 # Dog is named 'Pochi'

当然、dogの中身がないので失敗します。
テストが成功するように、dog.rbを修正します。

class Dog
  attr_accessor :name

  def initialize(name="Pochi")
    @name = name
  end
end

再度テストを実行してみる。

$ rspec spec/lib/dog_spec.rb
.

Finished in 0.0074 seconds (files took 0.53777 seconds to load)
1 example, 0 failures

今度は無事成功したのでOK! このテスト作成→失敗→修正→成功までの一連のサイクルがBDD(Behavior Driven Development)の基本的な流れになる。

その他のテスト例

describe User do
  describe '#greet' do
    before do
      @params = { name: 'たろう' }
    end
    context '12歳以下の場合' do
      before do
        @params.merge!(age: 12)
      end
      it 'ひらがなで答えること' do
        user = User.new(@params)
        expect(user.greet).to eq 'ぼくはたろうだよ。'
      end
    end
    context '13歳以上の場合' do
      before do
        @params.merge!(age: 13)
      end
      it '漢字で答えること' do
        user = User.new(@params)
        expect(user.greet).to eq '僕はたろうです。'
      end
    end
  end
end

describe '#greet' doは、Userクラスのgreetメソッドをテストするということ。
また、beforeは、テスト実行前に必要な共通処理を記載する。
contextは、直訳すると「文脈」や「状況」という意味。つまり、なにかしら条件が異なる場合はcontextを使ってテストを分割する。

インスタンス変数の代わりにletを使う

インスタンス変数の代わりに以下のようにletを使うこともできる。

require 'spec_helper'
require 'user'

describe User do
  describe '#greet' do
    let(:params) {{name: 'たろう'}}
    context '12歳以下の場合' do
      before do
        params.merge!(age: 12)
      end
      it 'ひらがなで答えること' do
        user = User.new(params)
        expect(user.greet).to eq 'ぼくはたろうだよ。'
      end
    end
    context '13以上の場合' do
      before do
        params.merge!(age: 13)
      end
      it '漢字で答えること' do
        user = User.new(params)
        expect(user.greet).to eq '僕はたろうです。'
      end
    end
  end
end

let(:params) {{name: 'たろう'}}がそう。{}が二重になってちょっとわかりにくいが、外側がRubyのブロックの{}で、内側がハッシュの{}だ。
let は遅延評価されるので、それが使われる時に初めて初期化される。つまり、上のテストコードを以下のように書き直すことが出来てしまう。

require 'spec_helper'
require 'user'

describe User do
  describe '#greet' do
    let(:user) { User.new(params) }
    let(:params) {{name: 'たろう', age: age}}
    context '12歳以下の場合' do
      let(:age) {12}
      it 'ひらがなで答えること' do
        expect(user.greet).to eq 'ぼくはたろうだよ。'
      end
    end
    context '13以上の場合' do
      let(:age) {13}
      it '漢字で答えること' do
        expect(user.greet).to eq '僕はたろうです。'
      end
    end
  end
end

age は params ハッシュの定義時に使われているが、各context内ではじめて実際の値が代入されている。