Skip to content

Commit

Permalink
feat(rds): add support for database instances (#2187)
Browse files Browse the repository at this point in the history
Specific classes are exposed for a source database instance (`DatabaseInstance`), an instance created from a snapshot (`DatabaseInstanceFromSnapshot`) and a read replica instance (`DatabaseInstanceReadReplica`).

Add construct for option groups and refactor parameter groups.

Add basic support for instance event rules.

Integration with Secrets Manager and secret rotation.

Closes #2075 
Closes #1693
  • Loading branch information
jogold authored and rix0rrr committed May 31, 2019
1 parent 421bf6d commit b864041
Show file tree
Hide file tree
Showing 23 changed files with 3,453 additions and 334 deletions.
94 changes: 68 additions & 26 deletions packages/@aws-cdk/aws-rds/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,8 @@
## AWS RDS Construct Library

The `aws-cdk-rds` package contains Constructs for setting up RDS instances.

> Note: the functionality this package is currently limited, as the CDK team is
> focusing on other use cases first. If your use case is not listed below, you
> will have to use achieve it using CloudFormation resources.
>
> If you would like to help improve the state of this library, Pull Requests are
> welcome.
Supported:

* Clustered databases

Not supported:

* Instance databases
* Setting up from a snapshot


### Starting a Clustered Database

To set up a clustered database (like Aurora), create an instance of `DatabaseCluster`. You must
To set up a clustered database (like Aurora), define a `DatabaseCluster`. You must
always launch a database in a VPC. Use the `vpcSubnets` attribute to control whether
your instances will be launched privately or publicly:

Expand All @@ -45,33 +26,84 @@ By default, the master password will be generated and stored in AWS Secrets Mana
Your cluster will be empty by default. To add a default database upon construction, specify the
`defaultDatabaseName` attribute.

### Starting an Instance Database
To set up a instance database, define a `DatabaseInstance`. You must
always launch a database in a VPC. Use the `vpcSubnets` attribute to control whether
your instances will be launched privately or publicly:

```ts
const instance = new DatabaseInstance(stack, 'Instance', {
engine: rds.DatabaseInstanceEngine.OracleSE1,
instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small),
masterUsername: 'syscdk',
vpc
});
```
By default, the master password will be generated and stored in AWS Secrets Manager.

Use `DatabaseInstanceFromSnapshot` and `DatabaseInstanceReadReplica` to create an instance from snapshot or
a source database respectively:

```ts
new DatabaseInstanceFromSnapshot(stack, 'Instance', {
snapshotIdentifier: 'my-snapshot',
engine: rds.DatabaseInstanceEngine.Postgres,
instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Large),
vpc
});

new DatabaseInstanceReadReplica(stack, 'ReadReplica', {
sourceDatabaseInstance: sourceInstance,
engine: rds.DatabaseInstanceEngine.Postgres,
instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Large),
vpc
});
```
Creating a "production" Oracle database instance with option and parameter groups:

[example of setting up a production oracle instance](test/integ.instance.lit.ts)


### Instance events
To define Amazon CloudWatch event rules for database instances, use the `onEvent`
method:

```ts
const rule = instance.onEvent('InstanceEvent', { target: new targets.LambdaFunction(fn) });
```

### Connecting

To control who can access the cluster, use the `.connections` attribute. RDS database have
To control who can access the cluster or instance, use the `.connections` attribute. RDS databases have
a default port, so you don't need to specify the port:

```ts
cluster.connections.allowFromAnyIpv4('Open to the world');
```

The endpoints to access your database will be available as the `.clusterEndpoint` and `.readerEndpoint`
The endpoints to access your database cluster will be available as the `.clusterEndpoint` and `.readerEndpoint`
attributes:

```ts
const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT"
```

For an instance database:
```ts
const address = instance.instanceEndpoint.socketAddress; // "HOSTNAME:PORT"
```

### Rotating master password
When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically:

[example of setting up master password rotation](test/integ.cluster-rotation.lit.ts)
[example of setting up master password rotation for a cluster](test/integ.cluster-rotation.lit.ts)

Rotation of the master password is also supported for an existing cluster:
```ts
new RotationSingleUser(stack, 'Rotation', {
new SecretRotation(stack, 'Rotation', {
secret: importedSecret,
engine: DatabaseEngine.Oracle,
target: importedCluster,
application: SecretRotationApplication.OracleRotationSingleUser
target: importedCluster, // or importedInstance
vpc: importedVpc,
})
```
Expand All @@ -87,3 +119,13 @@ The `importedSecret` must be a JSON string with the following format:
"port": "<optional: if not specified, default port will be used>"
}
```

### Metrics
Database instances expose metrics (`cloudwatch.Metric`):
```ts
// The number of database connections in use (average over 5 minutes)
const dbConnections = instance.metricDatabaseConnections();

// The average amount of time taken per disk I/O operation (average over 1 minute)
const readLatency = instance.metric('ReadLatency', { statistic: 'Average', periodSec: 60 });
```
107 changes: 0 additions & 107 deletions packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts

This file was deleted.

36 changes: 3 additions & 33 deletions packages/@aws-cdk/aws-rds/lib/cluster-ref.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import ec2 = require('@aws-cdk/aws-ec2');
import secretsmanager = require('@aws-cdk/aws-secretsmanager');
import cdk = require('@aws-cdk/cdk');
import { Token } from '@aws-cdk/cdk';
import { IResource } from '@aws-cdk/cdk';
import { Endpoint } from './endpoint';

/**
* Create a clustered database with a given number of instances.
*/
export interface IDatabaseCluster extends cdk.IResource, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget {
export interface IDatabaseCluster extends IResource, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget {
/**
* Identifier of the cluster
*/
Expand Down Expand Up @@ -80,33 +80,3 @@ export interface DatabaseClusterAttributes {
*/
readonly instanceEndpointAddresses: string[];
}

/**
* Connection endpoint of a database cluster or instance
*
* Consists of a combination of hostname and port.
*/
export class Endpoint {
/**
* The hostname of the endpoint
*/
public readonly hostname: string;

/**
* The port of the endpoint
*/
public readonly port: number;

/**
* The combination of "HOSTNAME:PORT" for this endpoint
*/
public readonly socketAddress: string;

constructor(address: string, port: number) {
this.hostname = address;
this.port = port;

const portDesc = Token.isToken(port) ? '{IndirectPort}' : port;
this.socketAddress = `${address}:${portDesc}`;
}
}
41 changes: 13 additions & 28 deletions packages/@aws-cdk/aws-rds/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import ec2 = require('@aws-cdk/aws-ec2');
import kms = require('@aws-cdk/aws-kms');
import secretsmanager = require('@aws-cdk/aws-secretsmanager');
import { Construct, DeletionPolicy, Resource, Token } from '@aws-cdk/cdk';
import { IClusterParameterGroup } from './cluster-parameter-group';
import { DatabaseClusterAttributes, Endpoint, IDatabaseCluster } from './cluster-ref';
import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref';
import { DatabaseSecret } from './database-secret';
import { Endpoint } from './endpoint';
import { IParameterGroup } from './parameter-group';
import { BackupProps, DatabaseClusterEngine, InstanceProps, Login } from './props';
import { CfnDBCluster, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated';
import { DatabaseEngine, RotationSingleUser, RotationSingleUserOptions } from './rotation-single-user';
import { SecretRotation, SecretRotationApplication, SecretRotationOptions } from './secret-rotation';

/**
* Properties for a new database cluster
Expand Down Expand Up @@ -111,7 +112,7 @@ export interface DatabaseClusterProps {
*
* @default - No parameter group.
*/
readonly parameterGroup?: IClusterParameterGroup;
readonly parameterGroup?: IParameterGroup;

/**
* The CloudFormation policy to apply when the cluster and its instances
Expand Down Expand Up @@ -241,7 +242,7 @@ export class DatabaseCluster extends DatabaseClusterBase {
/**
* The database engine of this cluster
*/
public readonly engine: DatabaseClusterEngine;
private readonly secretRotationApplication: SecretRotationApplication;

/**
* The VPC where the DB subnet group is created.
Expand Down Expand Up @@ -286,11 +287,11 @@ export class DatabaseCluster extends DatabaseClusterBase {
});
}

this.engine = props.engine;
this.secretRotationApplication = props.engine.secretRotationApplication;

const cluster = new CfnDBCluster(this, 'Resource', {
// Basic
engine: this.engine,
engine: props.engine.name,
dbClusterIdentifier: props.clusterIdentifier,
dbSubnetGroupName: subnetGroup.ref,
vpcSecurityGroupIds: [this.securityGroupId],
Expand Down Expand Up @@ -347,14 +348,15 @@ export class DatabaseCluster extends DatabaseClusterBase {

const instance = new CfnDBInstance(this, `Instance${instanceIndex}`, {
// Link to cluster
engine: props.engine,
engine: props.engine.name,
dbClusterIdentifier: cluster.ref,
dbInstanceIdentifier: instanceIdentifier,
// Instance properties
dbInstanceClass: databaseInstanceType(props.instanceProps.instanceType),
publiclyAccessible,
// This is already set on the Cluster. Unclear to me whether it should be repeated or not. Better yes.
dbSubnetGroupName: subnetGroup.ref,
dbParameterGroupName: props.instanceProps.parameterGroup && props.instanceProps.parameterGroup.parameterGroupName,
});

instance.options.deletionPolicy = deleteReplacePolicy;
Expand All @@ -375,13 +377,13 @@ export class DatabaseCluster extends DatabaseClusterBase {
/**
* Adds the single user rotation of the master password to this cluster.
*/
public addRotationSingleUser(id: string, options: RotationSingleUserOptions = {}): RotationSingleUser {
public addRotationSingleUser(id: string, options: SecretRotationOptions = {}): SecretRotation {
if (!this.secret) {
throw new Error('Cannot add single user rotation for a cluster without secret.');
}
return new RotationSingleUser(this, id, {
return new SecretRotation(this, id, {
secret: this.secret,
engine: toDatabaseEngine(this.engine),
application: this.secretRotationApplication,
vpc: this.vpc,
vpcSubnets: this.vpcSubnets,
target: this,
Expand All @@ -396,20 +398,3 @@ export class DatabaseCluster extends DatabaseClusterBase {
function databaseInstanceType(instanceType: ec2.InstanceType) {
return 'db.' + instanceType.toString();
}

/**
* Transforms a DatbaseClusterEngine to a DatabaseEngine.
*
* @param engine the engine to transform
*/
function toDatabaseEngine(engine: DatabaseClusterEngine): DatabaseEngine {
switch (engine) {
case DatabaseClusterEngine.Aurora:
case DatabaseClusterEngine.AuroraMysql:
return DatabaseEngine.Mysql;
case DatabaseClusterEngine.AuroraPostgresql:
return DatabaseEngine.Postgres;
default:
throw new Error('Unknown engine');
}
}
Loading

0 comments on commit b864041

Please sign in to comment.