13

OpsWorks isn't precompiling assets on deploy. I found this recipe in this thread but I think it's not complete though, or missing something because I get an error about release_path not being found.

precompile.rb:

Chef::Log.info("Running deploy/before_migrate.rb...")

Chef::Log.info("Symlinking #{release_path}/public/assets to #{new_resource.deploy_to}/shared/assets")

link "#{release_path}/public/assets" do
  to "#{new_resource.deploy_to}/shared/assets"
end

rails_env = new_resource.environment["RAILS_ENV"]
Chef::Log.info("Precompiling assets for RAILS_ENV=#{rails_env}...")

execute "rake assets:precompile" do
  cwd release_path
  command "bundle exec rake assets:precompile"
  environment "RAILS_ENV" => rails_env
end

logs:

undefined local variable or method `release_path' for ....

Any ideas? I do not know Chef at all and am trying to figure this out on the fly.

manafire
  • 5,984
  • 4
  • 43
  • 53
  • It looks like you have to provide the release_path, the path where your Rails application should reside on the deisgnation host. – cmur2 Jun 11 '13 at 21:33
  • This changes every time you deploy the app. It's dynamic so I can't hard code it in. – manafire Jun 11 '13 at 21:34

4 Answers4

10

Before OpsWorks supports the Asset Pipeline out of the box, you could do this. Create a file deploy/before_symlink.rb with the following content in your rails application.

run "cd #{release_path} && RAILS_ENV=production bundle exec rake assets:precompile"

If you deploy your Rails application to a different environment, change the RAILS_ENV.

If you use a NGINX/Unicorn stack, you have to modify the /assets resource. Just copy the following content in a file named unicorn/templates/default/nginx_unicorn_web_app.erb in your cookbooks.

upstream unicorn_<%= @application[:domains].first %> {
 server unix:<%= @application[:deploy_to]%>/shared/sockets/unicorn.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;

  keepalive_timeout 5;

  root <%= @application[:absolute_document_root] %>;

  <% if @application[:nginx] && @application[:nginx][:client_max_body_size] %>
    client_max_body_size <%= @application[:nginx][:client_max_body_size] %>;
  <% end %>

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;

    # If you don't find the filename in the static files
    # Then request it from the unicorn server
    if (!-f $request_filename) {
      proxy_pass http://unicorn_<%= @application[:domains].first %>;
      break;
    }
  }

  location /nginx_status {
    stub_status on;
    access_log off;
    allow 127.0.0.1;
    deny all;
  }

  location ~ ^/assets/ {
    expires 1y;
    add_header Cache-Control public;

    add_header ETag "";
    break;
  }

  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;

  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 -%>

  keepalive_timeout 5;

  root <%= @application[:absolute_document_root] %>;

  <% if @application[:nginx] && @application[:nginx][:client_max_body_size] %>
    client_max_body_size <%= @application[:nginx][:client_max_body_size] %>;
  <% end %>

  location / {
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;

    # If you don't find the filename in the static files
    # Then request it from the unicorn server
    if (!-f $request_filename) {
      proxy_pass http://unicorn_<%= @application[:domains].first %>;
      break;
    }
  }

  location ~ ^/assets/ {
    expires 1y;
    add_header Cache-Control public;

    add_header ETag "";
    break;
  }

  error_page 500 502 503 504 /500.html;
  location = /500.html {
    root <%= @application[:absolute_document_root] %>;
  }
}
<% end %>

If you use a Apache2/Passenger stack, you have to modify the /assets resource. Just copy the following content in a file named passenger_apache2/templates/default/web_app.conf.erb in your cookbooks.

<VirtualHost *:80>
  ServerName <%= @params[:server_name] %>
  <% if @params[:server_aliases] && !@params[:server_aliases].empty? -%>
  ServerAlias <% @params[:server_aliases].each do |a| %><%= "#{a}" %> <% end %>
  <% end -%>

  <% if @params[:mounted_at] -%>
  DocumentRoot /var/www
  <%= @params[:deploy][:passenger_handler] -%>BaseURI <%= @params[:mounted_at] %>
  <% else -%>
  DocumentRoot <%= @params[:docroot] %>
  <%= @params[:deploy][:passenger_handler] -%>BaseURI /
  <% end -%>
  <%= @params[:deploy][:passenger_handler] -%>Env <%= @params[:rails_env] %>

  <Directory <%= @params[:docroot] %>>
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
  </Directory>

  <Directory ~ "\.svn">
    Order allow,deny
    Deny from all
  </Directory>

  <Directory ~ "\.git">
    Order allow,deny
    Deny from all
  </Directory>

  <LocationMatch "^/assets/.*$">
    Header unset ETag
    FileETag None
    # RFC says only cache for 1 year
    ExpiresActive On
    ExpiresDefault "access plus 1 year"
  </LocationMatch>

  LogLevel info
  ErrorLog <%= node[:apache][:log_dir] %>/<%= @params[:name] %>-error.log
  CustomLog <%= node[:apache][:log_dir] %>/<%= @params[:name] %>-access.log combined
  CustomLog <%= node[:apache][:log_dir] %>/<%= @params[:name] %>-ganglia.log ganglia

  FileETag none

  RewriteEngine On
  Include <%= @params[:rewrite_config] %>*
  RewriteLog <%= node[:apache][:log_dir] %>/<%= @application_name %>-rewrite.log
  RewriteLogLevel 0

  # Canonical host
  #RewriteCond %{HTTP_HOST}   !^<%= @params[:server_name] %> [NC]
  #RewriteCond %{HTTP_HOST}   !^$
  #RewriteRule ^/(.*)$        http://<%= @params[:server_name] %>/$1 [L,R=301]

  RewriteCond %{REQUEST_URI} !\.(css|gif|jpg|jpeg|png)$
  RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
  RewriteCond %{SCRIPT_FILENAME} !maintenance.html
  RewriteRule ^.*$ /system/maintenance.html [L]

  Include <%= @params[:local_config] %>*
</VirtualHost>

<% if node[:deploy][@application_name][:ssl_support] -%>
<VirtualHost *:443>
  ServerName <%= @params[:server_name] %>
  <% if @params[:server_aliases] && !@params[:server_aliases].empty? -%>
  ServerAlias <% @params[:server_aliases].each do |a| %><%= "#{a}" %> <% end %>
  <% end -%>

  SSLEngine on
  SSLProxyEngine on
  SSLCertificateFile <%= node[:apache][:dir] %>/ssl/<%= @params[:server_name] %>.crt
  SSLCertificateKeyFile <%= node[:apache][:dir] %>/ssl/<%= @params[:server_name] %>.key
  <% if @params[:ssl_certificate_ca] -%>
  SSLCACertificateFile <%= node[:apache][:dir] %>/ssl/<%= @params[:server_name] %>.ca
  <% end -%>
  SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0

  <% if @params[:mounted_at] -%>
  DocumentRoot /var/www
  <%= @params[:deploy][:passenger_handler] -%>BaseURI <%= @params[:mounted_at] %>
  <% else -%>
  DocumentRoot <%= @params[:docroot] %>
  <%= @params[:deploy][:passenger_handler] -%>BaseURI /
  <% end -%>
  <%= @params[:deploy][:passenger_handler] -%>Env <%= @params[:rails_env] %>

  <Directory <%= @params[:docroot] %>>
    Options FollowSymLinks
    AllowOverride All
    Order allow,deny
    Allow from all
  </Directory>

  <Directory ~ "\.svn">
    Order allow,deny
    Deny from all
  </Directory>

  <Directory ~ "\.git">
    Order allow,deny
    Deny from all
  </Directory>

  <LocationMatch "^/assets/.*$">
    Header unset ETag
    FileETag None
    # RFC says only cache for 1 year
    ExpiresActive On
    ExpiresDefault "access plus 1 year"
  </LocationMatch>

  LogLevel info
  ErrorLog <%= node[:apache][:log_dir] %>/<%= @params[:name] %>-ssl-error.log
  CustomLog <%= node[:apache][:log_dir] %>/<%= @params[:name] %>-ssl-access.log combined
  CustomLog <%= node[:apache][:log_dir] %>/<%= @params[:name] %>-ssl-ganglia.log ganglia

  FileETag none

  RewriteEngine On
  Include <%= @params[:rewrite_config] %>-ssl*
  RewriteLog <%= node[:apache][:log_dir] %>/<%= @application_name %>-ssl-rewrite.log
  RewriteLogLevel 0

  # Canonical host
  #RewriteCond %{HTTP_HOST}   !^<%= @params[:server_name] %> [NC]
  #RewriteCond %{HTTP_HOST}   !^$
  #RewriteRule ^/(.*)$        http://<%= @params[:server_name] %>/$1 [L,R=301]

  RewriteCond %{REQUEST_URI} !\.(css|gif|jpg|jpeg|png)$
  RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
  RewriteCond %{SCRIPT_FILENAME} !maintenance.html
  RewriteRule ^.*$ /system/maintenance.html [L]

  Include <%= @params[:local_config] %>-ssl*
</VirtualHost>
<% end -%>

If you have questions feel free to ask.

Best Daniel

EDIT:

Or you just copy over this cookbooks https://github.com/neonlex/massive-octo-computing-machine I developed quickly. But OpsWorks should support this by default in the future.

  • Thank you kindly, Daniel. I managed to figure out something that eventually worked for me (answer posted below) and now that it's working I'm deathly afraid to try your cookbook for fear of breaking it. I'm curious to know what the Unicorn step is in your answer and any thoughts you have on what I came up with. Thanks! – manafire Jun 13 '13 at 15:44
  • @neonlex I used the repo you updated with it works fine. I tried replacing ruby shell command with execute recipe dsl in https://github.com/neonlex/massive-octo-computing-machine/blob/master/rails/libraries/rails_configuration.rb, gives error undefined method `execute'. any Idea why it didn't work! – goutham Jun 18 '13 at 06:39
  • I didn't need to modify /assets or any cook books. Simply having before_symlink.rb worked for me. I am on nginx and unicorn, using default Opsworks recipes. – Kirk Oct 05 '14 at 21:46
  • I really dislike polluting my source code with specific Deploy configuration / responsibility... Creating `deploy/before_symlink.rb` in the app's source, rather than doing something purely with Chef or OpsWorks, feels wrong – Don Cheadle Mar 23 '15 at 15:15
6

Note that if you're passing environment variables to your Rails app using the new OpsWorks feature, you'll need to include these variables in your rake invocation (as they don't get permanently sourced into the deploy user's environment).

I do the following (based on this article and this recipe) in deploy/before_migrate.rb:

Chef::Log.info("Precompiling assets for RAILS_ENV=" \
               "#{new_resource.environment['RAILS_ENV']}...")

execute 'rake assets:precompile' do
  cwd release_path
  command 'bundle exec rake assets:precompile'
  environment new_resource.environment
end
Ross
  • 1,415
  • 10
  • 10
5

I know very little about OpsWorks and Chef, but here's what I did to get it working.

First, I had to create a rails recipe that runs during the setup event to create the symlink directory for the assets. This sits in a public repo that OpsWorks can access.

cookbooks/rails/recipes/symlink_assets.rb:

node[:deploy].each do |application, deploy|
  Chef::Log.info("Ensuring shared/assets directory for #{application} app...")

  directory "#{deploy[:deploy_to]}/shared/assets" do
    group deploy[:group]
    owner deploy[:user]
    mode 0775
    action :create
    recursive true
  end
end

Then, in my app, I had to create deploy/before_migrate.rb:

Chef::Log.info("Running deploy/before_migrate.rb...")

Chef::Log.info("Symlinking #{release_path}/public/assets to #{new_resource.deploy_to}/shared/assets")

link "#{release_path}/public/assets" do
  to "#{new_resource.deploy_to}/shared/assets"
end

rails_env = new_resource.environment["RAILS_ENV"]
Chef::Log.info("Precompiling assets for RAILS_ENV=#{rails_env}...")

execute "rake assets:precompile" do
  cwd release_path
  command "bundle exec rake assets:precompile"
  environment "RAILS_ENV" => rails_env
end

This get called during the deployment process and compiles the assets.

manafire
  • 5,984
  • 4
  • 43
  • 53
3

In AWS Opsworks I use the following recipe:

execute 'rake assets:precompile' do
  cwd "#{node[:deploy_to]}/current"
  user 'root'
  command 'bundle exec rake assets:precompile'
  environment 'RAILS_ENV' => node[:environment_variables][:RAILS_ENV]
end

I run this command as root because the instance needs the right permission to write to the release path. Running the command as the deploy user renders a permission denied error.

Christian Fazzini
  • 19,613
  • 21
  • 110
  • 215
  • I was confused by the permission denied error, since OpsWorks seems to set the deploy user as the owner of everything in /shared. Turns out this user still creates its files with owner root (via its file mask setting), so it creates a log file and then can't edit it. – zrisher Jun 06 '14 at 00:13