EC2 リザーブドインスタンスの有効期限をSlackへ通知する Lambda (Python3.7版）

## 概要
* SSLサーバー証明書の更新作業は ACM で解放されたけど、RI は自動更新できないので有効期限の監視をしておかないと、いつの間にか期限が切れてオンデマンド料金を支払う事になってしまう（現になってしまったので、これを作った・・）。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/renew-ri/

* EC2 の他に RDS や ElastiCache や Elasticsearch でもリザーブドインスタンスを使っているものがある場合は、[まとめてチェックするこちら](https://qiita.com/tonishy/items/60554235c94435b62fc9)を使う。

* この Lambdaを設定した AWS アカウントの 全EC2リザーブドインスタンスの有効期限をチェックして、期限が切れる THRESHOLD 日前になったら Slack へ通知する。
* CloudWatch のイベントで１日一回、この Lambda を実行する （更新しない限り、一日一回通知が飛ぶ）
* State が active の有効期限をチェックする。

## 設定

### Lambda に付けるロールを作成する

#### ロールにアタッチするポリシー

* AWSLambdaBasicExecutionRole
 * AWS 管理ポリシー

* AmazonEC2DescribeReservedInstance
 * 下記の内容で作成する

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

#### ロールを作成

* ec2-reserved-instance-expiration-date-check
 * このロールを使用するサービス： Lambda
 * 上記２つのポリシーをアタッチする


### Lambda 関数を作成する
* 関数名
    * ec2-reserved-instance-expiration-date-check

* ランタイム
    * Python 3.7

* ロール
    * 上で作成した ec2-reserved-instance-expiration-date-check ロール

* コード

```python
import os
import boto3
import json
import logging
from pprint import pprint
from datetime import datetime, timedelta, timezone
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

THRESHOLD = int(os.environ['THRESHOLD'])
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
AWS_ACCOUNT_ALIAS = os.environ['AWS_ACCOUNT_ALIAS']

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    logger.info("Event: " + str(event))

    JST = timezone(timedelta(hours=+9), 'JST')
    now = datetime.now(JST).date()

    RI="EC2"
    client = boto3.client('ec2')
    response = client.describe_reserved_instances()
    for ReservedInstance in response['ReservedInstances']:
      if ReservedInstance['State'] == "active":
        ValidPeriod = (((ReservedInstance['End'].astimezone(JST).date()) - now).days)
        END = str((ReservedInstance['End']).astimezone(JST).date())
        if THRESHOLD > ValidPeriod:
          slack(RI, ReservedInstance['ReservedInstancesId'], ReservedInstance['InstanceType'], str(ReservedInstance['InstanceCount']), str(END), str(ValidPeriod))


def slack(RI, RIID, InstanceType, InstanceCount, End, ValidPeriod):
  color = "warning"
  icon = ":bulb:"
  SlackText=RI + " Reserved Instance Check"
  SlackTextAttachments = "AWS: " + AWS_ACCOUNT_ALIAS + "\n" + "RI ID: " + RIID + "\n" + "Instance Type: " + InstanceType + "\n" + "Instance Count: " + InstanceCount + "\n" + "End: " + End + " (only " + ValidPeriod + " days left)"
  slack_message = {
    'username': "AWS Lambda",
    'text': SlackText,
    'icon_emoji': icon,
    'attachments': [
      {
        "color": color,
        "text": SlackTextAttachments
      }
    ]
  }
  req = Request(SLACK_WEBHOOK_URL, json.dumps(slack_message).encode('utf-8'))
  try:
    response = urlopen(req)
    response.read()
    logger.info("Message posted to %s", SLACK_WEBHOOK_URL)
  except HTTPError as e:
    logger.error("Request failed: %d %s", e.code, e.reason)
  except URLError as e:
    logger.error("Server connection failed: %s", e.reason)

```

* 環境変数
    * キー：AWS_ACCOUNT_ALIAS<br />値：AWSアカウント名
    * キー：SLACK_WEBHOOK_URL<br />値：SlackのWebhook
    * キー：THRESHOLD<br />値：40

* タイムアウト
    * 1分



### CloudWatch イベント ルール
* スケジュール
    * Cron式：0 0 * * ? *

* ターゲット
    * Lambda関数：ec2-reserved-instance-expiration-date-check

* ルール名
    * reserved_instances_check



### Slack通知例
![slack1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/83f87237-7eec-bdb4-8173-f547541efc07.png)