RDS自動再起動保護




抄録
  • Amazon Relational Database Service(Amazon RDS)インスタンスを7日間以上停止することを必要としている顧客はautomatically started by Amazon RDS . データベースが起動され、停止するメカニズムがない場合顧客は、インスタンスの時給コストを支払う
  • DBインスタンスの停止と開始は、DBスナップショットの作成よりも高速であり、スナップショットを復元します.
  • このブログは、完全にServerlessでRDSクラスタを自動的に停止し、AWSリソースを作成するためにPulumiを使用する段階的なアプローチを提供します

  • 目次
  • Overview of Pulumi
  • Solution overview
  • Create RDS cluster with multiple instances
  • Create SNS topic and subscribe event to the RDS cluster
  • Create Lambda function which is subscribe to the SNS topic
  • Create lambda function to retrieve RDS cluster and instances status
  • Create lambda function to stop RDS cluster
  • Create lambda function to send slack
  • SFN IAM role to trigger lambda functions
  • Pulumi deploy stack
  • Conclusion

  • 🚀 Pulumiの概観
  • Why Pulumi? Pultureは、開発者に、彼らの好きな言語(例えばtypescript、JavaScript、PythonとGo)でコードとして基盤を書くのを可能にします.
  • ここでは、Pulumiプロジェクトとそのスタックを作成する
  • 新規プロジェクトの作成
  • pulumi new aws-typescript
    
  • AWSプロファイルの設定
  • スタックの作成/initpulumi stack init the Pulumi.<stack-name>.yaml が設定されていないので、config
  • pulumi config set aws:region ap-northeast-2
    pulumi config set aws:profile myprofile
    
  • Pulumiバッシュ完成
  • タイピングの怠惰な感じ?Pulumiのセットアップbashcomplete
  • pulumi gen-completion bash > /etc/bash_completion.d/pulumi
    
  • 更新.bashrc エイリアス
  • # add Pulumi to the PATH
    export PATH=$PATH:$HOME/.pulumi/bin
    alias plm='/home/vudao/.pulumi/bin/pulumi'
    complete -F __start_pulumi plm
    
  • 既存のインポート
  • フローをテストする新しいRDSクラスタを作成するには、既存のセキュリティグループまたはスタックに何かをインポートできます
  • pulumi import aws:ec2/securityGroup:SecurityGroup vpc_sg sg-13a02c7a
    
  • スタックをリフレッシュする
  • スタックによって管理されているリソースを手動で削除すると、リフレッシュを実行してスタックリソースの状態を更新できます
  • pulumi refresh
    

    🚀 解決概要
  • ソリューションは、RDSのイベント通知に依存します.一旦停止されたRDSインスタンスが停止した状態で最大時間を超えているので、AWSによって開始されますイベント(RDS - Event - 0154)はRDSによって生成されます.
  • RDSイベントは専用のSNSトピックにプッシュされますsns-rds-event .

  • ラムダ関数start-step-func-rds SNSトピックを購読しますsns-rds-event
  • この関数はメッセージをフィルタリングしますevent code : RDS-EVENT-0153 ( DBクラスタは最大許容時間を超えて起動しています).さらに、関数は、RDSインスタンスがタグ付けされていることを検証しますauto-restart-protection そして、タグ値がyes .
  • すべての条件が満たされると、ラムダ関数はAWSステップ関数状態マシンの実行を開始します.

  • AWSステップ関数ステートマシンは、インスタンスの状態を取得するためだけでなく、RDSインスタンスを停止しようとすると、2つのラムダ関数を統合します.
  • インスタンスの状態が'利用可能ではない場合には、状態機械は5分待ち、次に状態を再検査する.
  • 最後に、Amazon RDSのインスタンスの状態が'利用可能'ですステートマシンはAmazon RDSインスタンスを停止しようとします.
  • 注意:このブログは複数インスタンスのRDSクラスタを扱うためのものです RDS-EVENT-0154 : DBインスタンスは、最大許容時間を超えたために開始されます.

  • Pulumiを使ってIACを書き始めましょう

    🚀 複数のインスタンスを持つRDSクラスタを作成する
    • Create RDS cluster with one or more instances
    • Using the imported existing VPC (optional)

    rds.ts
    import * as aws from "@pulumi/aws";
    
    const vpc_sg = new aws.ec2.SecurityGroup("vpc_sg",
        {
            description: "Allows inbound and outbound traffic for all instances in the VPC",
            name: "vpc-sec",
            revokeRulesOnDelete: false,
            tags: {
                Name: "vpc-sec",
            }
        },
        {
            protect: true,
        }
    );
    
    export const rds_cluster = new aws.rds.Cluster('SelTestRdsEventSub', {
        //availabilityZones: ['ap-northeast-2a', 'ap-northeast-2c'],
        clusterIdentifier: 'my-test-rds-sub',
        engine: 'aurora-postgresql',
        masterUsername: 'postgres',
        masterPassword: '*****',
        dbSubnetGroupName: 'aws-test',
        databaseName: "mydb",
        skipFinalSnapshot: true,
        vpcSecurityGroupIds: [vpc_sg.id],
        tags: {
            'Name': 'my-test-rds-sub',
            'stack': 'pulumi-rds',
            'auto-restart-protection': 'yes'
        }
    });
    
    export const clusterInstances: aws.rds.ClusterInstance[] = [];
    
    for (const range = {value: 0}; range.value < 1; range.value++) {
        clusterInstances.push(new aws.rds.ClusterInstance(`SelRdsClusterInstance-${range.value}`, {
            identifier: `my-test-rds-sub-${range.value}`,
            clusterIdentifier: rds_cluster.id,
            instanceClass: aws.rds.InstanceType.T3_Medium,
            engine: 'aurora-postgresql',
            engineVersion: rds_cluster.engineVersion,
            dbSubnetGroupName: 'aws-test',
            tags: {
                'Name': `my-test-rds-sub-${range.value}`,
                'stack': 'pulumi-rds-instance',
                'auto-restart-protection': 'yes'
            }
        }))
    }
    


    🚀 SNSトピックを作成し、RDSクラスタにイベントを購読する
    • Create a SNS topic to receive events from RDS cluster
    • Create event subscription:
      • Target: the SNS topic
      • Source Type: Clusters (and point to the cluster which created from above step)
      • Specific event categories: notification

    index.ts
    import * as aws from "@pulumi/aws";
    import { state_machine_handler } from "./stepFunc";
    import { rds_cluster } from "./rds";
    
    
    const sns_rds_event = new aws.sns.Topic('SnsRdsEvent', {
        displayName: 'sns-rds-event',
        name: 'sns-rds-event',
        tags: {
            'Name': 'sns-rds-event',
            'stack': 'plumi-sns'
        }
    });
    
    const rds_event_sub = new aws.rds.EventSubscription('RdsEventSub', {
        enabled: true,
        name: 'rds-event-sub',
        eventCategories: ['notification'],
        sourceType: 'db-cluster',
        sourceIds: [rds_cluster.id],
        snsTopic: sns_rds_event.arn,
        tags: {
            'Name': 'rds-event-sub',
            'stack': 'pulumi-event'
        }
    });
    
    const sns_sub = new aws.sns.TopicSubscription('sns-topic-event-sub', {
        endpoint: state_machine_handler.arn,
        protocol: 'lambda',
        topic: sns_rds_event.arn
    });
    
    sns_rds_event.onEvent('sns-lambda-trigger', state_machine_handler, sns_sub)
    


    🚀 SNSトピックを購読するラムダ関数を作成する
    • The lambda function will be triggerd by SNS topic whenever there's event
    • The lambda function parses the event message to filter event ID RDS-EVENT-0153 and checks the RDS cluster tag for key:value auto-restart-protection: yes . If all conditions match, then the lambda function execute Step Functions state machine

    • Create IAM role which is consumed by lambda function

    iam-role
    export const allowRdsClusterRole = new aws.iam.Role("allow-stop-rds-cluster-role", {
        name: 'lambda-stop-rds-cluster',
        description: 'Role to stop rds cluster base on event',
        assumeRolePolicy: JSON.stringify({
            Version: "2012-10-17",
            Statement: [{
                Action: "sts:AssumeRole",
                Effect: "Allow",
                Sid: "",
                Principal: {
                    Service: "lambda.amazonaws.com",
                },
            }],
        }),
        tags: {
            'Name': 'lambda-stop-rds-cluster',
            'stack': 'pulumi-iam'
        },
    });
    
    const rds_policy = new aws.iam.RolePolicy("allow-stop-rds-cluster", {
        role: allowRdsClusterRole,
        policy: {
            Version: "2012-10-17",
            Statement: [
                {
                    Sid: "AllowRdsStatement",
                    Effect: "Allow",
                    Resource: "*",
                    Action: [
                        "rds:AddTagsToResource",
                        "rds:ListTagsForResource",
                        "rds:DescribeDB*",
                        "rds:StopDB*"
                    ]
                },
                {
                    Sid: "AllowSfnStatement",
                    Effect: "Allow",
                    Resource: "*",
                    Action: "states:StartExecution"
                },
                {
                    Sid: 'AllowLog',
                    Effect: 'Allow',
                    Resource: "arn:aws:logs:*:*:*",
                    Action: [
                        "logs:CreateLogGroup",
                        "logs:CreateLogStream",
                        "logs:PutLogEvents"
                    ],
                }
            ]
        },
    }, {parent: allowRdsClusterRole});
    

    • Create lambda function which is subscription of the SNS topic

    start-step-func-lambda
    export const state_machine_handler = new aws.lambda.Function('RdsSNSEvent',
        {
            code: new pulumi.asset.FileArchive('lambda-code/start-statemachine-execution-lambda/handler.tar.gz'),
            description: 'Lambda function listen to RDS SNS event topic to trigger step function',
            name: 'start-step-func-rds',
            handler: 'app.handler',
            runtime: aws.lambda.Runtime.Python3d8,
            role: handler.allowRdsClusterRole.arn,
            environment: {
                variables: {
                    'STEPFUNCTION_ARN': stepFunction.arn
                }
            },
            tags: {
                'Name': 'start-step-func-rds',
                'stack': 'pulumi-lambda'
            }
        },
        {
            dependsOn: [handler.allowRdsClusterRole]
        }
    );
    

    • Create step function state machine with flowing definitions

    StepfuncTS
    import * as aws from '@pulumi/aws';
    import * as pulumi from '@pulumi/pulumi';
    import * as handler from './handler';
    
    
    export const stepFunction = new aws.sfn.StateMachine('SfnRdsEvent', {
        name: 'sfn-rds-event',
        roleArn: handler.sfn_role.arn,
        tags: {
            'Name': 'sfn-rds-event',
            'stack': 'pulumi-sfn'
        },
        definition: pulumi.all([handler.retrieve_rds_status_handler.arn, handler.stop_rds_cluster_handler.arn, handler.send_slack_handler.arn])
            .apply(([retrieveArn, stopRdsArn, sendSlackArn]) => {
            return JSON.stringify({
                "Comment": "RdsAutoRestartWorkFlow: Automatically shutting down RDS instance after a forced Auto-Restart",
                "StartAt": "retrieveRdsClustertate",
                "States": {
                    "retrieveRdsClustertate": {
                        "Type": "Task",
                        "Resource": retrieveArn,
                        "TimeoutSeconds": 5,
                        "Retry": [
                            {
                            "ErrorEquals": [
                                "Lambda.Unknown",
                                "States.TaskFailed"
                            ],
                            "IntervalSeconds": 3,
                            "MaxAttempts": 2,
                            "BackoffRate": 1.5
                            }
                        ],
                        "Catch": [
                            {
                            "ErrorEquals": [
                                "States.ALL"
                            ],
                            "Next": "fallback"
                            }
                        ],
                        "Next": "isRdsClusterAvailable"
                    },
                    "isRdsClusterAvailable": {
                        "Type": "Choice",
                        "Choices": [
                            {
                            "Variable": "$.readyToStop",
                            "StringEquals": "yes",
                            "Next": "stopRdsCluster"
                            }
                        ],
                        "Default": "waitFiveMinutes"
                    },
                    "waitFiveMinutes": {
                        "Type": "Wait",
                        "Seconds": 300,
                        "Next": "retrieveRdsClustertate"
                    },
                    "stopRdsCluster": {
                        "Type": "Task",
                        "Resource": stopRdsArn,
                        "TimeoutSeconds": 5,
                        "Retry": [
                            {
                            "ErrorEquals": [
                                "States.Timeout"
                            ],
                            "IntervalSeconds": 3,
                            "MaxAttempts": 2,
                            "BackoffRate": 1.5
                            }
                        ],
                        "Catch": [
                            {
                            "ErrorEquals": [
                                "States.ALL"
                            ],
                            "Next": "fallback"
                            }
                        ],
                        "Next": "retrieveRdsClustertateStopping"
                    },
                    "retrieveRdsClustertateStopping": {
                        "Type": "Task",
                        "Resource": retrieveArn,
                        "TimeoutSeconds": 5,
                        "Retry": [
                            {
                            "ErrorEquals": [
                                "States.Timeout"
                            ],
                            "IntervalSeconds": 3,
                            "MaxAttempts": 2,
                            "BackoffRate": 1.5
                            }
                        ],
                        "Catch": [
                            {
                            "ErrorEquals": [
                                "States.ALL"
                            ],
                            "Next": "fallback"
                            }
                        ],
                        "Next": "isRdsClusterStopped"
                    },
                    "isRdsClusterStopped": {
                        "Type": "Choice",
                        "Choices": [
                            {
                            "Variable": "$.rdsClusterStatus",
                            "StringEquals": "stopped",
                            "Next": "sendSlack"
                            }
                        ],
                        "Default": "waitFiveMinutesStopping"
                    },
                    "waitFiveMinutesStopping": {
                        "Type": "Wait",
                        "Seconds": 300,
                        "Next": "retrieveRdsClustertateStopping"
                    },
                    "sendSlack": {
                        "Type": "Task",
                        "Resource": sendSlackArn,
                        "TimeoutSeconds": 5,
                        "End": true
                    },
                    "fallback": {
                        "Type": "Task",
                        "Resource": sendSlackArn,
                        "TimeoutSeconds": 5,
                        "End": true
                    }
                }
            });
        })
    });
    

    🚀 RDSクラスタとインスタンスのステータスを取得するラムダ関数を作成する

    retrieve-rds-status.ts
    export const retrieve_rds_status_handler = new aws.lambda.Function('RetrieveRdsStateFunc', {
        code: new pulumi.asset.FileArchive('lambda-code/retrieve-rds-instance-state-lambda/handler.tar.gz'),
        description: 'Lambda function to retrieve rds instance status',
            name: 'get-rds-status',
            handler: 'app.handler',
            runtime: aws.lambda.Runtime.Python3d8,
            role: allowRdsClusterRole.arn,
            tags: {
                'Name': 'get-rds-status',
                'stack': 'pulumi-lambda'
            }
    });
    


    🚀 RDSクラスタを停止するラムダ関数を作成する

    stop-rds.ts
    export const stop_rds_cluster_handler = new aws.lambda.Function('StopRdsClusterFunc', {
        code: new pulumi.asset.FileArchive('lambda-code/stop-rds-instance-lambda/handler.tar.gz'),
        description: 'Lambda function to retrieve rds instance status',
            name: 'stop-rds-cluster',
            handler: 'app.handler',
            runtime: aws.lambda.Runtime.Python3d8,
            role: allowRdsClusterRole.arn,
            tags: {
                'Name': 'stop-rds-cluster',
                'stack': 'pulumi-lambda'
            }
    });
    


    🚀 ラムダ送信機能

    send-slack.ts
    export const send_slack_handler = new aws.lambda.Function('SendSlackFunc', {
        code: new pulumi.asset.FileArchive('lambda-code/send-slack/handler.tar.gz'),
        description: 'Lambda function to send slack',
            name: 'rds-send-slack',
            handler: 'app.handler',
            runtime: aws.lambda.Runtime.Python3d8,
            role: allowRdsClusterRole.arn,
            tags: {
                'Name': 'rds-send-slack',
                'stack': 'pulumi-lambda'
            }
    });
    


    🚀 ラムダ関数をトリガーするSFN IAMロール

    sfn-role.ts
    export const sfn_role = new aws.iam.Role('SfnRdsRole', {
        name: 'sfn-rds',
        description: 'Role to trigger lambda functions',
        assumeRolePolicy: JSON.stringify({
            Version: "2012-10-17",
            Statement: [{
                Action: "sts:AssumeRole",
                Effect: "Allow",
                Sid: "",
                Principal: {
                    Service: "states.ap-northeast-2.amazonaws.com",
                },
            }],
        }),
        tags: {
            'Name': 'sfn-rds',
            'stack': 'pulumi-iam'
        }
    });
    


    🚀 Pulumiスタックを展開する

    🚀 結論
    • We now can save time and save money with this solution. Plus, we will receive slack message when there're events

    • Although Pulumi Supports Many Clouds and provisioner and can visulize the resources chart within the stack but there're more options such as AWS Cloud Development Kit (CDK)


    Ref: Field Notes: Stopping an Automatically Started Database Instance with Amazon RDS
    .ltag__user__id__512906 .アクションボタン
    背景色:こっち重要
    色:168度62 df 88!重要
    ボーダーカラー:こっち重要


    🚀 Vu Dao 🚀 フォロー
    🚀 AWSome Devops | AWS Community Builder | AWS SA || ☁️ CloudOpz ☁️

    vumdao / vumdao