Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deploying flinc with Capistrano

Deploying flinc with Capistrano

This presentation describes the deployment process of flinc.org and demonstrates several features of Capistrano by giving examples extracted from flinc's deploy scripts.

Benedikt Deicke

October 19, 2011
Tweet

More Decks by Benedikt Deicke

Other Decks in Programming

Transcript

  1. „Capistrano is a utility and framework for executing commands in

    parallel on multiple remote machines, via SSH. It uses a simple DSL (borrowed in part from Rake) that allows you to define tasks, which may be applied to machines in certain roles.“
  2. Servers and Roles server 'rails-1.flinc.org', :app, :web, :db, :primary =>

    true server 'rails-2.flinc.org', :app, :web server 'workers-1.flinc.org', :app, :worker server 'proxy.flinc.org', :proxy, :no_release => true
  3. Deployment configuration # Environments set :stages, ['staging', 'production'] set :default_stage,

    "staging" # Paths set :application, "flinc.org" set :deploy_to, "/var/www/#{application}" # User set :user, "deploy" set :use_sudo, false # Repository set :scm, :git set :repository, "[email protected]:flinc/flinc.git" set :deploy_via, :remote_cache capistrano-ext gem
  4. Task Example with Callback namespace :deploy do desc "[internal] Do

    something cool on the server" task :cool, :roles => :web do run <<-CMD echo "Cool!" CMD end end after "deploy:finalize_update", "deploy:cool"
  5. Restarting thin instances in two different ways namespace :thin do

    desc "[internal] Enables fast restarts for the current session" task :fast_restart { set(:thin_one_by_one, false) } [:start, :stop, :restart].each do |action| desc "[internal] Performs #{action} on all thin instances" task action, :roles => :web do config_path = fetch(:thin_config, "#{shared_path}/config/thin.yml") one_by_one = fetch(:thin_one_by_one, true) ? "-O" : "" bundler.run "thin #{action} -C #{config_path} #{one_by_one}" end end end before "deploy:migrations", "thin:fast_restart"
  6. Minimizing assets and pushing them before deploy namespace :assets do

    desc "Prepare assets, commit and push them." task :prepare do unless fetch(:skip_assets, false) git.ensure_clean_working_directory! git.on_branch(branch) do run_locally "bundle exec rake deploy:prepare" git.add "public/assets" git.commit "Prepared assets for deploy to #{rails_env}" git.push(git.current_branch) end end end end before "deploy:update_code", "assets:prepare" Runs command on local machine
  7. Tail the logs from all application servers namespace :logs do

    desc "Stream logs from current environment" task :tail, :except => { :no_release => true } do stream "tail -f #{current_path}/log/#{rails_env}.log" end end „Streams the result of the command from all servers that are the target of the current task.“
  8. namespace :rails do desc "Open the rails console on the

    primary server" task :console, :roles => :app, :primary => true do hostname = find_servers_for_task(current_task).first exec "ssh -l #{user} #{hostname} -t 'source ~/.profile && rvm use #{rvm_ruby_string} && #{current_path}/script/rails c #{rails_env}'" end end Open the rails console on the primary server „Identifies all servers that the given task should be executed on.“
  9. Checking the server setup with cap deploy:check # Depend on

    remote commands depend :remote, :command, 'convert' # Depend on remote debian packages depend :remote, :deb, 'zlib1g-dev', '' # Depend on remote directories depend :remote, :directory, "#{shared_path}/config", :roles => :app # Depend on remote files depend :remote, :file, '/etc/init.d/thin', :roles => :web # Depend on remote gems depend :remote, :gem, 'bundler', '>= 1.0.15'
  10. Setting up ~/.ssh/known_hosts on cap deploy:setup namespace :ssh do task

    :setup_known_hosts, :except => { :no_release => true } do known_hosts = '~/.ssh/known_hosts' run "mkdir -p #{File.dirname(known_hosts)}" run "if [ ! -f #{known_hosts} ]; then touch #{known_hosts}; fi" template.read("known_hosts").split(/$/).each do |line| next unless host = line.split(" ").first run <<-CMD if grep -q '#{host}' #{known_hosts}; then echo 'Host #{host} is already added.'; else echo '#{line.strip}' >> #{known_hosts}; echo 'Host #{host} added.'; fi CMD end end end after 'deploy:setup', 'ssh:setup_known_hosts'
  11. Setup the database.yml using templates namespace :database do desc "Creates

    the database.yml configuration file in shared path." task :setup, :except => { :no_release => true } do template.upload "database.yml.erb", "#{shared_path}/config/database.yml", :binding => binding end desc "[internal] Updates the symlink for database.yml file to the just deployed release." task :symlink, :except => { :no_release => true} do run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml" end end <%= rails_env %>: adapter: <%= fetch(:database_adapter, 'postgresql') %> encoding: <%= fetch(:database_encoding, 'utf8') %> reconnect: <%= fetch(:database_reconnect, 'false') %> database: <%= fetch(:database_name, "#{application}_#{rails_env}") %> pool: <%= fetch(:database_pool, 5) %> username: <%= fetch(:database_username, user) %> password: <%= Capistrano::CLI.password_prompt("Enter database password: ") %> host: <%= fetch(:database_host, '') %> port: <%= fetch(:database_port, '5432') %> Prompts the user for a password, so we don‘t need to check it into version control
  12. Enabling RVM support in your deploy scripts if ENV['rvm_path'] $:.unshift(File.expand_path('./lib',

    ENV['rvm_path'])) require 'rvm/capistrano' else raise 'You need a RVM environment to use the deploy scripts' end # Ruby Version Manager set :rvm_ruby_string, 'ruby-1.9.2-p180' set :rvm_type, :user
  13. Installing a new ruby version on the servers namespace :rvm

    do desc "Install the given version of rvm" task :install, :except => { :no_release => true } do ruby = fetch(:ruby_version, fetch(:rvm_ruby_string, '1.8.7')) old_shell = default_shell set :default_shell, "/bin/bash" run "rvm install #{ruby}" set :rvm_ruby_string, ruby set :default_shell, old_shell rvm.setup end end
  14. Bootstrapping a new ruby version namespace :rvm do desc "Sets

    up everything required to smoothly run the given ruby version" task :setup, :except => { :no_release => true } do run "rvm #{rvm_ruby_string} exec gem install bundler" run "rvm #{rvm_ruby_string} exec gem install god" run "rvm wrapper #{rvm_ruby_string} current bundle" run "rvm wrapper #{rvm_ruby_string} current god" bundle.install run "if [ -f /etc/init.d/god ]; then sudo /etc/init.d/god restart; fi;" end end
  15. Updating RVM and other useful related task namespace :rvm do

    desc "Updates rvm to the latest version" task :update, :except => { :no_release => true } do run "rvm get latest" run "rvm reload" end desc "Output RVM info on all servers" task :info, :except => { :no_release => true } do run "rvm info" end desc "Output the current ruby version on all servers" task :current, :except => { :no_release => true } do run "rvm current" end desc "Output the list of installed rubies on all servers" task :list, :except => { :no_release => true } do run "rvm list" end end
  16. Disabling the website by showing a maintenance page namespace :deploy

    do namespace :web do task :disable, :roles => :proxy do on_rollback { rm "#{shared_path}/system/maintenance.html" } deadline, reason = ENV['UNTIL'], ENV['REASON'] template.upload "maintenance.erb", "#{shared_path}/system/maintenance.html", :mode => 0644, :binding => binding end desc "Enables the application by removing the maintenance.html page" task :enable, :roles => :proxy do run "rm #{shared_path}/system/maintenance.html" end end end
  17. Preventing you from doing stupid stuff namespace :deploy do namespace

    :web do desc "[internal] Ensures that web is currently disabled" task :ensure, :roles => :proxy do begin run "test -f #{shared_path}/system/maintenance.html" rescue Capistrano::CommandError unless fetch(:force, false) logger.important "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" logger.important "Disable the website (deploy:web:disable) before doing this" logger.important "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" exit end end end end before 'deploy:migrations', 'deploy:web:ensure' before 'deploy:migrate', 'deploy:web:ensure'
  18. Automatically tagging deploys in Git namespace :deploy do namespace :tag

    do task :default do git.ensure_clean_working_directory! git.fetch("--tags") git.on_branch(branch) do git.tag(git.next_tag) git.tag("-d #{rails_env}-latest") git.push(":refs/tags/#{rails_env}-latest") git.tag("#{rails_env}-latest #{git.last_tag}") git.push("--tags") end end end end after "deploy:update_code", "deploy:tag"
  19. A simple Capistrano plugin to run commands with bundle exec

    module Deploy module Plugins module Bundler def run(command) top.run "BUNDLE_GEMFILE=#{current_path}/Gemfile bundle exec #{command}" end end end end Capistrano.plugin :bundler, Deploy::Plugins::Bundler
  20. Capistrano plugin to evaluate and upload templates module Deploy module

    Plugins module Templates def upload(template, destination, options = {}) put read(template, options.delete(:binding)), destination, options end def write(template, destination, options = {}) File.open(destination, "w") { |file| file.puts read(template, options.delete(:binding)) } end def read(template, binding = binding) path = File.join(File.dirname(__FILE__), '..', 'templates', template) template_data = IO.read(path) if File.extname(template) == ".erb" template_data = ERB.new(template_data).result(binding) end template_data end end end end Capistrano.plugin :template, Deploy::Plugins::Templates
  21. Parts of our git plugin for capistrano module Deploy module

    Plugins module Git def commit(message) run_locally "git commit -m '#{message}'" end def checkout(branch) run_locally "git checkout #{branch}" end def current_branch `git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e "s/* \\(.*\\)/\\1/"` end def on_branch(branch) old_branch = current_branch checkout(branch) yield checkout(old_branch) end end end end Capistrano.plugin :git, Deploy::Plugins::Git