指定したAuroraクラスターを停止する Lambda (Python3.7版）

## 概要
* Auroraクラスターの停止機能をようやく試してみようと思い検証してみたところ、停止状態のまま７日経過すると自動的に開始する機能だった。これは、必要なメンテナンスの更新が遅れ過ぎないようにするためにそうしているらしい。この一時停止機能を使って、Auroraを自動的に長期停止するものを作成してみた。
    * 参考：[Amazon Aurora DB クラスターの停止と開始
](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/aurora-cluster-stop-start.html)

* Auroraの自動停止は、Lambda と CloudWatch イベントで実行する
    * **Lambda**
        * Python3.7
        * Lambdaに特定のAuroraクラスター識別子を渡すことで、そのAuroraクラスターだけを停止する。
        * １つのLambda関数が停止できるAuroraクラスターは１つという拙いプログラムなので、停止したいAuroraクラスターが複数ある場合は、その数だけLambda関数を設定する必要がある （なので、ワンセットまるっと作成する CloudFormation を使う）。
        * 停止対象の Aurora クラスターの Status が available （正常起動）の場合に、stop_db_cluster() を実行する。
        * 停止対象のAuroraクラスターと同じAWSアカウントにLambdaを作成する。
    * **CloudWatch イベント**
        * Auroraクラスターが起動したことを察知するために、"AWS API Call via CloudTrail" で "StartDBCluster" イベントが発生したら、stop_db_cluster() しようと思ったが、イベント発生後すぐには クラスターのステータスが available にならず、stop_db_client() が失敗するので、cron式で毎時 Lambda を実行するようにしている。

* Auroraクラスターを停止したら Slack へ通知する。

## 説明
### CloudFormation（JSON版）
StopDBCluster.json

```json
{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description" : "Stop DB Cluster",
    "Parameters" : {
      "AwsAccountName" : {
        "Type" : "String"
      },
      "DbClusterIdentifier" : {
        "Type" : "String"
      }
    },
    "Resources": {
        "IAMRole" : {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument":
                {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": "lambda.amazonaws.com"
                            },
                            "Action": "sts:AssumeRole"
                        }
                    ]
                },
                "ManagedPolicyArns": [
                    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
                ],
                "Path": "/",
                "Policies" : [
                    {
                        "PolicyName" : { "Fn::Join" : [ "", ["StopDBCluster-", { "Ref" : "DbClusterIdentifier"} ] ] },
                        "PolicyDocument" : {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Sid" : "StopDBCluster",
                                    "Effect": "Allow",
                                    "Action": [
                                        "rds:StopDBCluster",
                                        "rds:DescribeDBClusters"
                                    ],
                                    "Resource": "*"
                                }
                            ]
                    }
                        }
                 ],
                "RoleName" : { "Fn::Join" : [ "", ["stop-db-cluster_", { "Ref" : "DbClusterIdentifier"} ] ] }
            }
        },
        "LambdaFunction" : {
            "DependsOn" : [ "IAMRole" ],
            "Type" : "AWS::Lambda::Function",
            "Properties" : {
                "Code" : {
                    "S3Bucket" : { "Fn::Join" : [ "", [ "lambda-function-", { "Ref" : "AWS::AccountId" } ] ] },
                    "S3Key" : "stop-db-cluster.py.zip"
                },
                "Environment" : {
                    "Variables" : {
                        "AWS_ACCOUNT_ALIAS": { "Ref" : "AwsAccountName" }, 
                        "DB_CLUSTER_IDENTIFIER": { "Ref" : "DbClusterIdentifier" } , 
                        "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/〜"
                     }
                },
                "FunctionName" : { "Fn::Join" : [ "", ["stop-db-cluster_", { "Ref" : "DbClusterIdentifier"} ] ] },
                "Handler" : "stop-db-cluster.lambda_handler",
                "Role" : { "Fn::GetAtt" : [ "IAMRole", "Arn" ] },
                "Runtime" : "python3.7",
                "Timeout" : 120
            }
        },
        "EventsRule": {
            "Type": "AWS::Events::Rule",
            "Properties": {
                "Description": "Reserved Instance Expiration Data Check",
                "Name" : { "Fn::Join" : [ "", ["stop-db-cluster_", { "Ref" : "DbClusterIdentifier"} ] ] },
                "ScheduleExpression" : "cron(0 0 * * ? *)",
                "State": "ENABLED",
                "Targets": [ {
                    "Arn": { "Fn::GetAtt": ["LambdaFunction", "Arn"] },
                    "Id": "TargetFunctionV1"
                } ]
            }
        },
        "PermissionForEventsToInvokeLambda": {
            "Type": "AWS::Lambda::Permission",
            "Properties": {
                "FunctionName": { "Ref": "LambdaFunction" },
                "Action": "lambda:InvokeFunction",
                "Principal": "events.amazonaws.com",
                "SourceArn": { "Fn::GetAtt": ["EventsRule", "Arn"] }
            }
        }
    },
    "Outputs" : {
        "IAMRole" : {
            "Description" : "IAM Role Name",
            "Value" : { "Ref" : "IAMRole" }
        },
        "LambdaFunction" : {
            "Description" : "Lambda Function Name",
            "Value" : { "Ref" : "LambdaFunction" }
        },
        "EventsRule" : {
            "Description" : "Events Rule Name",
            "Value" : { "Ref" : "EventsRule" }
        }
    }
}
```
* このCloudFormationのテンプレートをAWSマネジメントコンソールの CLoudFormation に読み込ますと、次のような入力画面がでる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/b8027636-6635-3b71-5ab8-8136d22eda26.png)
 * 次のように入力する
<br />**・スタックの名前**：任意。例えば、「StopDBCluster-クラスター識別子」
<br />**・AwsAccountName**：任意。AWSアカウントのエイリアス名。Slack通知のときに使う。
<br />**・DbClusterIdentifier**：停止するクラスターの識別子

* このCloudFormationで作られるもの
 * **IAMロール**
<br />**・ロール名**：stop-db-cluster_クラスター識別子
<br />**・ポリシー**：AWSLambdaBasicExecutionRole（既存のAWS管理ポリシー）
<br />**・インラインポリシー**：StopDBCluster-クラスター識別子（rds:StopDBClusterとrds:DescribeDBClustersを付加）
 
 * **Lambda関数**
<br />**・関数名**：stop-db-cluster_クラスター識別子
<br />**・実態のPythonコード**：「stop-db-cluster.py」は圧縮してS3に置く（後述）
 * **CloudWatchイベントルール**
<br />**・ルール名**：stop-db-cluster_クラスター識別子
<br />**・スケジュール**：Cron式「0 0 * * ? *」※GMT指定なので、日本時間に換算すると毎日９時

* Lambdaに渡す変数は、環境変数にセットする（CloudFormationで作成後でも、Lambdaの設定画面から変更可能）
 * "**AWS_ACCOUNT_ALIAS**": { "Ref" : "AwsAccountName" }, 
<br />CloudFormation実行時にWeb画面から入力したパラメータ「AwsAccountName」がセットされる。
 * "**DB_CLUSTER_IDENTIFIER**": { "Ref" : "DbClusterIdentifier" } 
<br />CloudFormation実行時にWeb画面から入力したパラメータ「DB_CLUSTER_IDENTIFIER」がセットされる。
 * "**SLACK_WEBHOOK_URL**": "https://hooks.slack.com/services/〜"
<br />Auroraの停止を通知するSlackのWebhook URLを入力する。

### stop-db-cluster.py （Python3.7版）
```python
import os
import json
import boto3
import logging
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

DB_CLUSTER_IDENTIFIER = os.environ['DB_CLUSTER_IDENTIFIER']
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):
    client = boto3.client('rds')
    response = client.describe_db_clusters(DBClusterIdentifier=DB_CLUSTER_IDENTIFIER)
    for DBCluster in response['DBClusters']:
        print (DBCluster['Status'])
        if DBCluster['Status'] == "available":
            client.stop_db_cluster(DBClusterIdentifier=DB_CLUSTER_IDENTIFIER)
            slack()

def slack():
    color = "good" 
    icon = ":bulb:" 
    SlackText="Aurora Cluster Stop" 
    SlackTextAttachments = "AWS: " + AWS_ACCOUNT_ALIAS + "\n" + "Cluster Name: " + DB_CLUSTER_IDENTIFIER
    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)
```
* 引数「DB_CLUSTER_IDENTIFIER」で渡された クラスター識別子の Status が available であれば、stop_db_cluster() を実行して、Slackへ通知する。
* この stop-db-cluster.py ファイルを zip で圧縮してから、S3の次の名前のバケットを作成してその中にアップロードする。
<br />lambda-function-nnnnnnnnnnnn
<br />（nnnnnnnnnn は AWSアカウントID）

## 設定
* 停止対象の Auroraクラスターと同じAWSアカウントの同じリージョンに、S3バケット「lambda-function-nnnnnnnnnnnn」を作成する（nnnnnnnnnnnn はそのAWSアカウントのID）。
* stop-db-cluster.py.zip ファイルを S3バケット「lambda-function-nnnnnnnnnnnn」へアップロードする。
* 停止対象の Auroraクラスターと同じAWSアカウントの同じリージョンの CloudFormation に StopDBCluster.json ファイルを読み込ませる。
* スタックの名前、パラメーター（AwsAccountName、DbClusterIdentifier）を入力して、CloudFormationを実行する。

## Aurora停止時のSlack通知例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/bd6fd768-9bdf-a70c-5b7f-acf469c0c94b.png)