NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

Aurora Serverless v2をCDKで作ってみた 〜世はまさに大Serverless時代〜

こんにちは、北海道の田舎に旅行して息子が熱だして軟禁状態になった志水です。田舎と熱が掛け合わさると冷や汗が止まらないので皆様お気をつけ下さい。
さて、少し前にAurora Serverless v2がGAされて所々で使われ始めてるのかなと思います。今回はそれをCDKでどうにかして作ってみたのでブログにしました。

Aurora Serverless v2とは

Aurora Serverless v2とは、簡単に言うとAuroraの自動垂直スケーリング機能です。今までAuroraはストレージ層のスケーリングは自動で行われていて、コンピューティング層のスケーリングもリーダーインスタンスはAmazon Aurora Auto Scalingで自動水平スケーリングが可能でした。

ただし、ライターインスタンスに関しては自動でスケーリングする方法が無く、手動で垂直スケーリングするかデータベース分割を行うしか方法がありませんでした。そこにAurora Serverless v2が颯爽と登場して、ライター・リーダーインスタンス共に自動垂直スケーリング出来るようになりました。

スケーリング性能が非常に優れているので、Auroraを使うのであれば積極的に使っていきたい機能になります。なので、ぼくの大好きなCDKで作っていきましょう!

CDKでの作り方

まずはCDKのドキュメントを探っていこうと思い、RDSのDatabaseClusterを眺めてみました。しかし、どうもServerlessという文言が見当たりません。次にServerlessClusterというServerless感たっぷりのクラスタが作れそうなものがあったので見てみました。しかし、ここにenableDataApi?というDataAPIが有効かどうかを返すメソッドがあり、DataAPIはそもそも現状v1のみの機能でv2には実装されていないのでちょっと違和感を感じました。
そこでググってみると、まさかのCDKのIssueにAurora Serverless v2サポートしてくれという課題があがっていました。つまり、CDKでは未サポートのようでした。更にCloudFormationのロードマップにもAurora Serverless v2のサポートに関するものがあり、まさかのCloudFormation(CFn)ですら未サポートという事実が分かり絶望していました。 (ブログ塩漬けにしてたらCFnはサポートされてましたね。気のせいだと思うので先に進みます)
つまり、CDKがサポートされるまでにCloudFormationのサポートもあるため、実装されるまでに時間がかかるのです。しかし私はCDKを愛しています。尊いのです。このようなことでは負けません。CDKのIssueにいるようなCDK狂の方々はカスタムリソースを利用して実装しているようでしたが、それをそのまま使っても動かなかったので、せっかくならとブログにしました。

カスタムリソースとは?

CloudFormation Custom Resourceとは、CFnが未対応のリソースや他サービスをプロビジョニングすることができます。 CDK Custom ResourceはCloudFormation Custom ResourceをCDKから実行できるようにしたもので、より簡単に利用することができます。Lambdaを使ってプロビジョニングすることが出来るので、APIがあるものは何でも対象にできることになります。

CDK Custom Resourceの作り方はLambdaを書くものとAWS APIを指定する二種類あります。AWS APIを指定するほうはLambdaを書く必要もないので非常に簡単に利用出来ますが、1個のAPIしか指定出来ないため、複雑なことが出来ません。

複雑なことをする場合はLambdaを利用することになりますが、その場合Lambdaの15分タイムアウト問題が出てきます。特に今回のようなRDSのプロビジョニングをターゲットにする場合はプロビジョニングに時間がかかるためタイムアウトについて考えていく必要があります。この15分タイムアウトの問題には対処法があり、プロビジョニングした内容を確認するLambdaを別途作成し、StepFunctionsでそのLambdaを回し続けることでLambdaのタイムアウト問題を解決出来ます。StepFunctionsはタイムアウトが最大1年なので、タイムアウトで問題になることは無くなるかと思われます。

今回はこのCDK Custom Resourceを利用して進めますが、プロビジョニングやチェックをするLambdaを始め、それらとCFnとのやり取りをするLambdaや15分タイムアウト対策のStepFunctionsを作成するため、作成するものが多いです。しかし、CDKならプロビジョニングやチェックをするLambdaのみ作成し、それ以外は自動作成されるため、CloudFormation Custom Resourceより圧倒的に楽に構築ができます。

CDK Custom Resourceでの作り方

まず、カスタムリソースで作っていく方法として、下記2個の方法があります

  1. 全部カスタムリソースで作成
  2. CDKリソースで対応できない部分のみカスタムリソースで作成

1の場合はCDKコードがカスタムリソースの定義のみになり簡易化されますが、Lambdaコードが複雑になります。逆に2はCDKコードがカスタムリソース以外にもCDKリソースについての定義が増えるため複雑になりますが、カスタムリソースの部分は最小限になるので簡易化されます。私はカスタムリソースを使うことは諸刃の剣だと思っていて、あまり責任範囲を増やしたくないので、2で進めたいと思います。

カスタムリソースの範囲 CDKコード Lambdaコード
1. 全部 簡単 複雑
2. 一部 複雑 簡単

では、カスタムリソースの範囲はAurora Serverless v2を作るうえでどこまで担当するかを考えていきます。Aurora Serverless v2を作るためには下記3点を設定していく必要があります。

  • Auroraバージョンを3.02.0以上に設定
  • クラスタにACU範囲を設定
  • インスタンスタイプをSERVERLESSに設定

この中でCDKリソースで出来る範囲としては、Auroraバージョン3.01.0のDBクラスタ・インスタンスを作成する部分になります。つまり、それ以外の下記がカスタムリソースの担当範囲になります。

  • Auroraバージョンを3.02.0へバージョンアップ
  • クラスタにACU範囲を設定
  • インスタンスタイプをSERVERLESSに設定

これらについて1個ずつカスタムリソースを作っていきます。理由としては、1個1個が時間がかかる処理となり、まとめて作ると設定変更を確認する部分のコーディングをLambda内部に持たせる必要がありますが、それをカスタムリソースの責任範囲に逃したいためです。
カスタムリソースを作るにあたって、Lambdaで作るかAPIで作るかの2択になります。即時設定が出来る部分のみAPIで行い、設定に時間がかかり変更確認が必要なものはLambdaで作成します。APIでも変更確認が出来ると良いので今後に期待です。即時変更が出来るのはACU範囲設定の部分のみで、それ以外は時間がかかるためLambdaで設定していきます。

  • Auroraバージョンを3.02.0へバージョンアップ
    • Lambda
  • クラスタにACU範囲を設定
    • API
  • インスタンスタイプをSERVERLESSに設定
    • Lambda

カスタムリソースで作るAurora Serverless v2

これらをまとめると、本日のAurora Serverless v2のレシピは下記の通りになります。

  1. CDKリソースでver3.1のクラスタを作成
  2. Lambdaでver3.2へバージョンアップ 2.1. バージョンアップの実施 2.2. ver3.2になっているか確認
  3. APIでACU範囲設定
  4. LambdaでインスタンスタイプをSERVERLESSに設定 4.1. SERVERLESSに設定 4.2. SERVERLESSになっていることを確認
  5. サーバレスの出来上がり

言語はPythonで進めます。説明に利用しているコードはここのリポジトリにまとめていますので、そのまま利用したい場合はこちらをご利用ください。
それでは作っていきましょう。

1. CDKリソースでver3.1のクラスタを作成

まずはシンプルにCDKリソースからDBクラスタをrds.DatabaseClusterから作成していきます。そのために必要なVPCも合わせて作っておきます。
バージョンは現在指定出来る最大のrds.AuroraMysqlEngineVersion.VER_3_01_0を指定しています。

vpc = ec2.Vpc(self, "VPC")
db_user = "luffy"
instance_count = 1

cluster = rds.DatabaseCluster(self, "AuroraCluster",
    cluster_identifier='luffy-cdk-aurora',
    engine=rds.DatabaseClusterEngine.aurora_mysql(
        version=rds.AuroraMysqlEngineVersion.VER_3_01_0
    ),
    instances=instance_count,
    instance_props=rds.InstanceProps(
        vpc=vpc,
    ),
    credentials=rds.Credentials.from_secret(
        rds.DatabaseSecret(self, "AuroraSecret", 
            username=db_user
        ), 
        db_user
    ),
)

2. Lambdaでver3.2へバージョンアップ

ここからメインのギア2になるカスタムリソースを使っていきます。
まずはLambdaリソースを作成します。今回はバージョンアップの実施用とそれを確認する用の2個作成します。

handler_name = "app.handler"
on_event = lambda_.Function(self, "VerUpFunctionPunk01",
    function_name='luffy_cluster_verup',
    runtime=lambda_.Runtime.PYTHON_3_9,
    handler=handler_name,
    code=lambda_.Code.from_asset('lambda/verup/create'),
)
is_complete = lambda_.Function(self, "VerUpCheckCompletePunk02",
    function_name='luffy_cluster_check_available',
    runtime=lambda_.Runtime.PYTHON_3_9,
    handler=handler_name,
    code=lambda_.Code.from_asset('lambda/verup/check'),
)

雑にRDS全許可のポリシーをそれぞれのLambdaに付与します。

lambda_policy = iam.PolicyStatement(
    resources=["*"],
    actions=[
        "rds:*",
    ]
)
on_event.add_to_role_policy(lambda_policy)
is_complete.add_to_role_policy(lambda_policy)

作成したLambdaをon_event_handler, is_complete_handlerに指定します。on_event_handlerがプロビジョニング用のLambdaを指定し、is_complete_handlerが完了しているか確認する用のLambdaを指定します。
また、各Lambdaに対してDBクラスタ名と対象バージョンをINPUT出来るように指定しています。

my_provider = cr.Provider(self, "MyProviderPunk03",
    on_event_handler=on_event,
    is_complete_handler=is_complete,
    provider_function_name="luffy_cluster_verup_provider",
)
version_up=CustomResource(self, "VerUpCustomResourcePunk04", 
    service_token=my_provider.service_token,
    properties={
        "DBClusterIdentifier": cluster.cluster_identifier,
        "DBEngineVersion": "8.0.mysql_aurora.3.02.0"
    },
)

最後に依存関係を注入します。 このカスタムリソースはバージョン3.1のクラスタとそのインスタンスが作成された後に実行される必要があるので、カスタムリソースに対してRDSインスタンスに対する依存を追加します。CustomResourcenode.add_dependencyで依存を追加でき、DBインスタンスはDBクラスタから自動で作成されるため、rds.DatabaseClusterから中を辿って探す必要があり、それがnode.find_childからインスタンス名より指定することが出来ます。

for i in range(1, instance_count + 1):
    version_up.node.add_dependency(cluster.node.find_child(f"Instance{i}"))

2.1. バージョンアップの実施

先程作成したLambdaのコードでバージョンアップ用のコードを作成していきます。
まず、入力されるのはCloudFormationのリソースの状態であるCreate/Update/DeleteとCDKから入力したDBクラスタ名と対象バージョンになります。まず、CloudFormationのリソースの状態を判定して各メソッドに振り分けをします。基本的にカスタムリソースを使う場合はこれが出てきます。

def handler(event, context):
    request_type = event['RequestType']
    if request_type == 'Create': return on_create(event)
    if request_type == 'Update': return on_update(event)
    if request_type == 'Delete': return on_delete(event)
    raise Exception("Invalid request type: %s" % request_type)

そこからCreate/Updateされた場合はDBクラスタを更新してバージョンアップを行いたいので、 modify_db_clusterに入力されたDBクラスタ名と対象バージョンを指定してバージョンアップを行います。Deleteはリソースが削除される時の挙動で、Createと逆の操作をする必要がありますが、RDSの制約としてバージョンダウンが出来ないので何もしないこととしています。

def on_create(event):
    props = event["ResourceProperties"]

    response = client.modify_db_cluster(
        DBClusterIdentifier=props['DBClusterIdentifier'],
        EngineVersion=props['DBEngineVersion'],
        ApplyImmediately=True,
    )

    cluster_name = response['DBCluster']['DBClusterIdentifier']
    return { 'PhysicalResourceId': cluster_name }

2.2. ver3.2になっているか確認

先程のバージョンアップをされているかのチェック用Lambdaについても作成していきます。今回はOUTPUT側が重要で、{ 'IsComplete': True/False }の形式で返す必要があり、TrueであればチェックOK、FalseであればチェックNGなので再度チェックという流れになります。その為、クラスタのバージョンとステータスが想定通りであれば{ 'IsComplete': True }を返すように作成します。入力は先程のバージョンアップのLambdaと同じものが利用できるので、describe_db_clustersのクラスタ名の識別とバージョン確認のバージョンに利用します。

def on_create(event):
    props = event["ResourceProperties"]
    db_cluster_name = props['DBClusterIdentifier']

    response = client.describe_db_clusters(
        DBClusterIdentifier=db_cluster_name,
    )

    db_engine_version = props['DBEngineVersion']
    db_cluster = response['DBClusters'][0]
    is_ready = db_cluster['Status'] == 'available' and db_cluster['EngineVersion'] == db_engine_version

    return { 'IsComplete': is_ready }

ここまで通れば、Aurora Serverless v2が利用できるバージョンまでクラスタが出来ました。あとはACUの設定です。

3. APIでACU範囲設定

次にACU設定になります。ACUは即時反映になるのでAWS APIを利用した設定になります。対象のAPIはリファレンスを確認しmodifyDBClusterServerlessV2ScalingConfigurationを設定すれば良いことがわかります。
また、先程Lambdaで設定していた流れと変わり、AWS APIを利用するパターンだとAwsSdkCallで利用したいAPIを指定し、AwsCustomResourceでCreate/Update/DeleteでどのAWS APIを利用するかを指定出来ます。

mod_cap_cluster_serverless = cr.AwsSdkCall(
    service="RDS",
    action="modifyDBCluster",
    parameters={
        "DBClusterIdentifier": cluster.cluster_identifier,
        "ServerlessV2ScalingConfiguration": {
            "MinCapacity": 0.5,
            "MaxCapacity": 1,
        },
        "ApplyImmediately": True,
    },
    physical_resource_id=cr.PhysicalResourceId.of(
        cluster.cluster_identifier
    ),
)

add_cap = cr.AwsCustomResource(self, "ModACUPunk05",
    on_create=mod_cap_cluster_serverless,
    on_update=mod_cap_cluster_serverless,
    policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
        resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE,
    ),
)

作成したカスタムリソースに直前で実施したバージョンアップの後に実施するように依存関係を設定します。

add_cap.node.add_dependency(version_up)

これでACUの設定も出来たので、あとはServerless化だけになります。

4. LambdaでインスタンスタイプをSERVERLESSに設定

ここではLambdaを指定してカスタムリソースを作成していくため、バージョンアップのコードとだいたい同じになります。違う部分でいうと、今回はRDSインスタンスに対しての設定になるため、インスタンス全てに対してServerless化が必要なので、インスタンス数分回しカスタムリソースと依存関係を設定します。もちろんLambdaの作成などもしますが、コードはほぼ同じなので割愛します。直前で実施したACU設定に依存するように設定しています。

for i in range(instance_count):
    instance_name = cluster.instance_identifiers[i]

    serverless_custom_resource=CustomResource(self, "ServerlessCustomResourcePunk06", 
        service_token=my_provider_serverless.service_token,
        properties={
            "DBInstanceIdentifier": instance_name,
        },
    )
    serverless_custom_resource.node.add_dependency(add_cap)

4.1. SERVERLESSに設定

Lambdaの最初の流れは同じでそれ以降の部分のAPIを変えてインスタンスタイプをSERVERLESSに変更します。今回はDeleteの設定も入れるため共通のインスタンスタイプ変更用関数を用意します。modify_db_instanceに入力されたDBインスタンス名と変更したいクラスをセットして更新していきます。

def update_instance(props, instance_class):
    db_instance_name = props['DBInstanceIdentifier']

    response = client.modify_db_instance(
        DBInstanceIdentifier=db_instance_name,
        DBInstanceClass=instance_class,
        ApplyImmediately=True,
    )

    return { 'PhysicalResourceId': response['DBInstance']['DBInstanceIdentifier'] }

上記を利用して、Create/Updateだとdb.serverlessを指定し、Deleteだと元に戻す操作をしたいのでデフォルトで指定されているdb.t3.mediumを指定してインスタンスタイプを変更します。

def on_create(event):
    props = event["ResourceProperties"]

    return update_instance(props, 'db.serverless')

def on_delete(event):
    physical_id = event["PhysicalResourceId"]
    props = event["ResourceProperties"]

    return update_instance(props, 'db.t3.medium')

4.2. SERVERLESSになっていることを確認

こちらもバージョンアップのやり方とほぼ同じです。入力されたDBインスタンス名を利用してdescribe_db_instancesでインスタンスの状態を確認し、ステータスが利用可能になりインスタンスクラスがdb.serverlessdb.t3.mediumになることを確認します。

db_instance_name = props['DBInstanceIdentifier']
response = client.describe_db_instances(
    DBInstanceIdentifier=db_instance_name,
)

db_instance = response['DBInstances'][0]
is_ready = db_instance['DBInstanceStatus'] == 'available' and db_instance['DBInstanceClass'] == db_instance_class

return { 'IsComplete': is_ready }

5. サーバレスの出来上がり

ということで出来たてほかほかのサーバレスがこちらになります。

ほかほかでとても美味しそうなサーバレスです。自動垂直スケーリングの香りもしていて最高ですね。これで宴をしていきましょう。

まとめ

ということで、Aurora Serverless v2をCDKカスタムリソースを駆使して作成しました。カスタムリソースは使うのに癖があり慣れるまでは難しいし運用に入れる部分も考える必要があります。ただ、今回のようにAPIさえあれば何でも出来るので、CDKの幅が広がるので覚えておいて損はないかと思います。また、Aurora Serverless v2も最高な機能でガンガン使っていきたい代物なので、皆さんも是非今回説明したコードで作っていってください。ほかほかで美味しいうちに食べちゃいましょう。

執筆者志水友輔

2021/2022 APN ALL AWS Certifications Engineers & 2021 AWS Top Engineer
大阪でAWSを中心としたクラウドの導入、設計、構築を専門に行っています。

Twitter:https://twitter.com/shimi023

Amazon著者ページ:Amazon.co.jp: 志水友輔:作品一覧、著者略歴