はじめに
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 | 難 | 失敗した場合の再実行などの細かく設定可能 |
ただし何がベストなのかアンチパターンなのか?それを自分で構築してみないことには分からないものだと思いながら、今年も残り少ないですが手を動かしていきたいと思います。