Thursday, April 16, 2009

Programmatically monitoring your EC2 instances

The EC2 C# library provides several classes that allow you to monitor and pass commands to your running instances and volumes. Each request class is coupled with a response class that is returned as an instance when you make a functions call using an instance of the EC2Service class.

The EC2Service class is the controller trough which requests and responses are passed. Classes used to monitor running instances use the Describe prefix, such as the DescribeVolumesRequest or DescribeInstancesRequest classes. Note that both the DescribeInstancesRequest and the DescribeVolumesRequest class expect lists of IDs. This means that you can request status reports from multiple as well as single instances and volumes.

I have created a wrapper class that can be used for the following:

1. Initialize an EC2 instances using an existing AMI.
2. Terminate an EC2 instance using an existing AMI.
3. Attach an EBS volume.
4. Detach an EBS volume.
5. Monitor the EBS volume attachment process.
6. Monitor the EBS volume detachment process.
7. Monitor the EC2 instance startup process.
8. Monitor the EC2 instance closing process.

EC2 wrapper:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Amazon.EC2;
using Amazon.EC2.Model;

namespace consoleEBS_And_EC2
{
class EC2Wrapper
{

private AmazonEC2 service;
private string _instance;
public string instance { get { return _instance; } set { _instance = value; } }
private bool _isRunning = false;
public bool isRunning { get { return _isRunning; } set { _isRunning = value; } }
private bool _volAttached;
public bool volAttached { get { return _volAttached; } set { _volAttached = value;} }
private string _volID;
public string volID { get { return _volID ;} set { _volID = value;} }
private string _runReservationId;
public string runReservationId { get { return this._runReservationId; } set { this._runReservationId = value;} }
private bool _detaching = false;
public bool detaching { get { return _detaching;} set {_detaching = value;} }
private bool _attaching = false;
public bool attaching { get { return _attaching; } set { _attaching = value ;} }





public EC2Wrapper(string accessKeyId, string secretAccessKey,string amiId)
{
try
{
service = new AmazonEC2Client(accessKeyId, secretAccessKey);


runYourInstances(amiId);



}
catch (Exception ex)
{
//Call logger here, then throw user friendly message.
//throw new Exception("Your instance did not start. Check your eventlog for details.");
throw ex;
}
}

private void runYourInstances(string amiId)
{
RunInstancesRequest request = new RunInstancesRequest();
////Set allowed number of instances:
request.MinCount = 1;
request.MaxCount = 1;
request.InstanceType = "m1.small";
//request.
Placement p = new Placement();
p.AvailabilityZone = "us-east-1a";
request.Placement = p;
request.ImageId = amiId;
RunInstancesResponse risp = this.service.RunInstances(request);
this.runReservationId = risp.RunInstancesResult.Reservation.ReservationId;
this.instance = (from r in risp.RunInstancesResult.Reservation.RunningInstance where risp.RunInstancesResult.Reservation.ReservationId == this.runReservationId select r.InstanceId).FirstOrDefault();

}
/// <summary>
/// Monitors running request until it either the timeout has elapsed (12 minutes) or the status is running.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public bool monitorStartingProcess()
{

int timeOutIterations = 0;
try
{

while (true)
{
RunningInstance ri = getRunningInstance();
if (ri != null)
{
this.isRunning = true;
break;
}
System.Threading.Thread.Sleep(1000);
timeOutIterations += 1;
if (timeOutIterations == 720)
break;

}
}
catch (Exception ex)
{
throw ex;
}
return this.isRunning;
}

private RunningInstance getRunningInstance()
{
DescribeInstancesRequest rs = new DescribeInstancesRequest();
rs.InstanceId = new List<string>() { this.instance };
DescribeInstancesResponse rsp = service.DescribeInstances(rs);
Reservation res = (from r in rsp.DescribeInstancesResult.Reservation where r.ReservationId == this.runReservationId select r).FirstOrDefault();
RunningInstance ri = (from r in res.RunningInstance where r.InstanceId == this.instance && r.InstanceState.Name == "running" select r).FirstOrDefault();

return ri;
}
/// <summary>
/// Terminates running instance.
/// </summary>
public void terminateRunning()
{
try
{
TerminateInstancesRequest rq = new TerminateInstancesRequest();
rq.InstanceId.Add(this.instance);
TerminateInstancesResponse rsp = this.service.TerminateInstances(rq);
this.isRunning = false;
}
catch (Exception ex)
{
throw ex;
}
}

public bool Attach_EBS_Volume(string id)
{
bool result = false;
AttachVolumeRequest vrq = new AttachVolumeRequest();
this.volID = id;
vrq.VolumeId = this.volID;
vrq.InstanceId = this.instance;
vrq.Device = "/dev/sda";
AttachVolumeResponse vrp = this.service.AttachVolume(vrq);
if (vrp.AttachVolumeResult.Attachment.Status == "attaching")
{
result = true;
this.attaching = true;
}
else
result = false;
return result;
}



public bool monitorVolumeAttachment()
{

int timeOutIterations = 0;
try
{

while (true)
{
DescribeVolumesRequest rs = new DescribeVolumesRequest();
rs.VolumeId = new List<string>() { this.volID };
DescribeVolumesResponse rsp = service.DescribeVolumes(rs);
Volume vol = (from v in rsp.DescribeVolumesResult.Volume where v.VolumeId == this.volID && v.Status == "in-use" select v).FirstOrDefault();
if (vol != null)
{
this.volAttached = true;
this.attaching = false;
break;
}
System.Threading.Thread.Sleep(1000);
timeOutIterations += 1;
if (timeOutIterations == 720)
break;

}
}
catch (Exception ex)
{
throw ex;
}
return this.isRunning;
}

public bool Detach_EBS_Volume()
{
bool result = false;
DetachVolumeRequest dvr = new DetachVolumeRequest();
dvr.InstanceId = this.instance;
dvr.Device = "/dev/sda";
dvr.Force = false;
dvr.VolumeId = this.volID;
DetachVolumeResponse dvs = this.service.DetachVolume(dvr);
if (dvs.DetachVolumeResult.Attachment.Status == "detaching")
{
result = true;
this.detaching = true;
}
else
result = false;
return result;
}


public bool monitorDetachVolumeRequest(string id)
{
int timeOutIterations = 0;
try
{

while (true)
{
DescribeVolumesRequest rs = new DescribeVolumesRequest();
rs.VolumeId = new List<string>() { id };
DescribeVolumesResponse rsp = service.DescribeVolumes(rs);
Volume vol = (from v in rsp.DescribeVolumesResult.Volume where v.Status == "available" && v.VolumeId == id select v).FirstOrDefault();
if (vol != null)
{
this.volAttached = false;
this.detaching = false;
break;
}
System.Threading.Thread.Sleep(1000);
timeOutIterations += 1;
if (timeOutIterations == 720)
break;

}
}
catch (Exception ex)
{
throw ex;
}
return this.volAttached;
}

public static bool isInstanceRunning(string id,string accesskey,string secretkey)
{
bool result = false;
AmazonEC2 ae = new AmazonEC2Client(accesskey,secretkey);
DescribeInstancesRequest rs = new DescribeInstancesRequest();
rs.InstanceId = new List<string>() { id };

DescribeInstancesResponse rsp = ae.DescribeInstances(rs);


var res = (from r in rsp.DescribeInstancesResult.Reservation select new { state =
(from ri in r.RunningInstance where ri.InstanceId == id && ri.InstanceState.Name == "running" select ri.InstanceState.Name.ToString()).FirstOrDefault()});
foreach (var inst in res)
if ((string) inst.state == "running")
result = true;
return result;

}
}
}


Invocation of wrapper:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace consoleEBS_And_EC2
{
class Program
{

static void Main(string[] args)
{

//Ubuntu 8.10 image, can use the small instance type which will save us money.
String ami = "ami-7dfd1a14";
string EBS_id = null;
try
{

Console.WriteLine("Type accessKeyId: ");
string accessKeyId = Console.ReadLine();
Console.WriteLine("Type secretAccessKey: ");
string secretAccessKey = Console.ReadLine();
EC2Wrapper wrp = new EC2Wrapper(accessKeyId, secretAccessKey, ami);
Console.WriteLine("Instance start request sent");
loopDescribeInstance(wrp, ami);
if (wrp.isRunning)
{
Console.WriteLine("Type the Id of the EBS volume you want to attach: ");
EBS_id = Console.ReadLine();
if (wrp.Attach_EBS_Volume(EBS_id))
{
Console.WriteLine("Volume attach request sent");
if (wrp.monitorVolumeAttachment())
{
Console.WriteLine("Volume attached successfully");
System.Threading.Thread.Sleep(20000);
if (wrp.Detach_EBS_Volume())
{
Console.WriteLine("Volume detached request sent");
if (!wrp.monitorDetachVolumeRequest(EBS_id))
{
Console.WriteLine("Volume detached successfully");
}
else
{
Console.WriteLine("Detach operation failed");
}
}
else
{
Console.WriteLine("Detach request failed");
}
}
else
{
Console.WriteLine("Attach operation timed out");
}
}
else
{
Console.WriteLine("Volume attach request failed");
}
}

Stop(wrp);
Console.ReadLine();
}
catch (Amazon.EC2.AmazonEC2Exception aex)
{
Console.WriteLine(aex.ToString());
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}

}

static void loopDescribeInstance(EC2Wrapper w, string id)
{

try
{

if (w.monitorStartingProcess())
{
Console.WriteLine("Instance started");

}
else
{
Console.WriteLine("Instance not started");

}

}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}

}

static void Stop(EC2Wrapper srvc)
{
srvc.terminateRunning();
Console.WriteLine("Running instance stopped");
}

}
}


Note that the EC2Wrapper class only allows us to start one instance, and that it has two properties called attaching or detaching which are used to indicate whether an instance is in the process of attaching or detaching an EBS volume. Once the volume is attached or detached, we leave the transitional state and the attaching and detaching properties are set to 'false'.

A drawback to the EC2 C# library is that it only contains one exception class, Amazon.EC2.AmazonEC2Exception, but the error codes of this class are fairly informative so it would be totally feasible to trap AmazonEC2Exceptions, read the error code and then throw our own custom exceptions based on the error code.