リザーブドインスタンスの有効期限が近づいたらSlackで通知する（マルチアカウント）

# 目的
複数アカウントを管理していてそれぞれのアカウントが所有しているリザーブドインスタンスの更新を忘れないようにSlackに通知してくれるLambdaを作りました。
※そんな需要はあるのでしょうか。

# 構成図

構成はざっくり以下のようにしました。

１．Lambdaを毎日9時にCRON実行するように設定
２．DynamoDBからアカウント情報を取得
３．他アカウントのリザーブドインスタンス情報を取得するための一時クレデンシャルを発行
４．リザーブドインスタンス情報を取得
５．期限が10日以内に迫っていればSlack通知


![image](https://qiita-image-store.s3.amazonaws.com/0/40136/d1dcfb0f-c168-f21e-2c8b-650f30f429be.png)

# DynamoDBのテーブル作成

以下のようにアカウントIDとアカウント名を紐付けるためのテーブルを作成します。
今回はaccountNumをプライマリキーとして「sample-account-info」テーブルを作成しました。
キャパシティユニットはR/W共に1でもいいと思います。

<img width="764" alt="Screen Shot 2016-07-24 at 09.21.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/f43d4b0f-a8ad-7e32-d523-7897f6b9041c.png">



# My AccountでのIAMロール設定
Lambda用のロールを作成します。
My Account内でIAM-Rolesを選択し「Create New Role」をクリックします。

ロール名は「sample-Reserved-Check-Role」としています。

<img width="609" alt="Screen Shot 2016-07-24 at 09.31.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/4ed14fc9-7c3f-0e46-90af-006b7d64d01b.png">

Slect Role Typeで「AWS Lambda」を選択します。

<img width="986" alt="Screen Shot 2016-07-24 at 09.32.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/827065dc-125d-2233-af1d-c49e8b1a1f45.png">

Attach Policyでは何も選択せずそのまま「Next Step」で進みそのままロールを作成します。

続いてポリシーを作成します。
まずは、DynamoDBへのアクセスを許可するポリシーを作成します。

```json:Allow-Scan-DynamoDB
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1469187451000",
            "Effect": "Allow",
            "Action": [
                "dynamodb:Scan"
            ],
            "Resource": [
                "arn:aws:dynamodb:ap-northeast-1:<my accountのID>:table/sample-account-info"
            ]
        }
    ]
}
```

続いて、STS用のポリシーを作成します。

```json:Allow-AssumeRole
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1468935164000",
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```

このポリシーをsample-Reserved-Check-Roleにアタッチします。

# 他アカウントでのIAMロール作成

他アカウントにログインし、リザーブドインスタンス情報を取得するためのロールを作成します。
IAM-Rolesを選択し「Create New Role」をクリックします。

ロール名を入力し、Select Role Typeで「Role for Cross-Account Access」-「Provide access between AWS accounts you own」を選択します。

<img width="987" alt="Screen Shot 2016-07-24 at 10.34.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/3e897c91-7b8a-38d7-818e-c815884fc306.png">

Account IDにMy AccountのIDを入力してそのままロールを作成します。
ロール名は「ReservedCheck」としました。

<img width="563" alt="Screen Shot 2016-07-24 at 10.36.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/795850c5-28fc-cb46-bf67-787c6cb160c2.png">

作成したロールには以下のポリシーをアタッチします。

```json:Allow-ReservedInstance-Describe
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1468935045000",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeReservedInstances"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```

続いて、ロールの「Trust Relationships」タブを選択して「Edit Trust Relationship」をクリックします。

<img width="795" alt="Screen Shot 2016-07-24 at 10.38.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/800df1cb-2bd0-1f30-428d-16adf420d07b.png">

以下に設定しmy accountで作成したロールと信頼関係を構築します。

```json:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<my accountのID>:role/sample-Reserved-Check-Role"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```


# Slackのincoming webhookの設定

以下の記事を参考に設定し、Webhook URLを控えておいてください。

[Slackにincoming webhook経由でpythonからメッセージをPOSTする](http://qiita.com/satoshi03/items/14495bf431b1932cb90b)

# コード
```javascript:index.js
/* ====================================================================== */
/**
*  リザーブドインスタンスの期限を監視/通知
*
*/
/* ====================================================================== */
'use strict';

console.log('Loading function');
const AWS = require('aws-sdk');
const Slack = require('slack-node');
const sts = new AWS.STS();
const slack = new Slack();

// アラート通知日数設定（アラート通知日数を変更する場合はここの数字を変更してください。）
const alertDays = 10;
// 通知用チャンネル設定（通知先チャンネルを変更する場合はここを変更してください。）
const channel = "#samplechannel";
// エラー通知用チャンネル設定（エラー通知先チャンネルを変更する場合はここを変更してください。）
const errChannel = "#errchannel";
// webhook用URI
const webhookUri = "https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxx";
slack.setWebhook(webhookUri);

exports.handler = function(event, context, callback) {

	const generator  = (function *() {
		try {
			// DynamoDBからアカウント情報を取得
			const accountInfo = yield getAccountInfo(generator);

			// 各アカウントごとにリザーブドインスタンスの期限を取得
			for (let i = 0, max = accountInfo.length; i < max; i++) {
				// 対象アカウントに対する一時クレデンシャルを発行
				const stsCredentials = yield createStsCredential(accountInfo[i].accountNum, generator);

				// リザーブドインスタンスの期限情報を取得
				const expiredDateList = yield getReservedEndDate(stsCredentials, generator);

				// 有効期限が迫っているかチェック
				const upcomingDaysList = checkExpiredDate(expiredDateList);

				// 通知対象がある場合はSlack通知
				if (upcomingDaysList.length !== 0) {
					const message = createMessage(accountInfo[i].accountName, upcomingDaysList);
					postMessage(channel, message.text, message.messageAttachments);
				}
			}

			callback(null, "success");
		} catch (e) {
			postMessage(errChannel, e.message);
			callback(e);
		}
	})();

	/* 処理開始 */
	generator.next();
};

// S3から設定情報をGET
function getAccountInfo(generator) {

	const docClient = new AWS.DynamoDB.DocumentClient();
	const table = "sampletable";

	const dynamoParams = {
		TableName : table
	};

	docClient.scan(dynamoParams, function(err, data) {
		if (err) {
			console.log(err);
			generator.throw(new Error('DynamoDBへのアクセスでエラーが発生しました'));
			return;
		} else {
			generator.next(data.Items);
		}
	});
}

// 対象アカウントに対する一時クレデンシャルを発行
function createStsCredential(accountNum, generator) {
	// AssumeRole Params
	const stsParams = {
		RoleArn: 'arn:aws:iam::' + accountNum + ':role/ReservedCheck', /* 各アカウントに作成したロールを指定 */
		RoleSessionName: 'ReservedCheckOf' + accountNum
	};

	// 一時トークン発行
	sts.assumeRole(stsParams, function(err, data) {
		if (err){
			generator.throw(new Error('一時トークンの発行でエラーが発生しました'));
			return;
		}
		generator.next({
			accessKeyId : data.Credentials.AccessKeyId,
			secretAccessKey : data.Credentials.SecretAccessKey,
			sessionToken : data.Credentials.SessionToken
		});
	});
}

// リザーブドインスタンスの期限情報を取得
function getReservedEndDate(credentials, generator) {
	const ec2 = new AWS.EC2(credentials);

	const ec2Params = {
		DryRun: false
	};

	ec2.describeReservedInstances(ec2Params, function(err, data) {
		if (err) {
			generator.throw(new Error('リザーブドインスタンス情報の取得でエラーが発生しました'));
			return;
		}

		const expiredDateList = [];
		for (let i = 0, max = data.ReservedInstances.length; i < max; i++) {
			expiredDateList[i] = {
				instanceId : data.ReservedInstances[i].ReservedInstancesId,
				expiredDate : data.ReservedInstances[i].End
			};
		}

		generator.next(expiredDateList);
	});
}

// 有効期限が迫っているかチェック
function checkExpiredDate(expiredDateList) {

	const now = new Date();
	const upcomingDaysList = [];

	// リザーブドインスタンスの終了日を1つずつチェックする
	for (let i = 0, index = 0, max = expiredDateList.length; i < max; i++) {
		// リザーブド終了日との差分（日数）を計算
		const diff = expiredDateList[i].expiredDate - now;
		const diffDays = Math.round(diff / 86400000);

		// 期限がalertDays以内だったら通知リストに追加
		if (0 < diffDays && diffDays <= alertDays) {
			upcomingDaysList[index] = {
				instanceId : expiredDateList[i].instanceId,
				upcomingDays : diffDays
			};
			index++;
		}
	}
	return upcomingDaysList;
}

// Slack通知用メッセージ作成
function createMessage(accountName, upcomingDaysList) {

	const message = {
		text : accountName + 'のリザーブドインスタンスの期限が近づいています。',
		messageAttachments : []
	};

	for (let i = 0, max = upcomingDaysList.length; i < max; i++) {
		message.messageAttachments[i] = {
			color : 'danger',
			text : 'リザーブドID :' + upcomingDaysList[i].instanceId + 'が後' + upcomingDaysList[i].upcomingDays + ' 日で終わりを迎えます。'
		};
	}

	return message;
}

function postMessage(channel, text, attachments) {

	const slackParams = {
		channel : channel,
		username : "ReservedMaster",
		text : text,
		icon_emoji : ":aws:",
		attachments : attachments
	};

	console.log('Send Message');
	slack.webhook(slackParams, function(err, response) {
		if (err) {
		  throw new Error('メッセージの送信でエラーが発生しました');
		}
		console.log(response);
	});
}

```

# 試してみる

リザーブドインスタンスが10日以内で切れるものがタイミングよくないため、今回は1,000日以内に切れるものを通知するように設定しました。
（alertDaysを1000に変更）
Lambdaにアップロードしてテスト実行してみます。

<img width="1262" alt="Screen Shot 2016-07-24 at 11.27.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/3235c3c7-35e2-1bc0-0e01-27b6df673554.png">

指定したSlackチャンネルにメッセージが届きました！

<img width="648" alt="Screen_Shot_2016-07-24_at_11_28_50.png" src="https://qiita-image-store.s3.amazonaws.com/0/40136/8e8b88df-2553-af08-6438-c03db5c41313.png">

# まとめ

リザーブドインスタンスの期限が切れそうなときにSlackに通知してくれるようになったため、更新忘れが防止できると思います。
所有しているリザーブドインスタンスの数が多くなれば有用ではないでしょうか。

以上、何か間違いや改善点があればコメントください！