techblog

RDSストレージオートスケーリングとシークレットマネージャー統合について検証してみた

このブログ記事では、Amazon RDSのストレージオートスケーリング機能とAWSシークレットマネージャーとの統合について検証します。Terraformを使用して環境を設定し、Pythonスクリプトを使ってデータをRDSインスタンスに挿入し、オートスケーリング機能の動作を確認します。この記事では、事前条件、環境構築のプロセス、ストレージオートスケーリングのテスト手順について詳しく説明します。また、RDSオートスケーリングを使用する際の制限事項や注意点についても取り上げます。

RDSストレージオートスケーリングとシークレットマネージャー統合について検証してみた

目次

  1. 概要
  2. 動作検証流れ
  3. 前提条件
  4. 環境構築
    1. RDSシークレットマネージャー統合機能について
  5. RDSストレージオートスケーリング
    1. RDSへデータを入れ続けるプログラム設置
    2. 必要パッケージインストール
    3. プログラム実行
    4. ストレージ枯渇後の動作確認
      1. RDSイベント取得
      2. オートスケーリング前後のCloudWatch画面
    5. RDSストレージオートスケーリング制限事項
  6. おわりに

概要

こんにちは、久方ぶりのブログ投稿となりますが、皆さまいかがお過ごしでしょうか。

さて、今回は、RDSのストレージオートスケーリング機能および、RDS作成時にシークレットマネージャーとの統合項目が増えておりましたのでこれらの動作確認をした記録となります。

動作検証流れ

本記事では、Terraformコードを用いて最初に検証する環境を作成します。

その後、PythonのプログラムからRDSに対して、データを入れていき、ストレージオートスケーリング動作を確認します。

※Terraformコード中は、シークレットマネージャー統合機能を使用せず、従来方式であるデータベースユーザおよびデータベースパスワードを直接指定するようにしています。

※シークレットマネージャー統合については、実際に筆者の方で動作確認し、気になった点を記載しています。

前提条件

  • EC2インスタンスへのAWS CLIコマンドインストール、IAMロールについては解説しません。
  • RDSへ接続可能なEC2については前提知識とし解説しません。
  • Terraformの基礎知識は解説しません。

環境構築

次のTerraformコードでは、VPC、サブネット、ストレージオートスケーリングを監視する為のCloudWatch Alarm、SNS通知等の設定を記載しております。

サブネットや、セキュリティグループ、通知用メールアドレス等いくつか環境に合わせて変更すべき箇所をコード中に明示していますので試される際は、ご承知ください。

また、作成されるRDSデータベースエンジンはMariaDBとなっています。

# rds.tf

variable "env_name" {
  description = "Environment name prefix"
  type        = string
  default     = "dev" # 環境名プレフィックスを指定
}

variable "subnet_ids" {
  description = "List of subnet IDs for the RDS subnet group"
  type        = list(string)
  default     = ["{既存のサブネットID、1番目を指定}", "{既存のサブネットID、2番目を指定}"] # 既存のサブネットIDを指定
}

variable "security_group_ids" {
  description = "List of existing security group IDs"
  type        = list(string)
  default     = ["{既存のセキュリティグループIDを指定}"] # 既存のセキュリティグループIDを指定
}

variable "notification_email" {
  description = "Email address for SNS notifications"
  type        = string
  default     = "{メールアドレスを指定}" # 通知を受け取るメールアドレスを指定
}

variable "allocated_storage" {
  description = "Initial allocated storage in GB"
  type        = number
  default     = 20 # 初期割り当てストレージ
}

variable "max_allocated_storage" {
  description = "Maximum allocated storage in GB"
  type        = number
  default     = 50 # 最大割り当てストレージ
}

# ストレージの10%未満の値を計算
locals {
  storage_threshold = var.allocated_storage * 0.10 * 1024 * 1024 * 1024 # 10%をバイトに変換
}

# SNSトピックの作成
resource "aws_sns_topic" "rds_storage_scaling_alerts" {
  name = "${var.env_name}-rds-storage-scaling-alerts"
}

# SNSトピックのサブスクリプション(メール通知)
resource "aws_sns_topic_subscription" "email_subscription" {
  topic_arn = aws_sns_topic.rds_storage_scaling_alerts.arn
  protocol  = "email"
  endpoint  = var.notification_email
}

# サブネットグループの作成
resource "aws_db_subnet_group" "rds_subnet_group" {
  name       = "${var.env_name}-rds-subnet-group"
  subnet_ids = var.subnet_ids

  tags = {
    Name = "${var.env_name}-RDSSubnetGroup"
  }
}

# RDSパラメータグループの作成
resource "aws_db_parameter_group" "mariadb_parameters" {
  name   = "${var.env_name}-mariadb-parameter-group"
  family = "mariadb10.11" # MariaDBの最新バージョンに合わせて変更

  parameter {
    name  = "max_connections"
    value = "100"
  }

  # 他のMariaDBに適用するべきパラメータをここに追加
}

# RDSインスタンスの作成
resource "aws_db_instance" "mariadb" {
  identifier            = "${var.env_name}-my-mariadb-instance"
  allocated_storage     = var.allocated_storage
  max_allocated_storage = var.max_allocated_storage
  storage_type          = "gp3"
  engine                = "mariadb"
  engine_version        = "10.11.8" # MariaDBの最新バージョンを指定
  instance_class        = "db.t3.medium"
  username              = "Admin"
  password              = "passw0rd" # manage_master_user_password が有効の場合、コメントアウトすること逆も然り
  #manage_master_user_password = true       # AWS Secrets Manager で管理を有効にする
  parameter_group_name   = aws_db_parameter_group.mariadb_parameters.name
  skip_final_snapshot    = true
  multi_az               = false # SingleAZに設定
  db_subnet_group_name   = aws_db_subnet_group.rds_subnet_group.name
  vpc_security_group_ids = var.security_group_ids
  availability_zone      = "ap-northeast-1a"

  tags = {
    Name = "${var.env_name}-MyMariaDBInstance"
  }
}

# CloudWatchアラームの作成
resource "aws_cloudwatch_metric_alarm" "rds_storage_alarm" {
  alarm_name          = "${var.env_name}-RDSStorageAutoScalingAlarm"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "FreeStorageSpace"
  namespace           = "AWS/RDS"
  period              = "300" # 5分間の期間
  statistic           = "Average"
  threshold           = local.storage_threshold # 10%未満の空きストレージ容量
  alarm_description   = "This alarm triggers when free storage space is less than 10% of the allocated storage for 5 minutes."
  dimensions = {
    DBInstanceIdentifier = aws_db_instance.mariadb.identifier
  }
  alarm_actions             = [aws_sns_topic.rds_storage_scaling_alerts.arn]
  ok_actions                = [aws_sns_topic.rds_storage_scaling_alerts.arn]
  insufficient_data_actions = [aws_sns_topic.rds_storage_scaling_alerts.arn]
}

RDSシークレットマネージャー統合機能について

manage_master_user_password を有効にするとRDSをAWS Secrets Managerで管理にできます。

RDSによってパスワード管理および自動ローテーション有効状態となり管理されます。

パスワードの更新が定期的になされ、シークレットの名前が明示的に指定できないため注意が必要となります。

RDSストレージオートスケーリング

RDSストレージオートスケーリングの動作確認をおこなう為にいくつか事前作業があります。

次のAWS CLIコマンドを使用し、作成したRDSのストレージサイズを確認します。

デプロイ時点で、20GBとなっていれば問題ないです。

# aws rds describe-db-instances --db-instance-identifier "dev-my-mariadb-instance" --query "DBInstances[0].AllocatedStorage" --output text

20

RDSへデータを入れ続けるプログラム設置

EC2インスタンスに次のPythonプログラムを設置します。

10MBのダミーデータを入れ続けRDSストレージを減らしていき、オートスケーリングを発火されるためのプログラムとなっています。

データベース接続情報項目については、各自環境に合わせてください。

ユーザやパスワードについては、Terraformコード内に記載しているものが使用されますが、hostについては各自環境へ置き換えが必要と推察されます。

# auto.py

import pymysql
import random
import string
import time
import multiprocessing

# データベース接続情報
host = 'dev-my-mariadb-instance.cnvmbmz0rzzo.ap-northeast-1.rds.amazonaws.com'
port = 3306
user = 'Admin'
password = 'passw0rd'
database = 'auto'
table = 'auto'
dummy_data_size_mb = 10  # ダミーデータのサイズ(MB単位)
num_processes = 10  # 並列実行するプロセスの数

# 1MB = 1024 * 1024 bytes
dummy_data_size_bytes = dummy_data_size_mb * 1024 * 1024

def create_database_if_not_exists(connection):
    with connection.cursor() as cursor:
        cursor.execute(f"CREATE DATABASE IF NOT EXISTS {database}")
        connection.commit()

def create_table_if_not_exists(connection):
    with connection.cursor() as cursor:
        cursor.execute(f"USE {database}")
        create_table_sql = f"""
        CREATE TABLE IF NOT EXISTS {table} (
            id INT AUTO_INCREMENT PRIMARY KEY,
            random_string MEDIUMTEXT,
            random_number INT
        )
        """
        cursor.execute(create_table_sql)
        connection.commit()

def generate_random_string(size_in_bytes):
    letters = string.ascii_lowercase
    return ''.join(random.choice(letters) for _ in range(size_in_bytes))

def insert_dummy_data():
    connection = pymysql.connect(
        host=host,
        port=port,
        user=user,
        password=password,
        database=database
    )
    with connection.cursor() as cursor:
        while True:
            random_string = generate_random_string(dummy_data_size_bytes)
            random_number = random.randint(1, 1000000)
            sql = f"INSERT INTO {table} (random_string, random_number) VALUES (%s, %s)"
            cursor.execute(sql, (random_string, random_number))
            connection.commit()
            print(f"Inserted data of size: {len(random_string) / (1024 * 1024):.2f} MB, Number: {random_number}")
            time.sleep(0.1)  # 0.1秒毎にデータを挿入

def main():
    # 初期設定のために一度だけ接続
    connection = pymysql.connect(
        host=host,
        port=port,
        user=user,
        password=password
    )

    try:
        create_database_if_not_exists(connection)
        create_table_if_not_exists(connection)
    finally:
        connection.close()

    # 並列プロセスの開始
    processes = []
    for _ in range(num_processes):
        process = multiprocessing.Process(target=insert_dummy_data)
        process.start()
        processes.append(process)

    try:
        for process in processes:
            process.join()
    except KeyboardInterrupt:
        print("Data insertion stopped by user.")
    finally:
        for process in processes:
            process.terminate()

if __name__ == "__main__":
    main()

必要パッケージインストール

EC2インスタンスに対して、次の通りパッケージをインストールします。

# dnf install mariadb
→MySQLクライエントです。データベースへの接続検証に使用可能です。

# dnf install pip
→pythonのパッケージ管理であるpipをインストールします。

# pip install pymysql
→プログラムで使用する為、インストールします。

※AWS CLIがない場合、インストールします。

AWS CLIの最新バージョンのインストールまたは更新 - AWS Command Line Interface

プログラム実行

プログラムを実行するとダミーデータが格納されていき最終的に、テーブルがフルですよというエラーが出ます。

RDSのストレージは、20GBありますので、ストレージを枯渇させるまで、プログラムの実行時間が長時間になる為、気長にストレージを使い切るまで待ちます。

# python auto.py


・[中略]

Inserted data of size: 10.00 MB, Number: 12934
Inserted data of size: 10.00 MB, Number: 454018
Inserted data of size: 10.00 MB, Number: 561060
Inserted data of size: 10.00 MB, Number: 400860
Process Process-9:
Traceback (most recent call last):
  File "/usr/lib64/python3.9/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib64/python3.9/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/root/auto.py", line 55, in insert_dummy_data
    cursor.execute(sql, (random_string, random_number))
  File "/usr/local/lib/python3.9/site-packages/pymysql/cursors.py", line 153, in execute
    result = self._query(query)
  File "/usr/local/lib/python3.9/site-packages/pymysql/cursors.py", line 322, in _query
    conn.query(q)
  File "/usr/local/lib/python3.9/site-packages/pymysql/connections.py", line 563, in query
    self._affected_rows = self._read_query_result(unbuffered=unbuffered)
  File "/usr/local/lib/python3.9/site-packages/pymysql/connections.py", line 825, in _read_query_result
    result.read()
  File "/usr/local/lib/python3.9/site-packages/pymysql/connections.py", line 1199, in read
    first_packet = self.connection._read_packet()
  File "/usr/local/lib/python3.9/site-packages/pymysql/connections.py", line 775, in _read_packet
    packet.raise_for_error()
  File "/usr/local/lib/python3.9/site-packages/pymysql/protocol.py", line 219, in raise_for_error
    err.raise_mysql_exception(self._data)
  File "/usr/local/lib/python3.9/site-packages/pymysql/err.py", line 150, in raise_mysql_exception
    raise errorclass(errno, errval)
pymysql.err.OperationalError: (1114, "The table 'auto' is full")

ストレージ枯渇後の動作確認

RDSイベント取得

次のコマンドを使用して、対象RDSのイベントを取得します。

無事にストレージオートスケーリングが実施されると次の通り遷移します。

  1. low storage
  2. configuration change
  3. Finished applying autoscaling-initiated modification to allocated storage.

# aws rds describe-events --source-identifier dev-my-mariadb-instance --source-type db-instance | jq

{
  "Events": [
    {
      "SourceIdentifier": "dev-my-mariadb-instance",
      "SourceType": "db-instance",
      "Message": "Storage autoscaling has triggered a pending scale storage task that will reach or exceed the maximum storage threshold. Increase the maximum storage threshold.",
      "EventCategories": [
        "failure"
      ],
      "Date": "2024-07-28T11:43:59.979000+00:00",
      "SourceArn": "arn:aws:rds:ap-northeast-1:{AWSアカウントID12桁}:db:dev-my-mariadb-instance"
    },
    {
      "SourceIdentifier": "dev-my-mariadb-instance",
      "SourceType": "db-instance",
      "Message": "The free storage capacity for DB Instance: dev-my-mariadb-instance is low at 0% of the provisioned storage [Provisioned Storage: 19.27 GB, Free Storage: 0 B]. You may want to increase the provisioned storage to address this issue.",
      "EventCategories": [
        "low storage"
      ],
      "Date": "2024-07-28T11:45:29.823000+00:00",
      "SourceArn": "arn:aws:rds:ap-northeast-1:{AWSアカウントID12桁}:db:dev-my-mariadb-instance"
    },
    {
      "SourceIdentifier": "dev-my-mariadb-instance",
      "SourceType": "db-instance",
      "Message": "Applying autoscaling-initiated modification to allocated storage.",
      "EventCategories": [
        "configuration change"
      ],
      "Date": "2024-07-28T11:45:34.406000+00:00",
      "SourceArn": "arn:aws:rds:ap-northeast-1:{AWSアカウントID12桁}:db:dev-my-mariadb-instance"
    },
    {
      "SourceIdentifier": "dev-my-mariadb-instance",
      "SourceType": "db-instance",
      "Message": "Finished applying autoscaling-initiated modification to allocated storage.",
      "EventCategories": [
        "configuration change"
      ],
      "Date": "2024-07-28T11:47:57.791000+00:00",
      "SourceArn": "arn:aws:rds:ap-northeast-1:{AWSアカウントID12桁}:db:dev-my-mariadb-instance"
    }
  ]
}

次のコマンドを使用して、RDSのストレージサイズを確認するとオートスケーリングの上限に設定している50GBになっていることが確認できます。

# aws rds describe-db-instances --db-instance-identifier "dev-my-mariadb-instance" --query "DBInstances[0].AllocatedStorage" --output text

50

暫くすると、ストレージ最適化(Storage-optimization)が走ります。

オートスケーリング前後のCloudWatch画面

次の画像通り、20GBから枯渇状態までいき、スケーリング後、上限値50GB - 消費したストレージサイズ20GB = 30GBの空きができスケーリングされました。

cloudwatch rds scaling image 画像をホバー/タップすると拡大表示されます

RDSストレージオートスケーリング制限事項

RDSのストレージオートスケーリングについて気になった点を記載します。

  • RDSストレージオートスケーリング発火条件

Amazon RDS ストレージの自動スケーリングによる容量の自動管理

使用可能な空き領域は、割り当てられたストレージの 10% 未満です。

低ストレージ状態は 5 分以上続きます。

最後のストレージ変更、あるいはインスタンスでストレージの最適化が完了してから少なくとも 6 時間経過しています。


  • 制限事項を抜粋しています。

追加ストレージは、10GBもしくは、現在割り当てられているストレージの10%と記載がありましたが、オートスケーリングの上限まで一気にスケーリングされたのは制限事項によるものです。

制限事項

オートスケーリングの場合、RDS は後続のオートスケーリングオペレーションのストレージサイズを予測します。後続のオペレーションが最大ストレージしきい値を超えると予測される場合、RDS では最大ストレージしきい値にオートスケーリングします。


監視面で気になった点は次の通りです。

CloudWatch Alarmに設定しているRDS FreeStorageSpace監視(dev-RDSStorageAutoScalingAlarm)の閾値は、ストレージオートスケーリングに追従しないのでオートスケーリング後のディスクサイズに沿った10%未満の閾値に設定する必要があります。

おわりに

RDSオートスケーリング便利な反面、注意すべきことがある為、気を付けて使っていきたいですね。