AWS CDKでRDSのリストアを実装する時に注意するポイント

記事タイトルとURLをコピーする

最近CDKデビューしたさとうです。

湿気で手がベタベタするので、この夏は湿気から逃れる避暑旅がしたいです🫠

それはともかく、AWS CDKのリストア運用に関するTipsをまとめてみました。

AWS CDK(Typescript、以下CDK)を前提としていること、ベストプラクティスに準拠した解説記事ではないことをご承知おきください。

CDKでRDSのリストアを実装するには?

CDKで構築したRDSに対してリストアを実行したいタイミングがあると思います。

RDSはリストア時に既存とは別の新しいRDSクラスタを作成する仕様なので、手動でリストアを実行するとリストア後のRDSがCDKの管理対象になりません。

そこでCDKでリストアを実行する方法が必要になりますが、 DatabaseClusterFromSnapshot というL2 Constructが用意されているので、こちらを使用してリストア後のRDSクラスタを定義すればよいことになります。

class DatabaseClusterFromSnapshot (construct) · AWS CDK

サンプルコード

実装のサンプルは以下のようになります。

rdsSnapShotARN をトリガーとしてリストアを実行し、リストア前後でRDSを置き換えるような実装にしています。

サンプルなのでARNをベタ書きしていますが、パラメータファイルなどで静的な値としてConstructの外から与える実装にすることが望ましいです。

    // 共通パラメータ
    const commonClusterProps = {
      engine: dbEngine,
      writer: rds.ClusterInstance.serverlessV2('writer'),
      securityGroups: [dbSecurityGroup],
      serverlessV2MinCapacity: 0.5,
      serverlessV2MaxCapacity: 1,
      defaultDatabaseName: 'db',
      storageEncrypted: true,
      deletionProtection: false,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      vpc: vpc,
      subnetGroup: dbSubnetGroup,
    };

    // スナップショットのARN
    const rdsSnapShotARN: string = '';

    // RDS Clusterの作成 or リストア
    if (rdsSnapShotARN) {
      this.rdsCluster = new rds.DatabaseClusterFromSnapshot(this, 'RestoredRdsCluster', {
        ...commonClusterProps,
        snapshotCredentials: rds.SnapshotCredentials.fromGeneratedSecret('admin'),
        snapshotIdentifier: rdsSnapShotARN,
      });
    } else {
      this.rdsCluster = new rds.DatabaseCluster(this, 'RdsCluster', {
        ...commonClusterProps,
        credentials: rds.Credentials.fromGeneratedSecret('admin'),
      });
    }

実装で注意するポイント

これだけで終わればよかったのですが、実装で気を付けた方がよさそうなポイントが何点かあります。上記のサンプルコードを前提に説明していきます。

①: スナップショットのARNをリストア実行時以外に変更しない

スナップショットのARNが変更されると新しいスナップショットが与えられたと判断し、リストアが再実行されます。

逆に言うとリストアを実行したい時はスナップショットのARNを変えるだけでOKです。

入力ミスなどで想定外の変更を防止するために、Gitなどで変更管理をすることが望ましいでしょう。

②: リストア前後のRemovalPolicyを明示する

RemovalPolicyはデフォルトで DESTROY になっており、先述の通りCDKは管理対象から外れるリストア前のRDSを削除しようとします。

実運用ではリストアが正常に完了したことを確認した上で削除するような運用にすることが大半だと思いますので、結果確認前に削除されると都合が悪いです。

RDSの削除保護を設定しているとデプロイエラーの原因にもなりますので、リソースが保持されるように RETAIN にしておきましょう。

③: RDSに依存するリソースが存在する場合、Construct IDを変更する

他のリソースがRDSに依存することがあると思います。

たとえば以下のように、RDSのエンドポイントのDNS名をRDSから参照するようにSSM Parameter Storeを実装していたとします。

リストアされるとDNS名も変わるので、このようなケースではリストアの実行時にSSM Parameter Storeの値も同時に更新されることを期待すると思います。

    // エンドポイントをSSM Parameter Storeに登録
    new ssm.StringParameter(this, 'ClusterEndpointParameter', {
      parameterName: `/rds/cluster-endpoint`,
      description: 'RDS Cluster Endpoint (Writer)',
      stringValue: this.rdsCluster.clusterEndpoint.hostname,
    });

ところが、リストア前後でどちらも同じConsturct ID RdsCluster を使用するとリストア後の差分は以下のようになり、SSM Parameter Storeの紐づけ直しは行われません。

RDSはreplace扱い

一方で、リストア後のConstruct IDを RestoredRdsCluster にするとSSM Parameter Storeの紐づけ直しがされることがわかります。

RDSは再作成扱い

恐らく、リストア前後で同一のConstruct IDを使用するとCFnを合成した際に差分を検出できないことが原因ではないかと思われます。

合成されたCFnを見るとSSM Parameter Storeは Fn::GetAtt で論理ID (=Construct ID) を参照していることがわかります。

論理IDが同じなら文字列で差分が検出されないということはもちろんですが、リストア後のDNS名はデプロイしないとわからないので動的参照でも解決できずにCDKが差分なしと判断する…ということなのではないかと思います。

  "ClusterEndpointParameter0E5F5C5B": {
   "Type": "AWS::SSM::Parameter",
   "Properties": {
    "Description": "RDS Cluster Endpoint (Writer)",
    "Name": "/rds/cluster-endpoint",
    "Type": "String",
    "Value": {
     "Fn::GetAtt": [
      "RdsCluster0F718D69",
      "Endpoint.Address"
     ]
    }
   },
   "Metadata": {
    "aws:cdk:path": "BlogRdsrestoreStack/ClusterEndpointParameter/Resource"
   }
  },

blog.serverworks.co.jp

Construct IDを変更する場合はConstruct IDとスナップショットのARNを変更する(リストアする)タイミングは一致させる必要があります。

ですので、Construct IDにARNから計算したハッシュ値を埋め込むなど、何らかの方法でライフサイクルを合わせる実装が必要です(サンプルコードはこの点を考慮していません)。

④: credentialsの取り扱いが異なる

DatabaseClusterDatabaseClusterFromSnapshot はそれぞれ異なるL2 Constructなので仕様も異なります。

どちらも credentials を引数(prop)に取ることができるのですが、DatabaseCluster とは異なり DatabaseClusterFromSnapshot には以下の制約があります。

class DatabaseClusterFromSnapshot (construct) · AWS CDK

credentials?⚠️
⚠️ Deprecated: use snapshotCredentials which allows to generate a new password
Type: Credentials (optional, default: A username of 'admin' (or 'postgres' for PostgreSQL) and SecretsManager-generated password that will not be applied to the cluster, use snapshotCredentials for the correct behavior.)

Credentials for the administrative user.

Note - using this prop only works with Credentials.fromPassword() with the username of the snapshot, Credentials.fromUsername() with the username and password of the snapshot or Credentials.fromSecret() with a secret containing the username and password of the snapshot.

デプロイ時にサンプルコードのように Credentials.fromGeneratedSecret でシークレットを自動生成する実装にしたい場合、 そのまま同じようにリストアで credentials に渡しても受け付けてくれません。

リストア後のRDSでデプロイ時と同じように再生成したい場合には、ドキュメントにも記載の通り snapshotCredentials を使用する必要があります。

credentials に 渡しても無視されてしまい、以下のようにSecrets Mnagerが再生成されないので注意が必要です。

Secrets Managerが再作成されない

まとめ

実装する中で気付いた点をまとめてみました。

かなりシンプルな実装でもそれなりに考慮点が出てきたので、CDKでリストア運用は思っていたほど簡単な実装ではなさそうです。

佐藤 航太郎(執筆記事の一覧)

エンタープライズクラウド部 クラウドモダナイズ課
2025年1月入社で何でも試したがりの雑食系です。