In simple cases, if you use Heroku, application deployment process can be as easy as one shell command. But Heroku does not provide enough scaling and flexibility for more advanced scenarios or more serious load.
If you need to test something and then be able to expand to thousands of requests per second, EC2 from Amazon Web Services is definitely the way to go. It provides you with a virtual system which is totally under your control. You can add additional storage, move storage between servers and increase CPU/memory in almost real-time.
The downside, though, is that you have to setup the whole application infrastructure by yourself: from frontend servers to deployment scripts to security customizations. There is no preferable way of doing one thing or another, so here I’m offering what worked perfectly for me, and what I was not able to find while surfing the Internet for solutions.
Overview
The problem we’re solving is Rails app deployment on a remote Amazon EC2 instance with Capistrano.
I’m considering Capistrano as the core component of the solution. Capistrano is used for application deployment and is pretty much industry standard these days. You can find lots of deployment recipes for it on the Internet. With Capistrano we can deploy Rails applications on remote servers, pre-configure databases, precompile assets and do some other fancy stuff.
RVM is used to install the latest Ruby version on the machine no matter the packaging system used. It allows to keep multiple Ruby/Rails versions and enables easy switching between them.
Bundler is a part of Rails installation. Bundler offers very efficient gem version control and deployment system. It will be used with Capistrano to install the necessary gems on the target machine.
Unicorn is a high-performance Unix-like Rack application server. We will be using it as a backend system here. nginx acts as a frontend, but its configuration is out of scope for the current post.
Directory Structure & Users
The directory structure is important for the two reasons:
- It provides a skeleton for services configuration.
- It serves us to harden application security.
The user we will be deploying our app under is ec2-user (standard Amazon EC2 user). We will also be using sudo to drop current user credentials and start Rails application under rails user. rails user is not allowed to write in our app’s dir, which is a good additional security measure.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/var/rails `-- [drwxrwxr-x ec2-user ec2-user] myapp |-- [lrwxrwxrwx ec2-user ec2-user] current -> /var/rails/myapp/releases/20121007163131 |-- [drwxrwxr-x ec2-user ec2-user] releases | |-- [drwxrwxr-x ec2-user ec2-user] 20121007162712 | `-- [drwxrwxr-x ec2-user ec2-user] 20121007163131 `-- [drwxrwxr-x ec2-user ec2-user] shared |-- [drwxrwxr-x ec2-user ec2-user] assets |-- [drwxrwxr-x ec2-user ec2-user] bundle |-- [drwxr-xr-x ec2-user ec2-user] cached-copy |-- [drwxrwxr-x ec2-user rails ] log |-- [drwxrwxr-x ec2-user rails ] pids `-- [drwxrwxr-x ec2-user ec2-user] system |
This is the final directory structure after $ cap deploy:setup and $ cap deploy commands have been run. You can see custom permissions on log and pids folders, as we want to allow unicorn (after doing $ sudo -u rails) to be able to write into them.
Unicorn
The unicorn configuration is pretty typical:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# 4 workers is enough for our app worker_processes 4 # App location @app = "/var/rails/myapp/current" # Listen on fs socket for better performance listen "#{@app}/tmp/sockets/unicorn.sock", :backlog => 64 # Nuke workers after 30 seconds instead of 60 seconds (the default) timeout 30 # App PID pid "#{@app}/tmp/pids/unicorn.pid" # By default, the Unicorn logger will write to stderr. # Additionally, some applications/frameworks log to stderr or stdout, # so prevent them from going to /dev/null when daemonized here: stderr_path "#{@app}/log/unicorn.stderr.log" stdout_path "#{@app}/log/unicorn.stdout.log" # To save some memory and improve performance preload_app true GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true # Force the bundler gemfile environment variable to # reference the Сapistrano "current" symlink before_exec do |_| ENV["BUNDLE_GEMFILE"] = File.join(@app, 'Gemfile') end before_fork do |server, worker| # the following is highly recomended for Rails + "preload_app true" # as there's no need for the master process to hold a connection defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect! end after_fork do |server, worker| # the following is *required* for Rails + "preload_app true", defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection end |
Gemfile
|
1 2 3 4 5 6 7 8 9 10 11 |
source 'https://rubygems.org' ... # Unicorn Web Server gem 'unicorn' # Deploy with Capistrano gem 'capistrano' # Capistrano RVM integration gem 'rvm-capistrano' |
Capistrano
This is the most interesting part, as we glue all the components together:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# Automatically precompile assets load "deploy/assets" # Execute "bundle install" after deploy, but only when really needed require "bundler/capistrano" # RVM integration require "rvm/capistrano" # Application name set :application, "myapp" # Application environment set :rails_env, :production # Deploy username and sudo username set :user, "ec2-user" set :user_rails, "rails" # App Domain set :domain, "myapp.com" # We don't want to use sudo (root) - for security reasons set :use_sudo, false # Target ruby version set :rvm_ruby_string, '1.9.3' # System-wide RVM installation set :rvm_type, :system # We use sudo (root) for system-wide RVM installation set :rvm_install_with_sudo, true # git is our SCM set :scm, :git # Use github repository set :repository, "git://github.com/myuser/myapp.git" # master is our default git branch set :branch, "master" # Deploy via github set :deploy_via, :remote_cache set :deploy_to, "/var/rails/#{application}" # We have all components of the app on the same server server domain, :app, :web, :db, :primary => true # Install RVM and Ruby before deploy before "deploy:setup", "rvm:install_rvm" before "deploy:setup", "rvm:install_ruby" # Apply default RVM version for the current account after "deploy:setup", "deploy:set_rvm_version" # Fix log/ and pids/ permissions after "deploy:setup", "deploy:fix_setup_permissions" # Fix permissions before "deploy:start", "deploy:fix_permissions" after "deploy:restart", "deploy:fix_permissions" after "assets # Clean-up old releases after "deploy:restart", "deploy:cleanup" # Unicorn config set :unicorn_config, "#{current_path}/config/unicorn.conf.rb" set :unicorn_binary, "bash -c 'source /etc/profile.d/rvm.sh && bundle exec unicorn_rails -c #{unicorn_config} -E #{rails_env} -D'" set :unicorn_pid, "#{current_path}/tmp/pids/unicorn.pid" set :su_rails, "sudo -u #{user_rails}" namespace :deploy do task :start, :roles => :app, :except => { :no_release => true } do # Start unicorn server using sudo (rails) run "cd #{current_path} && #{su_rails} #{unicorn_binary}" end task :stop, :roles => :app, :except => { :no_release => true } do run "if [ -f #{unicorn_pid} ]; then #{su_rails} kill `cat #{unicorn_pid}`; fi" end task :graceful_stop, :roles => :app, :except => { :no_release => true } do run "if [ -f #{unicorn_pid} ]; then #{su_rails} kill -s QUIT `cat #{unicorn_pid}`; fi" end task :reload, :roles => :app, :except => { :no_release => true } do run "if [ -f #{unicorn_pid} ]; then #{su_rails} kill -s USR2 `cat #{unicorn_pid}`; fi" end task :restart, :roles => :app, :except => { :no_release => true } do stop start end task :set_rvm_version, :roles => :app, :except => { :no_release => true } do run "source /etc/profile.d/rvm.sh && rvm use #{rvm_ruby_string} --default" end task :fix_setup_permissions, :roles => :app, :except => { :no_release => true } do run "#{sudo} chgrp #{user_rails} #{shared_path}/log" run "#{sudo} chgrp #{user_rails} #{shared_path}/pids" end task :fix_permissions, :roles => :app, :except => { :no_release => true } do # To prevent access errors while moving/deleting run "#{sudo} chmod 775 #{current_path}/log" run "#{sudo} find #{current_path}/log/ -type f -exec chmod 664 {} \\;" run "#{sudo} find #{current_path}/log/ -exec chown #{user}:#{user_rails} {} \\;" run "#{sudo} find #{current_path}/tmp/ -type f -exec chmod 664 {} \\;" run "#{sudo} find #{current_path}/tmp/ -type d -exec chmod 775 {} \\;" run "#{sudo} find #{current_path}/tmp/ -exec chown #{user}:#{user_rails} {} \\;" end # Precompile assets only when needed namespace :assets do task :precompile, :roles => :web, :except => { :no_release => true } do # If this is our first deploy - don't check for the previous version if remote_file_exists?(current_path) from = source.next_revision(current_revision) if capture("cd #{latest_release} && #{source.local.log(from)} vendor/assets/ app/assets/ | wc -l").to_i > 0 run %Q{cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets else logger.info "Skipping asset pre-compilation because there were no asset changes" end else run %Q{cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets end end end end # Helper function def remote_file_exists?(full_path) 'true' == capture("if [ -e #{full_path} ]; then echo 'true'; fi").strip end |
The End
That’s pretty much it. I always believed that reading code is the best way to understand what the author wanted to say. So here you will find mostly real-world examples and scenarios, with little or no additional description. If you don’t like this format or finding a hard time understanding what I’m writing about, please feel free to give your feedback below.
Good luck and happy coding! ![]()
Thanks for the article. Will there be a part 2 for nginx setup?
Thanks, Andreas
Yes, I’ll cover nginx and unicorn integration with several additional performance tweaks the next week. So stay tuned!
I was expecting an actual EC2 demo.
What exactly did you want to see covered? I was using EC2 AMI Linux t.micro instance, which is pretty much like CentOS installed from scratch
I also was hoping to see EC2 instances be auto created when deployed with Capistrano. That would help with the complete end to end process, similar to how Heroku pushes out new instances.
You can take a look at Rubber EC2 tools which does what you’re asking for.
Although its a little more complex than plain capistrano (which it uses BTW).
One thing that you’ll find if you’re using USR2 signals with Unicorn for zero-downtime deploys is that Bundler doesn’t load your updated Gemfile. The environment variable gets set to the absolute path of your code instead of the capistrano symlinked directory.
You can fix this by adding a before_exec hook to your unicorn config. Here’s an example:
https://gist.github.com/3872434
Patrick, thanks for the tip! I’ve updated the unicorn config above.
Any time. There’s more about the issue here: http://unicorn.bogomips.org/Sandbox.html
I love the article! One question – servers get rebooted, and in this example as there’s no init script for Unicorn, it’ll need manual intervention.
Do you have any pointers to a sensible init script that can manage Unicorn? I’ve tried here but I just can’t get the darned thing working reliably.
Thanks, Peter!
As for init scripts, I think there is no “right” and universal way of doing it. For example, I’ve seen several init.d script implementations which had very complex logic inside them to dynamically find all Rails apps and try to start them (as root!) during the bootup.
I believe this approach is kind of unnecessary here. For my deployments, I typically just add one-liner in /etc/rc.local file:
Of course, adding this line can be just another step in $ cap deploy:setup Capistrano scenario.
Not very sexy, though, but pretty much reliable
Here’s what I generally use for Unicorn installations…
https://gist.github.com/3918349
You’ll obviously have to tweak it for your own setup, but it should be enough to get you started. Just be aware that this is setup for zero-downtime deploys.
Here’s the init.d script I use for most of my Unicorn deploys.
https://gist.github.com/3918349
I have a similar setup, but my assets compilation takes forever (upwards of 30m). I’ve been looking for a way to precompile them on my local machine (which isn’t throttled by Amazon), and found this(http://www.rostamizadeh.net/blog/2012/04/14/precompiling-assets-locally-for-capistrano-deployment/), but it fails at the SCP transfer with a nondescript error, like “SCP upload failed” (no reason, no error code). Anyone met this issue?
Hey Emile,
Could you please give a link to your Capistrano recipe?
Here’s some of our Capistrano Recipes we’ve been using with SCALR to manage our EC2 instances.
https://github.com/donnoman/scalr
https://github.com/rickrussell/cap-recipes
https://github.com/donnoman/cap-recipes
https://github.com/donnoman/cap-recipes/blob/master/lib/cap_recipes/tasks/nginx_unicorn/install.rb
We use god to manage nginx/unicorn/resque. In some instances we’ve needed Upstart to keep an eye on god, haha. But it works well for us. We’re in the process of moving to chef/cucumber-chef
We use these in combination with the SCALR API and Jenkins to do our deploys. All you would need is a seperate repo with stages and set overrides.
Thank you for sharing!
Cheers