Aurora DBクラスター内のDBインスタンス間でメンテナンスウィンドウが重複しないように設定してみた

DBクラスターを作成した場合は、DBインスタンスのメンテナンスウィンドウを変更しておこう
2023.10.29

Aurora DBクラスター内のDBインスタンス間でメンテナンスウィンドウが重複しないようにしたい

こんにちは、のんピ(@non____97)です。

皆さんはAurora DBクラスター内のDBインスタンス間でメンテナンスウィンドウが重複しないようにしたいと思ったことはありますか? 私はあります。

AuroraではDBクラスターとDBインスタンスそれぞれにメンテナンスウィンドウを設定することができます。

AWS公式ドキュメントにはメンテナンスは変更のスコープによって、DBクラスター、DBインスタンスのメンテナンスウィンドウで適用されると記載がありました。

メンテナンスウィンドウ

システムメンテナンスを実行する時間帯。該当する場合は、システムメンテナンスにはアップグレードが含まれます。メンテナンス時間は、協定世界時 (UTC) の開始時間で、時間単位での実行期間です。

そのウィンドウを現在の時刻に設定した場合、保留中の変更が確実に適用されるように、現在の時刻からウィンドウの終わりまで 30 分以上必要です。

DB クラスターおよび DB クラスターの各 DB インスタンスのそれぞれに対してメンテナンス時間を設定できます。変更のスコープが DB クラスター全体である場合、変更は DB クラスターのメンテナンス時間中に実行されます。変更のスコープが DB インスタンスである場合、変更は DB インスタンスのメンテナンス時間中に実行されます。

DB クラスターのメンテナンスウィンドウとバックアップウィンドウは、重複させることはできません。

詳細については、「Amazon RDS メンテナンスウィンドウ」を参照してください。

Amazon Aurora DB クラスターの変更 - Amazon Aurora

ただし、DBクラスターとDBインスタンス間、DBクラスター内のDBインスタンス間でメンテナンスウィンドウが重複している場合の影響は記載がありませんでした。

ここで、各リソース間でメンテナンスウィンドウが重複していることによる弊害があるか気になりました。

メンテナンスウィンドウのタイミングになった際に、一斉にメンテナンスを開始し始めるのか、それとも「各リソース間でメンテナンスウィンドウが重複していたとしても、関係するリソースでメンテナンスが走っているのであれば自身のメンテナンスは行わない」など内部的にメンテナンスのタイミングを融通しあうのか気になるところです。

仮に前者の場合、DBインスタンスが同時にメンテナンスに入ると、DBにアクセスできない時間が長くなるのではと予想します。

そのため、後者であって欲しいところです。しかし、ドキュメントに書かれていないことを勝手に期待して、痛い目に遭うのは避けたいです。

ということで、各リソース間でメンテナンスウィンドウが重複しないように設定してみます。

いきなりまとめ

  • DBクラスターを作成した場合は、DBインスタンスのメンテナンスウィンドウを変更しておこう
  • 変更し忘れると、思わぬタイミングでメンテナンスが始まるかも
  • AWS CDKで頑張ればDBクラスターのメンテナンスウィンドウをベースに、DBインスタンスのメンテナンスウィンドウを設定できる

DBクラスターしかメンテナンスウィンドウを指定しない場合

まず、AWSマネジメントコンソールやAWS CLIからDBクラスター作成時に、DBインスタンスのメンテナンスウィンドウを設定することはできません。

DBクラスターのメンテナンスウィンドウしか設定することができません。

Auroraの作成ウィザードではDBクラスターのメンテナンスウィンドウしか設定する箇所がない

AWS CDKでもDatabaseClusterにはDBインスタンスのメンテナンスウィンドウを設定するパラメーターはありません。

実際にAWS CDKでDBインスタンスを2つ持つ、Aurora DBクラスターをデプロイしてみます。DBクラスター周りの設定は以下のとおりです。

./lib/constructs/aurora.ts

    // DB Cluster
    const dbCluster = new cdk.aws_rds.DatabaseCluster(this, "Default", {
      engine: cdk.aws_rds.DatabaseClusterEngine.auroraPostgres({
        version: cdk.aws_rds.AuroraPostgresEngineVersion.VER_15_3,
      }),
      writer: cdk.aws_rds.ClusterInstance.provisioned("Writer", {
        instanceType: cdk.aws_ec2.InstanceType.of(
          cdk.aws_ec2.InstanceClass.T3,
          cdk.aws_ec2.InstanceSize.MEDIUM
        ),
        allowMajorVersionUpgrade: false,
        autoMinorVersionUpgrade: true,
        enablePerformanceInsights: true,
        parameterGroup: dbParameterGroup15,
        performanceInsightRetention:
          cdk.aws_rds.PerformanceInsightRetention.DEFAULT,
        publiclyAccessible: false,
        instanceIdentifier: "db-instance-writer",
        caCertificate: cdk.aws_rds.CaCertificate.RDS_CA_RDS4096_G1,
      }),
      readers: [
        cdk.aws_rds.ClusterInstance.provisioned("Reader", {
          instanceType: cdk.aws_ec2.InstanceType.of(
            cdk.aws_ec2.InstanceClass.T3,
            cdk.aws_ec2.InstanceSize.MEDIUM
          ),
          allowMajorVersionUpgrade: false,
          autoMinorVersionUpgrade: true,
          enablePerformanceInsights: true,
          parameterGroup: dbParameterGroup15,
          performanceInsightRetention:
            cdk.aws_rds.PerformanceInsightRetention.DEFAULT,
          publiclyAccessible: false,
          instanceIdentifier: "db-instance-reader",
          caCertificate: cdk.aws_rds.CaCertificate.RDS_CA_RDS4096_G1,
        }),
      ],
      backup: {
        retention: cdk.Duration.days(7),
        preferredWindow: "16:00-16:30",
      },
      cloudwatchLogsExports: ["postgresql"],
      cloudwatchLogsRetention: cdk.aws_logs.RetentionDays.ONE_YEAR,
      clusterIdentifier: "db-cluster",
      copyTagsToSnapshot: true,
      defaultDatabaseName: "testDB",
      deletionProtection: false,
      iamAuthentication: false,
      monitoringInterval: cdk.Duration.minutes(1),
      monitoringRole,
      parameterGroup: dbClusterParameterGroup15,
      preferredMaintenanceWindow: "Sat:17:00-Sat:17:30",
      storageEncrypted: true,
      storageEncryptionKey: cdk.aws_kms.Alias.fromAliasName(
        this,
        "DefaultRdsKey",
        "alias/aws/rds"
      ),
      vpc: props.vpc,
      subnetGroup,
    });

AWS CDKでデプロイ後、DBクラスターのメンテナンスウィンドウを確認します。

$ aws rds describe-db-clusters \
  --db-cluster-identifier db-cluster \
  --query 'DBClusters[].PreferredMaintenanceWindow' \
  --output text
sat:17:00-sat:17:30

AWS CDKのコード内でpreferredMaintenanceWindowとして指定したSat:17:00-Sat:17:30が設定されています。

DBクラスター内のDBインスタンスのメンテナンスウィンドウも確認します。

$ aws rds describe-db-instances \
  --filters Name=db-cluster-id,Values=db-cluster \
  --query 'DBInstances[].{DBInstanceIdentifier:DBInstanceIdentifier, PreferredMaintenanceWindow:PreferredMaintenanceWindow}'
[
    {
        "DBInstanceIdentifier": "db-instance-reader",
        "PreferredMaintenanceWindow": "sat:10:01-sat:10:31"
    },
    {
        "DBInstanceIdentifier": "db-instance-writer",
        "PreferredMaintenanceWindow": "wed:08:00-wed:08:30"
    }
]

はい、DBクラスターのメンテナンスウィンドウと全く違う日時が指定されています。何なら曜日すら異なります。

ドキュメントには曜日はランダムで30分のメンテナンスウィンドウを割り当てると記載があったので、仕様どおりと言えます。

すべての DB クラスターには週次のメンテナンスウィンドウがあり、その期間内にシステムの変更が適用されます。メンテナンスウィンドウは、変更やソフトウェアのパッチなどが実行されるタイミングをコントロールする機会と考えます。メンテナンスイベントを特定の週に予定した場合、そのイベントはユーザーが指定した 30 分のメンテナンスウィンドウ中にスタートされます。ほとんどのメンテナンスイベントは 30 分のメンテナンスウィンドウ中に完了しますが、大規模なメンテナンスイベントは 30 分以上かかる場合があります。

30 分のメンテナンスウィンドウは、リージョンごとに決められた 8 時間の中でランダムに選択されます。DB クラスターの作成時にメンテナンスウィンドウを指定しないと、RDS でランダムに選択された曜日に 30 分のメンテナンスウィンドウが割り当てられます。

メンテナンスの適用中は、RDS で DB クラスターのリソースの一部が使用されます。わずかながらパフォーマンスに影響が出る場合があります。DB インスタンスでは、まれに、メンテナンスによるアップデートを完了するためにマルチ AZ フェイルオーバーが必要になる場合があります。

Amazon Aurora DB クラスターのメンテナンス - Amazon Aurora

実際にDBインスタンスのメンテナンスの情報を確認すると、メンテナンスウィンドウに基づいてメンテナンス候補日時が表示されていました。

デフォルトのDBインスタンスのメンテナンスウィンドウ

DBクラスターとDBインスタンスのメンテナンスウィンドウが異なるということを認知していない場合、思わぬタイミングでメンテナンスで開始され、サービスに影響を与えてしまうかもしれません。

DBクラスターを作成した場合は、DBインスタンスのメンテナンスウィンドウを変更しておきましょう。

DB インスタンスのメンテナンス期間

AWS CDKで重複しないメンテナンスウィンドウを自動で各DBインスタンスに設定する

AWS CDKでデプロイした後に手動でDBインスタンスのメンテナンスウィンドウを設定するのは少々面倒です。

DBクラスターに設定したメンテナンスウィンドウをベースに、DBクラスターとDBインスタンス間でメンテナンスウィンドウが重複しないように設定したいところです。

ということで、実装してみました。

やっていることとしては以下になります。

  • DBクラスターのメンテナンスウィンドウをベースに
  • 30分シフトしたメンテナンスウィンドウを
  • DBインスタンスの数分だけ生成
  • 生成したメンテナンスウィンドウをDBインスタンスに設定

実際のコードは以下のとおりです。

./lib/constructs/aurora.ts

    // DB Instance PreferredMaintenanceWindow
    // 指定した分数分、時間をシフトする
    // dayTime : ddd:hh24:mi
    const shiftTime = (dayTime: string, shiftMinutes: number) => {
      const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

      const day = dayTime.substring(0, 3);
      const time = dayTime.substring(4);

      const [hour, min] = time.split(":").map(Number);

      const totalMinutes = hour * 60 + min + shiftMinutes;
      const targetHour = Math.floor(totalMinutes / 60) % 24;
      const targetMinutes = totalMinutes % 60;
      const shiftedDays = Math.floor(totalMinutes / (24 * 60));

      const dayIndex = days.indexOf(day);
      const targetDay = days[(dayIndex + shiftedDays) % days.length];

      return [
        `${targetDay}:${targetHour.toString().padStart(2, "0")}:${targetMinutes
          .toString()
          .padStart(2, "0")}`,
      ];
    };
    
    // ベースとなるメンテナンスウィンドウから、指定した分数分シフトしたメンテナンスウィンドウを、指定した数分生成する
    // baseMaintenanceWindow : ddd:hh24:mi-ddd:hh24:mi
    const generateShiftMaintenanceWindows = (
      baseMaintenanceWindow: string,
      shiftMinutes: number,
      shiftCount: number
    ) => {
      const [baseStartDayTime, baseEndDayTime] =
        baseMaintenanceWindow.split("-");

      return [...Array(shiftCount)].map((_, i) => {
        const startDayTime = shiftTime(
          baseStartDayTime,
          shiftMinutes * (i + 1)
        );
        const endDayTime = shiftTime(baseEndDayTime, shiftMinutes * (i + 1));
        return `${startDayTime}-${endDayTime}`;
      });
    };

    // DBクラスターのL2 ConstructからDBインスタンスのL1 Constructを抽出
    const cfnDbInstances = dbCluster.node.children
      .filter(
        (child) => child.node.defaultChild instanceof cdk.aws_rds.CfnDBInstance
      )
      .map((child) => child.node.defaultChild) as cdk.aws_rds.CfnDBInstance[];

    // DBクラスターのメンテナンスウィンドウをベースに、30分シフトしたメンテナンスウィンドウを、DBインスタンスの数分だけ生成する
    // WriterよりもReaderの方が先にメンテナンスをして欲しいので、配列を逆順に並び替える
    const dbInstanceMaintenanceWindows = generateShiftMaintenanceWindows(
      "Sat:17:00-Sat:17:30",
      30,
      cfnDbInstances.length
    ).reverse();

    // 生成するメンテナンスウィンドウのリストを表示
    console.log(
      generateShiftMaintenanceWindows(
        "Sat:17:00-Sat:17:30",
        30,
        cfnDbInstances.length
      )
    );

    // DBクラスター内のDBインスタンスにメンテナンスウィンドウを指定
    cfnDbInstances.forEach((cfnDbInstance, i) => {
      cfnDbInstance.addPropertyOverride(
        "PreferredMaintenanceWindow",
        dbInstanceMaintenanceWindows[i]
      );
    });

cdk diffをしてみます。

$ npx cdk diff
[ 'Sat:17:30-Sat:18:00', 'Sat:18:00-Sat:18:30' ]
Stack AuroraStack
Resources
[~] AWS::RDS::DBInstance Aurora/Default/Writer AuroraWriter14FF9353 may be replaced
 └─ [+] PreferredMaintenanceWindow (may cause replacement)
     └─ Sat:18:00-Sat:18:30
[~] AWS::RDS::DBInstance Aurora/Default/Reader AuroraReader0522537B may be replaced
 └─ [+] PreferredMaintenanceWindow (may cause replacement)
     └─ Sat:17:30-Sat:18:00


✨  Number of stacks with differences: 1

DBクラスターのメンテナンスウィンドウSat:17:00-Sat:17:30から、30分間隔でシフトしたメンテナンスウィンドウがDBインスタンスに設定されそうですね。

デプロイ後、DBインスタンスのメンテナンスウィンドウを確認してみます。

$ aws rds describe-db-instances \
                     --filters Name=db-cluster-id,Values=db-cluster \
                     --query 'DBInstances[].{DBInstanceIdentifier:DBInstanceIdentifier, PreferredMaintenanceWindow:PreferredMaintenanceWindow}'
[
    {
        "DBInstanceIdentifier": "db-instance-reader",
        "PreferredMaintenanceWindow": "sat:17:30-sat:18:00"
    },
    {
        "DBInstanceIdentifier": "db-instance-writer",
        "PreferredMaintenanceWindow": "sat:18:00-sat:18:30"
    }
]

確かに、DBクラスターのメンテナンスウィンドウSat:17:00-Sat:17:30から、30分間隔でシフトしたメンテナンスウィンドウがDBインスタンスに設定されました。

DBクラスターを作成した場合は、DBインスタンスのメンテナンスウィンドウを変更しておこう

Aurora DBクラスター内のDBインスタンス間でメンテナンスウィンドウが重複しないように設定してみました。

DBクラスターを作成した場合は、DBインスタンスのメンテナンスウィンドウを変更しておきましょう。

使用したAWS CDKのコードは以下リポジトリに保存しています。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!