Automatically Take EBS Snapshots and Delete Old Ones with PHP Script

I am a big fan of Amazon’s RDS product, which takes automated nightly snapshots of your RDS Storage and deletes old snapshots after a specified amount of time. After reading that Amazon’s EBS drives sustain a 0.1% – 0.5% Annual Failure Rate, I also wanted to take automated nightly snapshots of my EBS drives. Deleting snapshots after a certain number of days was also of interest, because this way I am not over-paying for snapshot storage at Amazon or removing snapshots manually. I also wanted to email the results to myself after backups were taken.

I did some searching on Google and found a nice script by Chris at Applied Trust Engineering, Inc. that runs PHP from the command line to create an automatic snapshot. I used this script as an example to setup my script, so thanks, Chris. My script has support for:

  • Backup Multiple EBS Volumes
  • Protect against script running again and creating another snapshot too soon
  • Delete Snapshots after a Specified Period of Time
  • Script outputs Detailed Snapshot Information for these 5 categories:
    • Snapshots that succeeded
    • Snapshots that failed and had errors
    • Old Snapshots that were removed
    • Old Snapshots that had errors while trying to remove
    • Snapshots that were preserved
  • Includes PHPMailer code to email results of script to yourself

Script Setup

You will need: (all links open in new window)

  • My ebs_backup.php Code (download is a zip) (or look below)
  • Stores snapshot information in “./snapshot_information.json” – Make sure PHP can write this file
  • Configure the lines of code within the configuration comment blocks
  • Run script periodically to your needs with CRON or whatever
  • Requires AWS PHP SDK be configured for your AWS Account: http://aws.amazon.com/sdkforphp/
  • Optional PHPMailer Support to email results to yourself: http://phpmailer.worxware.com/ (configure PHPMailer at the very bottom of the script)

Screenshot of Output

Automatic EBS Snapshots

Source Code

snapshot_information.json: (just initialize it to an empty list and make sure PHP can write to it)

[ ]

ebs_backup.php:

<?

/**************************************************************************
|
|   Script to Automate EBS Backups
|   Run this script with CRON or whatever every X period of time to take
|   automatic snapshots of your EBS Volumes.  Script will delete old
|   snapshot after Y period of time
|   
|   Version 1.01 updated 2012-08-02
|
|   Copyright 2012 Caleb Lloyd
|
|
|   I offer no warrant or guarentee on this code - use at your own risk
|   You are free to modify and redistribute this code as you please
|
|   Requires AWS PHP SDK be configured for your AWS Account:
|       http://aws.amazon.com/sdkforphp/
|
|   Optional PHPMailer Support to email results to yourself
|       http://phpmailer.worxware.com/
|
|   Stores snapshot information in "./snapshot_information.json"
|       Make sure PHP can write this file
|
**************************************************************************/


/**************************************************************************
|   Begin Configuration
**************************************************************************/

//Declare the volumes that you want to backup
//The Volume ID's are the keys of the array, you can store any custom information you
//want in value array, or just keep it blank.  Make sure you keep it as a blank array
//because the script will fillthis up with values...
$volumes=array( 'vol-11111111'=>array(),
                'vol-22222222'=>array()
);

//Do not take a snapshot more than every X hours/minutes/days, etc. (uses strtotime)
//This prevents the script from running out of control and producing tons of snapshots
$snapshot_limit = '23 hours';

//Keep snapshots for this amount of time (also uses strtotime)
$keep_snapshots = '7 days 12 hours';

//Your path to the Amazon AWS PHP SDK
require_once 'path_to_aws_php_sdk/sdk.class.php';
//EC2 Region, view path_to_aws_php_sdk/services/ec2.class.php for definitions
$region='ec2.us-east-1.amazonaws.com';

//Your path to PHP Mailer (if you don't want to eamil yourself the results, you can get rid of this)
require_once('path_to_PHPMailer/class.phpmailer.php');
//Go to bottom of script to configure PHP Mailer settings


/**************************************************************************
|   End Configuration
**************************************************************************/

function snapshot_info($s)
{
    $info='<p>';
    $info.='Volume: '.$s['volume'].'<br />';
    $info.=(!empty($s['volume_name'])?'Volume Name: '.$s['volume_name'].'<br />':'');
    $info.=(!empty($s['snapshot'])?'Snapshot: '.$s['snapshot'].'<br />':'');
    $info.=(!empty($s['instance'])?'EC2 Instance: '.$s['instance'].'<br />':'');
    $info.=(!empty($s['device'])?'Device: '.$s['device'].'<br />':'');
    $info.=(!empty($s['error'])?'Error: '.$s['error'].'<br />':'');
    $info.=(!empty($s['datetime'])?'Date/Time: '.$s['datetime'].'<br />':'');
    $info.='</p>';
    return $info;
}

$success=array();
$failure=array();
$preserve=array();
$success_delte=array();
$failure_delete=array();

$ec2 = new AmazonEC2();
$ec2 = $ec2->set_region($region);

$latest_snapshot=array();

if (file_exists('snapshot_information.json'))
    $json=file_get_contents('snapshot_information.json');
else
    $json='[]';
$snapshots=json_decode($json,TRUE);
foreach ($snapshots as $s)
{
    if (!empty($lastest_snapshot[$s['volume']]))
    {
        if ($s['timestamp']>$lastest_snapshot[$s['volume']]['timestamp'])
        {
            $lastest_snapshot[$s['volume']]=$s;
        }
    }
    else
    {
        $lastest_snapshot[$s['volume']]=$s;
    }
}

foreach ($volumes as $volume => $v)
{
    $v['volume']=$volume;
    $v['instance']='Not Attached to an Instance';

    $volume_information = $ec2->describe_volumes(array('VolumeId' => $volume));
    $v['volume_name'] = '(volume has no tags)';
    if (!empty($volume_information->body->volumeSet->item->tagSet->item->value))
    {
        $v['volume_name'] = (string)$volume_information->body->volumeSet->item->tagSet->item->value;
    }
    $description = 'Volume '.$volume.(empty($v['volume_name'])?'':' ('.$v['volume_name'].')');
    
    if (!empty($volume_information->body->volumeSet->item->attachmentSet->item->status))
    {
        if ($volume_information->body->volumeSet->item->attachmentSet->item->status == "attached")
        {
            $v['device'] = (string)$volume_information->body->volumeSet->item->attachmentSet->item->device;
            $v['instance'] = (string)$volume_information->body->volumeSet->item->attachmentSet->item->instanceId;
            $description.=' attached to '.$v['instance'].' as '.$v['device'];
        }
    }
    else
    {
        $description.= ' ('.$v['instance'].')';
    }
    
    if ((!empty($lastest_snapshot[$volume]))&&($lastest_snapshot[$volume]['timestamp']>strtotime('-'.$snapshot_limit)))
    {
        $error=TRUE;
        $v['datetime']=date('Y-m-d H:i:s');
        $v['timestamp']=time();
        $v['error']='An Automatic Snapshot Already Exists for that volume in the past '.$snapshot_limit;
        $failure[]=$v;
    }
    else
    {
        $response = $ec2->create_snapshot($volume, array('Description'=>$description));
        if ($response->isOK())
        {
            $v['datetime']=date('Y-m-d H:i:s');
            $v['timestamp']=time();
            $v['snapshot']=(string)$response->body->snapshotId;
            $success[$v['snapshot']]=$v;
        }
        else
        {
            $error=TRUE;
            $v['datetime']=date('Y-m-d H:i:s');
            $v['timestamp']=time();
            $v['error']=(string)$response->body->Errors->Error->Message;
            $failure[]=$v;
        }
    }
}

if (!empty($snapshots))
{
    foreach ($snapshots as $snapshot => $s)
    {
        $s['snapshot']=$snapshot;
        if ($s['timestamp']<strtotime('-'.$keep_snapshots))
        {
            $response = $ec2->delete_snapshot($snapshot);
            if ($response->isOK())
            {
                $success_delete[$snapshot]=$s;
            }
            else
            {
                $error=TRUE;
                $s['error']=(string)$response->body->Errors->Error->Message;
                $failure_delete[$snapshot]=$s;
            }
        }
        else
        {
            $preserve[$snapshot]=$s;
        }
    }
    $snapshots_json=json_encode(array_merge($success,$preserve));
}
else
{
    $snapshots_json=json_encode($success);
}
file_put_contents('snapshot_information.json',$snapshots_json);

$message='';

if (!empty($success))
{
    $message.='<p><strong>The following Snapshots Succeeded:</strong></p>';
    foreach ($success as $v)
    {
        $message.=snapshot_info($v);
    }
}

if (!empty($failure))
{
    $message.='<p><strong>The following Snapshots Failed and had Errors:</strong></p>';
    foreach ($failure as $v)
    {
        $message.=snapshot_info($v);
    }
}

if (!empty($success_delete))
{
    $message.='<p><strong>The following old Snapshots were removed:</strong></p>';
    foreach ($success_delete as $v)
    {
        $message.=snapshot_info($v);
    }
}

if (!empty($failure_delete))
{
    $message.='<p><strong>The following old Snapshots had errors while trying to remove:</strong></p>';
    foreach ($failure_delete as $v)
    {
        $message.=snapshot_info($v);
    }
}

if (!empty($preserve))
{
    $message.='<p><strong>The following Snapshots were preserved:</strong></p>';
    foreach ($preserve as $v)
    {
        $message.=snapshot_info($v);
    }
}

echo $message;


/**************************************************************************
|   Begin PHPMailer Script
|   Remove Below This Line if you don't want to EMail results to yourself
|   This is the SMTP Example
|   For other examples in PHPMailer, go to path_to_PHPMailer/examples
**************************************************************************/

$mail = new PHPMailer(true); // the true param means it will throw exceptions on errors, which we need to catch

$mail->IsSMTP(); // telling the class to use SMTP

try {
  $mail->Host       = "mail.yourdomain.com"; // SMTP server
  $mail->SMTPDebug  = 2;                     // enables SMTP debug information (for testing)
  $mail->SMTPAuth   = true;                  // enable SMTP authentication
  $mail->Host       = "mail.yourdomain.com"; // sets the SMTP server
  $mail->Port       = 26;                    // set the SMTP port for the GMAIL server
  $mail->Username   = "yourname@yourdomain"; // SMTP account username
  $mail->Password   = "yourpassword";        // SMTP account password
  $mail->AddReplyTo('name@yourdomain.com', 'First Last');
  $mail->AddAddress('whoto@otherdomain.com', 'John Doe');
  $mail->SetFrom('name@yourdomain.com', 'First Last');
  
  $mail->Subject = 'EBS Snapshot Backup Information for '.date('Y-m-d').' - '.($error?'ERRORS':'Success');
  $mail->MsgHTML($message);
  
  $mail->Send();
  echo "Message Sent OK</p>\n";
} catch (phpmailerException $e) {
  echo $e->errorMessage(); //Pretty error messages from PHPMailer
} catch (Exception $e) {
  echo $e->getMessage(); //Boring error messages from anything else!
}

/**************************************************************************
|   End PHPMailer Script
|   Remove Above This Line if you don't want to EMail results to yourself
**************************************************************************/
?>