From f3f693f34a3e7bf4f62534eb6d8c1ad2c6430fb6 Mon Sep 17 00:00:00 2001 From: Darrin Eden Date: Sun, 12 Jun 2011 22:21:27 -0700 Subject: [PATCH] initial rails app cookbook --- README.md | 47 ++++++++++++++++++++ attributes/app.rb | 5 +++ attributes/bluepill.rb | 5 +++ attributes/db.rb | 5 +++ attributes/unicorn.rb | 3 ++ files/default/known_hosts | 2 + metadata.rb | 15 +++++++ recipes/bluepill.rb | 15 +++++++ recipes/database.rb | 14 ++++++ recipes/db_mysql.rb | 20 +++++++++ recipes/db_postgres.rb | 33 ++++++++++++++ recipes/db_sqlite3.rb | 7 +++ recipes/default.rb | 33 ++++++++++++++ recipes/deploy.rb | 50 ++++++++++++++++++++++ recipes/deploy_key.rb | 18 ++++++++ recipes/nginx.rb | 14 ++++++ recipes/node.rb | 12 ++++++ recipes/unicorn.rb | 19 ++++++++ templates/default/bluepill.erb | 32 ++++++++++++++ templates/default/database.sqlite3.yml.erb | 6 +++ templates/default/database.yml.erb | 8 ++++ templates/default/nginx.erb | 26 +++++++++++ templates/default/unicorn.erb | 42 ++++++++++++++++++ 23 files changed, 431 insertions(+) create mode 100644 README.md create mode 100644 attributes/app.rb create mode 100644 attributes/bluepill.rb create mode 100644 attributes/db.rb create mode 100644 attributes/unicorn.rb create mode 100644 files/default/known_hosts create mode 100644 metadata.rb create mode 100644 recipes/bluepill.rb create mode 100644 recipes/database.rb create mode 100644 recipes/db_mysql.rb create mode 100644 recipes/db_postgres.rb create mode 100644 recipes/db_sqlite3.rb create mode 100644 recipes/default.rb create mode 100644 recipes/deploy.rb create mode 100644 recipes/deploy_key.rb create mode 100644 recipes/nginx.rb create mode 100644 recipes/node.rb create mode 100644 recipes/unicorn.rb create mode 100644 templates/default/bluepill.erb create mode 100644 templates/default/database.sqlite3.yml.erb create mode 100644 templates/default/database.yml.erb create mode 100644 templates/default/nginx.erb create mode 100644 templates/default/unicorn.erb diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd7589c --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +Description +=========== + +A collection of [Heavy Water Software Inc's](http://hw-ops.com) +evolving opinions on best practises for configuring and integrating a +Rails application. + +Requirements +============ + +A Rails application code repository that is configured to manage gem +dependencies via Bundler. + +Attributes +========== + +* `node["app"]["repository"] ` - Code to deploy (defaults to an + example Rails 3.1 app) +* `node["db"]["adapter"]` - Database such as postgresql, mysql, or + sqlite3 (default) + + +Usage +===== + +Include "recipe[app::deploy]" in a run list to install an example +Rails 3.1 application backed by a SQLite3 database. + +If you're cloning a private repository add a deploy key at +app/files/default/deploy_key_id_rsa + +A more practical case might be to create an "app" role: + +```ruby +name "app" +description "My Rails app" + +run_list [ "role[base]", + "recipe[postgresql::server]", + "recipe[app::deploy]" ] + +default_attributes( "app" => { + "repository" => "git@github.com:me/myapp.git", + "use_deploy_key" => true }, + "db" => { + "adapter" => "postgresql" } ) +``` diff --git a/attributes/app.rb b/attributes/app.rb new file mode 100644 index 0000000..ebceeec --- /dev/null +++ b/attributes/app.rb @@ -0,0 +1,5 @@ +default[:app][:repository] = "https://github.com/RailsApps/rails3-devise-rspec-cucumber.git" +default[:app][:revision] = "master" +default[:app][:migrate] = true +default[:app][:rails_env] = "production" +default[:app][:use_deploy_key] = false diff --git a/attributes/bluepill.rb b/attributes/bluepill.rb new file mode 100644 index 0000000..eb354b7 --- /dev/null +++ b/attributes/bluepill.rb @@ -0,0 +1,5 @@ +default[:bluepill][:start_grace_time] = 16 +default[:bluepill][:stop_grace_time] = 8 +default[:bluepill][:restart_grace_time] = 24 +default[:bluepill][:mem_usage_mb] = 256 +default[:bluepill][:cpu_usage_percent] = 80 diff --git a/attributes/db.rb b/attributes/db.rb new file mode 100644 index 0000000..141a9cd --- /dev/null +++ b/attributes/db.rb @@ -0,0 +1,5 @@ +default[:db][:adapter] = "sqlite3" +default[:db][:name] = "app" +default[:db][:user] = "app" +default[:db][:pass] = "app" +default[:db][:host] = "127.0.0.1" diff --git a/attributes/unicorn.rb b/attributes/unicorn.rb new file mode 100644 index 0000000..3704cf9 --- /dev/null +++ b/attributes/unicorn.rb @@ -0,0 +1,3 @@ +default[:unicorn][:stand_alone] = true +default[:unicorn][:timeout] = 30 +default[:unicorn][:cow_friendly] = false diff --git a/files/default/known_hosts b/files/default/known_hosts new file mode 100644 index 0000000..9ad52a2 --- /dev/null +++ b/files/default/known_hosts @@ -0,0 +1,2 @@ +|1|3/H+7sv7Es48a30PNHocVqGB6Vo=|NIUB1atncTQJeeHRVlt6jpsGIlE= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== +|1|CLmkEH5jmHUXMT2JdPCL79dWSJU=|odQ02l0q5ZrQk4i82gQKcXmM/R4= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== diff --git a/metadata.rb b/metadata.rb new file mode 100644 index 0000000..1182067 --- /dev/null +++ b/metadata.rb @@ -0,0 +1,15 @@ +maintainer "Heavy Water Software Inc." +maintainer_email "darrin@heavywater.ca" +license "Apache 2.0" +description "Installs/Configures a Rails app" +long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) +version "0.1.0" + +supports "ubuntu" + +depends "xml" +depends "xslt" +depends "imagemagick" + +depends "nginx" +depends "bluepill" diff --git a/recipes/bluepill.rb b/recipes/bluepill.rb new file mode 100644 index 0000000..c29b71f --- /dev/null +++ b/recipes/bluepill.rb @@ -0,0 +1,15 @@ +include_recipe "bluepill" + +template "/etc/bluepill/app.pill" do + source "bluepill.erb" + variables( :environment => node[:app][:rails_env], + :start_grace_time => node[:bluepill][:start_grace_time], + :stop_grace_time => node[:bluepill][:stop_grace_time], + :restart_grace_time => node[:bluepill][:restart_grace_time], + :mem_usage_mb => node[:bluepill][:mem_usage_mb], + :cpu_usage_percent => node[:bluepill][:cpu_usage_percent] ) +end + +bluepill_service "app" do + action [ :enable, :load, :start ] +end diff --git a/recipes/database.rb b/recipes/database.rb new file mode 100644 index 0000000..4dee809 --- /dev/null +++ b/recipes/database.rb @@ -0,0 +1,14 @@ +directory "/var/www/shared/config" do + owner "www-data" + group "www-data" + recursive true +end + +case node[:db][:adapter] +when "mysql" + include_recipe "app::db_mysql" +when "postgresql" + include_recipe "app::db_postgres" +when "sqlite3" + include_recipe "app::db_sqlite3" +end diff --git a/recipes/db_mysql.rb b/recipes/db_mysql.rb new file mode 100644 index 0000000..904ca41 --- /dev/null +++ b/recipes/db_mysql.rb @@ -0,0 +1,20 @@ +include_recipe "mysql::client" + +mysql_database "create app database" do + host node[:db][:host] + username "root" + password node[:mysql][:server_root_password] + database node[:db][:name] + action :create_db +end + +template "/var/www/shared/config/database.yml" do + source "database.yml.erb" + owner "www-data" + group "www-data" + variables( :adapter => node[:db][:adapter], + :name => node[:db][:name], + :user => "root", + :pass => node[:mysql][:server_root_password], + :host => node[:db][:host] ) +end diff --git a/recipes/db_postgres.rb b/recipes/db_postgres.rb new file mode 100644 index 0000000..d7e4e5e --- /dev/null +++ b/recipes/db_postgres.rb @@ -0,0 +1,33 @@ +package "libpq-dev" + +template "/var/www/shared/config/database.yml" do + source "database.yml.erb" + owner "www-data" + group "www-data" + variables( :adapter => node[:db][:adapter], + :name => node[:db][:name], + :user => node[:db][:user], + :pass => node[:db][:pass], + :host => node[:db][:host] ) +end + +execute "create #{node[:db][:user]} database user" do + command "createuser --createdb --no-createrole --no-superuser #{node[:db][:user]} && \ + psql --command \"alter role #{node[:db][:user]} with encrypted password '#{node[:db][:pass]}'\"" + user "postgres" + not_if do + `sudo -u postgres psql --tuples-only --command=" \ + select rolname from pg_roles where \ + rolname = '#{node[:db][:user]}'"`.strip == "#{node[:db][:user]}" + end +end + +execute "create #{node[:db][:name]} database" do + command "createdb --encoding=UTF8 --owner=#{node[:db][:user]} #{node[:db][:name]}" + user "postgres" + not_if do + `sudo -u postgres psql --tuples-only --command=" \ + select datname from pg_database where \ + datname = '#{node[:db][:name]}'"`.strip == "#{node[:db][:name]}" + end +end diff --git a/recipes/db_sqlite3.rb b/recipes/db_sqlite3.rb new file mode 100644 index 0000000..482b941 --- /dev/null +++ b/recipes/db_sqlite3.rb @@ -0,0 +1,7 @@ +package "libsqlite3-dev" + +template "/var/www/shared/config/database.yml" do + source "database.sqlite3.yml.erb" + owner "www-data" + group "www-data" +end diff --git a/recipes/default.rb b/recipes/default.rb new file mode 100644 index 0000000..3bf166c --- /dev/null +++ b/recipes/default.rb @@ -0,0 +1,33 @@ +# +# Cookbook Name:: app +# Recipe:: default +# +# Copyright 2011, Heavy Water Software Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Seems like nokogiri and rmagick are pretty common Rails +# dependencies. Might as well get those out of the way. +include_recipe "xml" +include_recipe "xslt" +include_recipe "imagemagick" + +# Apparently Rails 3.1 requires a JavaScript interpreter for execjs +# and node is the most convenient to install. +include_recipe "app::node" + +include_recipe "app::database" +include_recipe "app::nginx" +include_recipe "app::unicorn" +include_recipe "app::bluepill" diff --git a/recipes/deploy.rb b/recipes/deploy.rb new file mode 100644 index 0000000..09956fd --- /dev/null +++ b/recipes/deploy.rb @@ -0,0 +1,50 @@ +include_recipe "app" + +if node[:app][:use_deploy_key] + include_recipe "app::deploy_key" +end + +gem_package "bundler" + +%w( shared + shared/pids + shared/system + shared/log ).each do |d| + directory "/var/www/#{d}" do + owner "www-data" + group "www-data" + recursive true + end +end + +deploy_revision "/var/www" do + repository node[:app][:repository] + revision node[:app][:revision] + user "www-data" + enable_submodules true + migrate node[:app][:migrate] + migration_command "bundle exec rake db:migrate" + environment "RAILS_ENV" => node[:app][:rails_env] + shallow_clone true + + before_migrate do + execute "bundle gems" do + command "bundle install " + + "--deployment --without development test " + + "--path /var/www/shared/bundle " + + "--binstubs /var/www/shared/bundle/bin" + user "www-data" + group "www-data" + cwd release_path + end + end + + restart do + execute "restart unicorn" do + command "bluepill app restart" + user "root" + end + end + + action node[:app][:deploy_action] +end diff --git a/recipes/deploy_key.rb b/recipes/deploy_key.rb new file mode 100644 index 0000000..88a5569 --- /dev/null +++ b/recipes/deploy_key.rb @@ -0,0 +1,18 @@ +directory "/var/www/.ssh" do + owner "www-data" + group "www-data" + recursive true +end + +cookbook_file "/var/www/.ssh/id_rsa" do + source "deploy_key_id_rsa" + owner "www-data" + group "www-data" + mode "600" +end + +cookbook_file "/var/www/.ssh/known_hosts" do + owner "www-data" + group "www-data" + mode "644" +end diff --git a/recipes/nginx.rb b/recipes/nginx.rb new file mode 100644 index 0000000..47f1b26 --- /dev/null +++ b/recipes/nginx.rb @@ -0,0 +1,14 @@ +include_recipe "nginx" + +template "/etc/nginx/sites-available/app" do + source "nginx.erb" + notifies :restart, "service[nginx]" +end + +nginx_site "default" do + enable false +end + +nginx_site "app" do + enable true +end diff --git a/recipes/node.rb b/recipes/node.rb new file mode 100644 index 0000000..9ded6bb --- /dev/null +++ b/recipes/node.rb @@ -0,0 +1,12 @@ +package "python-software-properties" + +execute "apt-get update" do + action :nothing +end + +execute "add-apt-repository ppa:jerome-etienne/neoip" do + not_if "apt-key list | grep 2D83C357" + notifies :run, "execute[apt-get update]", :immediately +end + +package "nodejs" diff --git a/recipes/unicorn.rb b/recipes/unicorn.rb new file mode 100644 index 0000000..27ee1ae --- /dev/null +++ b/recipes/unicorn.rb @@ -0,0 +1,19 @@ +gem_package "red_unicorn" if node[:unicorn][:stand_alone] + +directory "/etc/unicorn" + +directory "/var/run/unicorn" do + owner "www-data" + group "www-data" +end + +template "/etc/unicorn/app.rb" do + source "unicorn.erb" + owner "root" + group "root" + mode "644" + variables( :timeout => node[:unicorn][:timeout], + :cow_friendly => node[:unicorn][:cow_friendly], + :worker_processes => node[:cpu][:total] ) + notifies :restart, "bluepill_service[app]" +end diff --git a/templates/default/bluepill.erb b/templates/default/bluepill.erb new file mode 100644 index 0000000..87fc8ac --- /dev/null +++ b/templates/default/bluepill.erb @@ -0,0 +1,32 @@ +Bluepill.application("app") do |app| + app.process("unicorn") do |process| + process.pid_file = "/var/run/unicorn/unicorn.pid" + process.working_dir = "/var/www/current" + + process.start_command = "red_unicorn --unicorn-exec /usr/local/bin/unicorn --env <%= @environment %> start" + process.stop_command = "red_unicorn --unicorn-exec /usr/local/bin/unicorn stop" + process.restart_command = "red_unicorn --unicorn-exec /usr/local/bin/unicorn restart" + + process.uid = process.gid = "www-data" + + process.start_grace_time = <%= @start_grace_time %>.seconds + process.stop_grace_time = <%= @stop_grace_time %>.seconds + process.restart_grace_time = <%= @restart_grace_time %>.seconds + + process.monitor_children do |child_process| + child_process.stop_command = "kill -QUIT {{PID}}" + + child_process.checks( :mem_usage, + :every => 30.seconds, + :below => <%= @mem_usage_mb %>.megabytes, + :times => [3,4], + :fires => :stop ) + + child_process.checks( :cpu_usage, + :every => 30.seconds, + :below => <%= @cpu_usage_percent %>, + :times => [3,4], + :fires => :stop ) + end + end +end diff --git a/templates/default/database.sqlite3.yml.erb b/templates/default/database.sqlite3.yml.erb new file mode 100644 index 0000000..356a635 --- /dev/null +++ b/templates/default/database.sqlite3.yml.erb @@ -0,0 +1,6 @@ +--- +production: + adapter: sqlite3 + database: /var/www/shared/production.sqlite3 + pool: 5 + timeout: 5000 diff --git a/templates/default/database.yml.erb b/templates/default/database.yml.erb new file mode 100644 index 0000000..afadcc2 --- /dev/null +++ b/templates/default/database.yml.erb @@ -0,0 +1,8 @@ +--- +production: + adapter: <%= @adapter %> + database: <%= @name %> + username: <%= @user %> + password: <%= @pass %> + host: <%= @host %> + encoding: UTF8 diff --git a/templates/default/nginx.erb b/templates/default/nginx.erb new file mode 100644 index 0000000..513c7ae --- /dev/null +++ b/templates/default/nginx.erb @@ -0,0 +1,26 @@ +upstream app { + server unix:/var/run/unicorn/unicorn.socket fail_timeout=0; +} + +server { + listen 80; + root /var/www/current/public; + + location / { + try_files /system/maintenance.html $uri $uri.html $uri/index.html @rails; + } + + error_page 500 502 503 504 /500.html; + location = /500.html { + root /var/www/current/public; + } + + location @rails { + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + + proxy_pass http://app; + } +} diff --git a/templates/default/unicorn.erb b/templates/default/unicorn.erb new file mode 100644 index 0000000..dde6f7a --- /dev/null +++ b/templates/default/unicorn.erb @@ -0,0 +1,42 @@ +worker_processes <%= @worker_processes %> +working_directory "/var/www/current" +listen "/var/run/unicorn/unicorn.socket", :backlog => 64 +timeout <%= @timeout %> +pid "/var/run/unicorn/unicorn.pid" + +stderr_path "/var/www/shared/log/unicorn_stderr.log" +stdout_path "/var/www/shared/log/unicorn_stdout.log" + +if GC.respond_to?(:copy_on_write_friendly=) + preload_app <%= @cow_friendly %> + GC.copy_on_write_friendly = <%= @cow_friendly %> +end + +before_fork do |server, worker| + defined?(ActiveRecord::Base) and + ActiveRecord::Base.connection.disconnect! + + begin + uid, gid = Process.euid, Process.egid + user, group = "www-data", "www-data" + target_uid = Etc.getpwnam(user).uid + target_gid = Etc.getgrnam(group).gid + worker.tmp.chown(target_uid, target_gid) + if uid != target_uid || gid != target_gid + Process.initgroups(user, target_gid) + Process::GID.change_privilege(target_gid) + Process::UID.change_privilege(target_uid) + end + rescue => e + if RAILS_ENV == "development" + STDERR.puts "couldn't change user, oh well" + else + raise e + end + end +end + +after_fork do |server, worker| + defined?(ActiveRecord::Base) and + ActiveRecord::Base.establish_connection +end