低稼働な AWS EC2 を自動で停止する

# モチベーション
高額な EC2 (r4.16xlarge とか)を立ち上げた後、うっかり止め忘れるととんでもない料金を請求されてしまう。
ただ、利用中の ec2 を停止されるのも困る。
CPU 使用率を見て、使ってなさそうなら止めるようにしたい。

# やったこと
Lambda, CloudWatch を使って実装。

## 停止対象の EC2
* 特定の tag がつけられていること
* 過去3時間のメトリクスが取得できること
* 過去3時間の CPU 使用率が閾値以下であること

## 構成
![構成.jpg](https://qiita-image-store.s3.amazonaws.com/0/269590/7bbf0452-db8a-2206-0682-c4c51de5670d.jpeg)

## 処理フロー
1. CloudWatch から1時間に1度 Lambda を実行する
2. CloudWatch から EC2 のメトリクスを取得する
3. 条件を満たす EC2 を停止する

## 言語
Python3.6

## Lambda コードのバージョン管理
lambda のバージョン管理を利用する。
[classmethod さんの資料を参考](https://dev.classmethod.jp/cloud/aws/lambda-versioning/)

## Lambda の実装
1. Key="AutoStop", Value="true" のタグが付与された EC2 の Instance Id を取得する
2. CloudWatch から Instance Id 引きで CPU 使用率の datapoint を取得する
3. 使用中の EC2 か判定する
4. 条件を満たす EC2 の Instance Id を取得・停止する

```python
import boto3
import datetime

CPU_UTILIZATION_THRESHOLD = 1.0
METRIC_PERIOD_SECONDS = 300
METRIC_TIME_DELTA_HOURS = 3

def lambda_handler(event, context):
    target_id_list = get_target_instance_id_list()
    if target_list:
        client = boto3.client('ec2', "ap-northeast-1")
        client.stop_instances(InstanceIds=target_list)


def get_target_instance_id_list():
    client = boto3.client('ec2', "ap-northeast-1")
    resp = client.describe_instances()
    target_list = []
    for reservation in resp['Reservations']:
        print(len(reservation['Instances']))
        for instance in reservation['Instances']:
            instance_id = get_instance_id_of_target_tag(instance)
            if instance_id and not is_using_ec2(instance_id):
                target_list.append(instance_id)
        return target_list

def get_instance_id_of_target_tag(instance):
    if 'Tags' not in instance:
        return None

    if instance['State']['Name'] != 'running':
        return None

    for tag in instance['Tags']:
        if tag['Key'] == 'AutoStop' and tag['Value'] == 'true':
            return instance['InstanceId']

def get_datapoints(instance_id):
    client = boto3.client('cloudwatch', region_name='ap-northeast-1')

    # Get EC2 CPUUtilization
    response = client.get_metric_statistics(
        Namespace='AWS/EC2',
        MetricName='CPUUtilization',
        Dimensions=[
            {
                'Name': 'InstanceId',
                'Value': instance_id
            }
        ],
        StartTime=datetime.datetime.now() - datetime.timedelta(hours=METRIC_TIME_DELTA_HOURS),
        EndTime=datetime.datetime.now(),
        Period=METRIC_PERIOD_SECONDS,
        Statistics=['Average']
    )
    return response['Datapoints']

def is_using_ec2(instance_id):
    datapoints = get_datapoints(instance_id)

    if len(datapoints) < METRIC_TIME_DELTA_HOURS * 60 * 60 / METRIC_PERIOD_SECONDS:
        return False
    is_using = False
    for datapoint in datapoints:
        if datapoint['Average'] > CPU_UTILIZATION_THRESHOLD:
            is_in_use = True
    return is_using
```


# さいごに
今回は Lambda で実装したが、数が少なければ [CloudWatch を使った自動停止](https://aws.amazon.com/jp/about-aws/whats-new/2013/01/08/use-amazon-cloudwatch-to-detect-and-shut-down-unused-amazon-ec2-instances/) の方がスマートな解決方法かも。
営業日以外は止めたい場合は [Instance Scheduler](https://aws.amazon.com/jp/answers/infrastructure-management/instance-scheduler/) で対応できそう。