18
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

目的

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

構成図

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

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

image

DynamoDBのテーブル作成

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

Screen Shot 2016-07-24 at 09.21.50.png

My AccountでのIAMロール設定

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

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

Screen Shot 2016-07-24 at 09.31.26.png

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

Screen Shot 2016-07-24 at 09.32.52.png

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

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

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用のポリシーを作成します。

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」を選択します。

Screen Shot 2016-07-24 at 10.34.51.png

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

Screen Shot 2016-07-24 at 10.36.39.png

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

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

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

Screen Shot 2016-07-24 at 10.38.54.png

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

{
  "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する

コード

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にアップロードしてテスト実行してみます。

Screen Shot 2016-07-24 at 11.27.02.png

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

Screen_Shot_2016-07-24_at_11_28_50.png

まとめ

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

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

18
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?