Skip to content

Instantly share code, notes, and snippets.

@finack
Created March 17, 2012 06:18
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 14 You must be signed in to fork a gist
  • Save finack/2055725 to your computer and use it in GitHub Desktop.
Save finack/2055725 to your computer and use it in GitHub Desktop.
Example Chef Deploy Revision for Rails 3+
app = node[:rails][:app]
rails_base app[:name] do
ruby_ver app[:ruby_ver]
gemset app[:gemset]
end
%w{config log pids cached-copy bundle system}.each do |dir|
directory "#{app[:app_root]}/shared/#{dir}" do
owner app[:deploy_user]
group app[:deploy_user]
mode '0755'
recursive true
action :create
end
end
keys = Chef::EncryptedDataBagItem.load('rails', app[:name])
create_database_config app[:name] do
app_root node[:rails][:app][:app_root]
deploy_user node[:rails][:app][:deploy_user]
databases node[:rails][:app][:db][:databases]
aliases node[:rails][:app][:db][:aliases]
passwords keys["database_passwords"]
end
deploy_revision "#{app[:app_root]}" do
repo app[:repo]
revision cluster_settings["revision"]
user app[:deploy_user]
group app[:deploy_user]
enable_submodules true
environment({
"RAILS_ENV" => app[:environment],
})
shallow_clone false
if node["rails"][app[:name]].attribute?('one_time_action')
# rollback, deploy, force_deploy
action node["rails"][app[:name]]["one_time_action"]
node["rails"][app[:name]].delete('one_time_action')
else
action :deploy
end
migrate true
migration_command "/usr/local/rvm/bin/appname_bundle exec rake db:migrate --trace"
before_migrate do
template "#{release_path}/config/environments/#{app[:environment]}.rb" do
source "#{release_path}/config/environments/chef_development.rb.erb"
local true
owner app[:deploy_user]
group app[:deploy_user]
mode '0400'
variables(:environment_settings => app[:environment_settings].to_hash)
end
rvm_shell "Bundle install and assets precompile" do
ruby_string "#{app[:ruby_ver]}@#{app[:gemset]}"
cwd release_path
user app[:deploy_user]
group app[:deploy_user]
# common_groups = %w{development test cucumber}
# bundle install --deployment --path #{app[:app_root]}/shared/bundle --without #{common_groups.join(' ')}
code %{
bundle install --path #{app[:app_root]}/shared/bundle
}
end
end
before_restart do
rvm_shell "assets precompile" do
ruby_string "#{app[:ruby_ver]}@#{app[:gemset]}"
cwd release_path
user app[:deploy_user]
group app[:deploy_user]
# TODO I could not set the environment via the builtin command. Does not look like it is getting passed to popen4
# So instead of `environment "RAILS_ENV" => app[:environment]` I have it in the code block
code %{
export RAILS_ENV=#{app[:environment]}
bundle exec rake assets:precompile
}
end
end
symlink_before_migrate({
"config/database.yml" => "config/database.yml",
"config/environments/#{app[:environment]}.rb" => "config/environments/#{app[:environment]}.rb"
})
after_restart do
notify_airbrake release_path do
environment app[:environment]
repository app[:repo]
end
notify_newrelic release_path do
environment app[:environment]
end
end
end
#Used to configure variables for the app per environment (instead of attributes).
#I have since refactored how this works so treat this as example code, aka I
#have not tested this and it is not complete.
#FWIW using libraries in the cookbook to set these variables
#for smaller apps I would keep this in the attributes
app_name = 'yourname'
default = {
name: app_name
gemset: app_name
ruby_ver: 'ruby-1.8.7-p352',
deploy_user: 'deployer',
environment: node[:environment],
#...
}
staging = {
app_root: '/export/staging/yourname',
#...
}
case node[:environment]
when "staging"
rails_attributes = Chef::Mixin::DeepMerge.merge(default, staging)
end
# Don't know why I saved this in a seperate hash before the deep merge...
original_node_attributes = node.default.to_hash
node.default[:rails] = Chef::Mixin::DeepMerge.merge(rails_attributes, original_node_attributes["rails"])
define :create_database_config, :passwords=>{} do
databases = Hash.new
params[:databases].each do |db_name|
if node[:rails][params[:name]][:db].has_key?(db_name)
values = node[:rails][params[:name]][:db][db_name].to_hash
if params[:passwords].has_key?(db_name.to_s)
values["password"] = params[:passwords].fetch(db_name.to_s)
end
name = values.has_key?(name) ? values[:name] : db_name
databases.merge!({ name.to_s => values })
end
end
if params.has_key? :aliases
params[:aliases].each do |target,source|
databases[target.to_s] = databases[source.to_s]
end
end
file "#{params[:app_root]}/shared/config/database.yml" do
owner params[:deploy_user]
group params[:deploy_user]
mode '400'
content databases.to_yaml
end
end
define :notify_airbrake, :name=>nil, :environment=>nil, :repository=>nil do
script "notifying airbrake of deploy" do
interpreter "ruby"
user "deployer"
cwd params[:name]
code <<-EOS
require 'rubygems'
unless File.exist?("#{params[:name]}/config/initializers/airbrake.rb")
puts "!!! airbrake.rb not found. Not notifying Airbrake of the deploy !!!"
exit 0
end
# Setup the airbrake api_key
require 'airbrake'
require 'airbrake_tasks'
require "#{params[:name]}/config/initializers/airbrake.rb"
revision = `cd #{params[:name]} && git rev-parse HEAD`.chomp
print "Airbrake : "
AirbrakeTasks.deploy(:rails_env => '#{params[:environment]}',
:scm_revision => revision,
:scm_repository => '#{params[:repository]}',
:local_username => 'chef')
EOS
end
end
define :notify_newrelic, :deploy_path=>nil, :environment=>nil do
script "notifying new relic of deploy" do
interpreter "ruby"
user "deployer"
cwd params[:name]
code <<-EOS
require 'rubygems'
unless File.exist?("#{params[:name]}/config/newrelic.yml")
puts "!!! newrelic.yml not found. Not notifying New Relic of the deploy !!!"
exit 0
end
## When new relic is loaded it automagically loads up its configuration file
## And sets the config[:environment]. We are injecting the path to the configuration
## file and the environment via ENV variables.
ENV['APP_ROOT'] = "#{params[:name]}"
ENV['RAILS_ENV'] = "#{params[:environment]}"
require 'newrelic_rpm'
require 'new_relic/command'
config = NewRelic::Control.instance
revision = `git rev-parse HEAD`.chomp
deployment = NewRelic::Command::Deployments.new({:revision => revision, :user => 'chef'})
print "New Relic : "
deployment.run
EOS
end
end
define :rails_base, :ruby_ver=>nil, :gemset=>nil do
include_recipe "nodejs"
package "mysql-client"
package "libmysqlclient-dev"
gem_package "newrelic_rpm"
gem_package "airbrake"
directory '/export' do
owner 'root'
group 'root'
recursive true
action :create
only_if "test -e /export"
end
create_deploy_user
rvm_gemset params[:gemset] do
ruby_string params[:ruby_ver]
action :create
end
rvm_gem 'bundler' do
ruby_string "#{params[:ruby_ver]}@#{params[:gemset]}"
end
# Creates /usr/local/rvm/bin/appname_bundle
rvm_wrapper params[:name] do
ruby_string "#{params[:ruby_ver]}@#{params[:gemset]}"
binary 'bundle'
end
end
{
# Handwritten from memory, probably broken
"id": "yourname",
"database_passwords": {
"staging": "foo"
}
}
@millisami
Copy link

Great!
Have you packaged it any gem?

@millisami
Copy link

Or where can I find the cookbook for this?

@finack
Copy link
Author

finack commented Apr 7, 2012

I have not release this in a gem or cookbook. This should be pretty complete example. Are you looking for more?

@millisami
Copy link

Yes, I'ld like to see all the templates and the structure of the data bag as well.

@millisami
Copy link

And what is the app server it will use? I mean Unicorn, Nginx, et...

@millisami
Copy link

How the attributes will look like along with data_bag item as well?

@finack
Copy link
Author

finack commented Jul 11, 2012

Hey there!

Updated the code a bit. Hopefully that helped. I have been using one of the unicorn cookbooks (i think from opscode) and apache to serve all of this. I have been separating out the app deployment from the server as we also deploy delay jobs and other systems that need RoR but not unicorn. For example our run_lists kinda look like

  • www_front_end = app, app_apache # Want apache and assets
  • app_server = app, app_unicorn
  • dj_server = app, app_dj

Hopefully this answered some of your questions. I will try to get to others but will be delayed :) Have fun!!

@millisami
Copy link

@finack, Thanks for the update and its really helpful.
But a few pieces left out to make it work for me.

First, the following definition to create the database.

create_database_config app[:name] do
  app_root node[:rails][:app][:app_root]
  deploy_user node[:rails][:app][:deploy_user]
  databases node[:rails][:app][:db][:databases]
  aliases node[:rails][:app][:db][:aliases]
  passwords keys["database_passwords"]
end

What would be the structure of the attributes/default.rb file?
Does the following work or I think I need to change the attributes?

default['rails']['app']['app_root'] = "/home/deployer"
default['rails']['railsbp']['db']['railsbp_prod']['encoding'] = "utf8"
default['rails']['railsbp']['db']['railsbp_prod']['reconnect'] = "true"
default['rails']['railsbp']['db']['railsbp_prod']['adapter'] = "mysql2"

And lastly, what that source "#{release_path}/config/environments/chef_development.rb.erb" template looks like?

Yeah, I know I'm aksing a lot. But your help is directing me to the final deploy.

Can you address those queries?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment