Deployment to AWS OpsWorks

Deployment to AWS OpsWorks

When it comes to deployment, there are several options available. The hosting platform is entirely your choice. However, we recommend you deploy to AWS OpsWorks. We use OpsWorks in numerous production environments, and it works like a charm. This document covers the basic configuration and deployment steps.


Find in-depth information at Amazon OpsWorks.

The OpsWorks console

First, sign up for Amazon AWS. Then, in the OpsWorks configuration management console, do the following:

  • Define a stack for the production servers, name it, for example, “production” or “live.” Select OS: “Ubuntu”, Region: “eu-west-1.”

  • Define a “Rails App Server” type. Add at least the following OS packages to it: imagemagick, libxml2-dev, libxslt1-dev

  • Define one or more EC2 instances within the “Rails App Server” layer. Choose “c1.medium”, for example, as your instance size. Place each instance in a different availability zone within the region mentioned above.

  • Define a “Ruby on Rails” type application. The Rails environment should be “production.” Specify a repository that holds the application code, e.g. a GitHub URL or an S3 bucket.

Configuration

Your application has access to the CMS and other components by means of web services APIs. Therefore, you need to edit the OpsWorks stack and enter the access keys. A sample JSON configuration that also includes a third-party service as an example (Honeybadger) reads like this:

{
  "passenger": {
    "max_pool_size": 4,
    "pool_idle_time": 300
  },
  "application": {
    "secret_token": "YOUR_SESSION_SECRET_TOKEN"
  },
  "scrivito": {
    "tenant": "YOUR_SCRIVITO_CMS_ID",
    "api_key": "YOUR_SCRIVITO_API_KEY"
  },
  "honeybadger": {
    "api_key": "YOUR_HONEYBADGER_API_KEY"
  }
}

OpsWorks builds upon Chef, which passes this configuration to deployment recipes. One of the recipes executes custom deployment hooks defined in your application repository. We recommend you add the following lines to the deploy/before_symlink.rb file:

template "#{release_path}/config/scrivito.yml" do
  local true
  owner 'deploy'
  group 'root'
  mode 0664
  source "#{release_path}/deploy/templates/scrivito.yml.erb"
end

template "#{release_path}/config/custom_cloud.yml" do
  local true
  owner 'deploy'
  group 'root'
  mode 0664
  source "#{release_path}/deploy/templates/custom_cloud.yml.erb"
end

These lines generate the configuration files, config/scrivito.yml and config/custom_cloud.yml, from the deploy/templates/scrivito.yml.erb and deploy/templates/custom_cloud.yml.erb templates, respectively.

Now create the templates. First, deploy/templates/scrivito.yml.erb:

---
tenant: "<%= node['scrivito']['cms_id'] %>"
api_key: "<%= node['scrivito']['api_key'] %>"

Next, deploy/templates/custom_cloud.yml.erb:

---
application:
  secret_token: "<%= node['application']['secret_token'] %>"

honeybadger:
  api_key: "<%= node['honeybadger']['api_key'] %>"

Put the following code into config/initializers/secret_token.rb to read the secret token from the custom_cloud.yml configuration file:

# Be sure to restart your server when you modify this file.

# Your secret key for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies become invalid!
# Make sure the secret is at least 30 characters long and all random,
# no regular words or you'll be a victim of dictionary attacks.

def application_config
  config = YAML.load_file(Rails.root + 'config/custom_cloud.yml')
  config['application']
rescue Errno::ENOENT
  {}
end

Rails.application.config.secret_token = application_config['secret_token']

Add further entries to the stack configuration and to the deploy/templates/custom_cloud.yml.erb Chef template as needed.

Load balancer

Amazon provides load balancers for faster and more reliant web page delivery. Infopark recommends to use them. After creating a load balancer, you can add it to the layer in the OpsWorks console.

Faster deployments

Move the Scrivito SDK cache to /srv/www

Using the default settings, the Scrivito SDK stores caches of CMS objects as files in the #{Rails.root}/tmp/scrivito_cache directory. These files are not very large but there are lots of them. The OpsWorks deployment recipe runs chown -R multiple times over the directory containing all your apps (including the previous app releases). This process takes longer from deployment to deployment since the amount of cache files adds up on every new release. Deployment duration can then amount to minutes or even hours.

In order to solve this issue, cache files should be stored completely outside the app directory, for example in /srv/www:

Scrivito.configure do |config|
  # ...
  config.cache_path = "/srv/www/scrivito_cache"
end

By doing this, the cache directory is no longer rotated on deployments. Please ensure that the ownership of and the permissions for the cache directory have been properly set. Also, for best caching behaviour, we recommend to not place your Scrivito cache onto a file system for which noatime has been set.

It is important to collect the garbage from your cache regularly using rake scrivito:cache:gc to prevent your file system from running out of inodes. For scheduling, we recommend to use whenever:

# config/schedule.rb
every :day do
  rake 'scrivito:cache:gc'
end

This installs a crontab entry that runs the garbage collector rake task once a day.

Speed up rake assets:precompile

Install turbo-sprockets-rails3. It speeds up rake assets:precompile by only recompiling changed files.

Prevent the repo from being cloned on every deployment

The OpsWorks deployment recipe uses a cached copy of the repo which it updates and which it copies into the new release directory. Unfortunately, it deletes the repo prior to updating it, resulting in a fresh clone. But since the cached repo is never changed, it is safe to reuse it with all deployments.

Append the following configuration to the stack settings:

"deploy": {
   "YOUR-APP-NAME": {
      "delete_cached_copy": false
    }
},

By applying all these improvements, a typical deployment should take about a minute.

General considerations

Since the live server usually is available under a couple of URLs and hostnames, it is recommended to instruct search engines such as Google to only index the websites when accessed via their live URL. This is easily accomplished by integrating the rack-noindex gem:

# Gemfile:
gem 'rack-noindex'

# config/application.rb:
config.middleware.use Rack::Noindex, lambda { |env| !%w(www.example.com).include?(env['SERVER_NAME']) }

This code inserts a noindex HTTP header into the pages delivered if the server name is not equal to one of the live server names.