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


この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので十分ご注意ください。

はじめに

AWS Step Functionsを使用して、自動的に開始されたRDS DBインスタンスを停止してみるというブログを見まして影響を受けました。

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


構成図


ハンズオン

構築の流れ

1.VPC作成

2.RDS作成

3.Lambda作成

4.EventBridge作成

上記の順番で構築を行なっていきます。

最終的には、RDSを起動するとLambdaがRDSに対して停止をする動きをします。

1.VPC作成

以前記載したブログCloudFormationを使ってVPC構築に沿って、VPCを構築します。

 2.RDS作成

LambdaではRDSをタグ(キー:AutoStop、値:true)が付与されれいるものを対象とするので、Tagsに記載をしておきます。

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 日以上停止する方法を教えて下さい。

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のイベントパターンを確認した際の画面です。

RDSのイベントパターンはdetail.EventIDで該当させたいパターンの値を下記URLから探し記載します。

参考:Amazon RDS のイベントカテゴリとイベントメッセージ

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を起動させる

②RDSが再起動後、自動的にRDSが停止する

③EventBridgeのメトリクスを確認する

時間がUTCですが +9時間すると、RDSが再起動した時間とほぼ同じです。
Lambdaにsleepしないと、この時点で起動してしまい空振りしてしまいました。
起動通知=RDSが構築され終わっている訳ではないという想定をしています。

④Lambdaのログを確認する

こちらにも17:54に通知をうけてLambdaを実行して、その後180秒停止して再起動したRDSを止めに行っている動きを確認することが出来ました。


さいごに

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

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

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

Last modified: 2022-12-03

Author