電通総研 テックブログ

電通総研が運営する技術ブログ

開発環境のAurora Serverless費用節約術(CDKサンプルコード付き)

こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンターの耿です。

Amazon Aurora Serverlessは、コンピューティングとメモリのキャパシティ(Aurora 容量ユニット = ACU)をリクエスト数に応じて自動で変化させることができるデータベースサービスです。事前のスケーリング計画が不要になるほか、実際のワークロードに合わせてキャパシティを増減させるため、費用の最適化に向いています。

Aurora Serverlessは v1 と v2 の2つのバージョンが一般利用可能ですが、v1 の方はしばらくアクセスがないと 0 ACU まで落として一時停止する機能があり、コンピューティングとメモリの料金が全くかからなくなります。本番環境であれば利用されることはあまりないと思いますが、リクエスト数が限定的な開発環境ではこの機能をうまく利用することで、費用をさらに節約することができます。

しかし、 0 ACU で一時停止している状態のデータベースにアクセスしようとすると、キャパシティがないため初回接続が失敗してしまいます。本記事はシンプルにこの問題を解消し、費用を抑える方法を記載します。

Aurora Serverless の ACU 設定

Aurora Serverless は Aurora 容量ユニット (ACU) の最大値と最小値を指定することで、その間で負荷に応じてオートスケーリングします。v1に限って一時停止設定が可能で、リクエストが全くない時に 0 ACU までスケールダウンできます。

Aurora Serverless v1の一時停止設定 Aurora Serverless v1の一時停止設定

Aurora Serverless のバージョンの違い

2022年5月時点において、Aurora Serverless v1にできてv2にできないことがあります。

  • v2はData APIを利用できない
  • CloudFormationはまだv2をサポートしていない
  • v2は 0 ACU(一時停止)にできず、最小ACUは 0.5 である

ACUに関する補足として、2022年5月時点でv2のACU費用はv1の2倍に設定されています。すなわちv2を最小キャパシティ0.5 ACUで常時稼働した場合の費用は、v1を1 ACUで常時稼働した場合の費用と同等になります。v1は一時停止が可能なため、一時停止をした時間の分だけv2より費用を節約できます。

※その他のAurora Serverless v2の特徴は、以下の動画で詳しく解説されています。
JAWS-UG横浜 #44 Aurora Serverless v2

開発で遭遇した問題点

Data APIとCDKを利用したかったため、Aurora Serverless v1を利用して開発している環境があります。費用を抑えるために一時停止機能を有効にしていますが、0 ACUのときはコンピューティングとメモリの動作が完全に停止するため、初回接続ではデータベースへの接続が失敗するという問題に遭遇しました。

Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.; SQLState: 08S01

しばらく(体感30秒〜1分程度)するとデータベースが起動し、問題なく接続できるようになるのですが、費用を抑えつつ初回接続でも失敗しないようにしたかったため、その仕組みを検討しました。

実現したいこと

Aurora Serverlessにアクセスする可能性のある時間帯は一時停止をさせずに最低でも1 ACUで稼働させ、それ以外の時間帯は0 ACUへのスケールダウンを許容すれば、余分な費用を削減し、初回接続でエラーになる問題も解消できます。今回はアクセスする可能性のある時間帯は余裕を持って 7:00 ~ 22:00 と広めに定義しています。
整理すると、実現したいことは次のようになります。

  • 平日の7:00 ~ 22:00 は、Aurora Serverlessが 0 ACUにならないようしに、DBへの接続が常に成功するようにしたい
  • それ以外の時間(平日深夜と土日)は 0 ACUにスケールダウンしても良い

実現方法

  • Aurora クラスタが一時停止しない場合の最小 ACU を 1 とする
  • Aurora クラスタ2時間アクセスがなかった場合、一時停止するよう(0 ACU になるよう)に設定する
  • Aurora クラスタへ参照系のクエリを発行するLambda関数を作成し、平日の7:00 ~ 20:00 の間は1時間ごとに定期実行する

こうすることで、平日は20:00に最後のLambda関数が実行され、22:00まではAuroraクラスタが0 ACUにならないことが保証されます。夜間や土日はAuroraクラスタへの接続がないと一時停止し、次の平日の朝7:00に最初のLambda関数が実行され、Auroraクラスタが起動します。

システム構成図

他の選択肢

シビアに費用を抑える必要がない場合、一時停止設定を無効にし、0 ACUにスケールダウンしないようにすれば、データベースへのアクセスがなくても停止しなくなり、本記事で述べるようなワークアラウンドは必要ありません。

あるいは 0 ACUにスケールダウンした後の初回接続が失敗しても、起動を待ってからクエリを再実行することが許容できる場合も、本記事のワークアラウンドは必要ありません。

CDKコード

以下のCDKコードで環境を作成します。

import { Duration, Stack, StackProps } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as events from "aws-cdk-lib/aws-events";
import * as eventTargets from "aws-cdk-lib/aws-events-targets";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as rds from "aws-cdk-lib/aws-rds";
import { Construct } from "constructs";

export class MyStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const privateSubnetName = "myPrivateSubnet";

    // VPCを作成
    const vpc = new ec2.Vpc(this, "MyVpc", {
      cidr: "10.0.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          name: privateSubnetName,
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          cidrMask: 20,
        },
      ],
    });

    // Aurora Serverless v1 クラスタを作成
    const auroraCluseter = new rds.ServerlessCluster(this, "MyAuroraCluster", {
      engine: rds.DatabaseClusterEngine.AURORA_MYSQL,
      vpc: vpc,
      vpcSubnets: { subnetGroupName: privateSubnetName },
      // 最小ACUを1、最大ACUを2、一時停止までに必要な非アクティブ時間を2時間に指定
      scaling: { minCapacity: 1, maxCapacity: 2, autoPause: Duration.hours(2) },
      // Data APIを有効化
      enableDataApi: true,
    });

    // Lambda関数
    const awakeAuroraFunction = new lambdaNodejs.NodejsFunction(this, "MyFunction", {
      // Lambda関数へのファイルパス
      entry: "functions/awake-aurora-serverless.ts",
      runtime: lambda.Runtime.NODEJS_14_X,
      // Data APIを利用するため、AuroraクラスタARNとシークレットのARNを環境変数で渡す
      environment: {
        CLUSTER_ARN: auroraCluseter.clusterArn,
        SECRET_ARN: auroraCluseter.secret?.secretArn ?? "",
      },
    });

    // Lambda関数の実行ロールにData APIへのアクセスを許可するポリシーを追加
    if (auroraCluseter.secret?.secretArn) {
      const auroraDataApiPolicy = new iam.ManagedPolicy(this, "MyAuroraDataApiPolicy", {
        statements: [
          new iam.PolicyStatement({
            resources: [`${auroraCluseter.secret.secretArn}*`],
            actions: ["secretsmanager:GetSecretValue"],
            effect: iam.Effect.ALLOW,
          }),
          new iam.PolicyStatement({
            resources: [auroraCluseter.clusterArn],
            actions: ["rds-data:ExecuteStatement"],
            effect: iam.Effect.ALLOW,
          }),
        ],
      });
      awakeAuroraFunction.role?.addManagedPolicy(auroraDataApiPolicy);
    }

    // 日本時間 平日の 7:00 ~ 20:00 の間に1時間ごとにLambda関数を定期起動する
    // cron式は UTC で記載するため、「日曜 ~ 木曜の22:00 ~ 23:00」と「月曜 ~ 金曜の 0:00 ~ 11:00」の2つに分ける
    new events.Rule(this, "MyEventRule1", {
      schedule: events.Schedule.cron({ minute: "0", hour: "22-23", weekDay: "SUN-THU" }),
      targets: [new eventTargets.LambdaFunction(awakeAuroraFunction, { retryAttempts: 3 })],
    });
    new events.Rule(this, "MyEventRule2", {
      schedule: events.Schedule.cron({ minute: "0", hour: "0-11", weekDay: "MON-FRI" }),
      targets: [new eventTargets.LambdaFunction(awakeAuroraFunction, { retryAttempts: 3 })],
    });
  }
}

Lambda関数の部分では、 NodejsFunction コンストラクトを利用することで、TypeScript で記述した Lambda 関数の JavaScriptへのコンパイルからデプロイまでをCDKがやってくれます。

Auroraクラスタへクエリを発行するLambda関数は、 functions/awake-aurora-serverless.ts に以下のように実装します。Data APIを利用してクエリを発行します。

import * as RDS from "@aws-sdk/client-rds-data";
import { Handler } from "aws-lambda";

const client = new RDS.RDSDataClient({ region: "ap-northeast-1" });

export const handler: Handler = async () => {
  const input = {
    // Data APIを利用するための、AuroraクラスタのリソースARNとシークレットのARNを環境変数から取得する
    resourceArn: process.env.CLUSTER_ARN,
    secretArn: process.env.SECRET_ARN,
    // 発行するSQL文
    sql: "SHOW databases;",
  };
  const command = new RDS.ExecuteStatementCommand(input);
  await client.send(command);
};

発行するSQL文は更新系でなければ何でも良いですが、シンプルに SHOW databases; としています。

実際の料金

以上の構成でしばらく稼働した場合の料金を確認しました。

利用料金のグラフ

土日はAuroraクラスタが停止し、料金がかかっていないことがわかります。平日も24時間のうち15時間しか稼働していません。
今回の条件では一週間 24h * 7日 = 168h のうち、 15h * 5日 = 75h 稼働しているため、常時稼働に比べて 75h / 168h = 45% の費用に抑えることができています。
一方で平日の業務時間中は常に稼働状態のため、接続が失敗することがなくなり、今回の要件を満たすことができました。

執筆:@kou.kinyo2、レビュー:@higaShodoで執筆されました