AWS EC2 インスタンスが何台起動しているかをモニタリングする

[前回](https://qiita.com/aokad/items/622c319b3e0a2e36afbe) は AWS の予算管理機能を使用して限度額に近づいたらでアラートを出すようにしました。

今回は無駄遣いを見つけるために起動中の EC2 インスタンスの数を監視できるようにします。
これで、インスタンスの停止忘れや全体の起動数が把握できるようになるはずです。

## 構成

以下の流れで実現します。

![flow.PNG](https://qiita-image-store.s3.amazonaws.com/0/226829/62b65844-329c-4b54-3f9d-91c2d1418aa0.png)

 (1) AWS CloudWatch イベントは定期的に AWS Lamada 関数を実行する。
 (2) 呼び出された AWS Lambda 関数はインスタンス起動数を集計して、
 (3) AWS CloudWatch メトリクスに登録する。
 (4) AWS CloudWatch アラームはメトリクスを監視し、
 (5) 基準を超えたときに AWS SNS で通知する。

## 1. AWS Lambda 関数の実装

まず、AWS Lambda にインスタンス起動数を集計して AWS CloudWatch メトリクスに登録する関数を作成します。

### 1.1. AWS Lambda 関数を作成

AWS Lambda → 「関数の作成」→「一から作成」と移動して、以下を設定します。

 - 名前：monitoring_instances (任意)
 - ランタイム：Python 2.7
 - ロール：「カスタムロールの作成」

新しい画面でロールを作成し、「許可」をクリックします。ここではひとまずテンプレートのまま作成します。

 - ロール名：「lambda_monitoring_instances」

元の画面に戻り「関数の作成」ボタンをクリックします。

### 1.2. AWS Lambda 関数が使用するロールを編集

新しい画面を開いて先ほど作成したロールを編集します。
AWS IAM
 → 「ロール」
 →「lambda_monitoring_instances」（1.1 で作成したロール）
 → ポリシー名「oneClick_lambda_basic_execution_xxxxxx」の前についている右向きの三角形
 → 「ポリシーの編集」をクリックして、以下のアクセス権限をすべてのリソースに対して与えます。

 - ec2:DescribeInstances
 - ec2:DescribeInstanceStatus
 - ec2:DescribeRegions
 - cloudwatch:PutMetricData

JSON を直接編集する場合は以下を追加します。

```Json
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceStatus",
                "ec2:DescribeRegions",
                "cloudwatch:PutMetricData"
            ],
            "Resource": "*"
        }
```

もともと設定されているアクセス権限は AWS Lambda が AWS Cloudwatch に実行ログを作成するために必要です。消さないでください。
ポリシーを編集したら「ReviewPolicy」 → 「Save changes」で保存します。

### 1.3. AWS Lambda 関数の編集

1.1 で作成した関数を編集します。1.2 変更したロールを再読み込みしたいので、1.1 で開いたままにしている画面を再読み込みしてください。
関数コードの lambda_function.py を以下で置き換えます。
※コメント行は除いてください。

```Python
import json
import boto3

def lambda_handler(event, context):
    
    # まず、AWSの全リージョンを取得
    ec2 = boto3.client('ec2')
    regions = map(lambda x: x['RegionName'], ec2.describe_regions()['Regions'])
    
    # lambda関数の戻り値（デバッグ用なので最終的には空にしてもよい）
    status = []
    
    # 全リージョンのインスタンス数
    instance_all_num = 0
    
    # 各リージョンごとに処理する
    for region in regions:
    
        # 現在 "running" の状態にあるインスタンスを取得
        responce = boto3.client('ec2', region_name=region).describe_instances(
            Filters=[{"Name":"instance-state-name","Values":["running"]}]
        )
        
        # Reservationsが配列なので念のため、全要素を回してインスタンス数を取得
        instance_num = 0
        for r in responce["Reservations"]:
            instance_num += len(r["Instances"])
        instance_all_num += instance_num
        
        # AWS CloudWatchメトリクスにリージョンごとのインスタンス数を登録
        boto3.client("cloudwatch").put_metric_data(
            Namespace="COST_MONITOR", 
            MetricData=[{
                "MetricName": "instances", 
                "Dimensions": [{"Name":"region", "Value": region}], 
                "Value": instance_num, 
                "Unit": "Count"
            }]
        )
        # 関数のリターンとして、リージョンごとにインスタンス数をセット
        status.append({"region": region, "value": instance_num})
    
    # AWS CloudWatchメトリクスに全リージョンのインスタンス数を登録
    boto3.client("cloudwatch").put_metric_data(
        Namespace="COST_MONITOR", 
        MetricData=[{
            "MetricName": "instances", 
            "Dimensions": [{"Name":"region", "Value": "all-region"}], 
            "Value": instance_all_num, 
            "Unit": "Count"
        }]
    )
    status.append({"region": "all-region", "value": instance_all_num})
    return {
        "statusCode": 200,
        "body": json.dumps(status)
    }
```

我々はいろいろなリージョンを使用しているためリージョンごとに集計を分けました。
東京リージョンしか使用していないような場合は `region="ap-northeast-1"` のように固定としたり、各リージョンごとのメトリクスが不要であれば全リージョンの合計だけ出力するように変更してもよいでしょう。

### 1.4. AWS Lambda の設定とテスト実行

関数を編集したら以下設定します。

 - （ブラウザの下の方）基本設定：タイムアウト 1 分（我々の計測では18秒くらいかかります）
 - （ブラウザの上の方）テストイベントの設定：テストイベントの名前を適当に設定した後、「保存」
   ※今回は関数にパラメータを使用していないので、テストイベントの内容は影響ありません

設定が終わったら「保存」してください。次に「テスト」ボタンをクリックし、成功したら AWS Lambda の設定は完了です。
保存されたメトリクスは AWS Cloudwatch → メトリクス → COST_MONITOR → region で確認できます。
グラフ表示するにはすべてのもしくは任意のメトリクスにチェックを入れてください。

## 2. AWS SNS の設定

AWS Simple Notification Service (SNS) を使用してアラームを通知できるようにします。

### 2.1 トピックの作成

AWS SNS → 「トピック」 → 「新しいトピックの作成」と移動して、以下内容で設定します。

 - トピック名：cloudwatch_alert
 - 表示名：cloudwatch (10文字以内、任意)

### 2.2 トピックのポリシーの編集

先ほど作成したトピックにチェックを入れ、「アクション」→「トピックのポリシーの編集」と移動して、以下内容で設定します。

 - 次のユーザにこのトピックへのメッセージの発行を許可
    - 全員
 - 次のユーザーにこのトピックのサブスクリプションを許可
    - 全員

設定したら「ポリシーの更新」をクリックします。

### 2.3 サブスクリプションの作成

先ほど作成したトピックにチェックを入れ、「アクション」→「トピックへのサブスクリプション」と移動して、以下内容で設定します。

 - プロトコル：Email
 - エンドポイント：（配信したいメールアドレス）

「サブスクリプションの作成」をクリックしてサブスクリプションを作成します。
作成したサブスクリプションは AWS SNS → 「サブスクリプション」で確認できます。
作成後、配信先のメールアドレスに「AWS Notification - Subscription Confirmation」というタイトルで確認メールが来ますので、メール本文にある「Confirm subscription」をクリックしてください。
サブスクリプション ARN の値が「PendingConfirmation」から arn の形式になったら SNS の設定終了です。

## 3. AWS Cloudwatch の設定

AWS CloudWatch を使用して定期的に AWS Lambda を実行し、メトリクスの値を監視して閾値を超えていればアラートを出すようにします。
なお、メトリクスは AWS Lambda が作成しますので、設定不要です。

### 3.1 イベントの作成

作成した AWS Lambda 関数を定期的に呼び出すようにします。

AWS Cloudwatch → 「イベント」 → 「ルールの作成」と移動して、以下内容で設定します。

 - イベントソース：スケジュール
 - 一定の速度：15分（お好みで。5分～1時間くらいにしてもよいと思います）
 - ターゲット：Lambda 関数
 - 機能：monitoring_instances（作成した AWS Lambda 関数名を選択）

「設定の詳細」をクリックし、名前をつけて「ルールの作成」をクリックして保存します。
これで定期的に Lambda 関数が呼び出され、起動しているインスタンスの数がメトリクスに保存されるようになりました。

### 3.2 アラームの作成

メトリクスの値（現在起動しているインスタンスの数）を監視して、閾値を超えていればアラートを出すようにします。

AWS Cloudwatch → アラーム → 「アラームの作成」と移動して、以下内容で設定します。

#### 1. メトリクスの選択

「カスタムメトリクス」 → 「COST_MONITOR」 → 「all-region」 → 「次へ」

#### 2. アラームの定義

アラームのしきい値

 - 名前：Running Instance Number （半角英数字）(※)
 - 説明：全リージョンの起動インスタンス数が閾値をこえました (※)
 - 次の時: instances が >= 50
 - 期間：1/1データポイント

※ 名前や説明は任意に設定できますが、この内容でアラート（メール）が来るため、わかりやすく書いた方がよいです。

追加設定

 - 欠落データの処理方法：見つかりません（デフォルト）

アクション

 - アラームが次の時：状態：警告
 - 通知の送信先：cloudwatch_alert（通知リストの選択で先ほど作成した SNS トピックを選択）
 - メールリスト：（SNS トピックのサブスクリプションで設定したメールアドレスが表示される。触らない）

アラームのプレビュー

 - 間隔: 15分間
 - 統計: スタンダード

これでインスタンスの数が閾値を超えたらメールが来るようになりました。
以下は通知されてくるメールのサンプルです。（確認用なので、インスタンスの数が3を超えたら通知するようにしています）

```
Title: ALARM: "Running Instance Number" in Asia Pacific (Tokyo)

(内容抜粋)
Alarm Details:
- Name:                       Running Instance Number
- Description:                全リージョンの起動インスタンス数が閾値をこえました
- State Change:               OK → ALARM
- Reason for State Change:    Threshold Crossed: 1 out of the last 1 datapoints [3.0 (05/09/18 05:35:00)] was greater than or equal to the threshold (3.0) (minimum 1 datapoint for OK → ALARM transition).
- Timestamp:                  Wednesday 05 September, 2018 05:40:53 UTC
- AWS Account:                （省略）
```

なお、作成したアラートは AWS CloudWatch の TOP 画面で以下のようにグラフ化されています。
![alert.PNG](https://qiita-image-store.s3.amazonaws.com/0/226829/bad34b09-ebd9-11bc-4af6-f3244c644873.png)

### 3.3. ログの保存期間

さて、ここまでの設定で要件は満たしているのですが、最後にもう一つ設定しておきます。
AWS Lambda が実行されるとき、AWS CloudWatch ログを残しています。デフォルトの場合、無期限に保存されてしまいますので、適当な時期でクリアするようにしておきます。

AWS Cloudwatch → ログと移動して、/aws/lambda/monitoring_instances 横の「失効しない」をクリックします。
ここでは保持期「1週間」を選択して「OK」をクリックします。