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
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('[email protected]', 'First Last');
$mail->AddAddress('[email protected]', 'John Doe');
$mail->SetFrom('[email protected]', '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
**************************************************************************/
?>
