プログラマーのメモ書き

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

クロスアカウントを CLI でも使う

こちらの記事で書いたリザーブドインスタンスの期限切れを通知する処理は、結局、 AWS コンソールで提供される機能を使うことになったため、不要になりました。

でも、この時クロスアカウントアクセスの設定方法が分かったので、複数アカウント管理を簡単にするために、これを活用しようと思います。

前述の記事を試す途中で、IAMユーザーでクロスアカウントアクセスするのは試したので、次は、CLIからクロスアカウントアクセスをやろうと思います。

これがうまくいけば、管理対象のアカウントの IAM ユーザーを削減できるので、楽になりそうです。

設定

下記の IAM のチュートリアルにあるやり方は、STS で一時クレデンシャルを取得して、そのたびに、環境変数を設定するという方法です。

チュートリアル: AWS アカウント間の IAM ロールを使用したアクセスの委任 - AWS Identity and Access Management

たぶん、まっとうな方法(先の記事中でAPI経由で扱う時はこれと同じですね)なんですが、毎回やるのはどう考えても面倒です。

で、いい方法ないかな?と調べてみると、ここの設定を自動でやってくれる書き方がありました。

これだと、一度設定ファイルを書いておけば、CLI実行時のプロファイルを切り替えるだけで使えるので、現実的です。

~/.aws/config を次のようにしました。

[profile mor_test]
region = ap-northeast-1

[profile mor_test_cross]
region = ap-northeast-1
role_arn = arn:aws:iam::信頼するほうのアカウントID:role/引き受けられるロール名
source_profile = mor_test

mor_test がクロスアカウントを行うアカウント(信頼されるアカウント)になります。

ためす

さっそく試します。ec2 のリザーブドインスタンスの情報を取ってみます。

mor@DESKTOP-H6IEJF9:~$ aws ec2 describe-reserved-instances --profile mor_test_cross

An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::信頼されるほうのアカウントID:user/mor_test is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::信頼するほうのアカウントID:role/引き受けられるロール名
mor@DESKTOP-H6IEJF9:~$

しかし、なぜかうまくいかないです。

ちょっとネットを調べるとこんな記事がありました。

クロスアカウントなAWS CLI処理でハマった話 | Developers.IO

あ、引き受けさせたいロールの条件として、MFAは必須にしてましたね。信頼する側のロールにアタッチされているポリシーが下記のようになってます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::信頼されるほうのアカウントID:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
    }
  ]
}

でも、CLI で MFA どうやって扱うの?
ということで、MFAのやり方を調べてみます。

CLI での MFA

すぐにやり方は見つかりました。

AWS CLI での IAM ロールの使用 - AWS Command Line Interface

こちらを参考にして、クロスアカウント元になるユーザー(上記の例だと mor_test )に対して、MFAを設定します。

次に、 ~/.aws/config を編集して、 mfa_serial を追加します。

[profile mor_test]
region = ap-northeast-1

[profile mor_test_cross]
region = ap-northeast-1
role_arn = arn:aws:iam::信頼するほうのアカウントID:role/引き受けられるロール名
mfa_serial = arn:aws:iam::信頼されるほうのアカウントID:mfa/mor_test
source_profile = mor_test

この状態にして確かめると、

mor@DESKTOP-H6IEJF9:~$ aws rds describe-reserved-db-instances --profile mor_test_cross
Enter MFA code for arn:aws:iam::信頼されるほうのアカウントID:mfa/mor_test:

と画面に表示されて、信頼されるほうのアカウント mor_test の MFA のコードを入力するように求められます。 正しい、コードが入力できると、

{
    "ReservedDBInstances": [
        {
(後略)

のように、応答が返ってきました。

なお、下記のページにあるように、

MFA 保護 API アクセスの設定 - AWS Identity and Access Management

  • MFA に関する条件は、信頼する側(クロスアカウントされる側、または、引き受けられるロールを提供する側)で設定する
  • 信頼される側のポリシーでは、MFA条件は設定しない

とするのがよいようです。

また、MFAに関する条件をいろいろと設定することもできるようです。下記などが参考になります。

AWS グローバル条件コンテキストキー - AWS Identity and Access Management

なにはともあれ、これでクロスアカウントアクセスがコンソールでもCLIでもできるようになったので、アカウント管理が(多少は)楽になりそうです。

UDC三重のイベント『第3回 少子高齢化を斬る!』に参加してきました

先日2019年12月8日(日)に、三重大学工学部大会議室で開催された、UDC三重のイベント『第3回 少子高齢化を斬る!』に参加してきました。

udcmie.connpass.com

その様子を簡単にメモっときます。

江戸橋

三重大学へ行くには、電車を使う場合、近鉄江戸橋駅からテクテクと歩いていくのが定番です。 その途中にあるのが、駅名にもなってる江戸橋。

この江戸橋、ここしばらく架け替え工事をしていたのですが、なんと! いつの間にやら、新しくなって開通しています。

f:id:junichim:20191209000444j:plain

f:id:junichim:20191209000507j:plain

開通前は23号線のほうの橋を通って迂回していたのですが、これが結構遠回りだったので、開通はありがたいです。

会場

会場は三重大学工学部大会議室でした。

写真撮り忘れました・・・

会場はきれいで広くて全然問題ないのですが、会場周辺が道路工事だらけで、入り口までたどり着くのが困難だったのが困りものでした。

講演

イベント前半は講演が2つ。

タイトル 講演者
少子高齢化はシビックテックで 朝日航洋株式会社/IT DART/OSGeo.JP
嘉山 陽一 様
データ分析からみる少子高齢化 株式会社 オービタルネット
植田 粋美 様

一つ目の講演では、OSSや無料(範囲)で使えるクラウドサービスを活用してサービスを作ることができることの紹介と、それをシビックテックに活かしていけばいいという話。あと、講演後の質問で、シビックテック(とかボランティア)の活動と資金の話があって、これも興味深かったです。

二つ目の講演は、可視化することの大事さの話や、合計特殊出生率にまつわる話などを聞きました。三重だと熊野市が合計特殊出生率が2を超えていて、県内一だそうですが、特にアピールしている姿が見えないところの話とかも面白かったですね。

アイデアソン

イベント後半は参加者全員でアイデアソンしました。

今回のアイデアソンは、

  • 自分でテーマを決める
  • テーマを中心にして、思いつくキーワードでマンダラチャートを埋めていく
  • 二人でブレスト(相手を変えて何度か)
  • ブレストを踏まえて、アイデアを3つ書き出す
  • 書いたアイデアをみんなで投票

という流れの進行でした。

最初のほうにあった、テーマから連想されるキーワードを制限時間内に埋めていく作業が大変でしたが、ゲームっぽくて楽しかったですね。 自分の作ったのはこんな感じになりました(自分用のものなので字が読めないのは勘弁してください)。

f:id:junichim:20191209004257j:plain

アイデアは3つ考えてみました。1つはすぐに思いついたのですが、残り二つを絞り出すのが苦労しました。

f:id:junichim:20191209082350j:plain

f:id:junichim:20191209082409j:plain

f:id:junichim:20191209004342j:plain

最後に挙げた、高齢化と車をテーマにしたアイデアが、投票した結果、全体の3位に入りました。ありがとうございます。 (1位と2位のアイデアは写真撮り損ねてて、あやふやな記憶で書くのもはばかられるのでうまくご紹介できません、すいません)

本当は、ここからさらに実現可能な形に向けてアイデアを練っていくのでしょうが、今回はここまででした。

にしても、アイデアソン面白かったです。

あとは、なんとかここから頑張って、UDC2019のコンテストに応募することまで持っていきたいと思います。

urbandata-challenge.jp

UDC2019 のコンテストの応募のエントリー締め切りは12月28日(土)なので、ご興味ある方はぜひ応募してみてください。

あ、ちなみに、上記の写真のキーワードやアイデアも使ってもらっても全然かまいませんので、参考になればぜひ活用してください。

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

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