AWS Lambdaを使ってサーバーレスで起動するEC2のインスタンスタイプを監視する

起動するEC2のインスタンスタイプを監視する場合、定期的にEC2の情報を取得するなどをcronで設定すれば可能だと思いますが、サーバー管理がしたくないのでLambdaを使ってサーバーレスでEC2のインスタンスタイプを監視してみます。

本例ではEC2の起動をCloudTrailで検知し、Lambdaを実行してEC2の起動インスタンスタイプがt2.micro以外だった時にメールが届くようにします

# 処理の流れ

+ CloudTrail(AWS操作のログ監視)
+ CloudWatch,CloudWatchLogs(CloudTrailのログを保存。EC2起動のイベント「RunInstances」の場合にSNSへ通知するアラームを設定)
+ SNS(アラームを受けてLambdaを実行)
+ Lambda(現在起動しているEC2のリストを取得し、t2.microのものがある場合にSNSへ通知)
+ SNS(Lambdaからの実行によりメールを送信)

# メールを送信するSNSのトピックを作成する

Lambdaからメールを送るためのSNSトピックを作成します。

TopicNameは任意です。

今回の例では**MonitorEC2InstanceType**という名前で設定しました。

![Screen Shot 2015-07-05 at 8.14.42 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/1649a81c-815b-7c49-0590-b109d5839bb1.png "Screen Shot 2015-07-05 at 8.14.42 AM.png")


次に作成したトピックへの通知時にメールが送信される設定をします。
作成したトピックを選択し

+ Action->Subscribe to topic

を選択します。

![Screen Shot 2015-07-05 at 6.28.05 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/18eb55b7-d93e-2eb7-6af9-c02ce54549d7.png "Screen Shot 2015-07-05 at 6.28.05 PM.png")

Protocolで**Email**を選択し、Endpointで通知したいメールアドレスを入力します。Create Subscriptionによって登録したメールアドレスに確認メールが届くので到着したメールに記載されたリンクをクリックすると登録確認が完了となります。


# Lambdaファンクション実行用のロールの作成

今回の例ではLambdaから以下のAWSリソースにアクセスします。

+ CloudWatchLogs(Lambdaのログ保存)
+ EC2(EC2リストの取得)
+ SNS(トピックへのpublish)

上記より、事前にIAMより、上記へのアクセス可能なLambda用のロールを作成しておきます。

# Lambdaファンクションの作成

処理を実行するLambdaファンクションを作成します。

私の作成したものはGithubに配置しました。Gradleによってビルドが可能です。なお、通知するSNSを示す「private static final String TOPIC_ARN = "arn:aws:sns:ap-northeast-1:hogefuga";」という箇所は適宜先ほど作成したメール送信用のSNSのARNに設定変更してください。

[MonitorEc2InstanceType](https://github.com/toshihirock/MonitorEc2InstanceType)

処理概要としては以下の通りです。

+ SNSの通知を受ける
+ EC2のリストを取得し、t2.micro以外のリストがあるか確認
+ t2.micro以外のリストがある場合にSNSへ通知

以下コード

```lang:LambdaFunctionHandler.class
package com.sample.lambda;

import java.util.List;

import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
import com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord;
import com.amazonaws.util.json.JSONException;

public class LambdaFunctionHandler implements RequestHandler<SNSEvent, Object> {
	
	private static final String ACCEPTABLE_INSTANCE_TYPE = "t2.micro";
	private static final String TOPIC_ARN = "arn:aws:sns:ap-northeast-1:hogefuga";

	@Override
	public Object handleRequest(SNSEvent input, Context context) {
    	LambdaLogger logger = context.getLogger();
   		logger.log("start");
   		try {

   			logger.log("Input(json): " + input.toString());
    	
   			List<Instance> instances = Util.getInvalidEc2ist(ACCEPTABLE_INSTANCE_TYPE);
   			
   			if(instances == null || instances.isEmpty()) {
   				logger.log("don't exist invalid EC2 instance type.(EC2(" 
   			+ ACCEPTABLE_INSTANCE_TYPE + ") is acceptable.");
   				return "";
   			}

  			List<String> messageIds = Util.publish(instances, TOPIC_ARN);

    		if(messageIds == null || messageIds.isEmpty()) {
    			logger.log("cloud not publish SNS");
    			return "";
    		}

    		for( String messageId: messageIds) {
    			logger.log("messageId is " + messageId);
    		}
   		} catch (JSONException e) {
    		logger.log(e.getMessage());
            throw new RuntimeException(e);
		}

   		logger.log("end");
    	
    	return "OK";
	}
}
```

```lang:Util.java
package com.sample.lambda;

import java.util.ArrayList;
import java.util.List;

import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.model.PublishResult;
import com.amazonaws.util.json.JSONException;

public class Util {
    
    public static List<Instance> getInvalidEc2ist(String instanceType) throws JSONException {
    	List<Instance> instances = new ArrayList<Instance>();

    	AmazonEC2Client ec2 = new AmazonEC2Client();
    	ec2.setRegion(Region.getRegion(Regions.AP_NORTHEAST_1));
    	DescribeInstancesResult result = ec2.describeInstances();

    	for(Reservation reservation : result.getReservations()) {
    	  for(Instance instance : reservation.getInstances()) {
    		  if (!(instance.getInstanceType().equals(instanceType))) {
    			  instances.add(instance);
    		  }
    	  }
    	}
    	return instances;
    }
    
    public static List<String> publish(List<Instance> instances, String topicArn) throws JSONException {
    	List<String> messageIds = new ArrayList<String>();
    	
    	AmazonSNSClient sns = new AmazonSNSClient();
    	Region northEast1 = Region.getRegion(Regions.AP_NORTHEAST_1);
    	sns.setRegion(northEast1);
    	
    	for (Instance instance: instances) {
    		PublishResult result = sns.publish(topicArn,
    				"invalid EC2 instance type launched.Instance id = " 
    		+ instance.getInstanceId() + ", Instance Type = " + instance.getInstanceType());
    		messageIds.add(result.getMessageId());
    	}
    	return messageIds;
    }

}
```

`$gradle build`とすると**build/libs/MonitorEc2InstanceType-0.0.1-SNAPSHOT.jar**というjarファイルができているのでそれをLambdaのFunctionとして登録します。登録する際のHandlerには**com.sample.lambda.LambdaFunctionHandler::handleRequest**という形で登録します。

また、先ほど作成したLambda用のロールを付与忘れないようにしてください。

# Lambdaを実行するSNSのトピックの作成

先ほどと同じ要領でSNSからLambdaを実行するトピックを作成します。

やり方は同じですが、Protocolの選択の際にAWS Lambdaを選択し、Endpointでは先ほど作成したLambdaファンクションを選択して下さい。

![Screen Shot 2015-07-06 at 8.07.20 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/486aed68-5c83-3211-88e2-0d7216c844a7.png "Screen Shot 2015-07-06 at 8.07.20 AM.png")


# CloudTrailの有効化、CloudWatchLogsとの連携

CloudTrailを有効化していない場合、有効化してください。
また、CloudWatchLogsとの連携を有効化してください。

# CloudWatch,CloudWatchLogsの設定

CloudTrailで指定したCloudWatchLogsのLogGroupが存在するか確認します。

次にEC2起動のフィルタを作成します。
CloudWatchLogsと連携しているLogGroupの**0filters**となっている所を選択し、**Add Metric Filtter**を選択します。

![Screen Shot 2015-07-06 at 7.29.31 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/f6f405ac-8e5d-7b29-1114-45accca2721b.png "Screen Shot 2015-07-06 at 7.29.31 AM.png")


TestPatternには**{ $.eventName = "RunInstances" }**と入力することでEC2の起動を示すイベントの**RunInstances**のみをフィルタできます。

作成後、上記フィルタ発生時にSNSへ通知をするようにアラーム(Lambdaを実行するもの)を追加します。

LogGroupを再度確認すると0filtersが**1filters**となっているので選択します。すると以下のように先ほど作成したフィルタが確認できます。


![Screen Shot 2015-07-06 at 7.33.51 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/fca416cc-ba62-eba5-c946-3ba686908b4b.png "Screen Shot 2015-07-06 at 7.33.51 AM.png")

上記のCreate Alarmを選択してアラームを追加します。

![Screen Shot 2015-07-06 at 7.39.34 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/3cea73be-e982-06db-2692-a48a1a909f64.png "Screen Shot 2015-07-06 at 7.39.34 AM.png")


上記では5分ごとに1回以上RunInstaceメソッドが呼ばれた場合に先ほど作成したLambdaを呼び出すSNSを呼び出す設定としました。

# 確認

これで設定が終わったので内容を確認します。

正しく設定が出来ていればt2.micro以外のインスタンスが起動され、CloudTrailLogによってRunInstanceが記録されたタイミングでLambdaが実行され、以下のようなアラームメールが届くかと思います。(CloudTrailのログ書き込みは即時ではないので最大5分程度待つ必要があります。

```
invalid EC2 instance type launched.Instance id = i-a615db54, Instance Type = t2.small
```

# おまけ

CloudWatchLogsのフィルタを使ってEC2の起動時だけLambdaを実行するようにしてみましたが、その前にCloudTrailがS3にアップロードされた時点で都度そのログを解析し、SNS通知するものも事前に作ったので載せます。なお、これだとCloudTrailのアップロードの都度、Lambdaが実行され、ソースコードも増えるのであんまよくないと思います。。。

```lang:LambdaFunctionHandler.class
package com.sample.lambda;

import java.io.IOException;
import java.net.URLDecoder;
import java.util.List;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.s3.event.S3EventNotification.S3EventNotificationRecord;
import com.amazonaws.util.json.JSONException;
import com.amazonaws.util.json.JSONObject;

public class LambdaFunctionHandler implements RequestHandler<S3Event, Object> {
	
	private static final String ACCEPTABLE_INSTANCE_TYPE = "t2.micro";
	private static final String TOPIC_ARN = "arn:aws:sns:ap-northeast-1:hogefuga";

    @Override
    public Object handleRequest(S3Event s3event, Context context) {
    	LambdaLogger logger = context.getLogger();
   		logger.log("start");
    	try { 

    		logger.log("Input(json): " + s3event.toJson());
        
    		S3EventNotificationRecord record = s3event.getRecords().get(0);
    		String srcBucket = record.getS3().getBucket().getName();
    		String srcKey = record.getS3().getObject().getKey()
                .replace('+', ' ');
    		srcKey = URLDecoder.decode(srcKey, "UTF-8");
    		
    		logger.log("srcBucket = " + srcBucket + ", srcKey = " + srcKey);
    		
    		JSONObject cloudTrailLog = Util.getCloudTrailLog(srcBucket, srcKey);
    		
    		if(cloudTrailLog == null) {
    			logger.log("cloudTrailLog is null");
    			return "";
    		}
    		
  			logger.log("get cloudtrail log");
    		List<JSONObject> items = Util.getEc2ist(cloudTrailLog,
    				ACCEPTABLE_INSTANCE_TYPE);

    		if(items == null || items.isEmpty()) {
    			logger.log("don't exsits invalid EC2");
    			return "";
    		}
  			
  			List<String> messageIds = Util.publish(items, TOPIC_ARN);

    		if(messageIds == null || messageIds.isEmpty()) {
    			logger.log("cloud not publish SNS");
    			return "";
    		}

    		for( String messageId: messageIds) {
    			logger.log("messageId is " + messageId);
    		}

    	} catch (IOException e) {
    		logger.log(e.getMessage());
            throw new RuntimeException(e);
        } catch (JSONException e) {
    		logger.log(e.getMessage());
            throw new RuntimeException(e);
		}
        return "OK";
    }
}
```

``````lang:Util.java
package com.sample.lambda;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;

import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.model.PublishResult;
import com.amazonaws.util.json.JSONArray;
import com.amazonaws.util.json.JSONException;
import com.amazonaws.util.json.JSONObject;

public class Util {

    public static JSONObject getCloudTrailLog(String bucket, String key) throws IOException, JSONException {
    	JSONObject cloudTraillog = null;
    	
    	AmazonS3Client s3 = new AmazonS3Client();
    	Region northEast1 = Region.getRegion(Regions.AP_NORTHEAST_1);
    	s3.setRegion(northEast1);
    	
    	S3Object object = null;
    	object = s3.getObject(bucket, key);

    	if(object == null) return cloudTraillog;
    	
    	GZIPInputStream gzipInputStream = null;

    	try {
    		gzipInputStream = new GZIPInputStream(object.getObjectContent());
    		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    		for (;;) {
    			int iRead = gzipInputStream.read();
    			if (iRead < 0) break;
    			outputStream.write(iRead);
    		}
    		outputStream.flush();
    		outputStream.close();
    		cloudTraillog = new JSONObject(new String(outputStream.toByteArray()));
		} finally {
			if (gzipInputStream != null) {
				gzipInputStream.close();
				
			}
    	}
   		return cloudTraillog;
    }
    
    public static List<JSONObject> getEc2ist(JSONObject cloudTrailLog, String instanceType) throws JSONException {
    	List<JSONObject> instanceIds = new ArrayList<JSONObject>();
    	
    	JSONArray records = cloudTrailLog.getJSONArray("Records");

    	for(int i = 0;i < records.length(); i++) {
    	  JSONObject record = records.getJSONObject(i);
    	  if (record.getString("eventName").equals("RunInstances") 
    			  && record.has("responseElements")) {
    	    JSONObject item = record.getJSONObject("responseElements")
    	    		.getJSONObject("instancesSet").getJSONArray("items").getJSONObject(0);
    	    if (!(item.getString("instanceType").equals(instanceType))) {
    	    	instanceIds.add(item);
    	    }
    	  }
    	}
    	return instanceIds;
    }
    
    public static List<String> publish(List<JSONObject> items, String topicArn) throws JSONException {
    	List<String> messageIds = new ArrayList<String>();
    	
    	AmazonSNSClient sns = new AmazonSNSClient();
    	Region northEast1 = Region.getRegion(Regions.AP_NORTHEAST_1);
    	sns.setRegion(northEast1);
    	
    	for (JSONObject item: items) {
    		PublishResult result = sns.publish(topicArn,
    				"invalid EC2 instance type launched.Instance id = " 
    		+ item.getString("instanceId") + ", Instance Type = " + item.getString("instanceType"));
    		messageIds.add(result.getMessageId());
    	}
    	return messageIds;
    }

}
```