この記事は最終更新から1年以上経過しています。 気をつけてね。
Opscode Chefであるrailsアプリをデプロイするレシピを書いたのでメモ。
本Railsデプロイ概要
- 前提: rubyが入っている、postgresqlサーバは既にある
- start, stopは専用スクリプトを用意、
monit
配下で管理する。 - デプロイのプラットフォームはとりあえず
Joyent SmartOS
、多分ほかでもあまり違いはない。 - Rackupはunicorn, socketでnginxからリバースプロキシ
-
asset_pipiline
を使い、assets
以下はnginx
でホスト。
レシピ
関連Cookbooks
やdata_bags
のことは考えず、固有名称や複雑な箇所は説明用に改変してます。
chef
のdeploy
リソースは基本的にcapistrano/deploy
のように決められたフェーズがあり、それぞれにフックが設定されています。
フック時に実行する内容はRailsアプリケーションのリポジトリ側にdeploy/#{hook_name}.rb
のように置いておくこともできます。
## //
ではじまる行が書き下ろしの解説
rails_app/deploy.rb
#
# Cookbook Name:: rails_app
# Recipe:: deploy
#
## // デプロイする先のディレクトリを適当に設定
app_name = 'my_app'
dir_localsystem = ::File.join(node[:my_app_common][:basedir], app_name, 'shared/localsystem')
## // nodeのenvironmentをキーにして、各種接続情報をdata_bagsから取ってくる
## build settings
## // ブランチ情報やdeploy or force_deployを選べる
deploy_settings = data_bag_item(:deploy,:items)[node.environment]
## // Postgresのrole, password
connection= data_bag_item(:postgresql,:connections)[node.environment]
## // 色々URLを拾ってくる、ここではNginxの仮想ホストや、アプリケーションのコンフィグで使用
domains = data_bag_item(:domains,:items)[node.environment]
## // Postgresqlの構成次第で接続先を変えたい
pg_master = data_bag_item(:postgresql,:clusters)[node.environment]['master']
## // ここから`deploy`リソース
deploy_branch ::File.join(node[:my_app_common][:basedir], app_name) do
## // デプロイ基本情報
repo 'https://github.com/higanworks/my_app.git'
branch deploy_settings[app_name]['ref'] ## // 'master' が入ったりする
action deploy_settings[app_name]['action'] ## // 'deploy' または 'force_deploy'や 'roleback'とか
migrate deploy_settings[app_name]['migrate'] ## // true or false
## // 各種コマンド実行時の環境変数
environment "RAILS_ENV" => node.environment, 'BUNDLE_GEMFILE' => 'MyGemfile'
## // マイグレーションのフェーズで実行するコマンド。
migration_command 'bundle exec rake db:migrate'
## // デプロイ基本情報
create_dirs_before_symlink %w{tmp}
## // 標準のsymlinkにassetsを追加している
symlinks "system"=>"public/system",
"assets"=>"public/assets",
"pids"=>"tmp/pids",
"log"=>"log"
## // マイグレーション前のフック、sharedの下に吐いたコンフィグをsymlinkにして`rails_root`の下に持ってくる
## // キー側は `shared_dir`からの相対パス、ターゲット側は`release_path`からの相対パスで指定
symlink_before_migrate "config/database.yml" => "config/database.yml",
"config/app_config.yml" => "config/app_config.yml"
## // 以降`relase_path`は使えるけど、`shared_path`が使えたり使えなかったりする。。
## // Shared_Dirの下に必要なディレクトリを置く、Docによるつデフォルトで幾つか作りそうだけど作られなかったので明示。
before_migrate do
%w{config pids log assets system localsystem}.each do |dir|
directory ::File.join(release_path, "../../shared" , dir) do
action :create
end
end
## // database.yml の生成
## // dbの名前は"#{app_name}_#(node.environment)"
template ::File.expand_path(::File.join(release_path, '../../shared/config/database.yml')) do
cookbook 'my_app_common'
source 'database.yml.erb'
variables({
:connection => connection,
:domains => domains,
:pg_master => pg_master,
:app_name => app_name
})
end
## // アプリケーション固有のコンフィグを生成
template ::File.expand_path(::File.join(release_path, '../../shared/config/app_config.yml')) do
source 'app_config.yml.erb'
variables({
:domains => domains
})
end
## // unicornの設定
## // ワーカープロセス数をdata_bagから
## // socketのパスに`app_name`を使用
template ::File.expand_path(::File.join(release_path, '../../shared/config/unicorn.rb')) do
cookbook 'nginx_upstream'
source 'unicorn.rb.erb'
variables({
:worker_processes => node[:deploy][app_name][:unicorn_wps],
:app_name => app_name
})
end
## // bundlerを使っているのでマイグレーション実行前にbundle
## // Gemを似たようなrailsアプリと共有するため、一段階上のSharedに置く
bash "bundle install #{app_name}" do
cwd release_path
flags '-ex'
code <<-BASHCODE
bundle install --deployment --gemfile MyGemfile --without development test --path=#{::File.join(node[:my_app_common][:basedir], 'shared/bundle')}
BASHCODE
end
end
## // リスタート前のフック
before_restart do
## // data_bagに指定があったらassetsを生成する
## // RAILS_GROUPSの指定をするのでenviromentを設定
## asset pipeline
if deploy_settings[app_name]['assets']
bash "assets precompile #{app_name}" do
cwd release_path
environment "RAILS_ENV" => node.environment,
'BUNDLE_GEMFILE' => 'MyGemfile',
'RAILS_GROUPS' => 'assets'
flags '-ex'
code <<-BASHCODE
bundle exec rake assets:precompile
BASHCODE
end
end
## // monitからコントロールするため、制御スクリプトを設置する
## // 中身にはPATHやらENVやら色々設定してある
%W(start_#{app_name}.sh stop_#{app_name}.sh).each do |cmd|
template ::File.join(dir_localsystem, cmd) do
source cmd + '.erb'
mode '0700'
variables({
:app_name => app_name,
:pid_file => 'unicorn.pid',
:bundle_command => "bundle exec unicorn_rails -c #{::File.expand_path(::File.join(release_path, '../../shared/config/unicorn.rb'))} -D"
})
end
end
## // monitのコンフィグ設置/更新を監視してリロードする
## // 初回や、現在プロセスが起動してない場合などはInitalizeに少々もたつくのでSleepでwait.
## // waitはだいたい5秒くらい
execute 'monit_reload_' + app_name do
action :nothing
command "monit reload ; sleep #{node[:deploy][:monit][:sleep]}"
subscribes :run, "template[/opt/local/etc/monit/conf.avail/#{app_name}]"
end
## // monitのコンフィグファイルをincludeパスにリンクする
execute 'monit_ensite_' + app_name do
action :nothing
command "monitensite #{app_name}"
notifies :run, "execute[monit_reload_#{app_name}]"
not_if "test -f #{node[:monit][:dir]}/conf.enable/#{app_name}"
end
## // monit_binのLWRPを使ってコンフィグを生成する
monit_bin_check_process app_name do
action :create
type "pid"
pidfile ::File.join(node[:my_app_common][:basedir], app_name , 'shared/pids/unicorn.pid')
start_program ::File.join(node[:my_app_common][:basedir], app_name , 'shared/localsystem', "start_#{app_name}.sh")
stop_program ::File.join(node[:my_app_common][:basedir], app_name ,'shared/localsystem', "stop_#{app_name}.sh")
notifies :run, "execute[monit_ensite_#{app_name}]"
end
end
## // リスタートのコマンド。
## // 初回はrestartにならないが問題ない。
restart_command "monit restart #{app_name}"
after_restart nil
end
## // アプリケーションログのローテーションを設定しておく
## // mvは色々面倒なので copy & truncate
logadm "app_#{app_name}" do
path ::File.join(node[:my_app_common][:basedir], app_name , 'shared/log/*.log')
copy true
size '100m'
period '7d'
action :create
end
### add nginx upstream
## // nginxにホストさせる
## // domainsのデータバックからSSLのヴァーチャルホスト名を拾ってきている
## // 名前ベースのSSL(SNI)で複数のアプリケーションを実行
## // NginxでのUpstream設定やassets pipelineまわりは RailsCastsの#341を参考にしている
template ::File.join(node[:nginx_upstream][:conf_dir] , "conf.d/#{domains[app_name]}.conf") do
cookbook 'nginx_upstream'
source 'upstream_ssl.erb'
variables({
:app_name => app_name,
:assets => deploy_settings[app_name]['assets'],
:server_name => domains[app_name],
:upstreams => ["unix:#{::File.join(node[:my_app_common][:basedir], app_name , 'shared/pids/unicorn.sock')}"]
})
notifies :restart, 'service[nginx]'
end
所感
-
capistrano
が使えれば、流れは同じなのでわかりやすい - data_bagを使う所はCookbookにするほうが版の管理ができていいかもしれない。
- 開発中のデプロイ定期実行はアリ
- 本番更新には色々あるので、
node
のデフォルトランリストは環境周りの整備にしておきそれを定期実行、deploy, rollbackのランリスト(role)を指定してknife
から流してあげるのがいいかもしれない。
追記ログ
-
symlink_before_migrate
でassetsを作るとなぜかassets/assets
が増えたのでsymlink
に移動