diff --git a/deploy/attributes/deploy.rb b/deploy/attributes/deploy.rb index 8a40f89d79..de147cab78 100644 --- a/deploy/attributes/deploy.rb +++ b/deploy/attributes/deploy.rb @@ -41,7 +41,7 @@ when 'debian','ubuntu' default[:opsworks][:deploy_user][:group] = 'www-data' when 'centos','redhat','fedora','amazon' - default[:opsworks][:deploy_user][:group] = node['opsworks']['rails_stack']['name'] == 'nginx_unicorn' ? 'nginx' : 'apache' + default[:opsworks][:deploy_user][:group] = node['opsworks']['rails_stack']['name'].include?('nginx') ? 'nginx' : 'apache' end default[:opsworks][:rails][:ignore_bundler_groups] = ['test', 'development'] diff --git a/deploy/attributes/rails_stack.rb b/deploy/attributes/rails_stack.rb index 40d761e749..bbaa230119 100644 --- a/deploy/attributes/rails_stack.rb +++ b/deploy/attributes/rails_stack.rb @@ -26,6 +26,11 @@ normal[:opsworks][:rails_stack][:needs_reload] = true normal[:opsworks][:rails_stack][:service] = 'unicorn' normal[:opsworks][:rails_stack][:restart_command] = '../../shared/scripts/unicorn clean-restart' +when "nginx_puma" + normal[:opsworks][:rails_stack][:recipe] = "puma::rails" + normal[:opsworks][:rails_stack][:needs_reload] = true + normal[:opsworks][:rails_stack][:service] = 'puma' + normal[:opsworks][:rails_stack][:restart_command] = '../../shared/scripts/puma clean-restart' else raise "Unknown stack: #{node[:opsworks][:rails_stack][:name].inspect}" end diff --git a/deploy/definitions/opsworks_deploy.rb b/deploy/definitions/opsworks_deploy.rb index c4c8812103..146fa5332c 100644 --- a/deploy/definitions/opsworks_deploy.rb +++ b/deploy/definitions/opsworks_deploy.rb @@ -177,6 +177,12 @@ deploy deploy end + when 'nginx_puma' + puma_web_app do + application application + deploy deploy + end + else raise "Unsupport Rails stack" end diff --git a/deploy/metadata.rb b/deploy/metadata.rb index 484127a62b..6fc897bb57 100644 --- a/deploy/metadata.rb +++ b/deploy/metadata.rb @@ -13,6 +13,7 @@ depends "opsworks_agent_monit" depends "passenger_apache2" depends "unicorn" +depends "puma" depends "opsworks_java" depends "php" depends "mysql" diff --git a/deploy/recipes/rails-undeploy.rb b/deploy/recipes/rails-undeploy.rb index 5e57cb7633..283d72e023 100644 --- a/deploy/recipes/rails-undeploy.rb +++ b/deploy/recipes/rails-undeploy.rb @@ -57,6 +57,31 @@ action :run end + when 'nginx_puma' + include_recipe 'nginx::service' + + link "/etc/nginx/sites-enabled/#{application}" do + action :delete + only_if do + ::File.exists?("/etc/nginx/sites-enabled/#{application}") + end + notifies :restart, "service[nginx]" + end + + file "/etc/nginx/sites-available/#{application}" do + action :delete + only_if do + ::File.exists?("/etc/nginx/sites-available/#{application}") + end + end + + execute 'stop puma and restart nginx' do + command "sleep #{deploy[:sleep_before_restart]} && \ + /srv/www/#{application}/shared/scripts/puma stop" + notifies :restart, "service[nginx]" + action :run + end + else raise 'Unsupported Rails stack' end diff --git a/opsworks_ganglia/recipes/client.rb b/opsworks_ganglia/recipes/client.rb index 67befe8d0c..a14c4a1a6c 100644 --- a/opsworks_ganglia/recipes/client.rb +++ b/opsworks_ganglia/recipes/client.rb @@ -111,6 +111,8 @@ include_recipe 'opsworks_ganglia::monitor-apache' when 'nginx_unicorn' include_recipe 'opsworks_ganglia::monitor-nginx' + when 'nginx_puma' + include_recipe 'opsworks_ganglia::monitor-nginx' end end diff --git a/opsworks_ganglia/templates/default/conf.php.erb b/opsworks_ganglia/templates/default/conf.php.erb index 3fc12dc502..f475e6e42d 100644 --- a/opsworks_ganglia/templates/default/conf.php.erb +++ b/opsworks_ganglia/templates/default/conf.php.erb @@ -17,7 +17,7 @@ $conf['optional_graphs'] = array(); <% if (node[:opsworks][:layers].has_key?("rails-app") && node[:opsworks][:rails_stack][:name] == 'apache_passenger' && !node[:opsworks][:layers]['rails-app']['instances'].empty? ) -%> array_push($conf['optional_graphs'], 'passenger_memory_stats', 'passenger_status'); <% end -%> -<% if node[:opsworks][:layers].has_key?("rails-app") && node[:opsworks][:rails_stack][:name] == 'nginx_unicorn' && !node[:opsworks][:layers]['rails-app']['instances'].empty? -%> +<% if node[:opsworks][:layers].has_key?("rails-app") && node[:opsworks][:rails_stack][:name].include?('nginx') && !node[:opsworks][:layers]['rails-app']['instances'].empty? -%> array_push($conf['optional_graphs'], 'nginx_status'); <% end -%> <% if node[:opsworks][:layers].has_key?("web") && !node[:opsworks][:layers]['web']['instances'].empty? -%> diff --git a/opsworks_ganglia/templates/default/host_view_json.erb b/opsworks_ganglia/templates/default/host_view_json.erb index c4630c0735..43205c0d99 100644 --- a/opsworks_ganglia/templates/default/host_view_json.erb +++ b/opsworks_ganglia/templates/default/host_view_json.erb @@ -15,7 +15,7 @@ if node[:opsworks][:rails_stack][:name] == 'apache_passenger' reports << 'passenger_memory_stats_report' reports << 'passenger_status_report' - elsif node[:opsworks][:rails_stack][:name] == 'nginx_unicorn' + elsif node[:opsworks][:rails_stack][:name].include?('nginx') reports << 'nginx_status_report' else end diff --git a/passenger_apache2/metadata.rb b/passenger_apache2/metadata.rb index f49e39970e..2c285398d4 100644 --- a/passenger_apache2/metadata.rb +++ b/passenger_apache2/metadata.rb @@ -5,7 +5,7 @@ license "Apache 2.0" version "1.0.0" -%w{ packages gem_support apache2 nginx unicorn rails opsworks_initial_setup }.each do |cb| +%w{ packages gem_support apache2 nginx unicorn puma rails opsworks_initial_setup }.each do |cb| depends cb end diff --git a/passenger_apache2/recipes/rails.rb b/passenger_apache2/recipes/rails.rb index 8b36a429dc..418e5a8dd5 100644 --- a/passenger_apache2/recipes/rails.rb +++ b/passenger_apache2/recipes/rails.rb @@ -1,6 +1,7 @@ unless node[:opsworks][:skip_uninstall_of_other_rails_stack] include_recipe "nginx::uninstall" include_recipe "unicorn::stop" + include_recipe "puma::stop" end include_recipe "apache2" diff --git a/puma/attributes/customize.rb b/puma/attributes/customize.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/puma/attributes/default.rb b/puma/attributes/default.rb new file mode 100644 index 0000000000..f708882534 --- /dev/null +++ b/puma/attributes/default.rb @@ -0,0 +1,28 @@ +### +# Do not use this file to override the puma cookbook's default +# attributes. Instead, please use the customize.rb attributes file, +# which will keep your adjustments separate from the AWS OpsWorks +# codebase and make it easier to upgrade. +# +# However, you should not edit customize.rb directly. Instead, create +# "puma/attributes/customize.rb" in your cookbook repository and +# put the overrides in YOUR customize.rb file. +# +# Do NOT create an 'puma/attributes/default.rb' in your cookbooks. Doing so +# would completely override this file and might cause upgrade issues. +# +# See also: http://docs.aws.amazon.com/opsworks/latest/userguide/customizing.html +### + +include_attribute 'rails::rails' + +# Usually this should be a dynamic value but we are using threads in order to scale properly +default[:puma][:workers] = 4 +default[:puma][:preload_app] = true +default[:puma][:version] = '2.11.3' +default[:puma][:threads_min] = node[:rails][:max_pool_size] ? node[:rails][:max_pool_size]/8 : 1 +default[:puma][:threads_max] = node[:rails][:max_pool_size] ? node[:rails][:max_pool_size]/4 : 4 + +include_attribute "puma::customize" + + diff --git a/puma/definitions/puma_web_app.rb b/puma/definitions/puma_web_app.rb new file mode 100644 index 0000000000..617977c5e3 --- /dev/null +++ b/puma/definitions/puma_web_app.rb @@ -0,0 +1,17 @@ +define :puma_web_app do + deploy = params[:deploy] + application = params[:application] + + nginx_web_app deploy[:application] do + docroot deploy[:absolute_document_root] + server_name deploy[:domains].first + server_aliases deploy[:domains][1, deploy[:domains].size] unless deploy[:domains][1, deploy[:domains].size].empty? + rails_env deploy[:rails_env] + mounted_at deploy[:mounted_at] + ssl_certificate_ca deploy[:ssl_certificate_ca] + cookbook 'puma' + deploy deploy + template "nginx_puma_web_app.erb" + application deploy + end +end \ No newline at end of file diff --git a/puma/metadata.rb b/puma/metadata.rb new file mode 100644 index 0000000000..ffcf437863 --- /dev/null +++ b/puma/metadata.rb @@ -0,0 +1,9 @@ +name "puma" +description "Manage puma" +maintainer "AWS OpsWorks" +license "Apache 2.0" +version "1.0.0" + +depends "apache2" +depends "nginx" +depends "rails" \ No newline at end of file diff --git a/puma/recipes/default.rb b/puma/recipes/default.rb new file mode 100644 index 0000000000..fc6e16a9f9 --- /dev/null +++ b/puma/recipes/default.rb @@ -0,0 +1,5 @@ +ruby_block "ensure only our puma version is installed by deinstalling any other version" do + block do + ensure_only_gem_version('puma', node[:puma][:version]) + end +end \ No newline at end of file diff --git a/puma/recipes/rails.rb b/puma/recipes/rails.rb new file mode 100644 index 0000000000..11342438c8 --- /dev/null +++ b/puma/recipes/rails.rb @@ -0,0 +1,48 @@ +unless node[:opsworks][:skip_uninstall_of_other_rails_stack] + include_recipe "apache2::uninstall" +end + +include_recipe 'nginx' +include_recipe 'puma' + +# setup Unicorn service per app +node[:deploy].each do |application, deploy| + if deploy[:application_type] != 'rails' + Chef::Log.debug("Skipping puma::rails application #{application} as it is not an Rails app") + next + end + + opsworks_deploy_user do + deploy_data deploy + end + + opsworks_deploy_dir do + user deploy[:user] + group deploy[:group] + path deploy[:deploy_to] + end + + template "#{deploy[:deploy_to]}/shared/scripts/puma" do + mode '0755' + owner deploy[:user] + group deploy[:group] + source 'puma.service.erb' + variables(:deploy => deploy, :application => application) + end + + service "puma_#{application}" do + start_command "#{deploy[:deploy_to]}/shared/scripts/puma start" + stop_command "#{deploy[:deploy_to]}/shared/scripts/puma stop" + restart_command "#{deploy[:deploy_to]}/shared/scripts/puma restart" + status_command "#{deploy[:deploy_to]}/shared/scripts/puma status" + action :nothing + end + + template "#{deploy[:deploy_to]}/shared/config/puma.conf" do + mode '0644' + owner deploy[:user] + group deploy[:group] + source 'puma.conf.erb' + variables(:deploy => deploy, :application => application) + end +end \ No newline at end of file diff --git a/puma/recipes/stop.rb b/puma/recipes/stop.rb new file mode 100644 index 0000000000..9f83f1df8c --- /dev/null +++ b/puma/recipes/stop.rb @@ -0,0 +1,14 @@ +# stop Puma service per app +node[:deploy].each do |application, deploy| + if deploy[:application_type] != 'rails' + Chef::Log.debug("Skipping puma::rails application #{application} as it is not an Rails app") + next + end + + execute "stop puma" do + command "#{deploy[:deploy_to]}/shared/scripts/puma stop" + only_if do + File.exists?("#{deploy[:deploy_to]}/shared/scripts/puma") + end + end +end \ No newline at end of file diff --git a/puma/specs/default_spec.rb b/puma/specs/default_spec.rb new file mode 100644 index 0000000000..9d18ec0302 --- /dev/null +++ b/puma/specs/default_spec.rb @@ -0,0 +1,10 @@ +require 'minitest/spec' + +describe_recipe 'puma::default' do + include MiniTest::Chef::Resources + include MiniTest::Chef::Assertions + + it 'installs puma' do + assert system("#{node[:dependencies][:gem_binary]} list | grep puma | grep '#{node[:puma][:version]}'") + end +end \ No newline at end of file diff --git a/puma/specs/rails_spec.rb b/puma/specs/rails_spec.rb new file mode 100644 index 0000000000..e2859a89e1 --- /dev/null +++ b/puma/specs/rails_spec.rb @@ -0,0 +1,7 @@ +require 'minitest/spec' + +describe_recipe 'puma::rails' do + include MiniTest::Chef::Resources + include MiniTest::Chef::Assertions + +end \ No newline at end of file diff --git a/puma/specs/stop_spec.rb b/puma/specs/stop_spec.rb new file mode 100644 index 0000000000..0b135bdb4e --- /dev/null +++ b/puma/specs/stop_spec.rb @@ -0,0 +1,7 @@ +require 'minitest/spec' + +describe_recipe 'puma::stop' do + include MiniTest::Chef::Resources + include MiniTest::Chef::Assertions + +end \ No newline at end of file diff --git a/puma/templates/default/nginx_puma_web_app.erb b/puma/templates/default/nginx_puma_web_app.erb new file mode 100644 index 0000000000..71ed93a9ac --- /dev/null +++ b/puma/templates/default/nginx_puma_web_app.erb @@ -0,0 +1,80 @@ +upstream puma_<%= @application[:domains].first %> { + server unix:<%= @application[:deploy_to]%>/shared/sockets/puma.sock fail_timeout=0; +} + +server { + listen 80; + server_name <%= @application[:domains].join(" ") %> <%= node[:hostname] %>; + access_log <%= node[:nginx][:log_dir] %>/<%= @application[:domains].first %>.access.log; + root <%= @application[:absolute_document_root] %>; + + keepalive_timeout 5; + + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + deny all; + } + + <% if @application[:nginx] && @application[:nginx][:client_max_body_size] %> + client_max_body_size <%= @application[:nginx][:client_max_body_size] %>; + <% end %> + + location / { + try_files $uri @puma; + } + + location @puma { + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://puma_<%= @application[:domains].first %>; + } + + error_page 500 502 503 504 /500.html; + location = /500.html { + root <%= @application[:absolute_document_root] %>; + } + +} + +<% if @application[:ssl_support] %> +server { + listen 443; + server_name <%= @application[:domains].join(" ") %> <%= node[:hostname] %>; + access_log <%= node[:nginx][:log_dir] %>/<%= @application[:domains].first %>-ssl.access.log; + root <%= @application[:absolute_document_root] %>; + + keepalive_timeout 5; + + ssl on; + ssl_certificate /etc/nginx/ssl/<%= @application[:domains].first %>.crt; + ssl_certificate_key /etc/nginx/ssl/<%= @application[:domains].first %>.key; + <% if @application[:ssl_certificate_ca] -%> + ssl_client_certificate /etc/nginx/ssl/<%= @application[:domains].first %>.ca; + <% end -%> + + <% if @application[:nginx] && @application[:nginx][:client_max_body_size] %> + client_max_body_size <%= @application[:nginx][:client_max_body_size] %>; + <% end %> + + location / { + try_files $uri @puma; + } + + location @puma { + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://puma_<%= @application[:domains].first %>; + } + + error_page 500 502 503 504 /500.html; + location = /500.html { + root <%= @application[:absolute_document_root] %>; + } +} +<% end %> diff --git a/puma/templates/default/puma.conf.erb b/puma/templates/default/puma.conf.erb new file mode 100644 index 0000000000..6af7f3eb14 --- /dev/null +++ b/puma/templates/default/puma.conf.erb @@ -0,0 +1,50 @@ +workers <%= node[:puma][:workers] %> + +# We want puma to daemonize +daemonize + +# The working directory should be the current release's directory +directory "<%= @deploy[:deploy_to] %>/current" + +# Put the PID file into the shared directory +pidfile "<%= @deploy[:deploy_to] %>/shared/pids/puma.pid" + +# Redirect stdout, stderr in append mode (last param) +stdout_redirect "<%= @deploy[:deploy_to] %>/shared/log/puma.stderr.log", + "<%= @deploy[:deploy_to] %>/shared/log/puma.stdout.log", + true + +# We don't want to log every single request, nginx should do this for us +quiet + +# Min and max threads to use within puma +threads <%= node[:puma][:threads_min] %>, <%= node[:puma][:threads_max] %> + +rackup "<%= @deploy[:deploy_to] %>/current/config.ru" +environment "<%= @deploy[:rails_env] %>" + +# Bind to a unix socket without umask definitions +bind "unix://<%= @deploy[:deploy_to] %>/shared/sockets/puma.sock" + +# Make use of the advanced GC in 2.0 (?) +GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=) + +<% # Only preload the app if we want it to %> +<% if node[:puma][:preload_app] %> +preload_app! +<% end %> + +on_worker_boot do + # the following is highly recomended for Rails + "preload_app true" + # as there's no need for the master process to hold a connection + if defined?(ActiveRecord::Base) + ActiveRecord::Base.connection.disconnect! + end +end + +after_worker_boot do + # the following is *required* for Rails + "preload_app true", + if defined?(ActiveRecord::Base) + ActiveRecord::Base.establish_connection + end +end diff --git a/puma/templates/default/puma.service.erb b/puma/templates/default/puma.service.erb new file mode 100644 index 0000000000..4ccfeaf877 --- /dev/null +++ b/puma/templates/default/puma.service.erb @@ -0,0 +1,109 @@ +#!/usr/bin/ruby + +require 'etc' +require 'digest/md5' + +ROOT_PATH="<%= @deploy[:deploy_to] %>" +APP_NAME="<%= @application %>" +PID_PATH="<%= @deploy[:deploy_to] %>/shared/pids/puma.pid" + +def run_and_print_command(command) + puts command + system(command) || exit(1) +end + +def run_and_ignore_exitcode_and_print_command(command) + puts command + system(command) +end + +def puma_running? + if File.exists?(PID_PATH) && (pid = File.read(PID_PATH).chomp) && system("ps aux | grep #{pid} | grep -v grep > /dev/null") + pid + else + false + end +end + +def different_gemfile? + if File.exists?("#{ROOT_PATH}/current/Gemfile") + dir = Dir["#{ROOT_PATH}/releases/*"] + previous_release_path = dir.sort[dir.size-2] + if !previous_release_path.nil? && File.exists?("#{previous_release_path}/Gemfile") + return Digest::MD5.hexdigest(File.read("#{ROOT_PATH}/current/Gemfile")) != Digest::MD5.hexdigest(File.read("#{previous_release_path}/Gemfile")) + end + end + false +end + +def start_puma + if File.exists?("#{ROOT_PATH}/current/Gemfile") + puts "OpsWorks: Gemfile detected - running Puma with bundle exec" + run_and_ignore_exitcode_and_print_command "cd #{ROOT_PATH}/current && /usr/local/bin/bundle exec puma -C #{ROOT_PATH}/shared/config/puma.conf" + else + puts "OpsWorks: no Gemfile detected - running plain Puma" + run_and_ignore_exitcode_and_print_command "cd #{ROOT_PATH}/current && puma -C #{ROOT_PATH}/shared/config/puma.conf" + end +end + +def stop_puma + if puma_running? + if run_and_ignore_exitcode_and_print_command "kill -TERM `cat #{PID_PATH}`" + `rm #{PID_PATH}` + end + else + puts "You can't stop puma, because it's not running" + end +end + +def restart_puma + if puma_running? + run_and_ignore_exitcode_and_print_command "kill -USR2 `cat #{PID_PATH}`" + else + start_puma + end +end + +def clean_restart + if different_gemfile? + puts "Found a previous version with a different Gemfile: Doing a stop & start" + stop_puma if puma_running? + start_puma + else + puts "No previous version with a different Gemfile found. Assuming a quick restart without re-loading gems is save" + restart_puma + end +end + +def status_puma + if pid = puma_running? + puts "Puma <%= @application %> running with PID #{pid}" + return true + else + puts "Puma <%= @application %> not running" + return false + end +end + +Process::Sys.setuid(uid = Etc.getpwnam(<%= @deploy[:user].inspect %>).uid) +puts "Set Puma process UID to #{uid}" + +case ARGV[0] +when "start" + puts "Starting Puma #{APP_NAME}" + start_puma +when "stop" + puts "Stopping Puma #{APP_NAME}" + stop_puma +when "status" + status_puma +when "restart" + restart_puma +when "clean-restart" + clean_restart +else + puts "Usage: {start|stop|status|restart|clean-restart}" + exit 1 +end + +exit 0