7839

雑草魂エンジニアブログ

【Rails】SideKiqを用いて非同期処理(バックグラウンドジョブ)を実行する

今回、Rails のプロジェクトで、バックグラウンド処理を実装したくなり、SideKiq を用いて実装を行ったので、備忘録として残しておく。

ジョブ管理

Rails アプリケーションで非同期的に処理を実行したい場合などがある。
例えば、以下のように、API 経由でメールを送信する場合。

def send_email
  # recieve request
  mail.send()  # 処理が終わるまで待機する
  head(:ok) && return
end

同期処理の場合、mail.send() の処理が終わるまで、レスポンスを返すことができず、最悪の場合、リクエスタイムアウトになってしまう。こういう場合に、ジョブ管理を使うと、ジョブをキューに登録する処理だけ実行し、(並列処理をすることで)レスポンスを即座に返すことができる。

Rails5 には、ジョブ管理のフレームワークである Active Job がある。 ただし、Rails 自身が提供するのは、ジョブをメモリに保持するインプロセスのキューイングシステムだけであり、再起動などするとジョブは全て失われてしまう。

そのため、(Railsアプリケーションの起動とは別に)ジョブを処理するためにライブラリ固有のキューイングサービスを起動しておく必要がある。

詳細は、以下の公式ドキュメントを参考にして欲しい。

railsguides.jp

今回、Active Job とアダプタとして Sidekiq を使ってバックグラウンドジョブを実行することもできたが、Sidekiq 単体でも同様の処理が実行できたため、Sidekiq 単体での実装にすることとした。

Sidekiq とは

github.com

Simple, efficient background processing for Ruby.

Sidekiq はスレッドを使用して、同じプロセスで同時に多くのジョブを処理することができる。

Sidekiq を使うためには Client、Redis、Server の 3 つが必要となる。

  • Client:Railsアプリケーションサーバー(ジョブを登録する)
  • Redis:ジョブをキューイングするために必要(ジョブ管理をする)
  • Server:並列処理をする(ジョブを実行する)ために、Railsアプリケーションとは別プロセスとして起動しておく(bundle exec sidekiq で起動することができる)

Redis をインストールする

今回、開発環境を Docker としていたので、ruby の環境下に Redis を追加することにした。

FROM ruby:2.6.3-alpine

ENV LANG C.UTF-8
ENV WORKSPACE=/app

# 基本モジュールインストール
RUN apk update && \
  apk add --no-cache tzdata libxml2-dev curl-dev make gcc libc-dev build-base libxslt-dev g++ mariadb-dev less git && \
  rm -rf /usr/local/bundle/cache/* /usr/local/share/.cache/* /var/cache/* /tmp/* && \
  apk del libxml2-dev curl-dev make gcc libc-dev g++

# node + yarn
# python2はnodeインストール時の依存関係
RUN apk add --no-cache alpine-sdk \
  nodejs-current \
  nodejs-npm \
  yarn \
  python2

# install Redis
RUN apk add --update --no-cache redis

RUN mkdir -p $WORKSPACE
WORKDIR $WORKSPACE

ADD Gemfile $WORKSPACE/Gemfile
ADD Gemfile.lock $WORKSPACE/Gemfile.lock

RUN bundle install

COPY . $WORKSPACE

Redis を起動する

$ redis-server --appendonly yes --daemonize yes

Sidekiq を実装する

1. gemを追加する

gem 'sidekiq'
gem 'sinatra', require: false # ダッシュボードのため

bundle install を実行して、gem をインストールする。

2. 起動時の設定を追加する
サーバー側とクライアント側の2種類の設定を追記しておく。

Sidekiq.configure_server do |config|
    config.redis = { url: 'redis://localhost:6379' }
end

Sidekiq.configure_client do |config|
    config.redis = { url: 'redis://localhost:6379' }
end

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

:verbose: false
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:concurrency: 10
:queues:
  - default

4. Woker作成(ジョブを登録する) 今回は、Testという名前のジョブを作成する。

$ bundle exec rails g sidekiq:worker Test
      create  app/workers/testd_worker.rb
      create  test/workers/test_worker_test.rb
class TestWorker < ApplicationController
  include Sidekiq::Worker
  def perform()
    p 'Hello SideKiq'
  end
end

5. sidekiqを起動する

$ bundle exec sidekiq -C config/sidekiq.yml

6. ジョブを実行してみる Controller などから呼び出して実行することもできるが、試験的に Railsコンソールで実行してみる。(sidekiq を起動しいるシェルとは別に新しいシェルで実行する。)

$ bundle exec rails c
> TestWorker.perform_async()

sidekiq 側で、「Hello SideKiq」が表示され、ジョブを実行することが確認できた。

7. 管理画面を追加する

require 'sidekiq/web'
mount Sidekiq::Web, at: "/sidekiq"

再度、Rails サーバーを起動して、http://localhost:3000/sidekiq にアクセスすると、管理画面が表示される。

(おまけ)AWS EC2へのデプロイ

最後に、諸々の機能を実装して、本番のAWSにデプロイする際に、追加で実施した処理について残しておく。

Redis のインストール

$ amazon-linux-extras
6  postgresql10     available  [ =10  =stable ]
8  redis4.0             available  [ =4.0.5  =4.0.10  =stable ]
9  R3.4                   available  [ =3.4.3  =stable ]

$ sudo amazon-linux-extras install redis4.0

Redis と Sidekiq のデーモン化

今回、Redis と Sidekiq のデーモン化は、前回紹介した Supervisor で行うことにした。 (前回の記事:【Linux】Supervisor でプロセス制御(Rails + Puma / Capistrano) 参照)

それぞれの設定ファイルを以下のように準備する。

[program:redis]
command=/bin/redis-server /etc/redis.conf
autostart=true
autorestart=true
user=redis
[program:sidekiq]
directory=/srv/monoai-ranger-manager/current
command=/bin/bash -c "/opt/rbenv/shims/bundle exec sidekiq -e production -C config/sidekiq.yml"
autostart=true
autorestart=true
user=ec2-user
startsecs=10
stdout_logfile=/srv/monoai-ranger-manager/shared/log/sidekiq.stderr.log
logfile_maxbytes=1MB
logfile_backups=10
redirect_stderr=true

(注意)
-e production のオプションを付けないと、default が development になっているので、本番環境で環境変数の不一致が発生しているエラーが発生した。

supervisord を再起動させて、実行させる。

$ supervisorctl reload
Restarted supervisord

$ supervisorctl status
redis                  RUNNING   pid 17430, uptime 0:00:13
sidekiq              RUNNING   pid 17431, uptime 0:00:13

Capistrano 設定追加

以前は、Rails アプリケーションのみであったが、今回 Redis と Sidekiq が増えたので、Deploy 前後で全てのプロセスを再起動するように変更した。これで、deployコマンドのみでプロセスの再起動まで完結するようにできた。

~~ 省略 ~~
namespace :console do
  namespace :all do
    desc 'start service all'
    task :start do
      on roles(:app) do |_host|
        execute 'supervisorctl start all'
      end
    end

    desc 'stop service all'
    task :stop do
      on roles(:app) do |_host|
        execute 'supervisorctl stop all'
      end
    end
  end
end

before 'deploy:starting', 'console:all:stop'
after 'deploy:finished', 'console:all:start'

(おまけ)redis と sidekiq を別コンテナで起動する

Sidekiqは、Railsで非同期処理を行うためのライブラリであり、実行の際にはRailsのプロセスとは別に実行する必要がある。負荷を分散するために、SidekiqをRailsアプリケーションとは別のコンテナで動かしたいことがあるかもしれない。その場合は、以下の設定にすることで、簡単に実現することができる。

version: '3'
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      TZ: Asia/Tokyo
    ports:
      - 3306:3306
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - mysql-data:/var/lib/mysql
  redis:
    image: redis:alpine
    ports:
      - 6379:6379
  app: &app_base
    build:
      context: .
    volumes:
      - .:/app
      - gem-data:/app/vendor/bundle
      - log-data:/app/log
    ports:
      - 3000:3000
    depends_on:
      - db
      - redis
    stdin_open: true
    tty: true
  sidekiq:
    <<: *app_base
    command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"]
    ports:
      - 3001:3001
volumes:
  mysql-data:
  gem-data:
  log-data:

また、以下のライブラリを追加する。

〜省略〜
gem 'redis-namespace'

SideKiq の設定を以下のように変更する。

Sidekiq.configure_server do |config|
  config.redis = { url: 'redis://redis:6379', namespace: 'sidekiq' }
end
Sidekiq.configure_client do |config|
  config.redis = { url: 'redis://redis:6379', namespace: 'sidekiq' }
end

これで、コンテナを分離することができた。 (実際に、コンテナを分離することで、ログなどがそれぞれのコンテナに記載されてしまうので、正直ログを確認するのが面倒になってしまったので、そこあたりの設定をしっかりとしていきたい。)

まとめ

今回は、SideKiq を用いてジョブ管理をすることができた。定期処理や日時指定での実行も可能のため、今後も使っていきたいと思えた。

それでは、ステキな開発ライフを。

関連記事

serip39.hatenablog.com

【Linux】Supervisor でプロセス制御(Rails + Puma / Capistrano)

EC2(AmazonLinux2)に構築した Railsアプリケーションのプロセス管理、具体的にはデーモンプロセスとして動かすために、今回 Supervisor を使ったので、備忘録として残しておく。

  • Supervisor v4.2.1

Supervisor とは

supervisord.org

Supervisor とは、Python 製のプロセスの制御・管理ツールで、常時起動させたいスクリプトなどを簡単にデーモン化することができる。(デーモン化することで、クラッシュした際の再起動などを自動で行ってくれる。)

今回は、Rails + Puma で運用しているWEBアプリケーションの Puma のプロセスの制御を Supervisor で行えるようにした。

しかしながら、Linux であれば、「systemd」に service として登録することで同様の設定を簡単に行うことができる。ただし、systemd の場合、基本は root ユーザーで起動停止管理を行う必要がある。

今回、systemd ではなく、Supervisor を採用した大きな理由としては、Capistrano を使って、一般ユーザーでも起動・停止のプロセス管理を実施したかったことが挙げられる。

Supervisor のコマンド

Supervisor に関して、使用するコマンドは2種類ある。(プロセス管理を行うには、supervisord が事前に立ち上がっておく必要がある。)

  1. supervisord:supervisordの起動に使う
  2. supervisorctl:プロセスの管理・制御で使う

supervisord

supervisord の起動を行う

$ /usr/bin/supervisord -n -c /etc/supervisord.conf

オプションは複数あるが、上記ができていればいいのではないかと思う。

  • -c:設定ファイルを指定する
  • -n:フォアグラウンドで起動する(こちらを設定することで、ctrl + cで停止が可能となる。試験時には、便利。)

詳細は、公式ドキュメント 参照。

supervisorctl

プロセス全体の管理・制御を行う場合

# プロセス全体の起動
$ supervisorctl start all
# プロセス全体のステータス確認
$ supervisorctl status
# プロセス全体の停止
$ supervisorctl stop all
# プロセス全体の再起動
$ supervisorctl restart all
# 構成ファイルを再読み込みする(設定変更時、実施要)
$ supervisorctl reread
# supervisordを再起動する
$ supervisorctl reload

1つのプロセスの管理・制御を行う場合

# 起動
$ supervisorctl start <name>
# ステータス確認
$ supervisorctl status <name>
# 停止
$ supervisorctl stop <name>
# 再起動
$ supervisorctl restart <name>

詳細は、公式ドキュメント 参照。

EC2への設定方法

今回は、Puma のプロセスの制御を行う。

1. Supervisorインストール

python パッケージマネージャ(pip/easy_install)を使う。

$ sudo easy_install supervisor
$ supervisord -v
4.2.1

2. デフォルトの設定ファイルを生成する

$ echo_supervisord_conf > /etc/supervisord.conf

3. アプリケーション用の設定ファイルを追加する

$ sudo mkdir /etc/supervisord.d
$ sudo vim /etc/supervisord.d/sample-app.conf
# プロセス名を設定する。今回は、sample-appとする。
[program:sample-app]
# 実行ディレクトリを指定する(RailsのCapistranoのcurrentディレクトリ)
directory=/app/sample-app/current
# 実行コマンド(puma起動)
command=/bin/bash -c "/opt/rbenv/shims/bundle exec pumactl start"
environment=RAILS_ENV="production"
autostart=true
autorestart=true
startsecs=3
user=ec2-user
redirect_stderr=false
# logファイルの保存先を指定する
stderr_logfile=/app/sample-app/shared/log/sample-app.stderr.log
stdout_logfile=/app/sample-app/shared/log/sample-app.stdout.log

詳細な設定項目は、公式ドキュメント を参照。

4. 設定ファイルを変更する

変更内容は、以下。

  • アプリケーション用の設定ファイルを include できるようにする
  • web の管理コンソールを開放し、同時に root 以外のユーザーからのクライアントコマンド許可を行う(Capistranoで実行できるようにしておく)
$ sudo vim /etc/supervisord.conf
# 以下の3箇所のコメントアウトを外してあげる
[inet_http_server]
port=127.0.0.1:9001

[supervisorctl]
serverurl=http://127.0.0.1:9001

[include]
files = /etc/supervisord.d/*.conf
# rootユーザー以外の場合に、以下のエラーが出た
error: <class 'socket.error'>, [Errno 13] Permission denied: file: /usr/lib64/python2.7/socket.py line: 228

5. supervisord をデーモン化するために、systemd にサービスを登録する

Supervisor を使用するには supervisord をあらかじめ起動しておく必要があるため、systemd にサービスを登録して、supervisord をデーモン化しておく。

$ sudo vim /lib/systemd/system/supervisord.service
[Unit]
Description=Supervisor process control system for UNIX
Documentation=http://supervisord.org
After=network.target

[Service]
ExecStart=/usr/bin/supervisord -n -c /etc/supervisord.conf
ExecStop=/usr/bin/supervisorctl $OPTIONS shutdown
ExecReload=/usr/bin/supervisorctl $OPTIONS reload
KillMode=process
Restart=on-failure
RestartSec=50s

[Install]
WantedBy=multi-user.target

以下のコマンドを実行して、「supervisord.service」が systemd に登録されているか確認する。

systemctl list-unit-files --type=service

無事に、サービス登録がされていたら、以下のコマンドを実行して起動・ステータス確認・停止ができるかを確認する。

# 起動
sudo systemctl start supervisord
# 状態確認
sudo systemctl status supervisord
● supervisord.service - Supervisor process control system for UNIX
   Loaded: loaded (/etc/systemd/system/supervisord.service; disabled; vendor preset: disabled)
   Active: active (running) since 2020-12-01 00:00:00 UTC
# 停止
sudo systemctl stop supervisord

6. 起動している状態で、サービスの自動起動の設定を行う

$ sudo systemctl enable supervisord.service
Created symlink from /etc/systemd/system/multi-user.target.wants/supervisord.service to /usr/lib/systemd/system/supervisord.service.

7. 最後に、supervisor でプロセス制御を行う

# 起動
$ supervisorctl start sample-app
# ステータス確認
$ supervisorctl status sample-app
# 停止
$ supervisorctl stop sample-app
# 再起動
$ supervisorctl restart sample-app
# 設定ファイルを変更した場合には、再読み込みが必要
$ supervisorctl reload

無事に、Supervisor で Puma のプロセスを制御できれば設定は完了である。

きちんとデーモン化されているかを確認するために、以下を実行してみる。

# プロセスIDを確認する
$ supervisorctl status sample-app
sample-app        RUNNING   pid 26041, uptime 1:00:00
# プロセスを kill する
$ kill -9 26041
# 再度、プロセスIDを確認する
# プロセスIDが変わっていれば、デーモン化されていることが確認できたことになる
$ supervisorctl status sample-app
sample-app        RUNNING   pid 26045, uptime 1:00:00

これで、設定は完了である。

Capistrano の設定

Capistrano から supervisor を操作する場合は、以下の設定をしておく。

namespace :console do
  desc 'status service'
  task :status do
    on roles(:app) do |_host|
      execute 'supervisorctl status sample-app'
    end
  end
  desc 'start service'
  task :start do
    on roles(:app) do |_host|
      execute 'supervisorctl start sample-app'
    end
  end
  desc 'restart service'
  task :restart do
    on roles(:app) do |_host|
      execute 'supervisorctl restart sample-app'
    end
  end
  desc 'stop service'
  task :stop do
    on roles(:app) do |_host|
      execute 'supervisorctl stop sample-app'
    end
  end
end

namespace を console としているので、以下のようにして実行することができる。

# サービス再起動
bundle exec cap {stage} console:restart
# サービス確認
bundle exec cap {stage} console:status

まとめ

無事に Supervisor でプロセス制御を行うことができた。簡単に設定できて、非常に使いやすいのではないかと思えた。

それでは、ステキな開発ライフを。

関連記事

serip39.hatenablog.com

serip39.hatenablog.com

serip39.hatenablog.com

serip39.hatenablog.com

【Rails】Capistrano で Rails アプリケーションを自動デプロイ(サーバー設定/デプロイ編)

先日の【Rails】Capistrano で Rails アプリケーションを自動デプロイ(設定編)に続き、今回は実際にデプロイするにあたり、サーバー側で事前に設定した内容に関して、備忘録として残しておく。

サーバーの環境構築に関しては、【EC2】Rails5 環境構築(Ruby + MySQL5.7 + Node.js + Nginx) を参照してほしい。

GitHubSSH 設定

EC2に特定の人の GitHub のアカウントを紐付けてもいいが、今回は GitHubリポジトリに公開鍵を登録することによって、SSH でアクセスできるようにする。

1. EC2 で公開鍵・秘密鍵を作成する(パスワードの設定もできるが、今回は設定せずに進めた。)

$ cd ~/.ssh
$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ec2-user/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again:

2. 公開鍵を authorized_keys に追加して、この鍵ペアを使用してログインできるようにする

$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

(※注意※)
「>>」で追加すること。「>」で上書きしてしまうと、EC2 に SSH 接続できなくなってしまうので、注意。

3. Clone したい GitHubリポジトリに対して、公開鍵を登録する

GitHubリポジトリのページにアクセスして、「Settings > Deploy keys」 に先ほど生成した id_rsa.pub を登録する。

4. git cloneできるか確認する

一度、適当なフォルダを作り、ID/PASSを入力せずに、cloneできるか確認する。

$ mkdir /home/ec2-user/tmp
$ cd /home/ec2-user/tmp
$ git clone git@github.com:<--Repository Name-->

問題なく、clone できれば設定完了である。(/home/ec2-user/tmp は不要なので、削除しておく。)

Capistrano でデプロイするディレクトリ作成

Capistranoディレクトリ構造を作成する際に、mkdir: permission denied のエラーが発生した。解決方法としては、様々な方法がある。

  1. デプロイ先のディレクトリを誰でも書き込めるようにする
  2. デプロイ先のディレクトリにec2-userが所属するグループを設定する
  3. デプロイ先のディレクトリの所有者を変更する
  4. Capistrano側で、sudo権限でディレクトリを作成できるようにする

1は簡単であるが、ちょっとセキュリティ的に嫌だったので、今回は「2」を実施することにした。

$ sudo mkdir /app
$ ls -ld
drwxrwxr-x 2 root root 6 1218 04:49 .
# /app の所有グループをwheelに変更する
$ sudo chgrp wheel /app
$ ls -ld
drwxrwxr-x 2 root wheel 6 1218 04:49 
# wheelグループに書き込み権限を付与する
$ sudo chmod g+w /app

共通ファイルのアップロード

現状の設定ファイルは以下のようになっている。(先日の記事から一部、抜粋している。)

~~省略~~
# 共有ファイル
append :linked_files, 'config/database.yml', 'config/master.key', 'config/credentials.yml.enc', 'config/puma.rb', '.env'
# 共有フォルダ
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system', 'public/packs'

共有フォルダは自動で作成されるが、共有ファイルがないと、ファイル自体を参照することができないので、エラーが発生する。そのため、事前に config/database.yml, config/master.key, config/credentials.yml.enc, config/puma.rb, .env のファイルを作成する。

scp でのファイル転送、または直接サーバーへのファイル作成及び書き込みを行う。

$ scp [ローカルのファイルパス] [ユーザー名]@[サーバのIPアドレス]:[転送先のファイルパス]
$ scp database.yml ec2-user@test-staging:/app/sample-app/shared/config/database.yml

config/database.ymlは、それぞれの環境のDBのアクセス情報を記載しておく必要がある。config/puma.rbに関しては、次の章を参照してほしい。

Puma + Nginx の設定変更

アプリケーションのディレクトリが Capistranoディレクトリ構造に変わるので、参照するパスを変更する。(前回の設定は、
【Puma】Rails 5.2 + Puma + Nginx のデプロイ設定
を参照。)

# The directory to operate out of.
directory '/app/sample-app/current'
# Set the environment in which the rack's app will run. The value must be a string.
environment ENV.fetch("RAILS_ENV") { "production" }
# daemonize
daemonize false
# Store the pid of the server in the file at "path".
pidfile '/app/sample-app/current/tmp/pids/puma.pid'
# Use "path" as the file to store the server info state. This is used by "pumactl" to query and control the server.
state_path '/app/sample-app/current/tmp/pids/puma.state'
# Configure "min" to be the minimum number of threads to use to answer
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 16 }
threads threads_count, threads_count
# Bind the server to "url". "tcp://", "unix://" and "ssl://" are the only accepted protocols.
bind 'unix:///app/sample-app/shared/tmp/sockets/puma.sock'
on_restart do
# Code to run before doing a restart. This code should close log files, database connections, etc.
  ActiveRecord::Base.connection.disconnect! if defined?(ActiveRecord::Base)
end
# How many worker processes to run. 
workers 3
# Code to run in a worker before it starts serving requests.
on_worker_boot do
  ActiveSupport.on_load(:active_record) do
    ActiveRecord::Base.establish_connection
  end
end
# Preload the application before starting the workers
preload_app!
# Additional text to display in process listing
tag 'sample-app_puma'
# Verifies that all workers have checked in to the master process within the given timeout.
worker_timeout 60
# Change the default worker timeout for booting
worker_boot_timeout 60
# 各種ログのディレクトリ設定
access_log /app/sample-app/shared/log/nginx/nginx-access.log;
error_log /app/sample-app/shared/log/nginx/nginx-error.log;
# 処理を受け取る最大許容量
client_max_body_size 2G;

upstream app_server {
  server unix:///app/sample-app/shared/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name _;
  keepalive_timeout 5;
  # パスの変更
  root /app/sample-app/current/public;
  try_files $uri/index.html $uri.html $uri @app;
  location @app {
    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;
  location = /500.html {
    # パスの変更
    root /app/sample-app/current/public;
  }
}

ログファイルを作成しておく

$ sudo mkdir -p /app/sample-app/shared/log/nginx
$ sudo touch /app/sample-app/shared/log/nginx/nginx-access.log
$ sudo touch /app/sample-app/shared/log/nginx/nginx-error.log

設定ファイルに問題がないか確認を行う(successful が表示されれば、OK.)

$ sudo nginx -t
nginx: [warn] conflicting server name "_" on 0.0.0.0:80, ignored
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Nginxの設定を書き換えたので、再起動する

$ sudo systemctl reload nginx

デプロイ

これで、全ての準備が完了したので、いよいよデプロイである。

# staging環境の場合
$ bundle exec cap staging deploy
# production環境の場合
$ bundle exec cap production deploy

上記のコマンドだけで、それぞれのタスクが実行され、デプロイが完了する。本当にとても便利である。

デプロイしただけでは、設定が反映されなかった。Puma を再起動する必要があったので、注意が必要である。

まとめ

Capistrano で無事に Rails アプリケーションの自動デプロイを実施することができた。手作業で毎回、サーバーにSSH接続→git pull→build→…などをやる必要がなくなって、本当にとても楽である。

それでは、ステキな開発ライフを。

関連記事

serip39.hatenablog.com

serip39.hatenablog.com

serip39.hatenablog.com

【Rails】Capistrano で Rails アプリケーションを自動デプロイ(設定編)

Rails アプリケーションを本番、ステージング環境にデプロイする際に、同じ作業を何回もやるのは正直面倒である。今回、Capistrano というデプロイ自動化ツールを使ったので、備忘録を残しておく。

サーバーの環境構築に関しては、【EC2】Rails5 環境構築(Ruby + MySQL5.7 + Node.js + Nginx)を参照してほしい。

Capistrano とは

github.com

Capistrano とは、Ruby 製のデプロイ自動化ツールである。RailsJavaPHPなど、あらゆる言語やフレームワークのプロジェクトのデプロイを自動化することができる。

それぞれのタスクに応じた Capistrano のライブラリが用意されているので、それらを組み合わせたり、独自にタスクを作成することで、デプロイの設定や流れを簡単に作ることができる。

今回は、Railsアプリケーションの自動デプロイの設定について紹介する。デプロイの流れは以下のようになる。

  1. サーバーに SSH 接続する
  2. Capistranoディレクトリ構成を構築する(シンボリックリンクの作成)
  3. git clone
  4. rbenv の動作確認
  5. bundle install(gemインストール)
  6. yarn install(node packageインストール)
  7. yarn build

(今回のRails アプリケーションには、一部 Vue コンポーネントを組み込んでおり、webpack で build する必要がある。)

Capistranoフレームワークでは、デプロイ先のディレクトリ構成は以下のようになる。

Deploy.root
├─ current #公開されているアプリケーション(DocumentRoot)
├─ release #デプロイされたアプリケーションが保存されている
├─ shared  #バージョンが変わっても共通で参照されるファイル
├─ repo    #Capistranoの設定ファイル等
└─ revisions.log #リリースバージョンのログ

・releasesディレクト

Capistrano を通じてデプロイされたアプリケーションは、releasesディレクトリにバージョンごとに纏められる。デプロイ時に問題が発生しても、過去のデータが残っていることで、簡単に一つ前のバージョンに戻ることができる。

・currentディレクト

releasesディレクトリの中で最新のものが、自動的にcurrentディレクトリ内にコピーされる。(正確には、releasesの最新のディレクトリのシンボリックリンクがcurrentディレクトリに作られ、参照している。)DocumentRootには、current/publicを指定する。

$ ls -l
current -> /app/sample-app/releases/20201224144141

・sharedディレクト

バージョンが変わっても共通で参照されるファイルやディレクトリが格納される。具体的には、config の設定ファイル、log、public、tmp ディレクトリが格納される。(これらも、シンボリックリンクが作られて、currentディレクトリの各ファイルから参照される形になる。)

インストール方法

まずは、Rails アプリケーションに Capistrano をインストールする。Gemfile に以下を追記する。

group :development do
  gem 'capistrano', '~> 3.10', require: false
  gem 'capistrano-rails', '~> 1.4', require: false
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-yarn'
end
$ bundle install
# Capistrano の設定ファイルをインストールする
$ bundle exec cap install

上記のコマンドを実行することで、以下のディレクトリが作成される。

Rails.root
├── Capfile #必要なライブラリ、タスクを読み込む
├── config
│ ├─ deploy.rb #共通の設定ファイル
│ └─ deploy
│    ├─ production.rb #本番の設定ファイル
│    └─ staging.rb #ステージングの設定ファイル
└── lib
     └─ capistrano
        └─ tasks #オリジナルのタスクを作成する(Capfileで読み込ませる)

設定方法

設定する流れは、以下になる。

  1. Capfile で必要なライブラリ、タスクを記述する
  2. デプロイの設定を記述する
    • 共通の設定:config/deploy.rb
    • 環境別の設定:config/deply/{stage}.rb

Capfile

# Load DSL and set up stages
require "capistrano/setup"
# Include default deployment tasks
require "capistrano/deploy"
# Load the SCM plugin appropriate to your project:
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git
# Include tasks from other gems included in your Gemfile
require "capistrano/rbenv"
require "capistrano/bundler"
require "capistrano/rails/migrations"
require "capistrano/yarn"
# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

デプロイの設定

今回は、ステージング環境である staging.rb のみを紹介する。

# Capistranoのバージョンを指定
lock "~> 3.14.1"
# アプリケーションの名前
set :application, "sample-app"
# GitリポジトリのURL
set :repo_url, "git@github.com:sample-app.git"
# サーバー上でのデプロイ先
set :deploy_to, '/app/sample-app' #default: /var/www/my_app_name
# ログの出力設定
set :format, :airbrussh
"log/capistrano.log", color: :auto, truncate: :auto
# sudoを使う場合は、trueにしておく
set :pty, true
# 共有ファイル
append :linked_files, 'config/database.yml', 'config/master.key', 'config/credentials.yml.enc', 'config/puma.rb', '.env'
# 共有フォルダ
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system', 'public/packs'
# 5回分のreleasesを保持する
set :keep_releases, 5
#=======================
# Rails
set :rails_env, 'staging'
set :migration_role, 'db'
# db/migrateに変更がない場合は、Skip migration
set :conditionally_migrate, false
#=======================
# Yarn
set :yarn_flags, "--prefer-offline --no-progress"
set :yarn_roles, :app
set :yarn_env_variables, fetch(:yarn_env_variables, {})
set :yarn_bin, '/opt/nvm/versions/node/v14.15.2/bin/yarn'
desc 'yarn build'
after 'deploy:updated', :build do
  on roles :web do
    within release_path do
      execute :yarn
      execute :yarn, 'build'
    end
  end
end
#=======================
# Rbenv
set :rbenv_type, :system
set :rbenv_custom_path, '/opt/rbenv'
set :rbenv_ruby, '2.6.3'
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w(rake gem bundle ruby rails)
set :rbenv_roles, :all
#=======================
# Bundler
set :bundle_roles, :all
set :bundle_binstubs, -> { shared_path.join('bin') }
set :bundle_path, -> { shared_path.join('vendor', 'bundle') }
set :bundle_without, %w(development test).join(' ')
set :bundle_jobs, 8
#=======================
# Server
# Capistranoは実行ユーザの ~/.ssh/config に従うので、configのHostを指定する
server 'sample-staging',
       user: 'ec2-user',
       roles: %w(app web),
       ssh_options: {
         forward_agent: true
       }
#=======================

まとめ

今回は、デプロイ自動化ツールである Capistrano の設定方法に関して、まとめを行った。

次回、実際にデプロイする際に、サーバー側で一部設定をした後に、デプロイを行ったので、その部分に関しても、紹介したい。

それでは、ステキな開発ライフを。

関連記事

serip39.hatenablog.com

serip39.hatenablog.com

【Puma】Rails 5.2 + Puma + Nginx のデプロイ設定

先日、【EC2】Rails5 環境構築(Ruby + MySQL5.7 + Node.js + Nginx) について書いた。そして、今回は、アプリケーションサーバーである Puma について色々と調べて設定したので、備忘録を残しておく。(以前、Rails を使っていた時は、Unicorn が主流だったのに、Rails5 からは標準サーバーも Puma に変わったこともあり、今回は Puma を採用してみた。)

Pumaとは

github.com

Puma is a simple, fast, multi-threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications.

Puma は、シンプル、高速、マルチスレッドで多数の並列処理を実現できる Ruby/Rack アプリケーションの HTTP 1.1 サーバーである。

ここで、「マルチスレッド」、「Ruby/Rack アプリケーション」という言葉に注目してみる。

マルチスレッド

Puma はマルチスレッドで動いていることが大きな特徴である。対照的に、Unicornマルチプロセスで動いている。CPU は一度に1つのプロセスしか実行できず、マルチプロセスの場合、CPU のコア数に応じて並列処理できる数に制限がある。しかしながら、マルチスレッドの場合は、1つのプロセスに複数のスレッドが存在し、並行して処理を行うため、CPU のリソースが少ない中でも効率的に処理を行うことができる。

Ruby/Rack アプリケーション

Rack とは、Rubyにおけるサーバとアプリケーション/フレームワーク間のインターフェースの役割を果たすライブラリである。PythonWSGI に影響されて開発されたらしく、WebサーバとWebアプリケーション/フレームワーク間の標準インターフェースを定める「仕様」である。

rails serverコマンドは、Rack::Serverのオブジェクトを作成し、Webサーバーを起動している。

Railsガイド 参照

簡単な概略図はこのような形になる。

f:id:serip39:20201224001935p:plain

HTTP リクエストを web サーバーである Nginx や Apache で受け取り、Nginx はそのリクエストを Puma に Socket 通信で渡し、Puma はそのリクエストを Rack::Server に渡し、最終的に Rails の router に渡される。

インストールに関して

Rails5 以上であれば、標準Webサーバーなので、最初に生成された Gemfile に含まれているので、新規にインストールをする必要はないが、インストールする場合は、以下の通りである。

$ gem install puma
$ puma

Rails で開発をする場合に、rails server をすることで、Puma が起動している。

$ rails server

Puma starting in single mode...
* Version 3.9.1 (ruby 2.4.0-p0), codename: Private Caller
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000 
Use Ctrl-C to stop

設定に関して

Puma は、Railsの場合、config/puma.rb で詳細な設定をすることができる。もちろん、オプションコマンドで設定も可能であるが、毎回コマンドを打つのが面倒なので、設定ファイルに用いる。

開発時は、Default の設定で問題ないので以下の通りである。

# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
port        ENV.fetch("PORT") { 3000 }

# Specifies the `environment` that Puma will run in.
environment ENV.fetch("RAILS_ENV") { "development" }

# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }

# Specifies the number of `workers` to boot in clustered mode.
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }

# Use the `preload_app!` method when specifying a `workers` number.
# preload_app!

# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart

本番環境では、Nginx をリバースプロキシとして Puma の前段におき、UNIXドメインソケットを経由して Nginx とPuma が通信を行う。

Puma 側では、Pumaを起動した時に、「puma.sock」を生成するようにする。

#port        ENV.fetch("PORT") { 3000 }
#portの設置の部分はコメントアウトしておく。そうでないと、puma.sockを参照しないことがある模様。
bind "unix://#{Rails.root}/tmp/sockets/puma.sock"

Nginx 側では、/etc/nginx/conf.d/ に新規に設定ファイルを作成する。

upstream app_server {
  # for UNIX domain socket setups
  # 上記で設定したpuma.sockファイルの絶対パスを指定する
  server unix:///app/sample-app/tmp/sockets/puma.sock;
}
server {
  #ポート80番を許可する
  listen 80;
  #ホスト名を指定する
  server_name _;
  #静的ファイルのパスをドキュメントルートに設定する
  root /app/sample-app/public;
  # ドキュメントルート配下を以下の順番で検索する
  try_files $uri/index.html $uri.html $uri @app;
  # @appの場合は、以下の設定を読み込ませる
  location @app {
    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; #upstreamで指定した名前で設定する
  }
  # エラーページの参照先を設定する
  error_page 500 502 503 504 /500.html;
  location = /500.html {
    root /app/sample-app/public;
  }
}

Nginx の設定ファイルの修正が完了したら、設定に問題がないか確認を行う(successful が表示されれば、OK.)

$ sudo nginx -t
nginx: [warn] conflicting server name "_" on 0.0.0.0:80, ignored
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Nginxの設定を書き換えたので、再起動する

$ sudo systemctl reload nginx

Pumaを起動する(どのコマンドでも起動することができる)

$ bundle exec rails s
$ bundle exec puma
$ bundle exec pumactl start

これで設定は完了である。 http://<---IP address->:80 にアクセスすると、Railsアプリケーションが表示される。

コマンドに関して

Puma には、上記でも示したように、3つの制御方法が存在する。(個人的には、pumactl が使いやすいと思っている。)

  • rails sの場合
  • pumaコマンドの場合
  • pumactlの場合

rails sの場合

Railsアプリケーションの起動と同時に、Pumaも起動する

$ bundle exec rails s -e production

Puma ではデフォルトの設定ファイル config/puma.rb 以外にも、実行環境に応じたconfig/puma/[実行環境].rb という設定ファイルを参照させることができる。

-e productionの場合、config/puma/production.rb 参照する。(なかった場合は、デフォルトを参照する。)

停止する場合は、以下のように Puma のプロセスを確認して、kill する

# process IDの確認をする
$ ps aux | grep puma
# プロセスを停止させる
$ kill -9 <--process ID-->

pumaコマンドの場合

設定ファイルを指定して、起動する(-Cのオプションがない場合は、デフォルトを参照するが、設定することをオススメする)

$ bundle exec puma -C config/puma.rb

停止する場合は、rails s 同様に、プロセスを確認して、kill する

pumactlの場合

pumactl を使う場合は、オプションコマンドなどで設定をすることができないので、config/puma.rb にきちんと設定を書いておく必要がある。

# 起動する
$ bundle exec pumactl start
# 状態を確認する
$ bundle exec pumactl status
# 停止する
$ bundle exec pumactl stop
# 再起動する
$ bundle exec pumactl restart

まとめ

今回は、アプリケーションサーバーである Puma についてまとめを行った。

それでは、ステキな開発ライフを。

関連記事

serip39.hatenablog.com

serip39.hatenablog.com