AWS CDKでAmazon Aurora DB クラスターとAWS Secrets Managerとの統合を設定してみた

Escape hatchの影響範囲を理解しよう
2023.06.05

AWS CDKでもRDSとSecrets Managerの統合を設定したいな

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

皆さんはAWS CDKでもAmazon RDSとAmazon Secrets Managerの統合を設定したいなと思ったことはありますか? 私はあります。

こちらの機能を使うことで自動でRDSのマスターユーザーのシークレットが作成されます。特にアツいのが自動ローテーション用のLambda関数を作成する必要がない点ですね。

こちらの機能はCFnでも有効化することが可能です。具体的にはAWS::RDS::DBClusterまたはAWS::RDS::DBInstanceManageMasterUserPasswordtrueにします。

ManageMasterUserPassword

A value that indicates whether to manage the master user password with AWS Secrets Manager.

For more information, see Password management with AWS Secrets Manager in the Amazon RDS User Guide and Password management with AWS Secrets Manager in the Amazon Aurora User Guide.

Constraints:

  • Can't manage the master user password with AWS Secrets Manager if MasterUserPassword is specified.

Valid for: Aurora DB clusters and Multi-AZ DB clusters

Required: No

Type: Boolean

Update requires: No interruption

AWS::RDS::DBInstance - AWS CloudFormation

しかし、AWS CDKのL2 ConstructであるDatabaseClusterDatabaseInstanceにそのようなプロパティはありません。

マスターユーザーの認証情報の指定はcredentialsで行います。しかし、Credentialsを確認してもRDSとSecrets Managerの統合についての言及はありませんでした。良きようにやってくれるのでしょうか。

実際に試してみたので紹介します。

いきなりまとめ

  • Escape hatchでmanageMasterUserPasswordtrueに指定することでDBクラスターとAWS Secrets Managerとの統合が可能
  • manageMasterUserPasswordtrueにする場合はmasterUserPasswordは指定してはならない
    • addPropertyDeletionOverride("MasterUserPassword")で削除する
  • tryRemoveChild("Secret")としなければ、Secrets Managerとの統合されていないシークレットが作成されてしまう
  • masterUsernameをL1 Constructで指定しなければ、masterUsernameの参照エラーでデプロイできない
  • Secrets Managerとの統合によって作成されたシークレットのローテーション間隔を指定することも可能
    • Secrets Managerとの統合によって作成されたシークレットのARNはgetAtt("MasterUserSecret.SecretArn")で取得可能

やってみた

credentials に username と secretName を指定した場合

まず、credentialsusernamesecretNameを指定した場合の挙動を確認します。

以下のようにAmazon Aurora DB クラスターを定義します。

./lib/constructs/aurora.ts

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

export interface AuroraProps {
  vpc: cdk.aws_ec2.IVpc;
  securityGroup: cdk.aws_ec2.ISecurityGroup;
}

export class Aurora extends Construct {
  constructor(scope: Construct, id: string, props: AuroraProps) {
    super(scope, id);

    // DB Cluster Parameter Group
    const dbClusterParameterGroup = new cdk.aws_rds.ParameterGroup(
      this,
      "DbClusterParameterGroup",
      {
        engine: cdk.aws_rds.DatabaseClusterEngine.auroraPostgres({
          version: cdk.aws_rds.AuroraPostgresEngineVersion.VER_15_2,
        }),
        description: "aurora-postgresql15",
        parameters: {
          log_statement: "none",
          "pgaudit.log": "all",
          "pgaudit.role": "rds_pgaudit",
          shared_preload_libraries: "pgaudit",
        },
      }
    );

    // DB Parameter Group
    const dbParameterGroup = new cdk.aws_rds.ParameterGroup(
      this,
      "DbParameterGroup",
      {
        engine: cdk.aws_rds.DatabaseClusterEngine.auroraPostgres({
          version: cdk.aws_rds.AuroraPostgresEngineVersion.VER_15_2,
        }),
        description: "aurora-postgresql15",
      }
    );

    // Subnet Group
    const subnetGroup = new cdk.aws_rds.SubnetGroup(this, "SubnetGroup", {
      description: "description",
      vpc: props.vpc,
      subnetGroupName: "SubnetGroup",
      vpcSubnets: props.vpc.selectSubnets({
        onePerAz: true,
        subnetType: cdk.aws_ec2.SubnetType.PRIVATE_ISOLATED,
      }),
    });

    // Monitoring Role
    const monitoringRole = new cdk.aws_iam.Role(this, "MonitoringRole", {
      assumedBy: new cdk.aws_iam.ServicePrincipal(
        "monitoring.rds.amazonaws.com"
      ),
      managedPolicies: [
        cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
          "service-role/AmazonRDSEnhancedMonitoringRole"
        ),
      ],
    });

    // DB Cluster
    const clusterIdentifier = "db-cluster";
    new cdk.aws_rds.DatabaseCluster(this, "Default", {
      engine: cdk.aws_rds.DatabaseClusterEngine.auroraPostgres({
        version: cdk.aws_rds.AuroraPostgresEngineVersion.VER_15_2,
      }),
      writer: cdk.aws_rds.ClusterInstance.provisioned("Instance1", {
        instanceType: cdk.aws_ec2.InstanceType.of(
          cdk.aws_ec2.InstanceClass.T3,
          cdk.aws_ec2.InstanceSize.MEDIUM
        ),
        allowMajorVersionUpgrade: false,
        autoMinorVersionUpgrade: true,
        enablePerformanceInsights: true,
        parameterGroup: dbParameterGroup,
        performanceInsightRetention:
          cdk.aws_rds.PerformanceInsightRetention.DEFAULT,
        publiclyAccessible: false,
        instanceIdentifier: "db-instance1",
      }),
      backup: {
        retention: cdk.Duration.days(7),
        preferredWindow: "16:00-16:30",
      },
      cloudwatchLogsExports: ["postgresql"],
      cloudwatchLogsRetention: cdk.aws_logs.RetentionDays.ONE_YEAR,
      clusterIdentifier,
      copyTagsToSnapshot: true,
      credentials: {
        username: "postgresAdmin",
        secretName: `${clusterIdentifier}/postgresAdmin`,
      },
      defaultDatabaseName: "testDB",
      deletionProtection: false,
      iamAuthentication: false,
      monitoringInterval: cdk.Duration.minutes(1),
      monitoringRole,
      parameterGroup: dbClusterParameterGroup,
      preferredMaintenanceWindow: "Sat:17:00-Sat:17:30",
      storageEncrypted: true,
      vpc: props.vpc,
      securityGroups: [props.securityGroup],
      subnetGroup,
    });
  }
}

cdk deploy後、デプロイされたDBインスタンスの編集画面に遷移して、RDSとSecrets Managerとの統合が有効化されているか確認します。

AWS Secrets Manager でマスター認証情報を管理するにチェックが入っていない

AWS Secrets Manager でマスター認証情報を管理するにチェックが入っていないので、有効化はされていなさそうですね。

作成されたシークレットも確認します。

シークレットの確認

自動ローテーションは無効になっているということから、RDSとSecrets Managerとの統合が有効化されていないことがわかります。

せっかくなので、シークレットに保存されている認証情報を使ってDBクラスターに接続します。

# PostgreSQL 15のクライアントをインストール
$ sudo dnf install postgresql15
Last metadata expiration check: 0:00:28 ago on Sun Jun  4 23:50:34 2023.
Dependencies resolved.
=================================================================================================================================
 Package                                 Architecture         Version                            Repository                 Size
=================================================================================================================================
Installing:
 postgresql15                            x86_64               15.0-1.amzn2023.0.2                amazonlinux               1.6 M
Installing dependencies:
 postgresql15-private-libs               x86_64               15.0-1.amzn2023.0.2                amazonlinux               143 k

Transaction Summary
=================================================================================================================================
Install  2 Packages

Total download size: 1.8 M
Installed size: 6.5 M
Is this ok [y/N]: y
Downloading Packages:
(1/2): postgresql15-private-libs-15.0-1.amzn2023.0.2.x86_64.rpm                                  1.2 MB/s | 143 kB     00:00    
(2/2): postgresql15-15.0-1.amzn2023.0.2.x86_64.rpm                                               9.5 MB/s | 1.6 MB     00:00    
---------------------------------------------------------------------------------------------------------------------------------
Total                                                                                            7.4 MB/s | 1.8 MB     00:00     
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                                                                         1/1 
  Installing       : postgresql15-private-libs-15.0-1.amzn2023.0.2.x86_64                                                    1/2 
  Installing       : postgresql15-15.0-1.amzn2023.0.2.x86_64                                                                 2/2 
  Running scriptlet: postgresql15-15.0-1.amzn2023.0.2.x86_64                                                                 2/2 
  Verifying        : postgresql15-15.0-1.amzn2023.0.2.x86_64                                                                 1/2 
  Verifying        : postgresql15-private-libs-15.0-1.amzn2023.0.2.x86_64                                                    2/2 

Installed:
  postgresql15-15.0-1.amzn2023.0.2.x86_64                  postgresql15-private-libs-15.0-1.amzn2023.0.2.x86_64                 

Complete!

# インストールされたことを確認
$ psql --version
psql (PostgreSQL) 15.0

# シークレットから認証情報を取得
$ get_secrets_value=$(aws secretsmanager get-secret-value \
    --secret-id db-cluster/postgresAdmin \
    --region us-east-1 \
    | jq -r .SecretString)

# 環境変数に埋め込み
$ export PGHOST=$(echo "${get_secrets_value}" | jq -r .host)
$ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port)
$ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname)
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

# DBクラスターに接続
$ psql
psql (15.0, server 15.2)
SSL connection (protocol: TLSv1.2, cipher: AES128-SHA256, compression: off)
Type "help" for help.

testDB=>

接続できましたね。

Escape hatchで manageMasterUserPassword を true に指定

L2 ConstructでRDSとSecrets Managerとの統合を有効化することは難しそうなので、Escape hatchで無理やり指定します。

./lib/constructs/aurora.ts

    const cfnDbCluster = dbCluster.node
      .defaultChild as cdk.aws_rds.CfnDBCluster;
    cfnDbCluster.manageMasterUserPassword = true;

diffを確認します。

$ npx cdk diff
Stack AuroraStack
Resources
[~] AWS::RDS::DBCluster Aurora/Default Aurora2CBAB212
 └─ [+] ManageMasterUserPassword
     └─ true

ManageMasterUserPasswordtrueに指定されただけですね。

cdk deploy後、デプロイされたDBインスタンスの編集画面に遷移して、RDSとSecrets Managerとの統合が有効化されているか確認します。

AWS Secrets Manager でマスター認証情報を管理するにチェックが入っている

AWS Secrets Manager でマスター認証情報を管理するにチェックが入っているので、有効化はされていそうです。

Secrets Managerからも確認します。

RDSとSecrets Managerの統合で作成されたシークレット

このシークレットはAmazon RDS (rds) によって作成されましたとあることからRDSとSecrets Managerとの統合によって作成されたシークレットであることが分かります。

なお、統合によって作成されたシークレットにはDBクラスターエンドポイントやDB名、ポート番号の情報はないようです。

PostgreSQLのクライアントからシークレットを再取得せずにDBクラスターに接続してみます。

$ psql

psql: error: connection to server at "db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com" (10.1.1.118), port 5432 failed: FATAL:  password authentication failed for user "postgresAdmin"
connection to server at "db-cluster.cluster-cicjym7lykmq.us-east-1.rds.amazonaws.com" (10.1.1.118), port 5432 failed: FATAL:  password authentication failed for user "postgresAdmin"

パスワードが変わってしまったので接続できないですね。

Secrets Managerとの統合によって作成されたシークレットを取得して、再接続します。

# シークレットから認証情報を取得
$ get_secrets_value=$(aws secretsmanager get-secret-value \
    --secret-id 'rds!cluster-8a84d67a-61f3-421d-8cd4-aaa0c1877f56' \
    --region us-east-1 \
    | jq -r .SecretString)

# 環境変数に埋め込み
$ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username)
$ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password)

# DBクラスターに接続
$ psql
psql (15.0, server 15.2)
SSL connection (protocol: TLSv1.2, cipher: AES128-SHA256, compression: off)
Type "help" for help.

# DBクラスター内のロールの確認
testDB=> \du
                                                                                        List of roles
    Role name    |                         Attributes                         |                                                  
Member of                                                   
-----------------+------------------------------------------------------------+--------------------------------------------------
------------------------------------------------------------
 postgresAdmin   | Create role, Create DB                                    +| {rds_superuser}
                 | Password valid until infinity                              | 
 rds_ad          | Cannot login                                               | {}
 rds_iam         | Cannot login                                               | {}
 rds_password    | Cannot login                                               | {}
 rds_replication | Cannot login                                               | {}
 rds_superuser   | Cannot login                                               | {pg_read_all_data,pg_write_all_data,pg_monitor,pg_signal_backend,pg_checkpoint,rds_replication,rds_password}
 rdsadmin        | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                 | Password valid until infinity                              |

接続できましたね。裏で特殊なロールが作成されているのかなと思ったのですが、そのようなことはないようです。

Secrets Managerとの統合によって作成されたシークレットの自動ローテーション設定の変更

手動でSecrets Managerとの統合によって作成されたシークレットの自動ローテーション設定の変更してみます。

スケジュール式とウィンドウを変更して保存をクリックします。シークレットを保存するとすぐにローテーションさせます。次回のローテーションは、スケジュールに基づいて開始されます。にはチェックを入れたままにしておきます。

シークレットの自動ローテーション設定の変更

設定変更後、マネジメントコンソールからシークレットを再取得するとpasswordの値が変わっていることを確認できます。

ローテーションされたことを確認

CloudTrailから手動でシークレットのローテーション設定を変更した際のイベントを確認します。

手動でシークレットのローテーション設定を変更した際のイベント

SLRSessionというセッションによってパスワードのローテーションがされていますね。

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "AROA6KUFAVPUVTZY2EZVY:SLRSession",
        "arn": "arn:aws:sts::<AWSアカウントID>:assumed-role/AWSServiceRoleForRDS/SLRSession",
        "accountId": "<AWSアカウントID>",
        "accessKeyId": "ASIA6KUFAVPUUJF5PYUR",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "AROA6KUFAVPUVTZY2EZVY",
                "arn": "arn:aws:iam::<AWSアカウントID>:role/aws-service-role/rds.amazonaws.com/AWSServiceRoleForRDS",
                "accountId": "<AWSアカウントID>",
                "userName": "AWSServiceRoleForRDS"
            },
            "webIdFederationData": {},
            "attributes": {
                "creationDate": "2023-06-05T02:34:52Z",
                "mfaAuthenticated": "false"
            }
        },
        "invokedBy": "rds.amazonaws.com"
    },
    "eventTime": "2023-06-05T02:34:52Z",
    "eventSource": "secretsmanager.amazonaws.com",
    "eventName": "PutSecretValue",
    "awsRegion": "us-east-1",
    "sourceIPAddress": "rds.amazonaws.com",
    "userAgent": "rds.amazonaws.com",
    "requestParameters": {
        "secretId": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:rds!cluster-8a84d67a-61f3-421d-8cd4-aaa0c1877f56-psD95W",
        "clientRequestToken": "4f16bd74-5ddd-48f5-b24d-b2e9bacdd859",
        "versionStages": [
            "AWSPENDING"
        ]
    },
    "responseElements": {
        "arn": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:rds!cluster-8a84d67a-61f3-421d-8cd4-aaa0c1877f56-psD95W"
    },
    "requestID": "eeef0705-e89f-4deb-91b9-c65e8191b94d",
    "eventID": "92adb311-c7ba-45fa-847d-6f399040b549",
    "readOnly": false,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "<AWSアカウントID>",
    "eventCategory": "Management"
}

なお、一度キャンセルされていますが、キャンセル理由は特に書いてありませんでした。

{
    "eventVersion": "1.08",
    "userIdentity": {
.
.
(中略)
.
.
    },
    "eventTime": "2023-06-05T02:33:47Z",
    "eventSource": "secretsmanager.amazonaws.com",
    "eventName": "CancelRotateSecret",
    "awsRegion": "us-east-1",
    "sourceIPAddress": "<IPアドレス>",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
    "requestParameters": {
        "secretId": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:rds!cluster-8a84d67a-61f3-421d-8cd4-aaa0c1877f56-psD95W"
    },
    "responseElements": {
        "aRN": "arn:aws:secretsmanager:us-east-1:<AWSアカウントID>:secret:rds!cluster-8a84d67a-61f3-421d-8cd4-aaa0c1877f56-psD95W",
        "name": "rds!cluster-8a84d67a-61f3-421d-8cd4-aaa0c1877f56"
    },
    "requestID": "ff70429a-dae4-482f-a8af-60b15c622daf",
    "eventID": "29d44b8a-42d3-448b-a669-67d52ecd3112",
    "readOnly": false,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "<AWSアカウントID>",
    "eventCategory": "Management",
    "tlsDetails": {
        "tlsVersion": "TLSv1.3",
        "cipherSuite": "TLS_AES_128_GCM_SHA256",
        "clientProvidedHostHeader": "secretsmanager.us-east-1.amazonaws.com"
    },
    "sessionCredentialFromConsole": "true"
}

ローテーションされたパスワードを使ってDBクラスターに接続できることも確認できました。

統合されていないシークレットを削除する

以上でめでたしめでたし。

とはいきません。

というのも統合されていないシークレットは引き続き残っています。余計なリソースが作成されるのは避けたいところです。

どうにかして統合されていないシークレットを削除します。

まず、DBクラスターのcredentials.secretNameの指定をしないようにしてみます。

./lib/constructs/aurora.ts

      credentials: {
        username: "postgresAdmin",
      },

シークレットの名前が指定されなければ、シークレットも作成されないのでは? という淡い期待を込めています。

編集後diffを確認します。

$ npx cdk diff
Stack AuroraStack
Resources
[~] AWS::SecretsManager::Secret Aurora/Default/Secret AuroraSecret7ACECA7F replace
 └─ [-] Name (requires replacement)
     └─ db-cluster/postgresAdmin

replaceとなっているので、リソースは削除されず作り直されそうですね。

一旦気にせずcdk deployしてみます。

$ npx cdk deploy

✨  Synthesis time: 7.61s

AuroraStack:  start: Building 71646dd7c5446c0e5cce7a079467281bd6f33982d50d7bb1f555cef193c4747a:<AWSアカウントID>-us-east-1
AuroraStack:  success: Built 71646dd7c5446c0e5cce7a079467281bd6f33982d50d7bb1f555cef193c4747a:<AWSアカウントID>-us-east-1
AuroraStack:  start: Publishing 71646dd7c5446c0e5cce7a079467281bd6f33982d50d7bb1f555cef193c4747a:<AWSアカウントID>-us-east-1
AuroraStack:  success: Published 71646dd7c5446c0e5cce7a079467281bd6f33982d50d7bb1f555cef193c4747a:<AWSアカウントID>-us-east-1
AuroraStack: deploying... [1/1]
AuroraStack: creating CloudFormation changeset...
11:57:29 | UPDATE_FAILED        | AWS::RDS::DBCluster                         | Aurora2CBAB212
Resource handler returned message: "MasterUserPassword and ManageMasterUserPassword are mutually exclusive. Specify onl
y one of these parameters. (Service: Rds, Status Code: 400, Request ID: 6662f5ae-61ba-43cd-96e2-c3fdec0fab99)" (Request
Token: 1af22298-1f3d-31a7-5e4a-9c00feff136c, HandlerErrorCode: InvalidRequest)

11:57:34 | UPDATE_FAILED        | AWS::RDS::DBCluster                         | Aurora2CBAB212
Resource handler returned message: "MasterUserPassword and ManageMasterUserPassword are mutually exclusive. Specify onl
y one of these parameters. (Service: Rds, Status Code: 400, Request ID: d9fb353a-0fc3-4511-8b79-ba0d4c2fa9ed)" (Request
Token: 88bc765c-826d-5e6b-c3f3-960150bb7edc, HandlerErrorCode: InvalidRequest)


 ❌  AuroraStack failed: Error: The stack named AuroraStack failed to deploy: UPDATE_ROLLBACK_FAILED (The following resource(s) failed to update: [Aurora2CBAB212]. ): Resource handler returned message: "MasterUserPassword and ManageMasterUserPassword are mutually exclusive. Specify only one of these parameters. (Service: Rds, Status Code: 400, Request ID: 6662f5ae-61ba-43cd-96e2-c3fdec0fab99)" (RequestToken: 1af22298-1f3d-31a7-5e4a-9c00feff136c, HandlerErrorCode: InvalidRequest), Resource handler returned message: "MasterUserPassword and ManageMasterUserPassword are mutually exclusive. Specify only one of these parameters. (Service: Rds, Status Code: 400, Request ID: d9fb353a-0fc3-4511-8b79-ba0d4c2fa9ed)" (RequestToken: 88bc765c-826d-5e6b-c3f3-960150bb7edc, HandlerErrorCode: InvalidRequest)
    at FullCloudFormationDeployment.monitorDeployment (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:397:10236)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.deployStack2 [as deployStack] (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:400:149977)
    at async /<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:400:135508

 ❌ Deployment failed: Error: The stack named AuroraStack failed to deploy: UPDATE_ROLLBACK_FAILED (The following resource(s) failed to update: [Aurora2CBAB212]. ): Resource handler returned message: "MasterUserPassword and ManageMasterUserPassword are mutually exclusive. Specify only one of these parameters. (Service: Rds, Status Code: 400, Request ID: 6662f5ae-61ba-43cd-96e2-c3fdec0fab99)" (RequestToken: 1af22298-1f3d-31a7-5e4a-9c00feff136c, HandlerErrorCode: InvalidRequest), Resource handler returned message: "MasterUserPassword and ManageMasterUserPassword are mutually exclusive. Specify only one of these parameters. (Service: Rds, Status Code: 400, Request ID: d9fb353a-0fc3-4511-8b79-ba0d4c2fa9ed)" (RequestToken: 88bc765c-826d-5e6b-c3f3-960150bb7edc, HandlerErrorCode: InvalidRequest)
    at FullCloudFormationDeployment.monitorDeployment (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:397:10236)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.deployStack2 [as deployStack] (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:400:149977)
    at async /<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:400:135508

The stack named AuroraStack failed to deploy: UPDATE_ROLLBACK_FAILED (The following resource(s) failed to update: [Aurora2CBAB212]. ): Resource handler returned message: "MasterUserPassword and ManageMasterUserPassword are mutually exclusive. Specify only one of these parameters. (Service: Rds, Status Code: 400, Request ID: 6662f5ae-61ba-43cd-96e2-c3fdec0fab99)" (RequestToken: 1af22298-1f3d-31a7-5e4a-9c00feff136c, HandlerErrorCode: InvalidRequest), Resource handler returned message: "MasterUserPassword and ManageMasterUserPassword are mutually exclusive. Specify only one of these parameters. (Service: Rds, Status Code: 400, Request ID: d9fb353a-0fc3-4511-8b79-ba0d4c2fa9ed)" (RequestToken: 88bc765c-826d-5e6b-c3f3-960150bb7edc, HandlerErrorCode: InvalidRequest)

MasterUserPasswordManageMasterUserPasswordを同時に指定することはできないと怒られました。

MasterUserPasswordを指定しないようにaddPropertyDeletionOverrideを使います。

./lib/constructs/aurora.ts

    const cfnDbCluster = dbCluster.node
      .defaultChild as cdk.aws_rds.CfnDBCluster;
    cfnDbCluster.manageMasterUserPassword = true;
    cfnDbCluster.addPropertyDeletionOverride("MasterUserPassword");

編集後、diffを確認します。

$ npx cdk diff
Stack AuroraStack
Resources
[~] AWS::SecretsManager::Secret Aurora/Default/Secret AuroraSecret7ACECA7F replace
 └─ [-] Name (requires replacement)
     └─ db-cluster/postgresAdmin
[~] AWS::RDS::DBCluster Aurora/Default Aurora2CBAB212
 └─ [-] MasterUserPassword
     └─ {"Fn::Join":["",["{{resolve:secretsmanager:",{"Ref":"AuroraSecret7ACECA7F"},":SecretString:password::}}"]]}

MasterUserPasswordの参照が削除されましたね。

cdk deployします。

すんなりデプロイできましたが、作成されているリソースを確認するとシークレットが存在しているようでした。

シークレットが存在していることを確認

SecretというIDのリソースは不要なのでtryRemoveChildでDBクラスターのL2 Constructから削除します。

./lib/constructs/aurora.ts

    const cfnDbCluster = dbCluster.node
      .defaultChild as cdk.aws_rds.CfnDBCluster;
    cfnDbCluster.manageMasterUserPassword = true;
    cfnDbCluster.addPropertyDeletionOverride("MasterUserPassword");
    dbCluster.node.tryRemoveChild("Secret");

編集後、diffを確認します。

npx cdk diff
Stack AuroraStack
Resources
[-] AWS::SecretsManager::Secret Aurora/Default/Secret AuroraSecret7ACECA7F destroy
[-] AWS::SecretsManager::SecretTargetAttachment Aurora/Default/Secret/Attachment AuroraSecretAttachmentEAAB0558 destroy

シークレットがdestroyになりましたね! これは完全勝利の予感です。

cdk deployします。

$ npx cdk deploy

✨  Synthesis time: 7.47s

AuroraStack: deploying... [1/1]
AuroraStack: creating CloudFormation changeset...

 ❌  AuroraStack failed: Error [ValidationError]: Template format error: Unresolved resource dependencies [AuroraSecret7ACECA7F] in the Resources block of the template
    at Request.extractError (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:46245)
    at Request.callListeners (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:89237)
    at Request.emit (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:88685)
    at Request.emit (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:195114)
    at Request.transition (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:188666)
    at AcceptorStateMachine.runTo (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:153538)
    at /<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:153868
    at Request.<anonymous> (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:188958)
    at Request.<anonymous> (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:195189)
    at Request.callListeners (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:89405) {
  code: 'ValidationError',
  time: 2023-06-05T06:09:44.898Z,
  requestId: 'be6fb124-cdbf-4cf5-af1e-dcae92285640',
  statusCode: 400,
  retryable: false,
  retryDelay: 485.60885421936837
}

 ❌ Deployment failed: Error [ValidationError]: Template format error: Unresolved resource dependencies [AuroraSecret7ACECA7F] in the Resources block of the template
    at Request.extractError (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:46245)
    at Request.callListeners (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:89237)
    at Request.emit (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:88685)
    at Request.emit (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:195114)
    at Request.transition (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:188666)
    at AcceptorStateMachine.runTo (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:153538)
    at /<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:153868
    at Request.<anonymous> (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:188958)
    at Request.<anonymous> (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:195189)
    at Request.callListeners (/<ディレクトリパス>/aurora/node_modules/aws-cdk/lib/index.js:292:89405) {
  code: 'ValidationError',
  time: 2023-06-05T06:09:44.898Z,
  requestId: 'be6fb124-cdbf-4cf5-af1e-dcae92285640',
  statusCode: 400,
  retryable: false,
  retryDelay: 485.60885421936837
}

Template format error: Unresolved resource dependencies [AuroraSecret7ACECA7F] in the Resources block of the template

削除したはずのシークレットの論理IDAuroraSecret7ACECA7Fが参照されており、エラーとなっているようです。

どのような時にシークレットが作成されるのかAWS CDKのソースコードを確認します。

cluster.tsを確認すると、renderCredentialsで作成したcredentialsCfnDBClustermasterUsernamemasterUserPasswordに指定していますね。

aws-cdk/packages/aws-cdk-lib/aws-rds/lib/cluster.ts

export class DatabaseCluster extends DatabaseClusterNew {
  /**
   * Import an existing DatabaseCluster from properties
   */
  public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster {
    return new ImportedDatabaseCluster(scope, id, attrs);
  }

  public readonly clusterIdentifier: string;
  public readonly clusterResourceIdentifier: string;
  public readonly clusterEndpoint: Endpoint;
  public readonly clusterReadEndpoint: Endpoint;
  public readonly connections: ec2.Connections;
  public readonly instanceIdentifiers: string[];
  public readonly instanceEndpoints: Endpoint[];

  /**
   * The secret attached to this cluster
   */
  public readonly secret?: secretsmanager.ISecret;

  constructor(scope: Construct, id: string, props: DatabaseClusterProps) {
    super(scope, id, props);

    const credentials = renderCredentials(this, props.engine, props.credentials);
    const secret = credentials.secret;

    const cluster = new CfnDBCluster(this, 'Resource', {
      ...this.newCfnProps,
      // Admin
      masterUsername: credentials.username,
      masterUserPassword: credentials.password?.unsafeUnwrap(),
    });

    this.clusterIdentifier = cluster.ref;
    this.clusterResourceIdentifier = cluster.attrDbClusterResourceId;

    if (secret) {
      this.secret = secret.attach(this);
    }

    // create a number token that represents the port of the cluster
    const portAttribute = Token.asNumber(cluster.attrEndpointPort);
    this.clusterEndpoint = new Endpoint(cluster.attrEndpointAddress, portAttribute);
    this.clusterReadEndpoint = new Endpoint(cluster.attrReadEndpointAddress, portAttribute);
    this.connections = new ec2.Connections({
      securityGroups: this.securityGroups,
      defaultPort: ec2.Port.tcp(this.clusterEndpoint.port),
    });

    cluster.applyRemovalPolicy(props.removalPolicy ?? RemovalPolicy.SNAPSHOT);

    setLogRetention(this, props);
    if ((props.writer || props.readers) && (props.instances || props.instanceProps)) {
      throw new Error('Cannot provide writer or readers if instances or instanceProps are provided');
    }

    if (!props.instanceProps && !props.writer) {
      throw new Error('writer must be provided');
    }

    const createdInstances = props.writer ? this._createInstances(props) : legacyCreateInstances(this, props, this.subnetGroup);
    this.instanceIdentifiers = createdInstances.instanceIdentifiers;
    this.instanceEndpoints = createdInstances.instanceEndpoints;
  }

}

masterUsernameはシークレットの認証情報から参照していることになるので、ここが怪しいですね。

Escape hatchで直接CfnDBClustermasterUsernameを指定すれば解決しそうです。

util.tsからrenderCredentialsの動きも確認しておきます。

aws-cdk/packages/aws-cdk-lib/aws-rds/lib/private/util.ts

/**
 * Renders the credentials for an instance or cluster
 */
export function renderCredentials(scope: Construct, engine: IEngine, credentials?: Credentials): Credentials {
  let renderedCredentials = credentials ?? Credentials.fromUsername(engine.defaultUsername ?? 'admin'); // For backwards compatibilty

  if (!renderedCredentials.secret && !renderedCredentials.password) {
    renderedCredentials = Credentials.fromSecret(
      new DatabaseSecret(scope, 'Secret', {
        username: renderedCredentials.username,
        secretName: renderedCredentials.secretName,
        encryptionKey: renderedCredentials.encryptionKey,
        excludeCharacters: renderedCredentials.excludeCharacters,
        // if username must be referenced as a string we can safely replace the
        // secret when customization options are changed without risking a replacement
        replaceOnPasswordCriteriaChanges: credentials?.usernameAsString,
        replicaRegions: renderedCredentials.replicaRegions,
      }),
      // pass username if it must be referenced as a string
      credentials?.usernameAsString ? renderedCredentials.username : undefined,
    );
  }

  return renderedCredentials;
}

認証情報の指定がなければ、DatabaseSecretで認証情報を生成するようですね。

解決の糸口が見えたので、Escape hatchで直接CfnDBClustermasterUsernameを指定します。

./lib/constructs/aurora.ts

    const cfnDbCluster = dbCluster.node
      .defaultChild as cdk.aws_rds.CfnDBCluster;
    cfnDbCluster.manageMasterUserPassword = true;
    cfnDbCluster.masterUsername = "postgresAdmin";
    cfnDbCluster.addPropertyDeletionOverride("MasterUserPassword");
    dbCluster.node.tryRemoveChild("Secret");

編集後、diffを確認します。

$ npx cdk diff
Stack AuroraStack
Resources
[-] AWS::SecretsManager::Secret Aurora/Default/Secret AuroraSecret7ACECA7F destroy
[-] AWS::SecretsManager::SecretTargetAttachment Aurora/Default/Secret/Attachment AuroraSecretAttachmentEAAB0558 destroy
[~] AWS::RDS::DBCluster Aurora/Default Aurora2CBAB212 may be replaced
 └─ [~] MasterUsername (may cause replacement)
     └─ @@ -1,12 +1,1 @@
        [-] {
        [-]   "Fn::Join": [
        [-]     "",
        [-]     [
        [-]       "{{resolve:secretsmanager:",
        [-]       {
        [-]         "Ref": "AuroraSecret7ACECA7F"
        [-]       },
        [-]       ":SecretString:username::}}"
        [-]     ]
        [-]   ]
        [-] }
        [+] "postgresAdmin"

MasterUsernameがシークレットからの参照ではなくなりましたね。

その後のcdk deployも問題なく通りました。

デプロイ後、スタックのリソース一覧からもSecretsがなくなっているか確認します。

シークレットがなくなったことを確認

綺麗さっぱり消えていますね。

統合されているシークレットを手動でローテーションして、DBクラスターに接続できることまで確認できました。

Secrets Managerとの統合によって作成されたシークレットのローテーション間隔を指定する

最後にSecrets Managerとの統合によって作成されたシークレットのローテーション間隔を指定してみます。

これもEscape hatchでゴリ押します。おまけでOutputにシークレットのARNを出力できるか確認してみます。

./lib/constructs/aurora.ts

    const masterUserSecretArn = cfnDbCluster
      .getAtt("MasterUserSecret.SecretArn")
      .toString();

    new cdk.aws_secretsmanager.CfnRotationSchedule(this, "RotationSchedule", {
      secretId: masterUserSecretArn,
      rotationRules: {
        scheduleExpression: "cron(0 18 ? 1/1 7#1 *)",
        duration: "1h",
      },
    });

    new cdk.CfnOutput(this, "OutputMasterUserSecretArn", {
      value: masterUserSecretArn,
      exportName: "OutputMasterUserSecretArn",
    });

編集後、diffを確認します。

$ npx cdk diff
Stack AuroraStack
Resources
[+] AWS::SecretsManager::RotationSchedule Aurora/RotationSchedule AuroraRotationSchedule7E76A0E7

Outputs
[+] Output Aurora/OutputMasterUserSecretArn AuroraOutputMasterUserSecretArn16B0094A: {"Value":{"Fn::GetAtt":["Aurora2CBAB212","MasterUserSecret.SecretArn"]},"Export":{"Name":"OutputMasterUserSecretArn"}}

ローテーション間隔の指定ができそうですね。Secrets Managerとの統合によって作成されたシークレットのARNもgetAtt("MasterUserSecret.SecretArn")で取得できるようです。

デプロイ後、スタック内のリソースを確認するとAWS::SecretsManager::RotationScheduleのリソースがありますね。

AuroraRotationSchedule

シークレットを確認すると、ローテーション間隔がAWS CDKで指定した通りcron(0 18 ? 1/1 7#1 *)になっていることが確認できました。

ローテーション間隔が指定できたことを確認

また、OutputにもシークレットのARNが正しく出力されていました。

OutputMasterUserSecretArn

Escape hatchの影響範囲を理解しよう

AWS CDKでAmazon Aurora DB クラスターとAWS Secrets Managerとの統合を設定してみました。

やはりEscape hatchは痒い所に手が届く非常にありがたい考え方ですね。使いこなす場合はAWS CDKのソースコードを読むなどして影響範囲を理解しながら設定すると良いでしょう。

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

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

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