CloudWatch Events から Lambda (Python スクリプト) 経由で EC2 を自動起動/停止してみる

# ０．はじめに

これまで、ジョブ管理に「Zabbix」の Add-On の「Job Arranger for Zabbix」というものを使って、

Python スクリプトを定期実行して、EC2 インスタンスの自動起動/停止をしていたんですが、

出来れば AWS 内で完結出来ないかなぁ、と考えていて、

どうやら CloudWatch Events → Lambda (Python スクリプト) で出来るみたいなので試してみました。

* ※参考
    * [CloudWatch Events から Systems Manager 経由で Windows バッチを実行してみる \- Qiita](https://qiita.com/kusokamayarou/items/65092844c8b64d00760e)

# １．IAM ポリシーを作成する

1. 以下のページから、IAM ポリシーを作成します。
    * [IAM Management Console](https://console.aws.amazon.com/iam/home?region=ap-northeast-1#/policies)
<br><br>
    * <b><font color=red>※ SNS の設定は Lambda の DLQ 用です。不要であれば削除して下さい。必要な場合は、事前に SNS にトピックを作成しておきましょう。</font></b>
<br><br>
    * ![1001.png](https://qiita-image-store.s3.amazonaws.com/0/161939/2b36e274-93b4-b2bc-fde3-861386aa5f8b.png)

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeImages",
                "ec2:DeregisterImage",
                "logs:CreateLogStream",
                "ec2:DescribeInstances",
                "ec2:DeleteSnapshot",
                "ec2:StartInstances",
                "ec2:CreateImage",
                "ec2:DescribeSnapshots",
                "ec2:StopInstances",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "sns:Publish",
            "Resource": "arn:aws:sns:*:************:SysOps-Lambda-DLQ"
        }
    ]
}
```

# ２．IAM ロールを作成する

1. 以下のページから、IAM ロールを作成します。作成した IAM ポリシーを適用します。
    * [IAM Management Console](https://console.aws.amazon.com/iam/home?region=ap-northeast-1#/roles)
<br><br>
    * ![2001.png](https://qiita-image-store.s3.amazonaws.com/0/161939/18dc9eb6-a47e-9939-314f-8cb2abca6c1a.png)

# ３．Lambda の Python スクリプトを作成する

1. 以下のページから、Lambda のPython スクリプトを作成します。
    * [Lambda Management Console](https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/functions)
<br><br>
    * <img width="640" alt="3001.png" src="https://qiita-image-store.s3.amazonaws.com/0/161939/701bcd03-a780-b6e1-a9b2-bd58620d0abc.png">

```python:MaintenanceEC2_Instance_Start.py
#!/usr/bin/env python
# -*- coding: utf-8-unix; -*-
"""AWS Lambda Function - Maintenance EC2 Instances - Start
"""
from __future__ import print_function

import logging
import boto3
import os

# 「Python 3 で少しだけ便利になった datetime の新機能 - Qiita」
# <https://qiita.com/methane/items/d7ac3c9af5a2c659bc51>
from datetime import datetime, timezone, timedelta
TimeZone = timezone(timedelta(hours=+9), 'JST')

# 「Lambdaの本番業務利用を考える① - ログ出力とエラーハンドリング | ナレコムAWSレシピ」
# <https://recipe.kc-cloud.jp/archives/9968>
logger = logging.getLogger()
logLevelTable={'DEBUG':logging.DEBUG,'INFO':logging.INFO,'WARNING':logging.WARNING,'ERROR':logging.ERROR,'CRITICAL':logging.CRITICAL}
if os.environ.get('logging_level') in logLevelTable :
    logger.setLevel(logLevelTable[os.environ['logging_level']])
else:
    logger.setLevel(logging.WARNING)

#
StartDateTime = datetime.now(TimeZone)

def AmazonEC2_DescribeStartInstances(response_instances, TagKey, TagValueType):
    try:
        start_instances = []
        for n, k in enumerate(response_instances["Reservations"]):
            id = response_instances["Reservations"][n]["Instances"][0].get("InstanceId")
            state = response_instances["Reservations"][n]["Instances"][0]["State"].get("Name")
            tags = {}
            for nn, kk in enumerate(response_instances["Reservations"][n]["Instances"][0]["Tags"]):
                tags[response_instances["Reservations"][n]["Instances"][0]["Tags"][nn]["Key"]] = response_instances["Reservations"][n]["Instances"][0]["Tags"][nn]["Value"]
            name = tags.get("Name")
            #
            TagValue = None
            TagCheck = None
            if TagValueType == "DateTime-%H Sat:%H Sun:%H":
                TagValue = tags.get(TagKey)
                if TagValue:
                    TagValues = TagValue.split(" ")
                    TagValue = TagValues[0]
                    # Mon-Fri:0-4 Sat:5 Sun:6
                    StartDateTime_Weekday = StartDateTime.weekday()
                    if StartDateTime_Weekday >= 5:
                        TagValue = None
                        for i, v in enumerate(TagValues):
                            if StartDateTime_Weekday == 5 and v.startswith("Sat:"):
                                TagValue = v[-2:]
                                break
                            if StartDateTime_Weekday == 6 and v.startswith("Sun:"):
                                TagValue = v[-2:]
                                break
                TagCheck = datetime.strftime(StartDateTime, '%H')
            logger.info("TagValue:[%s]", TagValue)
            logger.info("TagCheck:[%s]", TagCheck)
            if TagValue is None or TagCheck is None or TagValue != TagCheck:
                logger.debug("(%04d/%04d) EC2 Instance | [--] ID:[%s] State:[%s] Tags:[%s]",
                n + 1, len(response_instances["Reservations"]), id, state, tags)
                continue
            logger.debug("(%04d/%04d) EC2 Instance | [Do] ID:[%s] State:[%s] Tags:[%s]",
            n + 1, len(response_instances["Reservations"]), id, state, tags)
            start_instances.append({
                "InstanceId" : id,
                "State" : state,
                "Tags" : tags,
            })

    except Exception as e:
        logger.exception('Error dosomething: %s', e)
    else:
        pass
    finally:
        pass
    return start_instances

def lambda_handler(event, context):
    try:
        global StartDateTime
        StartDateTime = datetime.now(TimeZone)
        logger.info("StartDateTime:[%s]", StartDateTime)
        #
        logger.info("event:[%s]", event)
        logger.info("context:[%s]", context)
        #
        DryRun = event.get("DryRun")
        if not DryRun:
            DryRun = False
        logger.info("DryRun:[%s]", DryRun)
        if not isinstance(DryRun, bool):
            return "[ERROR] Parameter DryRun Format is Bool"
        TagKey = event.get("TagKey")
        logger.info("TagKey:[%s]", TagKey)
        if not TagKey:
            return "[ERROR] Parameter TagKey is not set"
        if not isinstance(TagKey, str):
            return "[ERROR] Parameter TagKey Format is Str"
        TagValueType = event.get("TagValueType")
        logger.info("TagValueType:[%s]", TagValueType)
        if not TagValueType:
            return "[ERROR] Parameter TagValueType is not set"
        if not isinstance(TagValueType, str):
            return "[ERROR] Parameter TagValueType Format is Str"
        #
        ec2_client = boto3.client("ec2")
        response_instances = ec2_client.describe_instances()
        # Amazon EC2 - Start
        start_instances = AmazonEC2_DescribeStartInstances(response_instances, TagKey, TagValueType)
        for n, k in enumerate(start_instances):
            instanceid = k["InstanceId"]
            state = k["State"]
            tags = k["Tags"]
            logger.info(("(%04d/%04d) Start [Do] "
                "ID:[%s] State:[%s] Tags:[%s]"),
                n + 1, len(start_instances),
                instanceid, state, tags)
            if DryRun:
                logger.info("Start Instance - Dry Run. (DryRun is %s)", DryRun)
                continue
            response_start_instance = ec2_client.start_instances(
                InstanceIds=[instanceid])
            logger.info("Start Instance Response | [%s] | %s", instanceid, response_start_instance)
    except Exception as e:
        logger.exception('Error dosomething: %s', e)
    else:
        pass
    finally:
        pass
    #
    return "normal end"
```

```python:MaintenanceEC2_Instance_Stop.py
#!/usr/bin/env python
# -*- coding: utf-8-unix; -*-
"""AWS Lambda Function - Maintenance EC2 Instances - Stop
"""
from __future__ import print_function

import logging
import boto3
import os

# 「Python 3 で少しだけ便利になった datetime の新機能 - Qiita」
# <https://qiita.com/methane/items/d7ac3c9af5a2c659bc51>
from datetime import datetime, timezone, timedelta
TimeZone = timezone(timedelta(hours=+9), 'JST')

# 「Lambdaの本番業務利用を考える① - ログ出力とエラーハンドリング | ナレコムAWSレシピ」
# <https://recipe.kc-cloud.jp/archives/9968>
logger = logging.getLogger()
logLevelTable={'DEBUG':logging.DEBUG,'INFO':logging.INFO,'WARNING':logging.WARNING,'ERROR':logging.ERROR,'CRITICAL':logging.CRITICAL}
if os.environ.get('logging_level') in logLevelTable :
    logger.setLevel(logLevelTable[os.environ['logging_level']])
else:
    logger.setLevel(logging.WARNING)

#
StartDateTime = datetime.now(TimeZone)

def AmazonEC2_DescribeStopInstances(response_instances, TagKey, TagValueType):
    try:
        stop_instances = []
        for n, k in enumerate(response_instances["Reservations"]):
            id = response_instances["Reservations"][n]["Instances"][0].get("InstanceId")
            state = response_instances["Reservations"][n]["Instances"][0]["State"].get("Name")
            tags = {}
            for nn, kk in enumerate(response_instances["Reservations"][n]["Instances"][0]["Tags"]):
                tags[response_instances["Reservations"][n]["Instances"][0]["Tags"][nn]["Key"]] = response_instances["Reservations"][n]["Instances"][0]["Tags"][nn]["Value"]
            name = tags.get("Name")
            #
            TagValue = None
            TagCheck = None
            if TagValueType == "DateTime-%H Sat:%H Sun:%H":
                TagValue = tags.get(TagKey)
                if TagValue:
                    TagValues = TagValue.split(" ")
                    TagValue = TagValues[0]
                    # Mon-Fri:0-4 Sat:5 Sun:6
                    StartDateTime_Weekday = StartDateTime.weekday()
                    if StartDateTime_Weekday >= 5:
                        TagValue = None
                        for i, v in enumerate(TagValues):
                            if StartDateTime_Weekday == 5 and v.startswith("Sat:"):
                                TagValue = v[-2:]
                                break
                            if StartDateTime_Weekday == 6 and v.startswith("Sun:"):
                                TagValue = v[-2:]
                                break
                TagCheck = datetime.strftime(StartDateTime, '%H')
            logger.info("TagValue:[%s]", TagValue)
            logger.info("TagCheck:[%s]", TagCheck)
            if TagValue is None or TagCheck is None or TagValue != TagCheck:
                logger.debug("(%04d/%04d) EC2 Instance | [--] ID:[%s] State:[%s] Tags:[%s]",
                n + 1, len(response_instances["Reservations"]), id, state, tags)
                continue
            logger.debug("(%04d/%04d) EC2 Instance | [Do] ID:[%s] State:[%s] Tags:[%s]",
            n + 1, len(response_instances["Reservations"]), id, state, tags)
            stop_instances.append({
                "InstanceId" : id,
                "State" : state,
                "Tags" : tags,
            })

    except Exception as e:
        logger.exception('Error dosomething: %s', e)
    else:
        pass
    finally:
        pass
    return stop_instances

def lambda_handler(event, context):
    try:
        global StartDateTime
        StartDateTime = datetime.now(TimeZone)
        logger.info("StartDateTime:[%s]", StartDateTime)
        #
        logger.info("event:[%s]", event)
        logger.info("context:[%s]", context)
        #
        DryRun = event.get("DryRun")
        if not DryRun:
            DryRun = False
        logger.info("DryRun:[%s]", DryRun)
        if not isinstance(DryRun, bool):
            return "[ERROR] Parameter DryRun Format is Bool"
        TagKey = event.get("TagKey")
        logger.info("TagKey:[%s]", TagKey)
        if not TagKey:
            return "[ERROR] Parameter TagKey is not set"
        if not isinstance(TagKey, str):
            return "[ERROR] Parameter TagKey Format is Str"
        TagValueType = event.get("TagValueType")
        logger.info("TagValueType:[%s]", TagValueType)
        if not TagValueType:
            return "[ERROR] Parameter TagValueType is not set"
        if not isinstance(TagValueType, str):
            return "[ERROR] Parameter TagValueType Format is Str"
        #
        ec2_client = boto3.client("ec2")
        response_instances = ec2_client.describe_instances()
        # Amazon EC2 - Stop
        stop_instances = AmazonEC2_DescribeStopInstances(response_instances, TagKey, TagValueType)
        for n, k in enumerate(stop_instances):
            instanceid = k["InstanceId"]
            state = k["State"]
            tags = k["Tags"]
            logger.info(("(%04d/%04d) Stop [Do] "
                "ID:[%s] State:[%s] Tags:[%s]"),
                n + 1, len(stop_instances),
                instanceid, state, tags)
            if DryRun:
                logger.info("Stop Instance - Dry Run. (DryRun is %s)", DryRun)
                continue
            response_stop_instance = ec2_client.stop_instances(
                InstanceIds=[instanceid])
            logger.info("Stop Instance Response | [%s] | %s", instanceid, response_stop_instance)
    except Exception as e:
        logger.exception('Error dosomething: %s', e)
    else:
        pass
    finally:
        pass
    #
    return "normal end"
```

# ４．CloudWatch Events のルールを作成する

1. 以下のページから、CloudWatch Events のコンソールにアクセスし、「ルールの作成」を押下します。
    * [CloudWatch Management Console](https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#rules:)
<br><br>
    * <img width="640" alt="0001.png" src="https://qiita-image-store.s3.amazonaws.com/0/161939/24752ab7-af9b-b35d-9614-2385fa229876.png">
<br><br>
1. 「ステップ 1: ルールの作成」画面が表示されるので、以下の項目を設定し、「設定の詳細」ボタンを押下します。
    * イベントソース : ※任意。今回は、スケージュールドリブンの設定を例として記載。
        * ● 「スケジュール」
            * Cron 式 : 「0 * * * ? *」　※日本時間で、毎時 00 分に実行。
            * <font color="red"><b>※ UTC なので注意!!</b></font>
    * ターゲット : 「Lambda 関数」
        * 機能 : ※作成した Lambda 関数
        * バージョンエイリアスの設定 : ※適当に
        * 入力の設定 : 
            * ● 「定数 (JSON テキスト)」
            * `{ "TagKey": "StartTime", "TagValueType": "DateTime-%H Sat:%H Sun:%H", "DryRun": false }`
                * "TagKey" : EC2 インスタンスで参照するタグ名
                * "TagValueType" : EC2 インスタンスで参照するタグの設定形式
                * "DryRun" : ドライランの場合は、true に。
<br><br>
    * <img width="640" alt="4002.png" src="https://qiita-image-store.s3.amazonaws.com/0/161939/24e875d7-46e9-2973-889a-18ee9e7d45b0.png">
<br><br>
1. 「ステップ 2: ルールの詳細を設定する」画面が表示されるので、以下の項目を設定し、「ルールの作成」ボタンを押下します。
    * 名前 : ※任意
    * 説明 : ※任意
<br><br>
    * <img width="640" alt="4003.png" src="https://qiita-image-store.s3.amazonaws.com/0/161939/f8309109-a1e9-50e7-ac26-07d97989e50b.png">

# ５．自動起動させたい EC2 インスタンスにタグを設定する

1. 以下のページから、所定の EC2 インスタンスにタグを設定します。
    * [EC2 Management Console](https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-northeast-1#Instances:sort=tag:Name)
<br><br>
    * <img width="640" alt="5001.png" src="https://qiita-image-store.s3.amazonaws.com/0/161939/15c535e8-dd25-b1b7-49b3-17ae58d234af.png">


# ９９．ハマりポイント

* 特に、ハマったところはありませんが、
<br><br>
* Python スクリプトの書き方がこれでいいのか、すこぶる不安です…。ご指摘があれば、よろしくお願いします。

# ＸＸ．まとめ

次は、EC2 インスタンスの AMI バックアップスクリプトをやろうかと。