元フリーエンジニアライフ

Ruby on Rails とか MovableType とかAWSやってるフリーランスウェブエンジニアの記録でした。現在は法人成りしてIT社長。

OpsWorks + Rails でロールモデル的な役割分担をインスタンスにもたせるBK

OpsWorks + Rails

OpsWorksでRailsアプリを運用しようとした場合、通常の方法では、すべてのRailsアプリインスタンスが同じ設定になり、たとえば

といったことができません。

これを実現するために、標準で提供される「Rails App Server」の他に、カスタムLayerを作成し、その組み合わせでインスタンスごとの機能を作ることにしました。

参考

OpsWorksで複数RailsAppLayerを構築する - Qiita

こちらの記事を参考に、というより、これに加えて自分なりの工夫を加えた版になります。

カスタムCookbook

  • Layerごとのセットアップ、デプロイ処理
  • デプロイ時にwebサーバーの再起動が不要なケース

のため、カスタムCookbookを作成します。

インスタンス共通の処理

  • opsworks/recipes/setup.rb
  • opsworks/recipes/deploy.rb
#
# Cookbook Name:: opsworks
# Recipe:: setup
#
# Copyright 2015, Yuichi Takeuchi
#
# All rights reserved - Do Not Redistribute
#

# 以下は設定例
#package 'nodejs'
#package 'npm'
#nodejs_npm 'bower'
#yum_package 'fontconfig'
#yum_package 'freetype'
#nodejs_npm 'phantomjs'
# ImageMagick
#include_recipe "imagemagick"
#include_recipe "imagemagick::devel"
#
# Cookbook Name:: opsworks
# Recipe:: deploy
#
# Copyright 2015, Yuichi Takeuchi
#
# All rights reserved - Do Not Redistribute
#

node[:deploy].each do |application, deploy|
  next unless deploy[:application_type] == 'rails'

  # Rails App Web Server Layer が含まれていない場合 unicornを止めておく
  # (そもそも起動しないようにしたいが難しいようなので…)
  is_rails_web = node[:opsworks][:instance][:layers].select { |layer| layer.match(/\Arails-app-web(-.+)?\z/) }.any?
  unless is_rails_web
    execute 'stop unicorn and stop nginx' do
      command "sleep #{deploy[:sleep_before_restart]} && \
              #{deploy[:deploy_to]}/shared/scripts/unicorn stop"
      notifies :stop, "service[nginx]"
      action :run
    end
  end
end

Cronを実行するインスタンスでのみ必要な処理

  • opsworks/recipes/setup_cron.rb
  • opsworks/recipes/deploy_cron.rb
#
# Cookbook Name:: opsworks
# Recipe:: setup_cron
#
# Copyright 2015, Yuichi Takeuchi
#
# All rights reserved - Do Not Redistribute
#

# これは別にGemfileで入れても良いかも
gem_package 'whenever' do
  action :install
  version '0.9.4'
  options '--no-user-install'
end
#
# Cookbook Name:: opsworks
# Recipe:: deploy_cron
#
# Copyright 2015, Yuichi Takeuchi
#
# All rights reserved - Do Not Redistribute
#

# whenverでcronを登録
node[:deploy].each do |application, deploy|
  next unless deploy[:application_type] == 'rails'

  execute 'whenever' do
    command "bundle exec whenever --update-cron #{application}_#{deploy[:rails_env]}"
    environment 'RAILS_ENV' => deploy[:rails_env]
    cwd "#{deploy[:deploy_to]}/current"
    user deploy[:user]
    group deploy[:group]
  end
end

なお、この場合使用するschedule.rbは以下のような感じになります。

set :output, 'log/cron.log'
set :environment, ENV['RAILS_ENV'] if ENV['RAILS_ENV']
env :PATH, ENV['PATH']

job_type :runner,  "cd :path && bundle exec ruby bin/rails runner -e :environment ':task' :output"

every 5.minutes do
  runner 'Video.accept_all!'
end

バックグラウンドタスクを実行するインスタンスでのみ必要な処理

  • opsworks/recipes/setup_worker.rb
  • opsworks/recipes/deploy_worker.rb
#
# Cookbook Name:: opsworks
# Recipe:: setup_worker
#
# Copyright 2015, Yuichi Takeuchi
#
# All rights reserved - Do Not Redistribute
#

# God (system)
# bundleによるユーザースペースではイベントシステムが利用できないなど問題があるので必ずシステムで入れる
gem_package 'god' do
  action :install
  version '0.13.6'
  options '--no-user-install'
end
#
# Cookbook Name:: opsworks
# Recipe:: deploy_worker
#
# Copyright 2015, Yuichi Takeuchi
#
# All rights reserved - Do Not Redistribute
#

# config/god.rbがあればgodを使う
node[:deploy].each do |application, deploy|
  next unless deploy[:application_type] == 'rails'

  execute 'god' do
    only_if { File.exists?("#{deploy[:deploy_to]}/config/god.rb") }
    command "god terminate && god -c #{deploy[:deploy_to]}/config/god.rb -l #{deploy[:deploy_to]}/log/god.log"
    environment 'RAILS_ENV' => deploy[:rails_env]
    cwd "#{deploy[:deploy_to]}/current"
  end
end

デプロイ時にwebサーバーの再起動が不要なケース

WebサーバLayerが含まれていないインスタンスでは再起動が不要なので、デプロイ時の挙動を変更

###
# This is the place to override the deploy cookbook's default attributes.
#
# Do not edit THIS file directly. Instead, create
# "deploy/attributes/customize.rb" in your cookbook repository and
# put the overrides in YOUR customize.rb file.
###

# The following shows how to override the deploy user and shell:
#
#normal[:opsworks][:deploy_user][:shell] = '/bin/zsh'
#normal[:opsworks][:deploy_user][:user] = 'deploy'

# Web用Layerが含まれていない場合は restart 不要
unless node[:opsworks][:instance][:layers].select{|layer| layer.match(/\Arails-app-web(-.+)?\z/) }.any?
  normal[:opsworks][:rails_stack][:restart_command] = nil
  normal[:opsworks][:rails_stack][:needs_reload] = false
end

Stack

普通に作成すればOK

Use custom Chef cookbooksで先に作成したCookbookを使用するように設定します。

ssh://git@ssh.github.com:443/takeyuweb/myapp-cookbooks.git

また、僕はカスタムCookbookに必要な依存レシピをBerkshelfで管理しているので、それを使うようにしておきます。

Layer

Railsのコードベースが必要なもので共通の Rails App Server に加えて、カスタムLayerとして cron 用の Rails Cron Server 、ワーカー用の Rails Worker Server を作成しました。

各Layerのshortnameは「rails-app-*」というルールで設定しています。これは、Chefレシピ中でLayerを判定して処理を振り分ける際などに役立ちます。

  • Rails App Server rails-app-server
    • OpsWorks側ではじめから用意されているレイヤーをそのまま使います。
    • 通常のRailsアプリケーションですが、ELBの設定は行いません。
  • Rails App Web Server rails-app-web-server
    • このレイヤーを設定したインスタンスのみ、Webサーバーの起動とELBの登録を行うことにします。
  • Rails App Cron Server rails-app-cron-server
    • このレイヤーを設定したインスタンスのみ、CRONの登録と実行をすることにします。
  • Rails App Worker Server rails-app-worker-server
    • このレイヤーを設定したインスタンスのみ、ActiveJob等のワーカーを実行することにします。

以下、それぞれのLayerの設定内容

Rails App Server

Layer作成で App Server -> Rails App Server を選択して作成します。

Recipes

Custom Chef Recipes で必要なものを追加

  • opsworks::setup
  • opsworks::deploy

Network

Elasic Load Balancer がデフォルトで設定されるので外しておく

Rails Web Server

Layer type Custom

Name Rails Web Server

Short name rails-app-web-server

Network

Elasic Load Balancer を設定

Rails Cron Server

Short name rails-app-cron-server

Recipes

Custom Chef Recipes で必要なものを追加

  • opsworks::setup_cron
  • opsworks::deploy_cron

Rails Worker Server

Short name rails-app-worker-server

Recipes

Custom Chef Recipes で必要なものを追加

  • opsworks::setup_worker
  • opsworks::deploy_worker

App

Ruby on Railsアプリケーションとして普通に作成します。

Instances

Rails App Serverを作成後、起動前にそのインスタンスの役割を作成したカスタムレイヤーを組み合わせて設定してやります。

たとえば以下のような構成で起動したいときのLayerの割り振りを考えてみます。

  • 5台のインスタンスRailsソースコードを配置
  • 2台を常時起動し、Webアプリサーバ(Nginx+Unicorn)及びバックグラウンドタスク用のWorkerを起動
  • 常時起動の1台にCronを設定
  • 過負荷時に2台のWebアプリサーバを追加で起動
  • (夜間処理でバックグラウンドタスクがたくさん発行されるという想定で)バックグラウンドタスク実行専用のインスタンスを時間指定で起動

Rails App Server

Rails App Web Server

Rails App Cron Server

Rails App Worker Server