Ruby, JavaScript, Sass, iOS. Stinky Cheese & Beer Advocate. Working at CustomInk and loving it!

Automating Heroku PG Backups

On December 1st, Heroku deprecated their bundles add-on in favor of their new PG Backups. Even though there are other solutions for automating backups using this new add-on, none of them met my needs. I like to have a daily DB backup history, just in case you find something bad that happened "n" days earlier. Below is a simple rake task suitable to place in your rails lib/tasks/heroku.task file. I'll explain some things I learned below when writing this.

require 'heroku'
require 'heroku/command'

HEROKU_BACKUP_BUCKET = "#{Heroku::Command::Base.selected_application}-backups"

namespace :heroku do

  desc "Use the `heroku pgbackups` with my S3 bucket."
  task :backup => :connect_to_s3 do
    info = capture_heroku_command 'pgbackups'
    if heroku_existing_backup?(info)
      last_backup_info = info.split("\n").last.split(" | ")
      last_backup_id = last_backup_info[0]
      last_backup_time = last_backup_info[1]
      puts "Deleting last backup - ID: #{last_backup_id} BackupTime: #{last_backup_time}"
      heroku_command 'pgbackups:destroy', last_backup_id
    end
    heroku_command 'pgbackups:capture'
    backup_url = capture_heroku_command 'pgbackups:url'
    backup_filename = "#{Heroku::Command::Base.selected_application}_#{Time.now.xmlschema}.dump"
    backup_data = Net::HTTP.get_response(URI.parse(backup_url)).body
    AWS::S3::S3Object.store backup_filename, backup_data, HEROKU_BACKUP_BUCKET
  end

  task :connect_to_s3 do
    AWS::S3::Base.establish_connection!(
      :access_key_id     => ENV['AMAZON_ACCESS_KEY_ID'],
      :secret_access_key => ENV['AMAZON_SECRET_ACCESS_KEY'])
    begin
      AWS::S3::Bucket.find(HEROKU_BACKUP_BUCKET)
    rescue AWS::S3::NoSuchBucket
      AWS::S3::Bucket.create(HEROKU_BACKUP_BUCKET)
    end
  end

end

private

def heroku_command(*cmds)
  Heroku::Command::Base.command(*cmds)
end

def capture_heroku_command(*cmds)
  stdout = STDOUT
  StringIO.new.tap do |out|
    def out.flush ; end
    $stdout = out
    heroku_command(*cmds)
  end.string.chomp
ensure
  $stdout = stdout
end

def heroku_existing_backup?(info)
  info !~ /no backups/i
end

Once installed you can run something like bundle exec rake heroku:backup or if you are not using bundler, rake heroku:backup. Assuming you have your S3 credentials setup in the ENV variables, it will find or create a private bucket on your S3 account and upload an app-named and time-stamped dump file to that new bucket. If necessary, it will delete your latest backup on Heroku to make room for this new one. The code should be easy to change, so flavor to taste.

I tried to use the Heroku commands built into their plugin without resorting to command interpolation. The only problem was that the Heroku gem always wants to print to standard out and flush the buffer. So I created a few private helper methods that temporarily shim in a $stdout replacement that does not flush. This let's me run the Heroku commands from code and capture what would have been printed to standard out.

Lastly, since I could not find a way to automate this on Heroku via their cron add-on, I simply added a launchd plist to my desktop Mac that hit a shell script in my project folder to run the rake task. It is way past my skills to try and get RVM to work in the launchd.plist system since it is not a true shell. This is why the shell script uses my system ruby (installed via MacPorts). Here is the shell script below and the launchd plist which I placed in ~/Library/LaunchAgents with a name like com.actionmoniker.backupMyApp.plist. Just run launchctl load ~/Library/LaunchAgents/com.actionmoniker.backupMyApp.plist and this will run at 4am every morning. If any one finds out how to automate the execution of this rake task on Heroku, please drop me a line!

#! /bin/zsh
source /Users/kencollins/.zshenv
cd /Users/kencollins/repos/myapp && /opt/local/bin/rake heroku:backup
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.actionmoniker.backupMyApp</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/kencollins/repos/myapp/lib/bin/backup.sh</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Minute</key>
    <integer>0</integer>
    <key>Hour</key>
    <integer>4</integer>
  </dict>
</dict>
</plist>