プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

リザーブドインスタンスの有効期限切れを通知

表題の件。
リザーブドインスタンス(以下、RI と呼びます)の有効期限切れを通知する機能ですが、結論からすると、AWS コンソールで実現できます。

docs.aws.amazon.com

これですね。

今年(2019年)の5月から提供開始された機能のようです。

AWS Cost Explorer で予約の有効期限切れアラートを提供開始

Cost Explorer を開いて、ドキュメント通り操作すれば、メールを送ってくれます。

f:id:junichim:20191207212242p:plain

設定画面はこんな感じです。

通知を送る日は、7日前、30日前、60日前と決められた選択肢しかありませんが、まあ十分でしょう。 おまけに、組織のマスターアカウントであれば、連結アカウントで運用している RI 期限切れの通知もしてくれます。 願ったりなかったりですね。

というわけで、当初、これを自分でやってみようと思い、あれこれしたのですが、いまや不要になってしまいました(Cost Explorer のデフォルトの通知日数はどうしても嫌だ、とかメールではなく別の方法で送りたい、という場合には役立つかもしれませんが)。

とはいえ、もう組んでしまったので、紹介だけしておきます。

方針

RI の期限切れ、ないと不便だろうなと思い調べてみると、やはり似たようなことを考える人はいるもので、こちらが大変参考になりました。

qiita.com

今回は次のような感じになります。

  • 複数のアカウントをチェック
  • EC2 および RDS のリザーブドインスタンス
  • メールを送信(SNS経由で送信)

以下、実装のご紹介です。

EC2 の RI の有効期限の取得

EC2 の RI の有効期限を取得するのは簡単です。

async function getActiveEC2ReserveInstances(ec2) {
    let param = {
        Filters: [{
            Name: "state",
            Values: ["active"]
        }],
    };
    console.log("getActiveEC2ReserveInstances param: " + JSON.stringify(param));

    return new Promise(function(resolve, reject) {
        ec2.describeReservedInstances(param, function(err, data){
            if (err) {
                console.error("describeReservedInstances: " + JSON.stringify(err));
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

ここで、 ec2 オブジェクトは new AWS.EC2 で生成したものとしています。

いろいろとやり方はあると思いますが、上記では state が active なものに限定して取得しています。 いまから有効期限が切れようとしているものなので稼働中のもののみを対象とするということです。

取得した data.ReservedInstances[].End に RI の有効期限の末日が入っています。

RDS の RI の有効期限の取得

RDS も RI の情報を取得するだけなら簡単なのですが、EC2の場合とことなり、RDSで取得する場合は Filters が使えません。

なので、一旦すべての RI の情報を取得してから、自前で state が active なものを取り出します。こんな感じです。

async function getActiveRDSReserveInstances(rds) {
    let result = [];
    let data = await getRDSReserveInstances(rds);
    for (let instance of data.ReservedDBInstances ) {
        if (instance.State === "active") {
            result.push(instance);
        }
    }
    return result;
}
async function getRDSReserveInstances(rds) {
    return new Promise(function(resolve, reject) {
        rds.describeReservedDBInstances({}, function(err, data){
            if (err) {
                console.error("describeReservedDBInstances: " + JSON.stringify(err));
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

EC2 の場合と似たように、rds オブジェクトは new AWS.RDS で生成したものとしています。

RDS の場合は、Endが直接入っていないので、 StartTime と Duration から End を求めておきます。

async function getRDSReserveInstancesExpireDate(rds) {
    let result = [];
    let instances = await getActiveRDSReserveInstances(rds);
    console.log("active RDS RIs:" + JSON.stringify(instances));

    for (let instance of instances) {
        let edDt = new Date(instance.StartTime);
        edDt.setDate(edDt.getDate() + (instance.Duration / SEC_OF_DAY));

        let res = {
            resoruceType: "RDS",
            reservedInstanceId: instance.ReservedDBInstanceId,
            endDate: edDt,
        }
        result.push(res);
    }
    return result;
}

クロスアカウントアクセスの設定

さて、RI の有効期限が取得できるのはわかったので、異なるアカウントが持つ RI の情報をどう取得するかです。 元ネタとさせていただいた記事にもありますが、これは、クロスアカウントアクセスを設定することで実現できます。

クロスアカウントアクセスってややこしそうですが、まず基本の考えを下記などで紹介されている、IAM ユーザーによるクロスアカウントアクセスで理解しておきます。

わかりやすい記事がたくさんあるので、ここでは設定方法は省略します。

わかりにくいな?と思ったら、一度 AWS コンソール上で実際に試してみるとよく理解できると思います。

Lambda でのクロスアカウントアクセスの設定

さて、上で挙げたクロスアカウントアクセスでは、 あるアカウントの IAM ユーザーが別アカウントのロールに切り替えていました。 Lambda でこれと同じことを行うためには、 IAM ユーザーの代わりに、 Lambda 関数に設定している実行ロールに、切り替え許可(assumeRole)を与えればOKです。

具体的には IAM ユーザーの時と同様に、下記のようなポリシーを定義して、

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::別アカウントのアカウントID:role/切り替え先のロール名"
        }
    ]
}

Lambdaの実行ロールに当てればOKです。

Lambda でのクロスアカウント呼び出し

上記の設定ができれば、 Lambda でのクロスアカウント呼び出しが行えるようになります。

RI の情報を取得したい別アカウントの情報を下記のように定義しておきます。

let accountInfo = {
    accountName: "わかりやすいアカウント名",
    accountId: "アカウントID",
    remoteRole: "取得したいアカウントでのロールのARN"
};

これを使い、 STS の assumeRole を呼び、一時クレデンシャルを取得します。

async function getSTSCredential(accountInfo) {
    const sts = new AWS.STS();

    let param = {
        RoleArn: "arn:aws:iam::" + accountInfo.accountId + ":role/" + accountInfo.remoteRole,
        RoleSessionName: "cross_account_by_lambda",
    };
    console.log("getSTSCredential param: " + JSON.stringify(param));
    return new Promise(function(resolve, reject) {
        sts.assumeRole(param, function(err, data) {
            if (err) {
                console.error("getSTSCredential: " + JSON.stringify(err));
                reject(err);
            } else {
                resolve(data.Credentials);
            }
        });
    });
}

一時クレデンシャルが取得できれば、それを使って、AWS.EC2 や AWS.RDS クラスを生成します。

            // STS 経由でcredentialsを取得
            let credentials = await getSTSCredential(accountInfo);
            let param = {
                accessKeyId : credentials.AccessKeyId,
                secretAccessKey : credentials.SecretAccessKey,
                sessionToken: credentials.SessionToken,
            };
            console.log("credentials from STS: " + JSON.stringify(param));

            ec2 = new AWS.EC2(param);
            rds = new AWS.RDS(param);

これらのオブジェクトを使うことで、別アカウント上の RI の情報を取得することができます。

下記記事などにもクロスアカウントでの呼び出し例が載ってますので参考にしてください。

SNS 経由でのメールの送信

取得した RI の期限を調べて、期限が近付いていれば、SNS経由でメールを投げます。

NOTIFY_TO_SNS_TOPIC にSNSのトピック名が設定されており、このトピックのサブスクリプションとして、メールが設定されているとしています。

async function notifyViaSNS(accountName, cand) {
    const sns = new AWS.SNS();

    let param = {
        Subject: "Reserved Instance will be expired soon.",
        Message: 
            "Account Name: " + accountName + 
            "\nResource Type: " + cand.resoruceType + 
            "\nReserved Instance Id: " + cand.reservedInstanceId + 
            "\nEnd date: " + cand.endDate,
        TopicArn: NOTIFY_TO_SNS_TOPIC,
    };
    console.log("notifyViaSNS param: " + JSON.stringify(param));

    return new Promise(function(resolve, reject) {
        sns.publish(param, function(err, data){
            if (err) {
                console.error("publish: " + JSON.stringify(err));
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

上記のように publish を呼び出すことでメールを送ることができます。 Lambda でこれを動かすには、 Lambda のロールにも SNS の publish 呼び出しの権限を与えておく必要があります。

その他実装上の注意点

今回はこの Lambda を動かすアカウントにも EC2 の RI があるので、こちらも併せてチェックするようにしておきます。

あと、自分のアカウント以外にチェックするアカウントに関する情報は配列で持つようにしました。

動作確認

すべて設定できれば、動作確認を行います。 直近に有効期限が切れる RI がないので、200日ほど前をチェックする日時としておきます。

実際に動作させるとちゃんとメールが飛んでくることがわかりました。

最後は、これを CloudWatchEvent から毎日1回呼び出すようにすればOKです。

Gist にコードを載せてありますので、気になる方はチェックしてください。

参考

今年の10月に、EC2について RI の更新予約というのができるようになったそうです。

blog.serverworks.co.jp

今までは、期限切れの前日とかに1日重複されて購入するとか、切れてから少しのタイムラグを挟んで購入するとかでしたが、これを使えば期限切れと同時に自動で購入してくれるとのことです。 いたれりつくせりですね。