Friday, 1 October 2010

Control your EC2 instance with Google App Engine

Due to a lower budget I decided to move my applications to a micro instance.
To reduce costs even further, I thought about shutting down and restarting the instance on a daily basis. How to do that? Well, simple, just deploy a cron task to your App Engine application. Here is an example of a task file (cron.xml) :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<cronentries>
  <cron>
    <url>/manageInstancesController.json?action=startInstance
    <description>Starting instance
    <schedule>every monday, tuesday, wednesday, thursday, friday 08:00
    <timezone>Europe/London
  </cron> 
  <cron>
    <url>/manageInstancesController.json?action=shutdownInstance
    <description>Shutting down instance
    <schedule>every monday, tuesday, wednesday, thursday, friday 20:10
    <timezone>Europe/London
  </cron> 
</cronentries>



As you can see, the cron daemon calls two url's, namely,

/manageInstancesController.json?action=startInstance

and

/manageInstancesController.json?action=shutdownInstance.


We will need to implement the calls from the Spring controller to your EC2 account.
Beforehand, make sure you already have an active EC2 account and that you have an image you want to start. In my case I also need to attach an EBS volume to that instance.

For external connections, the GAE security model requires you to use the URLFetch utility. Hence The AWS API will not work for you. To connect to AWS, please download the following adapted AWS API jar here

Your web-inf/lib directory must contain the follwing libraries (they are all included in the distribution) :

appengine-api-1.0-sdk-1.3.5.jar
commons-codec-1.3.jar
commons-httpclient-3.0.1.jar
commons-logging-1.1.1.jar
jackson-core-asl-1.4.3.jar
stax-api-1.0.1.jar
stax-1.2.0.jar

Here is the implementation of the controller :

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Properties;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.PropertiesCredentials;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
import com.amazonaws.services.ec2.model.DetachVolumeRequest;
import com.amazonaws.services.ec2.model.DisassociateAddressRequest;
import com.amazonaws.services.ec2.model.EbsInstanceBlockDevice;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.InstanceBlockDeviceMapping;
import com.amazonaws.services.ec2.model.InstanceType;
import com.amazonaws.services.ec2.model.Placement;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.SecurityGroup;
import com.amazonaws.services.ec2.model.StopInstancesRequest;
import com.amazonaws.services.ec2.model.TerminateInstancesRequest;

public class ManageInstancesController extends MultiActionController {
 protected final Log logger = LogFactory.getLog(getClass());

    static AmazonEC2      ec2;
    static String publicIp = "yourIp";
    static String dataVolume = "yourVolume";
    static String imageId = "yourImage";
    static String availabilityZone = "eu-west-1a";
    static String accessKey = "yourAccessKey";
    static String secretKey = "yourSecretKey";
    static String keyName = "yourKeyName";

    private List getSecurityGroups(){
     List securityNames = new ArrayList();
     DescribeSecurityGroupsResult describeSecurityGroupsResult =  ec2.describeSecurityGroups();
  List securityGroups =  describeSecurityGroupsResult.getSecurityGroups();
  for (SecurityGroup securityGroup:securityGroups){
   securityNames.add(securityGroup.getGroupName());
  }
     return securityNames;
    }
    
 public ModelAndView startInstance(HttpServletRequest req,
   HttpServletResponse resp) throws Exception {
  logger.info("Logging to EC2 platform");  
        init();         
  logger.info("Logged to EC2 platform");

  RunInstancesRequest runInstancesRequest = new RunInstancesRequest();
  runInstancesRequest.setMaxCount(1);
  runInstancesRequest.setMinCount(1);
  
  runInstancesRequest.setKeyName(keyName);
  Placement placement = new Placement();
  placement.setAvailabilityZone(availabilityZone);
  runInstancesRequest.setInstanceType("t1.micro");
  runInstancesRequest.setSecurityGroups(getSecurityGroups());
  runInstancesRequest.setPlacement(placement);
  runInstancesRequest.setImageId(imageId);
  ec2.runInstances(runInstancesRequest);
        return null;  
 }

 
 
    private static void init() throws Exception { 
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        Properties properties = new Properties();
        properties.setProperty("accessKey", accessKey);
        properties.setProperty("secretKey", secretKey);
        properties.save(output, "");
        ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());        
        AWSCredentials credentials = new PropertiesCredentials(input);
        ec2 = new AmazonEC2Client(credentials);
        ec2.setEndpoint("https://eu-west-1.ec2.amazonaws.com");
    }
    
 public ModelAndView shutdownInstance(HttpServletRequest req,
   HttpServletResponse resp) throws Exception {
   logger.info("Logging to EC2 platform");  
         init();         
   logger.info("Logged to EC2 platform");
   
         try {
          
             DescribeInstancesResult describeInstancesRequest = ec2.describeInstances();
             List reservations = describeInstancesRequest.getReservations();
             Set instances = new HashSet();
             logger.info("You have " + reservations.size() + " Amazon EC2 reservation(s) running.");

             for (Reservation reservation : reservations) { 
              logger.info("You have reservation :" + reservation.getReservationId());
              instances.addAll(reservation.getInstances());
             }

             logger.info("You have " + instances.size() + " Amazon EC2 instance(s) running.");

             Map ebsMapping = new HashMap();
             for (Instance instance:instances){
              List blockList = instance.getBlockDeviceMappings();
              for (InstanceBlockDeviceMapping mapping:blockList){
               EbsInstanceBlockDevice device =  mapping.getEbs();
               String volume = device.getVolumeId();
               if (volume.equals(dataVolume))
                ebsMapping.put(volume, mapping.getDeviceName());
              }
              logger.info("Finding instance :" + instance.getInstanceId());
              logger.info("Image "+instance.getImageId());
              if ("running".equals(instance.getState().getName())){
               logger.info("Correct instance. Shutting down");    
               stopInstance(instance.getInstanceId());               
               releasePublicIP();
               detachAllDevices(instance.getInstanceId(),ebsMapping);
              } 

             }

             
             
         } catch (AmazonServiceException ase) {
          logger.error("Caught Exception: " + ase.getMessage());
          logger.error("Reponse Status Code: " + ase.getStatusCode());
          logger.error("Error Code: " + ase.getErrorCode());
          logger.error("Request ID: " + ase.getRequestId());
         }

  
        return null;  
 }
 
 private void stopInstance(String instanceId){
        TerminateInstancesRequest stopInstancesRequest = new TerminateInstancesRequest();
        List instanceIds = new ArrayList();
        instanceIds.add(instanceId);
        stopInstancesRequest.setInstanceIds(instanceIds);
        ec2.terminateInstances(stopInstancesRequest);
 }

 private void releasePublicIP(){
        DisassociateAddressRequest disassociateAddressRequest = new DisassociateAddressRequest();
        disassociateAddressRequest.setPublicIp(publicIp);
        ec2.disassociateAddress(disassociateAddressRequest);  
 }
 
 private void detachAllDevices(String instanceId,Map ebsMapping){
  for (Map.Entry entry:ebsMapping.entrySet()){
         DetachVolumeRequest detachRequest = new DetachVolumeRequest();
         detachRequest.setInstanceId(instanceId);
         detachRequest.setVolumeId(entry.getKey());
         detachRequest.setDevice(entry.getValue());
         ec2.detachVolume(detachRequest);    
  }
 }
 
 
 
}

About Me

My Photo