Periodic snapshots of EBS volumes using Lambda

This use case follows along the same lines as the earlier one, where we take backup of our instances in the form of an AMI as well as conduct a snapshot of that instance's EBS volume if the instances have a tag named backup.

To get started, your Lambda function needs to have permissions to be able to create snapshots, create an AMI, as well as change some snapshot attributes, and so on. Here is a snippet of the function's IAM role that we have created for this exercise:

{ 
  "Version": "2012-10-17", 
  "Statement": [ 
  { 
    "Sid": "myEC2Permissions", 
    "Effect": "Allow", 
    "Action": [ 
      "ec2:Describe*" 
    ], 
    "Resource": [                  
      "*" 
    ] 
  }, 
  { 
    "Sid": "myEC2AMIPermissions", 
    "Effect": "Allow", 
    "Action": [ 
      "ec2:CreateImage", 
      "ec2:Describe*", 
      "ec2:ModifyImageAttribute", 
      "ec2:ResetImageAttribute" 
    ], 
    "Resource": [ 
      "*" 
    ] 
  }, 
  { 
    "Sid": "myEC2SnapshotPermissions", 
    "Effect": "Allow", 
    "Action": [ 
      "ec2:CreateSnapshot", 
      "ec2:ModifySnapshotAttribute", 
      "ec2:ResetSnapshotAttribute" 
    ], 
    "Resource": [ 
      "*" 
    ] 
  }, 
  { 
    "Sid": "myLogsPermissions", 
    "Effect": "Allow", 
    "Action": [ 
      "logs:CreateLogGroup", 
      "logs:CreateLogStream", 
      "logs:PutLogEvents" 
    ], 
    "Resource": [ 
      "*" 
    ] 
  } 
  ] 
} 

With the IAM role setup, let's quickly understand how the function code actually works out. The code maintains two arrays--one for instance IDs and the other comprising their corresponding EBS volume IDs:

Let's take a look at the function code itself now:

'use strict'; 
console.log('Loading function'); 
const aws = require('aws-sdk'); 
const async = require('async'); 
const ec2 = new aws.EC2({apiVersion: '2016-11-15'}); 
let instanceIDs =[]; 
let volumeIDs = []; 
function createImage(instanceID, createImageCB){ 
  let date =
new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); //console.log("AMI name: "+instanceID+'-'+date); let createImageParams = { InstanceId: instanceID, /* required */ Name: 'AMI-'+instanceID+'-'+date /* required */ }; ec2.createImage(createImageParams,
function(createImageErr, createImageData) { if (createImageErr){ console.log(createImageErr, createImageErr.stack);
// an error occurred createImageCB(createImageErr); } else{ console.log("createImageData: ",createImageData);
// successful response createImageCB(null, "AMI created!!"); } }); } function createSnapShot(volumeID, createSnapShotCB){ let createSnapShotParams = { VolumeId: volumeID, /* required */ Description: 'Snapshot of volume: '+volumeID }; ec2.createSnapshot(createSnapShotParams,
function(createSnapShotErr, createSnapShotData) { if (createSnapShotErr){ console.log(createSnapShotErr, createSnapShotErr.stack);
// an error occurred createSnapShotCB(createSnapShotErr); } else{ console.log("createSnapShotData: ", createSnapShotData);
// successful response createSnapShotCB(null , "SnapShot created!!"); } }); } exports.handler = (event, context, callback) => { instanceIDs = []; volumeIDs =[]; let describeTagParams = { Filters: [ { Name: "key", Values: [ "backup" ] } ] }; let describeVolParams = { Filters: [ { Name: "attachment.instance-id", Values: [] } ] }; ec2.describeTags(describeTagParams,
function(describeTagsErr, describeTagsData)
{ if (describeTagsErr){ console.log(describeTagsErr, describeTagsErr.stack);
// an error occurred callback(describeTagsErr); } else{ console.log("describe tags data: ",
JSON.stringify(describeTagsData));
// successful response for(let i in describeTagsData.Tags){ instanceIDs.push(describeTagsData.Tags[i].ResourceId); describeVolParams.Filters[0].Values.push(
describeTagsData.Tags[i].ResourceId); } console.log("final instanceIDs array: "+instanceIDs); console.log("final describeVolParams: ",describeVolParams); ec2.describeVolumes(describeVolParams,
function(describeVolErr, describeVolData) { if (describeVolErr){ console.log(describeVolErr, describeVolErr.stack);
// an error occurred callback(describeVolErr); } else{ console.log("describeVolData:",describeVolData);
// successful response for(let j in describeVolData.Volumes){ volumeIDs.push(describeVolData.Volumes[j].VolumeId); } console.log("final volumeIDs array: "+volumeIDs); async.parallel({ one: function(oneCB) { async.forEachOf(instanceIDs,function (instanceID,
key, imageCB)
{ createImage(instanceID, function(createImageErr,
createImageResult){ if(createImageErr){ imageCB(createImageErr); } else{ imageCB(null, createImageResult); } }); }, function (imageErr) { if (imageErr){ return oneCB(imageErr); } oneCB(null, "Done with creating AMIs!"); }); }, two: function(twoCB) { async.forEachOf(volumeIDs,
function (volumeID, key, volumeCB)
{ //console.log("volumeID in volumeIDs: "+volumeID); createSnapShot(volumeID, function(createSnapShotErr,
createSnapShotResult){ if(createSnapShotErr){ volumeCB(createSnapShotErr); } else{ volumeCB(null, createSnapShotResult); } }); }, function (volumeErr) { if (volumeErr){ return twoCB(volumeErr); } twoCB(null, "Done with creating Snapshots!"); }); } }, function(finalErr, finalResults) { if(finalErr){ callback(finalErr); } callback(null, "Done!!"); }); } }); } }); };

With the code and IAM roles created, you can now go ahead to create the trigger. We are going to trigger this function using a CloudWatch scheduled event. You can alternatively even use a cron job or rate expression, as per your requirements. For this exercise, we have gone ahead and created rate expression for simplicity, as shown here:

Make sure your trigger is set to ENABLED before testing the function out. To test the function, simply create a new instance with a tag of backup or create a new tag with the same backup name to an existing set of instances as well.

Once the function is triggered based on the rate expression or cron job execution, you should see new AMIs and EBS volume snapshots created, as shown in the following screenshot:

Here's a handy tip! Although we have created a really useful instance backup utility here, it's equally important to have an automatic backup deletion system in place as well; otherwise, your account will be soon flooded with too many copies of AMIs and EBS volumes, which will pile up on your overall costs.

With this use case completed, let's look at yet another simple yet useful infrastructure management use case that can potentially enable you to govern your AWS accounts much better, as well as cut some costs in the process.