Skip to main content

About

This CDK construct library makes it possible to create databases, schemas, and roles in an Aurora Serverless v2, RDS Database Cluster, Database Instance, or AWS DSQL clusters. PostgreSQL, MySQL, and DSQL databases are supported.

This construct library is intended to be used in enterprise environments, and works in isolated subnets.

semantic-release: Release badge

Requirements

  • CDK v2.
  • Node.js 22.14 or newer for consumers, as declared in package.json's engines. This is the library's supported minimum, independent of its development tools.

Installation

 npm i cdk-rds-sql

For .NET (available from the first NuGet release):

dotnet add package BerenddeBoer.CdkRdsSql

The .NET namespace is BerenddeBoer.CdkRdsSql. Node.js is also required when using the .NET bindings.

Usage

Provider

First setup your VPC and create your cluster:

import { Duration, RemovalPolicy } from "aws-cdk-lib"
import * as ec2 from "aws-cdk-lib/aws-ec2"
import * as rds from "aws-cdk-lib/aws-rds"

const vpc = ec2.Vpc.fromLookup(this, "Vpc", {
  vpcId: this.node.tryGetContext("vpc-id"),
})

const cluster = new rds.ServerlessCluster(this, "Cluster", {
  vpc: vpc,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
  },
  engine: rds.DatabaseClusterEngine.AURORA_POSTGRESQL,
  parameterGroup: rds.ParameterGroup.fromParameterGroupName(
    this,
    "ParameterGroup",
    "default.aurora-postgresql11"
  ),
  removalPolicy: RemovalPolicy.DESTROY,
  scaling: {
    autoPause: Duration.minutes(60),
    minCapacity: rds.AuroraCapacityUnit.ACU_2,
    maxCapacity: rds.AuroraCapacityUnit.ACU_2,
  },
})

Then create a provider which will connect to your database. For a cluster:

import { Provider } from "cdk-rds-sql"

const provider = new Provider(this, "Provider", {
  vpc: vpc,
  cluster: cluster,
  secret: cluster.secret!,
})

For an instance:

import { Provider } from "cdk-rds-sql"

const provider = new Provider(this, "Provider", {
  vpc: vpc,
  instance: instance,
  secret: cluster.secret!,
})

The provider will setup a lambda, which normally lives in the same VPC as the database. You can give a different VPC, as long as that VPC has access to the VPC of the database. Only the provider lambda will talk to your database.

The provider will by default use the private isolated subnet of the VPC. Your isolated network must have a VPC endpoint to AWS Secrets Manager and possibly KMS as well. If you want to use a subnet with egress access in case you have no such VPC endpoints, specify the subnet as follows:

import { Provider } from "cdk-rds-sql"

const provider = new Provider(this, "Provider", {
  vpc: vpc,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
  },
  cluster: cluster,
  secret: cluster.secret!,
})

Roles

Create a postgres role (or mysql user) for a cluster as follows:

import { Role } from "cdk-rds-sql"

const role = new Role(this, "Role", {
  provider: provider,
  roleName: "myrole",
  databaseName: "mydb",
})

This will automatically create a secret just like ServerlessCluster does, with all the connection info needed for this user. It's secret value is a JSON like:

{
  "dbClusterIdentifier": "teststack-clustereb1186t9-sh4wpqfdyfvu",
  "password": "very-long-and-boring",
  "dbname": "mydb",
  "engine": "postgres",
  "port": 5432,
  "host": "teststack-clustereb1186t9-sh4wpqfdyfvu.cluster-cgudolabssna.us-east-1.rds.amazonaws.com",
  "username": "myrole"
}

You can access the secret via role.secret.

Instead of databaseName you can also specify database to reference an existing database. The role will not be created until the database has been created.

If you want to make the role the owner of a new database, just specify the databaseName here, and create the database later.

Connection limit (MySQL/MariaDB)

Cap how many connections a MySQL or MariaDB user may open:

const role = new Role(this, "Role", {
  provider: provider,
  roleName: "myrole",
  databaseName: "mydb",
  maxUserConnections: 5,
})

Create and update then run:

ALTER USER 'myrole'@'%' WITH MAX_USER_CONNECTIONS 5

Omit maxUserConnections to leave the user unlimited, which is the existing behaviour. Omitting it does not clear a limit that was already applied; set 0 to restore the server default (unlimited unless the global max_user_connections is set). PostgreSQL and DSQL throw if this property is set.

IAM Authentication

Instead of password-based authentication, you can create roles that use AWS IAM database authentication. This eliminates the need to store database passwords and provides enhanced security through AWS IAM policies.

import { Role } from "cdk-rds-sql"

const iamRole = new Role(this, "IamRole", {
  provider: provider,
  roleName: "myiamrole",
  databaseName: "mydb",
  enableIamAuth: true,
})

When enableIamAuth is set to true:

  • No secret is created - the role.secret property will be undefined
  • The database role is created without a password
  • Your application must generate an IAM authentication token at runtime

This is the same behavior as DSQL clusters, which always use IAM authentication.

Requirements for IAM Authentication:

  • SSL connections are required (enabled by default in this library)
  • Your application must have IAM permissions to connect to the database
  • The database user/role name must match the IAM identity

IAM Policy Example:

Your application will need an IAM policy like this to connect:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["rds-db:connect"],
      "Resource": [
        "arn:aws:rds-db:region:account-id:dbuser:cluster-resource-id/myiamrole"
      ]
    }
  ]
}

Generating an IAM Authentication Token:

At runtime, your application needs to generate an authentication token using the AWS SDK. Here's an example using the AWS SDK for JavaScript:

import { Signer } from "@aws-sdk/rds-signer"
import { Client } from "pg"

const signer = new Signer({
  hostname: "your-cluster.cluster-xxxxx.us-east-1.rds.amazonaws.com",
  port: 5432,
  username: "myiamrole",
  region: "us-east-1",
})

const token = await signer.getAuthToken()

const client = new Client({
  host: "your-cluster.cluster-xxxxx.us-east-1.rds.amazonaws.com",
  port: 5432,
  user: "myiamrole",
  database: "mydb",
  password: token,
  ssl: { rejectUnauthorized: true },
})

await client.connect()

Both PostgreSQL and MySQL databases support IAM authentication. For more details, see the AWS RDS IAM Database Authentication documentation.

MySQL support

In MySQL users are created with '%' as value for the host. It is hard to do this better:

  • Determine the CIDR blocks used by a VPC is not trivial.
  • For imported VPCs you cannot specify the IPv6 CIDR.
  • CIDRs might change without the system knowing, meaning applications could lose access to the database at random times, such as when a container is restarted.

Database

Create a database as follows:

import { Database } from "cdk-rds-sql"

const database = new Database(this, "Database", {
  provider: provider,
  databaseName: "mydb",
})

You can provide an owner, which makes it easy to create database owned by a new user:

const database = new Database(this, "Database", {
  provider: provider,
  databaseName: "mydb",
  owner: role,
})

Schema

Create a schema in the default database as follows:

import { Schema } from "cdk-rds-sql"

new Schema(this, "Schema", {
  provider: provider,
  schemaName: "myschema",
})

Or in another database:

const database = new Database(this, "Database", {
  provider: provider,
  databaseName: "mydb",
})

new Schema(this, "Schema", {
  provider: provider,
  schemaName: "myschema",
  databaseName: database.databaseName,
})

One may need a role permitted for using schema:

new Schema(this, "Schema", {
  provider: provider,
  schemaName: "myschema",
  databaseName: database.databaseName,
  role: role,
})

Sql

You can insert arbitrary SQL into your database with the Sql construct:

import { Sql } from "cdk-rds-sql"

const sql = new Sql(this, "Sql", {
  provider: provider,
  database: database,
  statement: "create table t (i int)",
})

Create a table if it does not exist, and grant a role privileges:

const sql = new Sql(this, "Sql", {
  provider: provider,
  database: database,
  statement: `
create table if not exists t (i int);
grant select on t to myrole;
`,
})

Rollback sql on stack deletion:

const sql = new Sql(this, "Sql", {
  provider: provider,
  database: database,
  statement: `
create table if not exists t (i int);
grant select on t to myrole;
`,
  rollback: `
DO $$BEGIN
  IF EXISTS (select from pg_database WHERE datname = 't') THEN
    IF EXISTS (select from pg_catalog.pg_roles WHERE rolname = 'myrole') THEN
      revoke select t from myrole;
    END IF;
    drop table t;
  END IF;
END$$;
`,
})

Note that there is no synchronisation between various Sql constructs, in particular the order in your code does not determine the order in which your SQL is executed. This happens in parallel, unless you specify an explicit dependency via sql.node.addDepency().

There are a lot of concerns when using Sql:

  • When you update your Sql, your previous Sql is not "rolled back", the new Sql is simply executed again.
  • When you delete your Sql construct the rollback is executed if specified
  • When permission are granted via Sql they must be removed via rollback to succesfully remove the role
  • Currently the Sql constructs has less than 5 minutes to execute its work.
  • It is unknown how large your SQL can be.

Parameters

Some lambda constructs, in particular Bref, do not support secrets out of the box. Use the parameterPrefix property to create SSM parameters in addition to a secret:

const role = new Role(this, "Role", {
  provider: provider,
  roleName: "myrole",
  databaseName: "mydb",
  parameterPrefix: "/my-app/",
})

This will create /my-app/username, /my-app/password and such.

To access parameters you will need IAM permissions such as:

initialPolicy: [
  new iam.PolicyStatement({
	actions: ["ssm:GetParameter", "ssm:GetParameters"],
	resources: [
	  // Grant access to all parameters under the base path
	  `arn:aws:ssm:${this.region}:${this.account}:parameter/my-app/*`,
	],
	effect: iam.Effect.ALLOW,
  })
],

Note that your VPC will need an SSM Parameters interface endpoint to support this.

AWS DSQL Support

This construct also supports AWS DSQL. DSQL is a great number of limitations around it's postgresql support, so there is some learing curve. This library supports creating roles, schemas, and executing SQL on DSQL clusters. Creating databases is not supported by DSQL.

DSQL Provider Setup

DSQL clusters don't require a VPC or secrets since they use IAM authentication:

import * as dsql from "aws-cdk-lib/aws-dsql"
import { Provider } from "cdk-rds-sql"

const dsqlCluster = new dsql.CfnCluster(this, "DsqlCluster", {
  deletionProtectionEnabled: false,
})

const provider = new Provider(this, "Provider", {
  cluster: dsqlCluster,
  // No VPC or secret needed for DSQL
})

You can access DSQL from within a VPC, but cdk-rds-sql has not been specifically tested with that scenario.

DSQL Roles

DSQL always uses IAM authentication, so the enableIamAuth property is ignored:

import { Role } from "cdk-rds-sql"

const role = new Role(this, "Role", {
  provider: provider,
  roleName: "myrole",
})

Using DSQL

Connecting to DSQL requires a couple of things:

  1. Your IAM role needs the dsql:DbConnect IAM permission to connect to the cluster.

  2. You probably want a normal postgres role (not "admin") to use from your apps.

  3. Your IAM role needs to be connected to your postgres role: use the IamGrant construct for that:

    new IamGrant(this, "LambdaIamGrant", {
      provider: provider,
      roleName: role.roleName,
      resourceArn: queryLambda.role!.roleArn,
    })
    
  4. From your code use the Dsql signer to create the password:

    import { DsqlSigner } from "@aws-sdk/dsql-signer"
    import { Client } from "pg"
    
    const region =
      process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"] || "us-west-2"
    
    // Generate DSQL auth token using AWS SDK
    const signer = new DsqlSigner({
      hostname: host,
      region,
    })
    
    const authToken = await signer.getDbConnectAuthToken()
    
    const client = new Client({
      host,
      port: 5432,
      user: dbUser,
      database: dbName,
      password: authToken,
      ssl: { rejectUnauthorized: false },
    })
    
    await client.connect()
    

IPv6

If you use the provider in an IPv6 subnet you probably need these settings:

import { Provider } from "cdk-rds-sql"

const provider = new Provider(this, "Provider", {
  ...
  functionProps: {
	ipv6AllowedForDualStack: true,
	allowAllIpv6Outbound: true,
  },
}

Disabling SSL

The default connection to RDS is ssl enabled. You can disable ssl by setting the ssl option to false:

const provider = new Provider(this, "Provider", {
  vpc: vpc,
  instance: instance,
  secret: cluster.secret!,
  ssl: false, // default is true
})

Working on this code

This code is managed by projen. mise installs Node 24, pnpm 12, and hk for Git hooks. Biome handles formatting and linting.

Development and CI use Node 24. Testcontainers is a development-only dependency; its dependency tree requires newer APIs than the consumer minimum of Node 22.14.

After cloning:

mise trust
mise install
mise run setup

When migrating an existing clone, replace the old pre-commit hooks with mise exec -- hk install --mise --force.

Run all checks with mise run check, or format and lint with pnpm exec projen biome. Tests require Docker. TypeScript stays on the 6.0 release line supported by jsii 6.0.

The pre-commit hook runs the full build, tests, and packaging, so Docker must be running. When VPC_ID is set, changes under src/ also run CDK synthesis against that VPC; this requires AWS credentials. Unset VPC_ID to skip synthesis.

The development tsconfig uses node16 module resolution, including package exports. Jest tasks enable Node's VM module support for native dynamic imports such as the MySQL driver import.

Testing

Test code via projen with:

pnpm exec projen test

Tests do not update snapshots automatically. To intentionally refresh them, run pnpm exec projen test:update and review the snapshot diff.

You can run the PostgreSQL sample stack with:

pnpm exec projen integ:deploy:postgresql:serverless

If you want to use an existing vpc:

pnpm exec cdk deploy --context vpc-id=vpc-0123456789 TestRdsSqlServerlessV2Stack

You can also test DSQL support with:

pnpm exec projen integ:deploy:dsql

To do

  • Update role: will not revoke connect to previous database if database name has changed.

Metadata

Release files for cdk-rds-sql 9.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cdk-rds-sql 9.1.1
File Size Uploaded Provenance
cdk_rds_sql-9.1.1.tar.gz 696.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for cdk-rds-sql 9.1.1
File Interpreter ABI Platform
cdk_rds_sql-9.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 1.4 MB

Release files / cdk_rds_sql-9.1.1.tar.gz

Download URL cdk_rds_sql-9.1.1.tar.gz
Size 696.3 kB
Tags Source
SHA-256 checksum
How to use checksums
0b4f8b69a3a7b02d74e12b64ec32c944fe5a05de1748ac6e0e635a0abef0ae8f
BLAKE2b-256 checksum
How to use checksums
95020f9f3b1dce8f1bfe14be87dd29f022150eb0d5a657df87d72ed1421c5436
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.14.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / cdk_rds_sql-9.1.1-py3-none-any.whl

Download URL cdk_rds_sql-9.1.1-py3-none-any.whl
Size 693.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c13371d00217ec4b5fd9ddad7d142d7d356385e1a6bad92689df2055f96123a3
BLAKE2b-256 checksum
How to use checksums
c2d846ecde0b57db1336297cd9561cdfd46f06b499c387d0676175a7ef985b06
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.14.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for cdk-rds-sql 9.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cdk_rds_sql-9.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page