Latest Posts

If you are using Chef to manage your server deployments (and you should be!), then you'll likely have run into the situation where you install a new system Ruby and need to install a few system-wide gems to bootstrap your install (think Bundler/Rake).

However, when you install the system ruby, and use the standard Chef gem install resource:

gem_package "bundler"

then you will run into trouble as Chef will try to use the original system ruby binary when installing the gem for you.

The Fix

After some Googling, and time on StackOverflow, I've put together a workable solution. All it does it reload the Ohai resource (updating information about your system/Ruby binary), and performs the gem installation in a ruby_block so that it does not run at compile-time.

#=========================
# Install system Ruby
#=========================

## install ruby here....

#=========================
# Install gems in new ruby
#=========================

# Need to reload OHAI to ensure the newest ruby is loaded up
ohai "reload" do
  action :reload
end

["bundler", "rake", "...."].each do |gem_to_install|
  ruby_block "install #{gem_to_install} in new ruby" do
    block do
      g = Chef::Resource::GemPackage.new(gem_to_install)
      g.gem_binary "#{node['languages']['ruby']['bin_dir']}/gem"
      g.run_action(:install)
    end
    action :create
  end
end

That ought to do it! Hope it saves someone some time....

I got tired of SSHing into an Amazon EC2 instance every time I spun one up and writing the same code to mount an EBS volume. (I also had to search around to find out how to do this in the first place - hopefully this helps anyone else looking as well.)

I started doing this with the Net:SSH Ruby library, as it can accept a number of commands in the same SSH session via a block, but I found the simplest way to do this was to write one script to handle the SSH connection to the server, and then pipe a second local script into the SSH session to accomplish the task.

Script 1

You would call this script like so: /path/to/the/script/script1.sh amazonEC2 xx.xx.xx.xx. Make sure to chmod +x the script.

#!/usr/bin/env bash

if [ "$1" = "amazonEC2" ]; then
  USER="ubuntu"
  SERVER_ADDRESS=$2
else
  echo 'Invalid argument...'
  exit 1
fi

ssh $USER@$SERVER_ADDRESS 'bash -s' < script2.sh

Script 2

Ensure this script is in the same directory as the first.

#!/usr/bin/env bash

MOUNT_NAME="snaphost_data"

sudo mkfs.ext4 /dev/xvdf
sudo mkdir -m 000 /$MOUNT_NAME
echo "/dev/xvdf /$MOUNT_NAME auto noatime 0 0" | sudo tee -a /etc/fstab
sudo mount /dev/xvdf /$MOUNT_NAME

Set the MOUNT_NAME variable to whatever you would like the drive to be mounted as, and you are away to the races!

SublimeText2 is great for editing code and even basic text documents. For the most part is has completely replaced Microsoft Word or Pages in my daily workflow.

I've really been appreciating the ability to customize the layout of the editor, split-pane editing, and a ton of simple (and extendable) keyboard shortcuts.

My SublimeText2 Editor Layout

My SublimeText2 Editor Layout

Theming

ST2 is extremely customizable in its visual appearance. Play around with it to find something you find comfortable. You'll want text to readable, with a comfortable background so avoid eye strain. It also helps to get syntax highlighting for coding.

Basic Program Theme

First you'll want to pick up a theme for the editor itself. Think sidebars, tabs etc. I've used the Soda Theme since I started with ST2 and it has worked out great.

Soda Theme

Soda Theme for SublimeText2

Colour Schemes

Obviously this one will be personal preference, but it helps to use a legible set of colours that are comfortable for you. I had been using the Railscasts Theme for some time, and just recently swapped over to a GitHub Theme to shake things up.

GitHub Colour Scheme

GitHub Colour Scheme for SublimeText2

RailsCasts Colour Scheme

RailsCasts colour scheme for SublimeText2

Configuration

Although it is an incredibly powerful editor, it is important to get your configuration settings right to get the most out of it. ST2 has built-in functionality to format your text as you go so you don't have to worry about it.

You can also set up syntax-specific configuration files in order to provide different behvaiour between Ruby files and Markdown files for example.

Here are my configuration files, use what you will, but be sure to play around and see what works for you!

General Config

{
  "font_face": "Ubuntu Mono",
  "font_options":
  [
      "subpixel_antialias"
  ],
  "font_size": 16.0,

  "theme": "Soda Light.sublime-theme",
  "color_scheme": "Packages/GitHub Colour Scheme/GitHub.tmTheme",
  // "color_scheme": "Packages/RailsCasts Colour Scheme/RailsCastsColorScheme.tmTheme",

  "bold_folder_labels": true,
  "margin": 0,
  "caret_style": "phase",
  "rulers": [80],
  "soda_classic_tabs": true,
  "tab_size": 2,

  "trim_trailing_white_space_on_save": true,
  "ensure_newline_at_eof_on_save": true,
  "translate_tabs_to_spaces": true,

  "auto_complete_commit_on_tab": true,
  "shift_tab_unindent": true,
  "highlight_line": true,
  "scroll_past_end": true,
  "move_to_limit_on_up_down": true,
  "highlight_modified_tabs": true,
  "detect_indentation": true,

  "ignored_packages":
  [
    "Vintage"
  ]
}

Custom Key Mapping

[
  // to see output:
  // sublime.log_commands(True)

  // open this keybindings file
  {
    "keys": ["super+ctrl+,"],
    "command": "open_file",
    "args": {"file": "${packages}/User/Default ($platform).sublime-keymap"}
  },

  // go to line
  {
    "keys": ["ctrl+super+l"],
    "command": "show_overlay",
    "args": {"overlay": "goto", "text": ":"}
  },

  // copy file name
  {
    "keys": ["super+shift+c"],
    "command": "copy_path_to_clipboard"
  },

  // ctags
  {
    "keys": ["ctrl+]"],
    "command": "navigate_to_definition"
  },

  {
    "keys": ["super+alt+p"],
    "command": "rebuild_tags"
  },

  // go to alternate file
  // super+ctrl+. will show spec and implementation in split view
  {
    "keys": ["ctrl+shift+down"],
    "command": "open_rspec_file"
  },

  // Duplicate selection
  { "keys": ["ctrl+shift+d"], "command": "duplicate_line" },

  // show in project drawer
  { "keys": ["ctrl+super+r"], "command": "reveal_in_side_bar"},

  // select coffeescript twin
  {
    "keys": ["ctrl+shift+down"],
    "command": "open_coffee_twin",
    "context": [ {"key": "selector", "operator": "equal", "operand": "source.coffee"} ]
  },

  // compile and display coffeescript
  {
    "keys": ["super+b"],
    "command": "compile_and_display_js",
    "context": [ {"key": "selector", "operator": "equal", "operand": "source.coffee"} ]
  },

  // close all other tabs
  {
    "keys": ["super+alt+w"],
    "command": "close_other_tabs"
  },

  // focus on side bar
  {
    "keys": ["ctrl+shift+tab"],
    "command": "focus_side_bar"
  },

  // focus back in buffer
  {
    "keys": ["ctrl+tab"],
    "command": "focus_group",
    "args": {"group": 0}
  },

  // TEST STUFF

  // single test
  { "keys": ["super+shift+r"], "command": "run_single_ruby_test" },
  // test file
  { "keys": ["super+shift+t"], "command": "run_all_ruby_test" },
  // test file
  { "keys": ["super+shift+e"], "command": "run_last_ruby_test" },

  // show test panel
  { "keys": ["super+shift+x"], "command": "show_test_panel" },

  // verify ruby syntax
  { "keys": ["alt+shift+v"], "command": "verify_ruby_file" },

  // switch between code and test in single mode
  { "keys": ["super+period"], "command": "switch_between_code_and_test", "args": {"split_view": false}, "context" : [
    { "key": "selector", "operator": "equal", "operand": "source.ruby", "match_all": true }
  ]},

  // switch between code and test in split view
  { "keys": ["super+ctrl+period"], "command": "switch_between_code_and_test", "args": {"split_view": true}, "context" : [
    { "key": "selector", "operator": "equal", "operand": "source.ruby", "match_all": true }
  ]} ,

  // go to symbol
  {
    "keys": ["super+t"],
    "command": "show_overlay",
    "args": {"overlay": "goto", "text": "@"}
  },

  { "keys": ["ctrl+super+k"], "command": "toggle_side_bar" },

  // ["super+shift+v"] is default the command for paste and indent. Some folks may want this
  // to be the default behavior - simply add this to the Key Bindings - User file to swap the keybindings.
  { "keys": ["super+v"], "command": "paste_and_indent" },
  { "keys": ["super+shift+v"], "command": "paste" }

]

Markdown & MultiMarkdown Settings

{
  // Which file extensions go with this file type?
  "extensions":
  [
      "md",
      "mdown",
      "mdwn",
      "mmd",
      "txt"
  ],

  // Set to true to turn spell checking on by default
  "spell_check": true,
  "trim_trailing_white_space_on_save": false
}

RubyTest Configuration

{
  "erb_verify_command": "erb -xT - {file_name} | ruby -c",
  "ruby_verify_command": "ruby -c {file_name}",

  "run_ruby_unit_command": "ruby -Itest {relative_path}",
  "run_single_ruby_unit_command": "ruby -Itest {relative_path} -n '{test_name}'",

  "run_cucumber_command": "cucumber {relative_path}",
  "run_single_cucumber_command": "cucumber {relative_path} -l{line_number}",

  "run_rspec_command": "bundle exec rspec {relative_path}",
  "run_single_rspec_command": "bundle exec rspec {relative_path} -l{line_number}",

  "ruby_unit_folder": "test",
  "ruby_cucumber_folder": "features",
  "ruby_rspec_folder": "spec",

  "ruby_use_scratch" : false,
  "save_on_run": false,
  "ignored_directories": [".git", "vendor", "tmp"]
}

Packages

The extensibility of SublimeText2 is one of its killer features. There are a pile of useful plugins out there - most of which I haven't had a chance to use yet. In any case, here's a list of plugins which I use frequently and I've found quite helpful:

  • HAML
  • HTML
  • JavaScript
  • Markdown
  • MarkdownEditing (adds a nice comfortable colour scheme, and a ton of useful keyboard shortcuts)
  • Rails
  • RSpec
  • Ruby
  • RubyTest (lets you run tests from inside of the ST2 editor)
  • SideBarEnhancements

Wrap-up

I'm no expert with ST2, but have been using it enough to get comfortable with setup and configuration. Happy to answer any questions.

Something I discovered recently - you can't just sit back and write, and expect people to listen to what you have to say. Not so much a discovery, more so a realization that what people tell you over and over is truly correct.

I've written a few posts on this blog, usually with very little traffic. I wrote one decently useful post on dev/prod parity using Vagrant/Chef and on a whim shared it with Peter Cooper from the Ruby Show and Ruby Weekly.

Lo-and-behold, sharing my content with people who actually care worked! I got over 1,300 visits to this page within a few days, my first blog comments, and even requests for a followup (I ended up following up with my custom Chef/Ubuntu recipes.)

Increased traffic via sharing content with people who care

If you find yourself spending hours on carefully crafted posts and nobody is coming to read it - give some thought to who would truly benefit from what you have to share.

Don't just write and then sit back and expect people to show up. Spread the word to people who care about what you're discussing.

Download the episode of the Ruby Show here!

In my last post, I described how to combine Vagrant and Chef to achieve dev/prod parity.

Due to the feedback I received, this post is intended to share some of those Chef recipes to get you up and running quickly.

For now I'm not going to share this on Github, as I'd need to go through the entire thing and make sure I wasn't crazy and posted a password in there somewhere. However with everything I put on this post it should be a simple copy/paste.

My Run List

Here is the node JSON file that bootstraps my deployment server.

{
  "run_list": [
    "recipe[apt]",
    "recipe[ohai]",
    "recipe[build-essential]",
    "recipe[openssl]",
    "recipe[git]",
    "recipe[user]",
    "recipe[logrotate]",
    "recipe[brandon::add_deployer_user_REAL_VM]",
    "recipe[brandon::change_ssh_port]",
    "recipe[ruby_build]",
    "recipe[rbenv::user]",
    "recipe[brandon]",
    "recipe[fail2ban]", // Fail2Ban needs to come after 'brandon::change_ssh_port' so that ssh port has been correctly changed
    "recipe[brandon::fail2ban_setup]",
    "recipe[postgresql::server]",
    "recipe[postgresql::client]",
    "recipe[nginx::http_gzip_static_module]",
    "recipe[nginx::http_ssl_module]",
    "recipe[nginx::source]",
    "recipe[brandon::set_nginx_conf]",  // Has to come after installation of nginx
    "recipe[redis::server]", // Has to come after brandon (create log directory)
    "recipe[brandon::set_redis_log_permissions]",
    "recipe[brandon::upstart_recipes]",  // Has to come after recipe[brandon] as monit must be installed already
    "recipe[brandon::logrotate]" // Leave at end to ensure logfile in place
  ],
  "username": "deployer",
  "time_zone": "Canada/Mountain",
  "ssh_port": 555,
  "rbenv": {
    "user_installs": [
      {
        "user": "deployer",
        "rubies": [
          "1.9.3-p194"
        ],
        "global": "1.9.3-p194",
        "gems": {
          "1.9.3-p194": [
            {
              "name": "bundler"
            },
            {
              "name": "rake"
            },
            {
              "name": "awesome_print"
            }
          ]
        }
      }
    ]
  },
  "user": {
    "ssh_keygen": true
  },
  "nginx": {
    "default_site_enabled": false,
    "workers": 4,
    "init_style": "init",
    "source": {
      "prefix": "/opt/nginx"
    }
  },
  "redis": {
    "init_style": "init",
    "install_type": "source",
    "config": {
      "logfile": "/var/log/redis-server.log"
    }
  },
  "memcached": {
    "memory": 64
  },
  "upstart": {
    // Whether or not to boot services on system startup
    "memcached": true,
    "mongo": true,
    "monit": true,
    "nginx": true,
    "postgres": true,
    "redis": true
  }
}

A fair chunk of those recipes are self-explanatory, or just load default cookbooks directly from an Opscode repo cookbook. I'll go through some of the custom recipes here.

Customized Deployment Recipes

Contents of the 'Brandon' Recipe

The brandon recipe really just calls a number of individual recipes that I've kept together in a single location. The only trouble is that some of the other recipes had to be run at specific times, and that's why you will see something like this in the run list:

"recipe[brandon::add_deployer_user_REAL_VM]"

In those cases, it directly pulls the recipe from the /site_cookbooks/brandon directory.

Here are the contents of the default.rb recipe:

include_recipe "brandon::update_packages"
include_recipe "brandon::ensure_ruby_setup"
include_recipe "brandon::install_irb"
include_recipe "brandon::copy_dotfiles"
include_recipe "brandon::set_timezone"
include_recipe "brandon::install_nodejs" # Need a javascript runtime
include_recipe "brandon::set_up_postgres_hstore"
include_recipe "brandon::install_mongodb"
include_recipe "brandon::install_memcached"
include_recipe "brandon::install_monit"
include_recipe "brandon::firewall"

Add Deployer User

user_account 'deployer' do
  # keys for file ~/.ssh/authorized keys
  ssh_keys  ['paste your public ssh key here']
end

group "admin" do
  members ['brandon', 'deployer']
  action :create
end

group "sudo" do
  members ['brandon', 'deployer']
  action :create
end

Change the Default SSH Port

Because everyone is going to guess 22...

execute "change the ssh port for security purposes" do
  command <<-EOC
    sudo service ssh stop
    sudo mv /etc/ssh/sshd_config /etc/ssh/sshd_config_old
    sudo sed 's/Port 22/Port #{node['ssh_port']}/g' /etc/ssh/sshd_config_old | sudo sed 's/PermitRootLogin yes/PermitRootLogin no/g' > /tmp/sshd_config
    sudo mv /tmp/sshd_config /etc/ssh/sshd_config
    sudo service ssh start
  EOC
  action :run
  creates "/etc/ssh/sshd_config_old"
end

execute "disable password login for security purposes" do
  command <<-EOC
    sudo service ssh stop
    sudo mv /etc/ssh/sshd_config /etc/ssh/sshd_config_old2
    sudo sed 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config_old2 | sed 's/^ChallengeResponseAuthentication yes/ChallengeResponseAuthentication no/' | sed 's/^#ChallengeResponseAuthentication yes/ChallengeResponseAuthentication no/' > /tmp/sshd_config
    sudo mv /tmp/sshd_config /etc/ssh/sshd_config
    sudo service ssh start
  EOC
  action :run
  creates "/etc/ssh/sshd_config_old2"
end

Update Packages

execute "upgrade packages" do
  action :run
  command "apt-get -y upgrade"
end

Ensure Ruby is Set Up

execute "set up path for bash" do
  command <<-EOC
    sudo echo 'export PATH="/home/#{node['username']}/.rbenv/bin:$PATH"' >> /home/#{node['username']}/.bashrc
    sudo echo 'eval "$(rbenv init -)"' >> /home/#{node['username']}/.bashrc
  EOC
  action :run
end

Install IRB

execute "install irb" do                                             
    action :run
    command "apt-get -y install irb"      
end

Copy Dotfiles

template "/home/#{node['username']}/.gemrc" do
  source "gemrc"
  owner "#{node['username']}"
  group "#{node['username']}"
  mode "0755"
end

template "/home/#{node['username']}/.gitignore_global" do
  source "gitignore_global"
  owner "#{node['username']}"
  group "#{node['username']}"
  mode "0755"
end

template "/home/#{node['username']}/.irbrc" do
  source "irbrc"
  owner "#{node['username']}"
  group "#{node['username']}"
  mode "0755"
end

Set Timezone

bash "modify timezone" do
  code <<-EOC
    sudo cp /etc/timezone /etc/timezone_old
    echo "#{node['time_zone']}" > /etc/timezone    
    dpkg-reconfigure -f noninteractive tzdata
  EOC

  creates "/etc/timezone_old"
end

Install NodeJS

Because you need a JavaScript runtime

execute "install nodejs" do                                      
    action :run
    command "apt-get -y install nodejs"      
end

Set up PostgreSQL HStore Add-in

# Allows installation of hStore
execute "install postgres contrib" do                                             
    action :run
    command "apt-get -y install postgresql-contrib-9.1"      
end

# Install hStore on all databases created on this node
execute "hstore extension on template1" do
  command <<-EOC
    sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS hstore";
  EOC
  action :run
end

Install MongoDB

bash "install mongodb" do
  code <<-EOC
    apt-key adv --keyserver keyserver.ubuntu.com --recv 7F0CEB10
    touch /etc/apt/sources.list.d/10gen.list
    echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' > /etc/apt/sources.list.d/10gen.list
    apt-get update
    apt-get install mongodb-10gen
  EOC

  creates "/etc/apt/sources.list.d/10gen.list"
end

Install Memcached

execute "install nodejs" do                                         
    action :run
    command "apt-get -y install memcached"      
end

Install Monit

execute "install monit" do                                             
    action :run
    command "apt-get -y install monit"      
end

directory "/etc/monit.d" do
  owner "root"
  group "root"
  mode 0755
end

Set up Firewall Rules

# open standard ssh port, enable firewall
firewall_rule "ssh" do
  port node['ssh_port']
  protocol :tcp
  action :allow
  notifies :enable, "firewall[ufw]"
end

# open standard http port to tcp traffic only; insert as first rule
firewall_rule "http" do
  port 80
  protocol :tcp
  position 1
  action :allow
end

firewall "ufw" do
  action :nothing
end

execute "limit ssh retries" do
  command "ufw limit ssh/tcp"
  action :run
end

Fail2Ban Setup

template "/etc/fail2ban/jail.local" do
  source "fail2ban_jail_local"
  owner "root"
  group "root"
  mode 0644
  notifies :restart, "service[fail2ban]"
end

Set NGinx Configuration

service "nginx" do
  action :stop
end

execute "move default nginx conf" do
  action :run
  command "mv /etc/nginx/nginx.conf /etc/nginx/nginx_old.conf"
end

template "/etc/nginx/nginx.conf" do
  source "nginx_conf"
  owner "root"
  group "root"
  mode 0644
  notifies :start, "service[nginx]"
end

Set Redis Log Permissions

execute "open up redis log" do
  action :run
  command "chmod 0766 /var/log/redis-server.log"
end

Get Upstart Doing the Hard Work for You!

Because the Upstart configuration files include a start on runlevel[2345] for any services you specify in the node JSON file, we don't want the old services getting in the way.

service "memcached" do
  action :stop
end

service "nginx" do
  action :stop
end

service "mongodb" do
  action :stop
end

service "redis" do
  action :stop
end

service "redis-server" do
  action :stop
end

service "postgresql" do
  action :stop
end

service "monit" do
  action :stop
end

execute "Stop use of services" do
  command <<-EOC
    sudo update-rc.d -f memcached remove
    sudo update-rc.d -f nginx remove
    sudo update-rc.d -f mongodb remove
    sudo update-rc.d -f redis remove
    sudo update-rc.d -f redis-server remove
    sudo update-rc.d -f postgresql remove
    sudo update-rc.d -f monit remove
  EOC
  action :run
end

Drop in Upstart config files

template "/etc/init/memcached.conf" do
  source "upstart_memcached.erb"
  owner "root"
  group "root"
  mode "0755"
end

template "/etc/init/nginx.conf" do
  source "upstart_nginx.erb"
  owner "root"
  group "root"
  mode "0755"
end

execute "move mongo conf" do
    action :run
    command "mv /etc/init/mongodb.conf /etc/init/mongodb_old"
end

template "/etc/init/mongodb.conf" do
  source "upstart_mongo.erb"
  owner "root"
  group "root"
  mode "0755"
end

template "/etc/init/redis.conf" do
  source "upstart_redis.erb"
  owner "root"
  group "root"
  mode "0755"
end

template "/etc/init/postgres.conf" do
  source "upstart_postgres.erb"
  owner "root"
  group "root"
  mode "0755"
end

template "/etc/init/monit.conf" do
  source "upstart_monit.erb"
  owner "root"
  group "root"
  mode "0755"
end

Last, but not least - LogRotate

This sets up just a single logrotate entry. But that's because I do the rest of this from Capistrano (in case this server doesn't have both a web server and database, for example)

logrotate_app "fail2ban" do
  cookbook "logrotate"
  path "/var/log/fail2ban.log"
  frequency "weekly"
  rotate 4
  create "644 root admin"
end

Templates

A bunch of these recipes copy templated ERB files over onto your server. The contents of these files goes into site_cookbooks/templates/default.

Fail2Ban Jail Local

[ssh-ddos]
enabled = true

gemrc

gem: --no-ri --no-rdoc
install: --no-ri --no-rdoc
update: --no-ri --no-rdoc

GitIgnore Global

# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
#   git config --global core.excludesfile ~/.gitignore_global
#
# Here are some files you may want to ignore globally:

# scm revert files
**.orig

# Mac finder artifacts
.DS_Store

# Netbeans project directory
/nbproject/

# RubyMine project files
.idea

# Textmate project files
/*.tmproj

# vim artifacts
**.swp

Irbrc

require 'irb/completion'
require 'ap'
require 'hirb'
require 'pp'

IRB.conf[:AUTO_INDENT] = true
IRB.conf[:USE_READLINE] = true

Hirb.enable

Hirb::Formatter.dynamic_config['ActiveRecord::Base']

# http://blog.nicksieger.com/articles/2006/04/23/tweaking-irb
ARGV.concat ["--readline", "--prompt-mode", "simple"]

require 'irb/ext/save-history'
IRB.conf[:SAVE_HISTORY] = 500
IRB.conf[:HISTORY_FILE] = File.expand_path('~/.irb_history')

# http://ozmm.org/posts/time_in_irb.html
def time(times = 1)
  require 'benchmark'
  ret = nil
  Benchmark.bm { |x| x.report { times.times { ret = yield } } }
  ret
end

# list object methods
def local_methods(obj=self)
  (obj.methods - obj.class.superclass.instance_methods).sort
end

def ls(obj=self)
  width = `stty size 2>/dev/null`.split(/\s+/, 2).last.to_i
  width = 80 if width == 0
  local_methods(obj).each_slice(3) do |meths|
    pattern = "%-#{width / 3}s" * meths.length
    puts pattern % meths
  end
end

# reload this .irbrc
def IRB.reload
  load __FILE__
end

puts "Yes! config loaded"

NGinx Conf

This only sets up the general configuration - the app specific configurations are set up in Capistrano.

user www-data www-data;
worker_processes  <%= node['nginx']['workers'] || 2 %>;

error_log /var/log/nginx/nginx_error.log;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
  accept_mutex on; # because worker processes > 1
}

http {
  include       mime.types;
  default_type  application/octet-stream;

  log_format custom_format [$host] ($time_local): "$request" | Status: $status | URI: $uri;

  access_log /var/log/nginx/nginx_catchall.log custom_format;

  sendfile        on;
  tcp_nopush     on;
  tcp_nodelay off;

  # open_file_cache max=1000 inactive=20s;
  # open_file_cache_valid 30s;
  # open_file_cache_min_uses 2;

  keepalive_timeout  5;

  gzip on;
  gzip_http_version 1.0;
  gzip_comp_level 2;
  gzip_proxied any;
  gzip_min_length 500;
  gzip_disable "MSIE [1-6]\.";
  gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript application/json;

  include /etc/nginx/sites-enabled/*;
}

Upstart Memcached

<% if node['upstart']['memcached'] %>
start on runlevel [2345]
stop on runlevel [!2345]
<% end %>

respawn
respawn limit 10 5

exec /usr/bin/memcached -m <%= node['memcached']['memory'] %> -p 11211 -u memcache -l 127.0.0.1

Upstart MongoDB

limit nofile 20000 20000

# wait 300s between SIGTERM and SIGKILL.
kill timeout 300

pre-start script
    mkdir -p /var/lib/mongodb/
    mkdir -p /var/log/mongodb/
end script

<% if node['upstart']['mongo'] %>
start on runlevel [2345]
stop on runlevel [!2345]
<% end %>

respawn
respawn limit 10 5

script
  ENABLE_MONGODB="yes"
  if [ -f /etc/default/mongodb ]; then . /etc/default/mongodb; fi
  if [ "x$ENABLE_MONGODB" = "xyes" ]; then
        if [ -f /var/lib/mongodb/mongod.lock ]; then
                rm /var/lib/mongodb/mongod.lock
                sudo -u mongodb /usr/bin/mongod --config /etc/mongodb.conf --repair
        fi
        exec start-stop-daemon --start --quiet --chuid mongodb --exec  /usr/bin/mongod -- --config /etc/mongodb.conf
  fi
end script

Upstart Monit

limit core unlimited unlimited

<% if node['upstart']['monit'] %>
start on runlevel [2345]
stop on runlevel [!2345]
<% end %>

respawn
respawn limit 10 5

exec /usr/bin/monit -Ic /etc/monit/monitrc

Upstart NGinx

<% if node['upstart']['nginx'] %>
start on runlevel [2345]
stop on runlevel [!2345]
<% end %>

env DAEMON=<%= node['nginx']['source']['prefix'] %>/sbin/nginx
env PID=/var/run/nginx.pid

expect fork
respawn
respawn limit 10 5
#oom never

pre-start script
        $DAEMON -t
        if [ $? -ne 0 ]
                then exit $?
        fi
end script

exec $DAEMON

Upstart PostgreSQL

description "PostgreSQL 9.1 Server"
author "PostgreSQL"

<% if node['upstart']['postgres'] %>  
start on runlevel [2345]
stop on runlevel [!2345]
<% end %>

respawn
respawn limit 10 5

pre-start script
    if [ -d /var/run/postgresql ]; then
        chmod 2775 /var/run/postgresql
    else
        install -d -m 2775 -o postgres -g postgres /var/run/postgresql
    fi
end script
exec su -c "/usr/lib/postgresql/9.1/bin/postgres -D /var/lib/postgresql/9.1/main -c config_file=/etc/postgresql/9.1/main/postgresql.conf" postgres

Upstart Redis

<% if node['upstart']['redis'] %>
start on runlevel [2345]
stop on runlevel [!2345]
<% end %>

expect fork
respawn
respawn limit 10 5

exec sudo su - redis -c "/opt/redis/bin/redis-server /etc/redis/redis.conf"

That's It!

You should now have a fully provisioned deployment server. Now you just use Capistrano to:

  • Set up your app-specific Nginx configuration file
  • Set your Unicorn config
  • Create postgres database / user
  • Use Foreman to export app-specific upstart files

I hope you found this post useful. (I'd appreciate a few +1's if so :D )

Intro

The convenience of VirtualBox & Vagrant makes dev/prod parity possible for even the newest developers. No longer do you have to deploy to production and cross your fingers... You can have an environment exactly matching your Production environment running on your local computer, whether you develop on a Mac or Linux (or even a P.C. - probably... I'm sure there's a way to get it working).

Combining Vagrant with Chef allows the provisioning process to be completely repeatable. Not only does this allow for completely throwaway environments, but it also ensures your production and staging boxes are completely identical.

Installation

  1. Install the appropriate VirtualBox and Vagrant packages from the downloads page on their website. (I have used the Vagrant package rather than installing from a Gem.)
  2. Install the Knife-solo gem to automate Chef cookbook creation, server bootstrapping and cookbook usage: gem install knife-solo
  3. Set up your local chef repo:
    • knife kitchen chef_repo
    • (It would be smart to use version control for this repo - i.e. Git with Github/Bitbucket)
    • Copy cookbooks to your chef_repo/cookbooks directory. Consider directly cloning these cookbooks from their respective GitHub repos to ensure you can keep them up to date.

Configuration of your Vagrant Box

What we are going to do is use Vagrant to load up a simple Ubuntu 12.04LTS box, saved as "Ubuntu1204LTS". This box can be created and destroyed at will. Then Knife-Solo will be used to apply a Chef server setup remotely via SSH. We can test this server on our local machine, and then use the exact same Chef provisioning recipe on the production server for precise dev/prod parity.

Set up your VagrantFile

This goes at the root of your chef_repo directory. Mine looks like this:

Vagrant::Config.run do |config|
  config.vm.box = "Ubuntu1204LTS"
  config.vm.box_url = "http://dl.dropbox.com/u/1537815/precise64.box"
  config.vm.network :hostonly, "192.168.0.10"
  config.ssh.forward_agent = true
end

The forward_agent option will allow you to use local SSH keys to log in to remote services directly from the Vagrant/remote host. The network option will set up the Vagrant box using your local network at the IP address of your choice. I've provided the box_url so that the correct file will automatically download the first time you set it up.

If you want to test your Vagrant box:

vagrant up # This will load the base Ubuntu 12.04 box, set up any port forwarding you specify, configure SSH etc
vagrant ssh # This will get you into your box

Once you are finished, simply vagrant halt (or vagrant suspend). To wipe the slate clean (and get rid of the box) vagrant destroy.

Set up your Chef provisioning run_list

This goes in chef_repo/nodes/default.json:

{
  "run_list": [
    "recipe[apt]",
    "recipe[ohai]",
    "recipe[build-essential]",
    "recipe[openssl]"
    .....
}

This is the key to setup of your Ubuntu server. There are infinite resources on the internet that will help configure the server - don't reinvent the wheel! I'll provide an overview of the recipes I use at the end of this post.

You can reference any recipe here that is located in chef_repo/cookbooks. They will run in the exact order you specify. That directory is intended for cookbooks that come directly off the internet (the equivalent of a vendor directory). It is intended that you put your own cookbooks in the site_cookbooks directory.

If you create a main cookbook, just place your instructions in the recipes/default.rb file. You can reference other recipes in the same folder from this file (via include_recipe). Alternatively you can include other recipes from this cookbook in your run list directly: "recipe[main::my_other_recipe]".

Deploy Locally

You will use the knife-solo gem to automate this process. You will use the same commands to deploy to your production server. First, create an SSH key on your local machine, and copy the public key over to the vagrant box. That will allow us to easily SSH into the vagrant box using the same commands as we would on the production machine. (Vagrant provides a handy vagrant ssh command which you can use to quickly SSH into the machine. I use this when playing around with the system, but I'd like to keep the actual deployment commands identical.)

cat ~/.ssh/vagrant_vm.pub | ssh [email protected] "cat >> ~/.ssh/authorized_keys; chmod 600 ~/.ssh/authorized_keys"

Vagrant boxes all have the vagrant user automatically configured. I use this for basic local deployment, but you'll likely need to set it up for a different user on the final box.

Next, we use knife prepare to copy the cookbooks to the remote machine.

knife prepare [email protected]

Then instruct chef to provision the server using the relevant node file:

knife cook [email protected] nodes/default.json

Finally we remove traces of the Chef config from the remote machine:

knife wash_up [email protected]

Creating a throwaway environment

Once you are happy with the local dev box, you can save tons of time by pre-packaging the system in order so that a fresh system is a simple as vagrant destroy vagrant up.

We will package up a box with our custom environment as follows:

vagrant package
vagrant box add my_box_name package.box
rm package.box
vagrant destroy

Now in your app project (Ruby on Rails or whatever), just throw in a VagrantFile that looks like the following:

Vagrant::Config.run do |config|
  config.vm.box = "my_box_name"
  config.vm.network :hostonly, "192.168.0.10"
  config.ssh.forward_agent = true
end

And you can vagrant up in your project file and then deploy to 192.168.0.10 with a tool like Capistrano.

Configuration of your Production Box

You'll need to get a VPS or dedicated host with one of the numerous companies on the internet. The only difference in deploying to your remote VPS is simply adjusting the knife-solo commands to reflect your VPS IP.

However, you'll probably want to run slightly different recipes on the remote VPS, especially if you plan to use separate boxes as an app server, database or web server. All you need to do is create a new run list in chef_repo/nodes/anything.json, and point to that file with knife-solo:

knife cook [email protected] nodes/anything.json

If you copy the run list directly from default.json, your production box will be an exact mirror of your Vagrant box. You should have precise dev/prod parity.

Ubuntu Server Recipes

The Chef world is huge - far more than I'd ever be able to cover here. Not only that, there are far more knowledgeable people than me that you should be listening to!

In any case - I use Chef provisioning to load the following onto the Ubuntu box:

{
  "run_list": [
    "recipe[apt]",
    "recipe[ohai]",
    "recipe[build-essential]",
    "recipe[openssl]",
    "recipe[git]",
    "recipe[user]",
    "recipe[logrotate]",
    "recipe[brandon::add_deployer_user]",
    "recipe[ruby_build]",
    "recipe[rbenv::vagrant]",
    "recipe[rbenv::user]",
    "recipe[brandon]",
    "recipe[fail2ban]",
    "recipe[brandon::fail2ban_setup]",
    "recipe[postgresql::server]",
    "recipe[postgresql::client]",
    "recipe[nginx::http_gzip_static_module]",
    "recipe[nginx::http_ssl_module]",
    "recipe[nginx::source]",
    "recipe[brandon::set_nginx_conf]",  // Has to come after installation of nginx
    "recipe[redis::server]",
    "recipe[brandon::upstart_recipes]", // Has to come after recipe[brandon] as monit must be installed already
    "recipe[brandon::logrotate]" // Leave at end to ensure logfile in place
  ],
  ....
  // Additional configuration options

Most of these packages are self explanatory. I use rbenv to manage Ruby installations on the server in order to keep it simple. I found it more confusing to run using rvm as a result of the shell modifications that were necessary.

The recipes inside the brandon cookbook include the following:

include_recipe "brandon::update_packages"
include_recipe "brandon::ensure_ruby_setup"
include_recipe "brandon::install_irb"
include_recipe "brandon::copy_dotfiles"
include_recipe "brandon::set_timezone"
include_recipe "brandon::install_nodejs" # Need a javascript runtime
include_recipe "brandon::set_up_postgres_hstore"
include_recipe "brandon::install_mongodb"
include_recipe "brandon::install_memcached"
include_recipe "brandon::install_monit"
include_recipe "brandon::firewall"

These are also fairly self-explanatory. If anyone wants to copy these recipes let me know.

Some of these will not be necessary when you deploy to your production box - e.g. rbenv::vagrant. This recipe puts wrappers around rbenv specifically for a Vagrant VM. Be sure to exclude this recipe from your nodes/list.json that deploys to production.

What's next?

There are a number of setup options that I have purposefully left out of Chef's scope. These are items that I feel I would want app-specific control over, for example Upstart and Monit configuration. The idea being that with simple node configuration, you can set up repeatable Ubuntu servers. Upstart and Monit configuration will depend heavily on what the node is being used for, and may even vary depending on the specific app. I include these recipes in my server's Capistrano setup - perhaps for my next post!

P.S. When running on your Vagrant VM, it may be a good idea to run as a Staging environment to customize how e-mail is sent, etc.

EDIT: Check out my next post for specifics on Chef recipes to bootstrap your Rails development server on Ubuntu.

Ruhoh is already incredibly easy to use. Make it that much easier.

Quickly Create a Draft and Prepare to Edit

Set an alias in your .zshrc or .bashrc file so that you can type draft and be away to the races. This requires a blog alias that goes to the root of your Ruhoh blog directory.

alias draft='blog && ruhoh draft && cd posts && ls -t | head -1 | xargs open && cd ../'

Commit new post information, push to your website and ping Google Webmaster with your Sitemap

Finished writing your draft post using the alias above? Make sure to put something in the title entry. Notice how your file name is still untitled-n.md (and not on your website)? That can be fixed with a simple rake task. Saves time and the need to remember to regenerate your RSS & Sitemap!

# /RakeFile

require 'open-uri'

namespace :website do

  # Make sure to change this!!
  sitemap_location = "http://brandonparsons.me/sitemap.xml"

  desc "Pings search engines with sitemap"
  task :ping_google do
    http = open("http://www.bing.com/webmaster/ping.aspx?siteMap=#{sitemap_location}")
    http = open("http://www.google.com/webmasters/sitemaps/ping?sitemap=#{sitemap_location}")
  end

  desc "Commits new post info, pushes website and pings google with sitemap"
  task :commit_posts_push_and_sitemap do
    branch = `git branch`
    if /master/i.match(branch)

      puts "Titleizing....\n"
      `ruhoh titleize`

      puts "Checking drafts.  If your recent post is in here, it won't get pushed online!\n***********\n\n"
      puts `grep draft posts/*`
      puts "\n*************\n"

      # Update sitemap
      # Run something here to update your local sitemap page

      # Update RSS
      # Run something here to update your RSS feed

      `git add .`
      `git add -u`
      `git commit -am 'Blog post update with sitemap push'`
      `git push origin master`

      puts "\n*********\nIf this is a new post, you may want to do 'rake website:ping_google'\n*******\n"
      # Rake::Task["website:ping_google"].invoke
    else
      puts "YOU WERE NOT ON MASTER BRANCH.  DID NOT DO ANYTHING"
    end
  end

end

Set that ask as your default rake task - just type rake to deploy.

task :default => 'website:commit_posts_push_and_sitemap'

Oops... You just closed the file you were working on?

Wouldn't it be useful to have a command to open up your most recently modified post?

alias last='blog && cd posts && ls -t | head -1 | xargs open && cd ../'

Don't forget to create the blog alias I was talking about...

I recently implemented social tracking on my blog - thought I would share the code. You can obviously find this elsewhere on the internet, but thought it would be handy for me to have in one place for other projects.

What I track

I track the following events:

  • Visits to social profiles (Twitter/Google Plus) as an event
  • Social shares (tweets, facebook 'likes' and Google +1's)
  • Twitter follows (via a twitter button)

The last two items are tracked using Google's "new" social tracking framework. Instead of doing it all via events, you can now take advantage of advanced integration in Google Analytics to try to measure the impact of social networks on your site.

The Code

Social Profile Link Events

Here is the code I use to push profile visits to Google Analytics as Events:

  $("#twitter-link").click(function() {
    _gaq.push(['_trackEvent', 'social', 'twitter-profile-visit']);
  });

  $("#googleplus-link").click(function() {
    _gaq.push(['_trackEvent', 'social', 'gplus-profile-visit']);
  });

  $("#linkedin-link").click(function() {
    _gaq.push(['_trackEvent', 'social', 'linkedin-profile-visit']);
  });

  $("#github-link").click(function() {
    _gaq.push(['_trackEvent', 'social', 'github-profile-visit']);
  });

  $("#feed-link").click(function() {
    _gaq.push(['_trackEvent', 'social', 'rss-subscribe']);
  });

Simply attach CSS ID's to your social profile buttons and you are off to the races (e.g. <a href="https://twitter.com/intent/user?screen_name=bkparso" id='twitter-link'><i class='icon-twitter-sign'></i></a>).

Next up is the code for integrating social shares with Google Analytics.

Social Share Integration

This is a bit more complicated than the event tracking as a result of needing to interact with the IFrames these buttons drop onto your site.

First you'll need to put some asynchronously executed javascript near the top of your page:

<!-- For Twitter Widgets -->
<script type="text/javascript" charset="utf-8">
  window.twttr = (function (d,s,id) {
    var t, js, fjs = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)) return; js=d.createElement(s); js.id=id;
    js.src="//platform.twitter.com/widgets.js"; fjs.parentNode.insertBefore(js, fjs);
    return window.twttr || (t = { _e: [], ready: function(f){ t._e.push(f) } });
  }(document, "script", "twitter-wjs"));
</script>

<!-- For Google +1 Button -->
<script type="text/javascript">
  window.___gcfg = {
    lang: 'en-US'
  };

  (function() {
    var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
    po.src = 'https://apis.google.com/js/plusone.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
  })();
</script>

<!-- For Facebook 'Like' button -->
<div id="fb-root"></div>
<script>(function(d, s, id) {
  var js, fjs = d.getElementsByTagName(s)[0];
  if (d.getElementById(id)) return;
  js = d.createElement(s); js.id = id;
  js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=330932300336907";
  fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>

You then drop in your standard social buttons HTML (you will want to use the XFBML version of the Facebook like button):

<table>
  <tbody>
    <tr>
      <td>
        <script src="http://connect.facebook.net/en_US/all.js#xfbml=1"></script>
        <fb:like send="false" layout="button_count" width="450" show_faces="false" width="100"></fb:like>
      </td>
      <td>
        <a href="https://twitter.com/share" class="twitter-share-button" data-lang="en" data-via="bkparso" data-count="none">Tweet</a>
      </td>
      <td>
        <g:plusone href='http://brandonparsons.me/about' annotation='none'></g:plusone>
      </td>
    </tr>
  </tbody>
</table>
</div>
<br />
<p style='padding-left:73px;'><a href="https://twitter.com/bkparso" class="twitter-follow-button" data-show-count="false">Follow @bkparso</a></p>

And now the complicated JavaScript (thanks Google!):

  FB.Event.subscribe('edge.create', function(targetUrl) {
    _gaq.push(['_trackSocial', 'facebook', 'like', targetUrl]);
  });

  FB.Event.subscribe('edge.remove', function(targetUrl) {
    _gaq.push(['_trackSocial', 'facebook', 'unlike', targetUrl]);
  });

  FB.Event.subscribe('message.send', function(targetUrl) {
    _gaq.push(['_trackSocial', 'facebook', 'send', targetUrl]);
  });

  function extractParamFromUri(uri, paramName) {
    if (!uri) {
      return;
    }
    var regex = new RegExp('[\\?&#]' + paramName + '=([^&#]*)');
    var params = regex.exec(uri);
    if (params != null) {
      return unescape(params[1]);
    }
    return;
  }

  function trackTwitterShare(intent_event) {
    if (intent_event) {
      var opt_pagePath;
      if (intent_event.target && intent_event.target.nodeName == 'IFRAME') {
            opt_target = extractParamFromUri(intent_event.target.src, 'url');
      }
      _gaq.push(['_trackSocial', 'twitter', 'tweet', opt_pagePath]);
    }
  }

  function trackTwitterFollow(intent_event) {
    if (intent_event) {
      var label = intent_event.data.user_id + " (" + intent_event.data.screen_name + ")";
      //pageTracker._trackEvent('twitter_web_intents', intent_event.type, label);
      _gaq.push(['_trackSocial', 'twitter', 'follow']);
    };
  }

  //Wrap event bindings - Wait for async js to load
  twttr.ready(function (twttr) {
    //event bindings
    twttr.events.bind('tweet', trackTwitterShare);
    twttr.events.bind('follow', trackTwitterFollow);
  });

As a final gotcha - since you are using the XFBML version of the Facebook like button, you'll need the following in your HTML tag:

<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:fb="http://ogp.me/ns/fb#">

(Note the xmlns:fb entitiy).

And we're done!

I've tested this out on my website and it all appears to work. You'll obviously need to adjust some of the code (e.g. to make sure your Facebook App ID is yours, not mine!).

Let me know if you see any issues with this.