プログラマーのメモ書き

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

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

表題の件。
リザーブドインスタンス(以下、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日重複されて購入するとか、切れてから少しのタイムラグを挟んで購入するとかでしたが、これを使えば期限切れと同時に自動で購入してくれるとのことです。 いたれりつくせりですね。

QNAP NAS out of memory が起こりました

いまごろ公開してますが、去年(2018年)の12月頃の出来事でした。

2台の QNAP (TS-251+, TS-231P, QTS バージョンはいまとなっては不明ですが、両方ともたぶん 4.3.5.0760 あたりだと思います) で NAS to NAS のバックアップテストをしていたら、 メールに NAS out of memory のエラーメールが飛んできました。

内容を確認すると、

  Simple yet powerful NAS    <http://www.qnap.com/>Go to QNAP  

[Warning][nas01] Hardware Status

NAS Name: nas01
Severity: Warning
Date/Time: 2018/12/02 06:46:02

App Name: Hardware Status
Category: Kernel
Message: [Hardware Status] NAS out of memory. Started kill process: 21074 "wizReq.cgi". Disable some applications to free up memory, or expand the system memory.

<http://www.qnap.com>(c)2018 QNAP Systems, Inc.

というものです。 wizReq.cgi ってのをキーワードにして検索してみると、

WizReq.cgi - high memory usage - QNAP NAS Community Forum

というディスカッションがありました。

どうも、 NAS to NAS でのバックアップに起因するエラーのようです。 このディスカッション先にあるように、 Backup Station から Hybrid backup Sync に切り替えてみて、再度試したところ問題なく動作するようになりました。

取り急ぎ、類似の症状で困ってる人もいるかもしれないのでメモっときます。

SoftEther VPN Server 設定の再確認

リモートアクセスVPN を QNAP 上で動かす』 で書いたように、外部から家のLAN環境に入るため、 SoftEther VPN Server を QNAP 上の VM で動かしています。

去年、ルータ買い替え(顛末はこちらの記事などご覧ください)&新しい仕事場に引っ越し、などいろいろとあり、その後、VPN 接続をきちんと試してなかったので、改めて試してみた話をメモっておきます。

設定確認

結論から言うと、リモートアクセスVPN を QNAP 上で動かす の記事と同じ設定のままで問題なく接続できました。
ちなみに、上記記事の時点から、QNAPのファームウェアをアップデートしていることもあり、ネットワーク周りの概要の画面はこんな感じになります。

f:id:junichim:20191205150326p:plain

ちなみに、今の状態は

  • ルータ NVR510 v15.01.14
  • QNAP, QTS 4.4.1.1117

です。

別の設定方法

実は、上記設定を試す際に、いろいろと試していたら、 VM に 仮想NICを2つ追加するところまでは同じようにして、 QNAP 上の仮想スイッチを追加せずとも VPN が構成できました。

f:id:junichim:20191205150841p:plain

こちらは、どちらかというと以前の ESXi での構成に近い感じですね(物理NICが一つで、仮想NICが2つ)。

まあ、考えてみれば当たり前なんですが、一応できるよ、ということで載せておきます。

余談

余談です。

実は、ルータ買い替え直後、新しいルーターで一度試したのですが、この時は、 VPN Server に接続できませんでした (こちらの記事にあるように、QNAP の再設定時に発覚しました)。

本来は NAT-T で何もしなくてもポートが開くことを期待していたのですが、なぜか(この時の)NVR510 の場合はうまくいかなかったようです。

でも、今回試した際は問題なく接続できました(当時の詳細な記録が残っていないため戻って検証できないのが残念です)。

VPN Server に接続できなかった後、引っ越しして、その後いろいろと変更したところとしては、

  • ルータのファームウェアアップデート v15.01.13 -> v15.01.14
  • QNAP のファームウェアアップデート 4.3.5.0760 かそれ以前 -> 4.3.6.0805 (4.4.1.1117 の前にこれでも接続に成功してます)

などがあります。
とはいえ、リリースノートなどを見ても関係しそうなところはないんですよねー。こうなるとまあ正直ちょっとわかんないですね。

少なくとも、現在の環境では問題なく動作しております。

参考

どこまで関係あるかわかりませんが、Yamaha ルータではNAT-Tが効かない、というような話もあるようです。

YAMAHA ルータと Splatoon 2 - mura日記 (halfrack)

でも、Yamahaルータのドキュメントでは、 NAT-T で使う UDP の場合は、 Symmetric NAT 相当にはならないようなので、どうなんでしょうかね? (TCP の場合はポートセービングIPマスカレードで動作して、この動作が Symmetric NAT と同じということのようです)

ちなみに、上記の記事で紹介されている STUNner を使ってみたら、いまの NVR510 は Port Restricted Cone NAT と判定されました。

f:id:junichim:20191205225651p:plain

Symmetric NAT だとか Port Restricted NAT ってなんだろう?と思い、ついでに調べてみると、こんな感じの説明がありました。

『なんとか cone NAT 』系のものは、『送信元の IPアドレスとポート番号が同じであれば、変換後のIPアドレスとポート番号も同じものを使う』というのがポイントのようです。 個別の違いは、外部からの通信があった場合に通すときの条件の違いのようです。
一方、 Symmetric NAT は送信元のIPアドレスとポート番号に加えて、送信先のIPアドレスとポート番号も含めて、変換後のIPアドレスとポート番号を決定する、というもののようです。

このため、cone NAT の場合は、下記記事に説明されているような手順で、 NAT 越えを実現することができるということです。

[24日目] NAT Traversalって知ってますか | Cerevo TechBlog

なるほど、勉強になりますね。

で、 NVR510 はこちらの記事中で触れらているように、UDPに対しては、ポートセービングIPマスカレードが使われず、 なんとか cone NAT 系の振る舞いをしているということのようです。

そうなるとますます、最初にNAT-Tが動いていないように見えたのが謎ですね。

ちなみに、最初は下記のサイトの説明からは、 port Restricted NAT と Symmetric NAT って一度送信した相手からしかNATを通さないので同じじゃない?って思ってしまいました。

NAT変換後のIPアドレスとポート番号の決め方についての話が抜けていたので、 port Restricted NAT と Symmetric NAT が同じように思えたようです。

あと、SoftEther で使ってる NAT-T については下記などが参考になりそうです。

VPN の NAT トラバーサル (UDP ホールパンチング) 機能 - 登 大遊 (Daiyuu Nobori) の個人日記

NATまわりはややこしいなー。