Practical Chef and Capistrano for Your Rails App

40
Practical Chef and Capistrano For Your Rails Application Dan Ivovich SLS Internal Dec 2012 12/17/12

description

Dan Ivovich walks through how to use Chef and Capistrano for your Rails application.

Transcript of Practical Chef and Capistrano for Your Rails App

Page 1: Practical Chef and Capistrano for Your Rails App

Practical Chef and CapistranoFor Your Rails Application

Dan Ivovich

SLS Internal Dec 201212/17/12

Page 3: Practical Chef and Capistrano for Your Rails App

What is the goal?

● Build a machine that can run the application quickly and repeatedly

● Make deploying the app, upgrading components, adding components, etc seamless and easy

Page 4: Practical Chef and Capistrano for Your Rails App

Who does what?● Chef

○ User / SSH Keys○ Web server○ Database○ Postfix○ Redis / Memcached○ Monit○ NewRelic Server monitoring○ /etc/hosts○ rbenv & Ruby○ Application binary dependencies (i.e.

Sphinx)

Page 5: Practical Chef and Capistrano for Your Rails App

Who does what?

● Capistrano○ Virtual Hosts○ Unicorn init.d script○ Unicorn.rb○ Monit process monitors○ Normal Capistrano Stuff

Page 6: Practical Chef and Capistrano for Your Rails App

Why both?

● Use each for what it is best at● Chef is for infrastructure● Capistrano is for the app● Could have more than one Capistrano app with

the same Chef config● Chef config changes infrequently, Capistrano

config could change more frequently

Page 7: Practical Chef and Capistrano for Your Rails App

How? - Chef

● Standard Recipes● Custom Recipes● Recipes assigned to Roles● Roles assigned to Nodes● Nodes with attributes to tailor the install

Page 8: Practical Chef and Capistrano for Your Rails App

How? - Capistrano

● Standard Tasks● Custom Tasks● Templating files

Page 9: Practical Chef and Capistrano for Your Rails App

Chef - Getting started● Gemfile

● knife kitchen chef-repo○ Create the folder structure you need

● Solo specific stuff (chef-repo/.chef/knife.rb)

● knife cookbook site install nginx○ Get the nginx cookbook and anything it

needs

Page 10: Practical Chef and Capistrano for Your Rails App

Custom cookbook● knife cookbook create your_app_custom

● Edit:chef-repo/cookbooks/your_app_custom/recipes/default.rb

Page 11: Practical Chef and Capistrano for Your Rails App

package "logrotate"

rbenv_ruby node['your_app']['ruby_version']

rbenv_gem "bundler" do ruby_version node['your_app']['ruby_version']end

template "/etc/hosts" do source "hosts.erb" mode "0644" owner "root" group "root"end

Page 12: Practical Chef and Capistrano for Your Rails App

directory "#{node['your_app']['cap_base']}" do action :create owner 'deploy' group 'deploy' mode '0755'end

directory "#{node['your_app']['deploy_to']}/shared" do action :create owner 'deploy' group 'deploy' mode '0755'end

template "#{node['your_app']['deploy_to']}/shared/database.yml" do source 'database.yml.erb' owner 'deploy' group 'deploy' mode '0644'end

Page 13: Practical Chef and Capistrano for Your Rails App

Recipe Templates● chef-repo/cookbooks/your_app_custom/templates/default/database.yml.erb

<%= node['your_app']['environment'] %>: adapter: <%= node['your_app']['adapter'] %> database: <%= node['your_app']['database'] %> username: <%= node['your_app']['database_user'] %><% if node['your_app']['database_password'] %> password: <%= node['your_app']['database_password'] %><% end %> host: <%= node['your_app']['database_host'] %> encoding: utf8 min_messages: warning

Page 14: Practical Chef and Capistrano for Your Rails App

Node Attributes● Application namespace

○ chef-repo/cookbooks/your_app_custom/attributes/default.rbdefault['your_app']['cap_base'] = '/home/deploy/apps'default['your_app']['deploy_to'] = '/home/deploy/apps/your_app'default['your_app']['environment'] = 'production'default['your_app']['database'] = 'your_app'default['your_app']['adapter'] = 'postgresql'default['your_app']['database_user'] = 'postgres'default['your_app']['database_password'] = (node['postgresql']['password']['postgres'] rescue nil)default['your_app']['database_host'] = 'localhost'default['your_app']['ruby_version'] = '1.9.2-p320'

Page 15: Practical Chef and Capistrano for Your Rails App

Node Attributes

"your_app" : { "environment" : "production", "database" : "your_app", "database_user" : "your_app_db_user", "database_host" : "db1", "hosts" : { "db1" : "nn.nn.nn.nn" }},

● For your Node configuration

Page 16: Practical Chef and Capistrano for Your Rails App

Define Roles

name "web_server"description "web server setup"run_list [ "recipe[build-essential]", "recipe[annoyances]", "recipe[openssl]", "recipe[openssh]", "recipe[sudo]", "recipe[postgresql::client]", "recipe[users_solo::admins]", "recipe[sphinx]", "recipe[imagemagick]", "recipe[nginx]", "recipe[rbenv]", "recipe[postfix]", "recipe[monit]", "recipe[your_app_custom]"]default_attributes 'build-essential' => { 'compiletime' => true }

chef-repo/roles/web_server.rb

Page 17: Practical Chef and Capistrano for Your Rails App

Node Configuration{ "openssh" : { "permit_root_login" : "no", "password_authentication": "no" }, "authorization" : { "sudo" : { "groups" : [ "admin", "sudo" ], "passwordless" : true } }, "rbenv" : { "group_users" : [ "deploy" ] }, "sphinx" : { "use_mysql" : false, "use_postgres" : true }, "your_app" : { "environment" : "production", "database" : "your_app", "database_user" : "your_app_db_user", "database_host" : "db1", "hosts" : { "db1" : "nn.nn.nn.nn" } }, "run_list": [ "role[web_server]" ]}

Page 18: Practical Chef and Capistrano for Your Rails App

Not so bad!

Page 19: Practical Chef and Capistrano for Your Rails App

Go!● bundle exec knife bootstrap -x super_user node_name \

--template-file=ubuntu-12.04-lts.erb

● bundle exec knife cook super_user@node_name

● Relax!

Page 20: Practical Chef and Capistrano for Your Rails App

Capistrano - Getting Started

● Add capistrano and capistrano-ext● Capify● deploy.rb

Page 21: Practical Chef and Capistrano for Your Rails App

Capistrano - deploy.rbrequire 'bundler/capistrano'require 'capistrano/ext/multistage'

load 'config/recipes/base'load 'config/recipes/nginx'load 'config/recipes/unicorn'load 'config/recipes/monit'

set :default_environment, { 'PATH' => "/opt/rbenv/shims:/opt/rbenv/bin:$PATH", 'RBENV_ROOT' => "/opt/rbenv"}set :bundle_flags, "--deployment --quiet --binstubs --shebang ruby-local-exec"set :use_sudo, falseset :application, 'your_app'set :repository, '[email protected]:you/your_app.git'set :deploy_to, '/home/deploy/apps/your_app'set :deploy_via, :remote_cache

Page 22: Practical Chef and Capistrano for Your Rails App

Capistrano - deploy.rbset :branch, 'master'set :scm, :gitset :target_os, :ubuntuset :maintenance_template_path, File.expand_path("../recipes/templates/maintenance.html.erb", __FILE__)

default_run_options[:pty] = truessh_options[:forward_agent] = true

namespace :custom do desc 'Create the .rbenv-version file' task :rbenv_version, :roles => :app do run "cd #{release_path} && rbenv local 1.9.2-p320" endend

before 'bundle:install', 'custom:rbenv_version'

Page 23: Practical Chef and Capistrano for Your Rails App

Capistrano - recipes/base.rb

def template(from, to) erb = File.read(File.expand_path("../templates/#{from}", __FILE__)) put ERB.new(erb).result(binding), toend

def set_default(name, *args, &block) set(name, *args, &block) unless exists?(name)end

Page 24: Practical Chef and Capistrano for Your Rails App

Capistrano - recipes/monit.rbset_default(:alert_email, "[email protected]")namespace :monit do desc "Setup all Monit configuration" task :setup do unicorn syntax restart end after "deploy:setup", "monit:setup"

task(:unicorn, roles: :app) { monit_config "unicorn" }

%w[start stop restart syntax].each do |command| desc "Run Monit #{command} script" task command do with_user "deploy" do sudo "service monit #{command}" end end endend

Page 25: Practical Chef and Capistrano for Your Rails App

Capistrano - recipes/monit.rb

def monit_config(name, destination = nil) destination ||= "/etc/monit/conf.d/#{name}.conf" template "monit/#{name}.erb", "/tmp/monit_#{name}" with_user "deploy" do sudo "mv /tmp/monit_#{name} #{destination}" sudo "chown root #{destination}" sudo "chmod 600 #{destination}" endend

Page 26: Practical Chef and Capistrano for Your Rails App

Capistrano - recipes/nginx.rb

namespace :nginx do desc "Setup nginx configuration for this application" task :setup, roles: :web do template "nginx_unicorn.erb", "/tmp/nginx_conf" sudo "mv /tmp/nginx_conf /etc/nginx/sites-enabled/#{application}" sudo "rm -f /etc/nginx/sites-enabled/default" restart end after "deploy:setup", "nginx:setup"

%w[start stop restart].each do |command| desc "#{command} nginx" task command, roles: :web do sudo "service nginx #{command}" end endend

Page 27: Practical Chef and Capistrano for Your Rails App

Capistrano - templates/nginx_unicorn.erbupstream unicorn { server unix:/tmp/unicorn.<%= application %>.sock fail_timeout=0;}server { listen 80 default deferred; server_name your_app_domain.com; root <%= current_path %>/public; if (-f $document_root/system/maintenance.html) { return 503; } error_page 503 @maintenance; location @maintenance { rewrite ^(.*)$ /system/maintenance.html last; break; } location ^~ /assets/ { gzip_static on; expires max; add_header Cache-Control public; } try_files $uri/index.html $uri @unicorn; location @unicorn { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; proxy_pass http://unicorn; } error_page 500 502 /500.html; error_page 504 /504.html; client_max_body_size 4G; keepalive_timeout 10; server_tokens off;}

Page 28: Practical Chef and Capistrano for Your Rails App

Capistrano - recipes/unicorn.rb

set_default(:unicorn_user) { user }set_default(:unicorn_pid) { "#{current_path}/tmp/pids/unicorn.pid" }set_default(:unicorn_config) { "#{shared_path}/config/unicorn.rb" }set_default(:unicorn_log) { "#{shared_path}/log/unicorn.log" }set_default(:unicorn_workers) { if rails_env == "production" 10 else 3 end}set_default(:unicorn_timeout, 30)

Page 29: Practical Chef and Capistrano for Your Rails App

Capistrano - recipes/unicorn.rbnamespace :unicorn do desc "Setup Unicorn initializer and app configuration" task :setup, roles: :app do run "mkdir -p #{shared_path}/config" template "unicorn.rb.erb", unicorn_config template "unicorn_init.erb", "/tmp/unicorn_init" run "chmod +x /tmp/unicorn_init" sudo "mv /tmp/unicorn_init /etc/init.d/unicorn_#{application}" sudo "update-rc.d -f unicorn_#{application} defaults" end after "deploy:setup", "unicorn:setup"

%w[start stop restart].each do |command| desc "#{command} unicorn" task command, roles: :app do sudo "service unicorn_#{application} #{command}" end after "deploy:#{command}", "unicorn:#{command}" endend

Page 30: Practical Chef and Capistrano for Your Rails App

Capistrano - templates/unicorn.rb.erb

root = "<%= current_path %>"working_directory rootpid "#{root}/tmp/pids/unicorn.pid"stderr_path "#{root}/log/unicorn.log"stdout_path "#{root}/log/unicorn.log"listen "/tmp/unicorn.<%= application %>.sock"worker_processes <%= unicorn_workers %>timeout <%= unicorn_timeout %>preload_app true

before_exec { |server| ENV['BUNDLE_GEMFILE'] = "#{root}/Gemfile" }

Page 31: Practical Chef and Capistrano for Your Rails App

Capistrano - templates/unicorn.rb.erb

before_fork do |server, worker| # Disconnect since the database connection will not carry over if defined? ActiveRecord::Base ActiveRecord::Base.connection.disconnect! end # Quit the old unicorn process old_pid = "#{server.config[:pid]}.oldbin" if File.exists?(old_pid) && server.pid != old_pid begin Process.kill("QUIT", File.read(old_pid).to_i) rescue Errno::ENOENT, Errno::ESRCH # someone else did our job for us end endend

Page 32: Practical Chef and Capistrano for Your Rails App

Capistrano - templates/unicorn.rb.erb

after_fork do |server, worker| # Start up the database connection again in the worker if defined?(ActiveRecord::Base) ActiveRecord::Base.establish_connection end child_pid = server.config[:pid].sub(".pid", ".#{worker.nr}.pid") system("echo #{Process.pid} > #{child_pid}")end

Page 33: Practical Chef and Capistrano for Your Rails App

Capistrano - t/monit/unicorn.erb

check process <%= application %>_unicorn with pidfile <%= unicorn_pid %> start program = "/etc/init.d/unicorn_<%= application %> start" stop program = "/etc/init.d/unicorn_<%= application %> stop"

<% unicorn_workers.times do |n| %> <% pid = unicorn_pid.sub(".pid", ".#{n}.pid") %> check process <%= application %>_unicorn_worker_<%= n %> with pidfile <%= pid %> start program = "/bin/true" stop program = "/usr/bin/test -s <%= pid %> && /bin/kill -QUIT `cat <%= pid %>`" if mem > 200.0 MB for 1 cycles then restart if cpu > 50% for 3 cycles then restart if 5 restarts within 5 cycles then timeout alert <%= alert_email %> only on { pid } if changed pid 2 times within 60 cycles then alert<% end %>

Page 34: Practical Chef and Capistrano for Your Rails App

Whoa!

Page 35: Practical Chef and Capistrano for Your Rails App

But really, it is just a bunch of Erb for files you

already have

Page 36: Practical Chef and Capistrano for Your Rails App

Did you see the trick?

● after "deploy:setup", "nginx:setup"

So we can...

● cap staging deploy:setup deploy:migrations

Page 37: Practical Chef and Capistrano for Your Rails App

From the top!

Page 38: Practical Chef and Capistrano for Your Rails App

Ready?!? Here we go!

1. New VM at my_web_app in your .ssh/config2. Create chef-repo/nodes/my_web_app.json3. In chef-repo:

bundle exec knife bootstrap node_name \ --template-file=ubuntu-12.04-lts.erb

4. bundle exec knife cook root@my_web_app5. In app directory:

create/edit config/deploy/staging.rb6. cap staging deploy:setup deploy:migrations7. Hit the bars

Page 39: Practical Chef and Capistrano for Your Rails App

Thoughts....● Vagrant and VMs are you friend. Rinse and repeat

● It is ok to tweak your Chef stuff and re-cook, but I always

like to restart with a fresh VM once I think I'm done

● Capistrano tweaks should be easy to apply, especially with

tasks like nginx:setup, unicorn:setup etc.

● Chef issues are harder to debug and more frustrating than

Capistrano issues, another reason to put more app specific

custom stuff in Capistrano and do standard things in Chef