RDS起動をEventBridgeで検知してLambdaで停止する構築ハンズオン

## はじめに
[AWS Step Functionsを使用して、自動的に開始されたRDS DBインスタンスを停止してみる](https://cloud5.jp/saitou-handson-rdsautostop/)というブログを見まして影響を受けました。

思い返すと自分が設定している環境では、RDSを自動停止するにはEventBridgeのCronでスケジュールを利用して停止させてばかりだなと思いまして、RDSの起動イベント通知をEventBridgeで検知してRDSを停止するLambdaを構築してみたいと思いましてハンズオンしました。

------------

## 構成図
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-03-19.41.58-640x253.png)

------------

## ハンズオン
### 構築の流れ
#### 1.VPC作成
#### 2.RDS作成
#### 3.Lambda作成
#### 4.EventBridge作成
上記の順番で構築を行なっていきます。

最終的には、RDSを起動するとLambdaがRDSに対して停止をする動きをします。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-03-18.12.58-640x106.png)


### 1.VPC作成
以前記載したブログ[CloudFormationを使ってVPC構築](https://cloud5.jp/cfn_vpc/)に沿って、VPCを構築します。

###　2.RDS作成
LambdaではRDSをタグ（キー：AutoStop、値：true）が付与されれいるものを対象とするので、`Tags`に記載をしておきます。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  RDS for  MySQL Create
###メタデータ
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
      - Label:
          default: "RDS Configuration"
        Parameters:
          - DBInstanceName
          - MySQLMajorVersion
          - MySQLMinorVersion
          - DBInstanceClass
          - DBInstanceStorageSize
          - DBInstanceStorageType
          - DBName
          - DBMasterUserName
          - DBPassword
          - MultiAZ
    ParameterLabels:
      DBInstanceName:
        default: "DBInstanceName"
      MySQLMajorVersion:
        default: "MySQLMajorVersion"
      MySQLMinorVersion:
        default: "MySQLMinorVersion"
      DBInstanceClass:
        default: "DBInstanceClass"
      DBInstanceStorageSize:
        default: "DBInstanceStorageSize"
      DBInstanceStorageType:
        default: "DBInstanceStorageType"
      DBName:
        default: "DBName"
      DBMasterUserName:
        default: "DBUserName"
      DBPassword:
        default: "DBPassword"
      MultiAZ:
        default: "MultiAZ"
      CopyTagsToSnapshot:
        default: "CopyTagsToSnapshot"

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  PJPrefix:
    Type: String
    Default: "cfn-inamura"
  DBInstanceName:
    Type: String
    Default: "mysql"
  MySQLMajorVersion:
    Type: String
    Default: "8.0"
  MySQLMinorVersion:
    Type: String
    Default: "28"
    AllowedValues: [ "31", "30", "28", "27", "26", "25", "23" ]
  DBInstanceClass:
    Type: String
    Default: "db.t2.micro" 
  DBInstanceStorageSize:
    Type: String
    Default: "20"
  DBInstanceStorageType:
    Type: String
    Default: "gp2"
  DBName:
    Type: String
    Default: "db"
  DBMasterUserName:
    Type: String
    Default: "dbuser"
    NoEcho: true
    MinLength: 1
    MaxLength: 16
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
    ConstraintDescription: "must begin with a letter and contain only alphanumeric characters."
  DBPassword: 
    Default: "dbpassword"
    NoEcho: true
    Type: String
    MinLength: 8
    MaxLength: 41
    AllowedPattern: "[a-zA-Z0-9]*"
    ConstraintDescription: "must contain only alphanumeric characters."
  MultiAZ: 
    Default: "false"
    Type: String
    AllowedValues: [ "true", "false" ]
  CopyTagsToSnapshot:
    Default: "false"
    Type: String
    AllowedValues: [ "true", "false" ]

Resources: 
# ------------------------------------------------------------#
#  DBInstance MySQL
# ------------------------------------------------------------#
  DBInstance: 
    Type: "AWS::RDS::DBInstance"
    Properties: 
      DBInstanceIdentifier: !Sub "${PJPrefix}-${DBInstanceName}"
      Engine: MySQL
      EngineVersion: !Sub "${MySQLMajorVersion}.${MySQLMinorVersion}"
      DBInstanceClass: !Ref DBInstanceClass
      AllocatedStorage: !Ref DBInstanceStorageSize
      StorageType: !Ref DBInstanceStorageType
      DBName: !Ref DBName
      MasterUsername: !Ref DBMasterUserName
      MasterUserPassword: !Ref DBPassword
      DBSubnetGroupName: !Ref DBSubnetGroup
      PubliclyAccessible: false
      MultiAZ: !Ref MultiAZ
      PreferredBackupWindow: "18:00-18:30"
      PreferredMaintenanceWindow: "sat:19:00-sat:19:30"
      AutoMinorVersionUpgrade: false
      DBParameterGroupName: !Ref DBParameterGroup  
      VPCSecurityGroups:
        - !Ref RDSSecurityGroup
      CopyTagsToSnapshot: !Ref CopyTagsToSnapshot
      BackupRetentionPeriod: 7
      Tags: 
        - Key: "Name"
          Value: !Ref DBInstanceName
        - Key: "AutoStop"
          Value: "true"
    DeletionPolicy: "Delete"

# ------------------------------------------------------------#
#  DBParameterGroup
# ------------------------------------------------------------#
  DBParameterGroup:
    Type: "AWS::RDS::DBParameterGroup"
    Properties:
      Family: !Sub "MySQL${MySQLMajorVersion}"
      Description: !Sub "${PJPrefix}-${DBInstanceName}-parm"

# ------------------------------------------------------------#
#  SecurityGroup for RDS (MySQL)
# ------------------------------------------------------------#
  RDSSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !ImportValue cfn-inamura-vpc
      GroupName: !Sub "${PJPrefix}-${DBInstanceName}-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${PJPrefix}-${DBInstanceName}-sg"
# Rule
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !ImportValue cfn-inamura-vpc-cidr

# ------------------------------------------------------------#
#  DBSubnetGroup
# ------------------------------------------------------------#
  DBSubnetGroup: 
    Type: "AWS::RDS::DBSubnetGroup"
    Properties: 
      DBSubnetGroupName: !Sub "${PJPrefix}-${DBInstanceName}-subnet"
      DBSubnetGroupDescription: "-"
      SubnetIds: 
        - !ImportValue cfn-inamura-private-subneta
        - !ImportValue cfn-inamura-private-subnetc

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#                
Outputs:
#DBInstance
  DBInstanceID:
    Value: !Ref DBInstance
    Export:
      Name: !Sub "${PJPrefix}-${DBInstanceName}-id"

  DBInstanceEndpoint:
    Value: !GetAtt DBInstance.Endpoint.Address
    Export:
      Name: !Sub "${PJPrefix}-${DBInstanceName}-endpoint"

  DBName:
    Value: !Ref DBName
    Export:
      Name: !Sub "${PJPrefix}-${DBInstanceName}-dbname"

```


### 3.Lambda作成
EventBridgeをトリガーにして起動するLambdaです。
RDSの起動をEventBridgeが受け付けてからLambdaを起動するような構築ですが、実際に起動させてみるとLambdaが空振りしてしまいました。
そのためコードに`time.sleep(180)`を記述することで、RDSの再起動が終わり切るのを待つようにしています。
上記の理由のためTimeout値は`300`秒としています。

※この時点で従来利用している`cron`の方がリアルタイムではないものの失敗が少ないのではないかと思いました。

※Lambdaを`time.sleep(180)`しているので、この構築一つだけであれば問題ないですが、金額及び起動時間のリソース的な観点からも、好ましくなさそうという個人の感想です。

 参考：[Amazon RDS インスタンスを 7 日以上停止する方法を教えて下さい。](https://aws.amazon.com/jp/premiumsupport/knowledge-center/rds-stop-seven-days/)

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description:
  Lambda Create
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Lambda Configuration"
        Parameters:
        - FunctionName
        - Description
        - Handler
        - MemorySize
        - Runtime
        - Timeout

# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  FunctionName:
    Type: String
    Default: "cfn-lmd-inamura"
  Description:
    Type: String
    Default: "cfn-lmd-inamura"
  Handler:
    Type: String
    Default: "index.lambda_handler"
  MemorySize:
    Type: String
    Default: "128"
  Runtime:
    Type: String
    Default: "python3.9"
  Timeout:
    Type: String
    Default: "300"

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  Lambda
# ------------------------------------------------------------#
  Lambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import boto3
          import os
          import time
          rds = boto3.client('rds')
          KEY = os.environ['KEY']
          VALUE = os.environ['VALUE']

          def lambda_handler(event, context):
              print("StoppingRDS_Funcrtion Start")
              time.sleep(180)
              dbs = rds.describe_db_instances()
              for db in dbs['DBInstances']:
                  #Check if DB instance is not already stopped
                  if (db['DBInstanceStatus'] == 'available'):
                      DoNotStop=1
                      try:
                          GetTags=rds.list_tags_for_resource(ResourceName=db['DBInstanceArn'])['TagList']
                          for tags in GetTags:
                              if(tags['Key'] == str(KEY) and tags['Value'] == str(VALUE)):
                                  result = rds.stop_db_instance(DBInstanceIdentifier=db['DBInstanceIdentifier'])
                                  print ("Stopping instance: {0}.".format(db['DBInstanceIdentifier']))
                          if(DoNotStop == 1):
                              DoNotStop=1
                      except Exception as e:
                          print ("No Target RDS {0}.".format(db['DBInstanceIdentifier']))
                          print(e)
                          
          if __name__ == "__main__":
              lambda_handler(None, None)


      Description: !Ref Description
      FunctionName: !Ref FunctionName
      Handler: !Ref Handler 
      MemorySize: !Ref MemorySize
      Runtime: !Ref Runtime
      Timeout: !Ref Timeout
      Environment:
        Variables:
          KEY: AutoStop
          VALUE: true
      Role: !GetAtt LambdaRole.Arn

  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${FunctionName}-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: !Sub "${FunctionName}-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "logs:CreateLogGroup"
                Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*"
              - Effect: "Allow"
                Action:
                  - "rds:DescribeDBInstances"
                  - "rds:StopDBInstance"
                  - "rds:AddTagsToResource"
                  - "rds:ListTagsForResource"
                  - "states:StartExecution"
                Resource: "*"



# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#                
Outputs:
  LambdaArn:
    Value: !GetAtt Lambda.Arn
    Export:
      Name: !Sub "${FunctionName}-arn"
```

### 4.EventBridge作成
RDSの再起動を検知すると、Lambdaを起動させます。

下記は、構築後のマネコンからEventBridgeのイベントパターンを確認した際の画面です。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-03-18.42.34.png)
RDSのイベントパターンは`detail.EventID`で該当させたいパターンの値を下記URLから探し記載します。

参考：[Amazon RDS のイベントカテゴリとイベントメッセージ](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/USER_Events.Messages.html)
```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  EventBridge gets s3 events and sends them to lambda
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Eventbridge Configuration"
        Parameters:
          - Name
          - EventBusName
          - State
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  EventBusName:
    Type: String
    Default: "default"
  Name:
    Type: String
    Default: "cfn-evb-rdsstop-inamura"
  State:
    Type: String
    Default: "ENABLED"

# ------------------------------------------------------------#
#  EventBridge
# ------------------------------------------------------------#
Resources:
  RDSStopRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "Get S3 events and send to Lambda"
      EventBusName: !Ref EventBusName
      Name: !Ref Name
      State: !Ref State
      EventPattern:
        source: 
          - aws.rds
        detail-type:
          - RDS DB Instance Event
        detail.EventID:
          - RDS-EVENT-0006
      Targets:
        - Arn: !ImportValue cfn-lmd-inamura-arn
          Id: "cfn-lmd-inamura"
  
  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: cfn-lmd-inamura
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt 'RDSStopRule.Arn'
```

### 挙動の確認
#### ①停止しているRDSを起動させる
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-03-18.53.32-640x29.png)

#### ②RDSが再起動後、自動的にRDSが停止する
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-03-18.55.53-640x58.png)

#### ③EventBridgeのメトリクスを確認する
時間がUTCですが +9時間すると、RDSが再起動した時間とほぼ同じです。
Lambdaに`sleep`しないと、この時点で起動してしまい空振りしてしまいました。
起動通知＝RDSが構築され終わっている訳ではないという想定をしています。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-03-19.10.06.png)

#### ④Lambdaのログを確認する
こちらにも17：54に通知をうけてLambdaを実行して、その後180秒停止して再起動したRDSを止めに行っている動きを確認することが出来ました。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-03-19.11.56-640x113.png)

------------

## さいごに
構築後の個人的な感想のまとめです。
自分の現場レベルでは、1.のCronでRDSを停止できれば十分満足できるので採用されないのかと思います。
本気で止めたい場合は3.のStepFunctionsを採用するだろうし、そういった意味ではあまりよくない方法なんだろうなと腑に落としました。

|No.|リソース名|構築難易度 | 理由|
|---|---|---|---|
|1.|EventBridge(Cron)|易|導入しやすい|
|2.|EventBridge(イベント検知)|中|cronよりは柔軟な設定はできるが、StepFunctions程は出来ない|
|3.|StepFunctions|難|失敗した場合の再実行などの細かく設定可能|

ただし何がベストなのかアンチパターンなのか？それを自分で構築してみないことには分からないものだと思いながら、今年も残り少ないですが手を動かしていきたいと思います。