OpsWorksで複数RailsAppLayerを構築する



この記事は[AWS vol.2 Advent Calendar 2014](http://qiita.com/advent-calendar/2014/aws-vol2)の2日目です。記事書きたいと思った時に本家が埋まっていたのでvol2を立てました。誰か他の日に書いてくれると嬉しいです。

今回は、最近業務で利用しているOpsWorksの話です。
OpsWorksでは、サービスを運用するための、EC2インスタンスの管理からDeploy処理までのことをAWSのマネジメントコンソール上で管理することができて非常に便利なサービスです。

しかし、現状のOpsWorksの仕組みでは、組み込みのRailsAppLayerの設定を利用してしまうと、複数のLayerでRailsAppLayerを利用することができなくなってしまいます（組み込みのLayer設定は1レイヤーずつしか利用できない）。ここで問題にはなるのは、Webサーバとしては運用したくはないがRailsをDeployしたい場合、例えばSidekiqなどの非同期ジョブを専用のサーバで運用したい場合などです。この場合、Webサーバー自体は別Layerで運用しつつも、Sidekiq側もWebサーバーと同様の構成（RubyやBundlerのバージョン設定）やソースコードの配置までは、RailsAppLayerと同じものを利用したいです。しかし、OpsWorksのコンソール上ではデフォルトでこのような構成についての対応は準備されていません。

今回の記事は、その組み込み設定済みLayerは1つずつしか使えない制約を乗り越えるために、既存のRailsAppLayerの設定をCustomLayerで再利用するためのWorkaroundバッドノウハウについての紹介です。

OpsWorksのこれらの機能はOpsWorks専用のChefのCookbooks（[aws/opsworks-cookbooks](https://github.com/aws/opsworks-cookbooks)）によって実現されていて、Deploy処理までもがCookbooksによって記述されています。なので、自前で独自の構成を作成するためにCustom Cookbooksを作成していきます。

今回、実現したい複数RailsLayerでは、Webサーバ側がNginx＋Unicornの構成の場合について紹介します。OpsWroksのbuilt-in Cookbooksが標準で用意しているattributes/customize.rbに好きな設定を書く方法と、同名のファイルを用意してbuilt-in cookbooksのファイルを強制的に上書きする方法を駆使していきます。

### CustomLayerの作成

OpsWorks上にLayerを追加する際にCustomを選択します。通常の名前は適当で良いのですが、short nameには、Webサーバとして運用したいLayerについては `rails-app-main` と、それ以外のLayerには`rails-app-job`など好きな名前を入力します。
この理由はあとで記述します。

### Ruby, rubygemsのバージョンの指定

まず、Custom Layerで、RailsAppLayerの時にコンソールで設定できたRubyやBundlerのバージョンをCookbooksのattributesで指定していきます。これらはCustomJSONで渡しても良いのですが、CustomJSONは設定漏れが発生しがちなのであんまりおすすめしないです。


```rb:ruby/attributes/customize.rb
normal[:opsworks][:ruby_version] = '2.1'
normal[:opsworks_rubygems][:version] = '2.2.2'
```

```rb:opsworks_bundler/attributes/customize.rb
normal[:opsworks_bundler][:version] =  '1.7.6'
normal[:opsworks_bundler][:manage_package] = true
```

### Railsアプリとしてdeploy

CustomLayerをRailsアプリとしてdeployするためにdeploy cookbooksをwrapするcookbooksを作ります。
OpsWorksのdeploy

ポイントとしては、node[:opsworks][:instance] というのがOpsWorksが各EC2インスタンスでCookbooksを適用する時に構築するnodeオブジェクトとなっていて、node[:ospworks][:instance][:layers]の中に、各EC2インスタンスがOpsworks上でどのLayerに属しているかが値として入っていて、ここに'rails-app'を入れておくことで、built-inのdeploy cookbooksの中でRailsAppLayerとしてdeploy時にサーバの再起動が行われるようになります（https://github.com/aws/opsworks-cookbooks/blob/release-chef-11.10/deploy/definitions/opsworks_deploy.rb#L81-L83）。

`/\Arails-app-main(-.+)?\z/`の正規表現は前述のshort nameのルールを作ったためです。

```rb:deploy_your_app/attributes/default.rb
default[:deploy_your_app][:application_name] = 'your_app_name'
default[:deploy][:your_app_name][:application_type] = 'rails'

if node[:opsworks][:instance][:layers].first =~ /\Arails-app-main(-.+)?\z/
  force_override[:opsworks][:instance][:layers] = [*node[:opsworks][:instance][:layers], 'rails-app']
end
```

次に、Nginx+UnicornをCustomLayerで利用するためにdeploy cookbooksにattributesを設定します。rails_stackはどうやらRailsAppLayerを設定した時にコンソール上で設定できる値のようです。上と同様にrails-app-mainの時にrestart_commandを設定しておきます。

```rb:deploy/attributes/customize.rb
normal[:opsworks][:rails_stack][:name] = 'nginx_unicorn'
normal[:opsworks][:rails_stack][:recipe] = "unicorn::rails"
normal[:opsworks][:rails_stack][:needs_reload] = true
normal[:opsworks][:rails_stack][:service] = 'unicorn'

# 再起動するのはrails-app-mainで始まるlayerだけ!!
if node[:opsworks][:instance][:layers].select { |layer| layer =~ /\Arails-app-main/ }.size > 0
  normal[:opsworks][:rails_stack][:restart_command] = '../../shared/scripts/unicorn clean-restart'
else
  normal[:opsworks][:rails_stack][:restart_command] = ''
end
```

### レイヤーごとのdeploy処理

レイヤー毎にdeploy処理を作ります。基本的にはdeploy::railsをinclude_recipeで再利用してやれば、Layerに設定したshort nameに応じて振る舞いが変わるのでおっけーです。


```rb:deploy_your_app/recipes/job.rb
application = node[:deploy_mitene][:application_name]
deploy = node[:deploy][application]

include_recipe "deploy::rails"

# 何か振る舞いを変えたい場合はここに記述する
# include_recipe "sidekiq::start"
```

deploy後に実行したい処理、例えばSidekiqの起動処理などをinclude_recipeで書いても良いと思います。


### バグ回避

最後に、バグ回避。これがないと落ちるのでプルリクエストしてみても良いかもしれないです。
OpsWorks Layersの行にnilチェック追加。これ変更しないとこけますｗ


```rb:opsworks_stack_state_sync/templates/default/motd.erb
 This instance is managed with AWS OpsWorks.

   ######  OpsWorks Summary  ######
   Operating System: <%= `head -1 /etc/issue | sed -e 's/ \\\\.*//'`.chomp %>
   OpsWorks Instance: <%= @instance[:hostname] %>
   OpsWorks Instance ID: <%= @instance[:id] %>
   OpsWorks Layers: <%= @instance[:layers].map{|id| @layers[id] && @layers[id][:name] }.join(', ') %>
   OpsWorks Stack: <%= @stack[:name] %>
   EC2 Region: <%= @instance[:region] %>
   EC2 Availability Zone: <%= @instance[:availability_zone] %>
   EC2 Instance ID: <%= @instance[:aws_instance_id] %>
   Public IP: <%= @instance[:ip] %>
   Private IP: <%= @instance[:private_ip] %>
<% if @stack[:vpc_id] -%>
   VPC ID: <%= @stack[:vpc_id] %>
   Subnet ID: <%= @instance[:subnet_id] %>
<% end -%>

 Visit http://aws.amazon.com/opsworks for more information.

```

## 最後に

30分ぐらいで書いたので時間がなくスクリーンショットなど貼れてなくてわかりづらく申し訳ありませんが、こんな感じのバッドノウハウを活用すると複数RailsLayerが使えることがわかっていただけたかと思います。OpsWorks上でも機能改善して標準でこういう構成がいけると嬉しいです！


参考記事：
http://www.superpumpup.com/multi-rails-app-opsworks




OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks OpsWorks