SuperPumpup (dot com)
General awesomeness may be found here.

13 February 2014

Deploying a Multi-Rails-App OpsWorks Stack

I've written before about how OpsWorks kind of pushes you into a weird architecture because of their default behaviors for applications in a stack.

Namely, the system tries to push all the applications on all of the layers, and chaos ensues.

But, it turns out I needed to finally move our various apps into one stack, and after a few days of poking, prodding, waaaaaaaaaaiting for machines, I got it working, so I thought I'd document it.

Step 1: Set up an OpsWorks stack

See a previous post for some references

Step 2: Add an application (use the regular "Apps" button on the left to declare the type, repository, key, etc)

Creating OpsWorks Application

You will create a different of these applications for each application you want to deploy to your stack.

Step 3: Create a custom cookbook to deploy that app

Here's an annotated example

├── attributes
│   └── default.rb
├── metadata.rb
└── recipes
    └── default.rb



# The bundler and nginx recipes look for stack variables to configure themselves.
# Oddly, if you have a single Rails App Layer, it will set these variables.
# Then if you delete that layer after making lots of progress, you may
# inexplicably regress half a day's work...
set['opsworks_bundler']['version'] = '1.5.1'
set['manage_package'] = true
set['opsworks_rubygems']['version'] = '2.2.0'
set[:opsworks][:rails_stack][:name] = 'nginx_unicorn'

# Note that setting this using 'default' will let it easily get overridden
default[:deploy][:application_name] = 'your_application_name'


depends 'deploy'
depends 'unicorn'
depends 'nginx'


application = node[:deploy][:application_name]
deploy = node[:deploy][application] "Deploying #{application} using ownlocal_deploy::default"

include_recipe "apache2::uninstall"
include_recipe "nginx"
include_recipe "unicorn"

opsworks_deploy_user do
  deploy_data deploy

opsworks_deploy_dir do
  user deploy[:user]
  group deploy[:group]
  path deploy[:deploy_to]

opsworks_rails do
  deploy_data deploy
  app application

opsworks_deploy do
  deploy_data deploy
  app application

# Wrapping the following two commands in this deploy[:domains].present? block is necessary so that
# deploying other applications no-ops well

if deploy[:domains].present?
  nginx_web_app application do
    Chef::Log.debug("Calling nginx_web_app with #{deploy}")
    application deploy
    cookbook "nginx"

  unicorn_web_app do
    application application
    deploy deploy

Step 5: Set up your layer

Now you need to set up your layer like this:

Custom Layer Recipe Setup

Now, deploying this should "Just Work".

Step 6: Generalize this

I wound up renaming that first recipe to ownlocal_deploy (since my company is - amazingly - OwnLocal). Then I deploy with very thin wrapper cookbooks:

├── attributes
│   └── default.rb
├── metadata.rb
└── recipes
    └── default.rb


set[:deploy][:application_name] = 'adforge'


depends 'ownlocal_deploy'


include_recipe 'ownlocal_deploy'

Step 7: (Optional) Make the built-in deploy::default no-op

Oddly, running the deploy::default recipe will cycle through all the node[:deploy] values and run code. When you have more than one application, and are generally "polluting" that namespace like I am by setting application names in recipes, it turns out that some of it works, some doesn't.

Actually this entire (anti-?)pattern of:

node[:deploy].each do |application, deploy|
    ...all the code...

is terrible in this setting because most of these things break. That's why I preferred to set the [:cookbook][:application_name] in the wrapper, and then use:

application = node[:cookbook][:application_name]
deploy = node[:deploy][application]

Instead. If weird things are failing, look for that pattern and kill it.

Step 8: (Mandatory) Revel in your splendor

This is what I wound up with:

Multi-Rails App OpsWorks Stack

Adforge and Persistor are the two different Rails applications deployed at this point. Adforge also has an FTP server layer, and then a Sidekiq layer. We are using ElasticCache Redis & Memcached, and RDS datatabases at this point, so those layers are not present in this stack.

Note that the Persistor application layer is not accessible to the big bad internet, and but is critical, so behind an internal load balancer.

The Bastion layer is how I connect to the boxes that are in the private subnets of my VPC.

And that's it. If you want help, feel free to tweet me @sototallysweet

Categories: Ruby on Rails Software