Skip to content

Commit

Permalink
feat(rds): Support rolling instance updates
Browse files Browse the repository at this point in the history
Support defining the instance update behaviour of RDS instances. This allows to switch between bulk (all instances at once) and rolling updates (one instance after another). While bulk updates are faster, they have a higher risk for longer downtimes as all instances might be simultaneously unreachable due to the update. Rolling updates take longer but ensure that all but one instance are not updated and thus downtimes are limited to the (at most two) changes of the primary instance.

We keep the current behaviour, namely a bulk update, as default.

This implementation follows proposal A by  hixi-hyi in issue aws#10595.
  • Loading branch information
spanierm42 committed Jul 12, 2022
1 parent cad6fc5 commit 50559a6
Show file tree
Hide file tree
Showing 12 changed files with 3,561 additions and 2 deletions.
29 changes: 28 additions & 1 deletion packages/@aws-cdk/aws-rds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,36 @@ new rds.DatabaseClusterFromSnapshot(this, 'Database', {
});
```

### Updating the database instances in a cluster

An update of the instances in a database cluster sometimes implies some interruptions.
E.g., if you change the instance class, the corresponding instance must be restarted and is not available during that time.
If you change all instances in a cluster, all instances might get restarted at the same time and basically making your database unavailable for some time.
You can adapt that update behaviour with the `instanceUpdateBehaviour` property.

The default option, `rds.InstanceUpdateBehaviour.BULK`, does not impose any restrictions on the instance update behaviour.
Thus, a change of instances can lead to downtime of your database cluster.
Alternatively, you can set `instanceUpdateBehaviour` to `rds.InstanceUpdateBehaviour.ROLLING`.
Then CDK makes sure that at most instance at a time is being updated.
This increases the overall update time but allows to change, e.g., the instance type of the database instances without a downtime caused by restarts of all database instances at the same time.

```ts
declare const vpc: ec2.Vpc;
const cluster = new rds.DatabaseCluster(this, 'Database', {
engine: rds.DatabaseClusterEngine.auroraMysql({ version: rds.AuroraMysqlEngineVersion.VER_3_01_0 }),
instances: 2,
instanceProps: {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.SMALL),
vpc,
},
// optional, defaults to rds.InstanceUpdateBehaviour.BULK
instanceUpdateBehaviour: rds.InstanceUpdateBehaviour.ROLLING,
});
```

## Starting an instance database

To set up a instance database, define a `DatabaseInstance`. You must
To set up an 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:

Expand Down
37 changes: 37 additions & 0 deletions packages/@aws-cdk/aws-rds/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ interface DatabaseClusterBaseProps {
*/
readonly instanceProps: InstanceProps;

/**
* The ordering of updates for instances
*
* @default InstanceUpdateBehaviour.BULK
*/
readonly instanceUpdateBehaviour?: InstanceUpdateBehaviour;

/**
* The number of seconds to set a cluster's target backtrack window to.
* This feature is only supported by the Aurora MySQL database engine and
Expand Down Expand Up @@ -275,6 +282,25 @@ interface DatabaseClusterBaseProps {
readonly copyTagsToSnapshot?: boolean;
}

/**
* The orchestration of updates of multiple instances
*/
export enum InstanceUpdateBehaviour {
/**
* In a bulk update, all instances of the cluster are updated at the same time.
* This results in a faster update procedure.
* During the update, however , all instances might be unavailable at the same time and thus a downtime might occur.
*/
BULK = 'BULK',

/**
* In a rolling update, one instance after another is updated.
* This results in at most one instance being unavailable during the update.
* If your cluster consists of more than 1 instance, the downtime periods are limited to the time a primary switch needs.
*/
ROLLING = 'ROLLING'
}

/**
* A new or imported clustered database.
*/
Expand Down Expand Up @@ -805,6 +831,7 @@ interface InstanceConfig {
*/
function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBaseProps, subnetGroup: ISubnetGroup): InstanceConfig {
const instanceCount = props.instances != null ? props.instances : 2;
const instanceUpdateBehaviour = props.instanceUpdateBehaviour ?? InstanceUpdateBehaviour.BULK;
if (Token.isUnresolved(instanceCount)) {
throw new Error('The number of instances an RDS Cluster consists of cannot be provided as a deploy-time only value!');
}
Expand Down Expand Up @@ -852,6 +879,8 @@ function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBase
);
const instanceParameterGroupConfig = instanceParameterGroup?.bindToInstance({});

const instances: CfnDBInstance[] = [];

for (let i = 0; i < instanceCount; i++) {
const instanceIndex = i + 1;
const instanceIdentifier = props.instanceIdentifierBase != null ? `${props.instanceIdentifierBase}${instanceIndex}` :
Expand Down Expand Up @@ -895,6 +924,14 @@ function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBase

instanceIdentifiers.push(instance.ref);
instanceEndpoints.push(new Endpoint(instance.attrEndpointAddress, portAttribute));
instances.push(instance);
}

// Adding dependencies here to ensure that the instances are updated one after the other.
if (instanceUpdateBehaviour === InstanceUpdateBehaviour.ROLLING) {
for (let i = 1; i < instanceCount; i++) {
instances[i].node.addDependency(instances[i-1]);
}
}

return { instanceEndpoints, instanceIdentifiers };
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-rds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
"@aws-cdk/integ-runner": "0.0.0",
"@aws-cdk/integ-tests": "0.0.0",
"@aws-cdk/cfn2ts": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
Expand Down
32 changes: 31 additions & 1 deletion packages/@aws-cdk/aws-rds/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as cxapi from '@aws-cdk/cx-api';
import {
AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, Credentials, DatabaseCluster,
DatabaseClusterEngine, DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup, DatabaseSecret,
DatabaseInstanceEngine, SqlServerEngineVersion, SnapshotCredentials,
DatabaseInstanceEngine, SqlServerEngineVersion, SnapshotCredentials, InstanceUpdateBehaviour,
} from '../lib';

describe('cluster', () => {
Expand Down Expand Up @@ -122,6 +122,36 @@ describe('cluster', () => {
});
});

test('can create a cluster with ROLLING instance update behaviour', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
new DatabaseCluster(stack, 'Database', {
engine: DatabaseClusterEngine.AURORA,
instances: 5,
instanceProps: {
vpc,
},
instanceUpdateBehaviour: InstanceUpdateBehaviour.ROLLING,
});

// THEN
const instanceResources = Template.fromStack(stack).findResources('AWS::RDS::DBInstance');
const instances = Object.keys(instanceResources);
const instanceDependencies = Object.values(instanceResources)
.map(properties => (properties.DependsOn as string[]).filter(dependency => instances.includes(dependency)));
// check that there are only required dependencies to form a chain of dependant instances
for (const dependencies of instanceDependencies) {
expect(dependencies.length).toBeLessThanOrEqual(1);
}
// check that all but one instance are a dependency of another instance
const dependantInstances = instanceDependencies.flat();
expect(dependantInstances).toHaveLength(instances.length - 1);
expect(instances.filter(it => !dependantInstances.includes(it))).toHaveLength(1);
});

test('can create a cluster with imported vpc and security group', () => {
// GIVEN
const stack = testStack();
Expand Down
45 changes: 45 additions & 0 deletions packages/@aws-cdk/aws-rds/test/integ.rolling-instance-updates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as ec2 from '@aws-cdk/aws-ec2';
import * as cdk from '@aws-cdk/core';
import * as integTests from '@aws-cdk/integ-tests';
import * as constructs from 'constructs';
import * as rds from '../lib';

interface RollingInstanceUpdateTestStackProps extends cdk.StackProps {
instanceUpdateBehaviour: rds.InstanceUpdateBehaviour;
}

class RollingInstanceUpdateTestStack extends cdk.Stack {
constructor(scope: constructs.Construct, id: string, props: RollingInstanceUpdateTestStackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs: 2,
});

new rds.DatabaseCluster(this, 'DatabaseCluster', {
engine: rds.DatabaseClusterEngine.AURORA,
instances: 3,
instanceProps: {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.SMALL),
vpc,
},
removalPolicy: cdk.RemovalPolicy.DESTROY,
instanceUpdateBehaviour: props.instanceUpdateBehaviour,
});
}
}


// Beginning of the test suite
const app = new cdk.App();
new integTests.IntegTest(app, 'InstanceUpdateBehaviorTests', {
testCases: [
new RollingInstanceUpdateTestStack(app, 'BulkUpdate', {
instanceUpdateBehaviour: rds.InstanceUpdateBehaviour.BULK,
}),
new RollingInstanceUpdateTestStack(app, 'RollingUpdate', {
instanceUpdateBehaviour: rds.InstanceUpdateBehaviour.ROLLING,
}),
],
});

app.synth();
Loading

0 comments on commit 50559a6

Please sign in to comment.