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.

Fixes aws#10595
  • Loading branch information
spanierm42 committed Apr 23, 2022
1 parent dacc9ab commit 5be1417
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 3 deletions.
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 @@ -40,6 +40,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 @@ -267,6 +274,25 @@ interface DatabaseClusterBaseProps {
readonly storageEncryptionKey?: kms.IKey;
}

/**
* 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 @@ -716,6 +742,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 @@ -763,6 +790,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 @@ -806,6 +835,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
48 changes: 45 additions & 3 deletions packages/@aws-cdk/aws-rds/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,21 @@ import { testFutureBehavior } from '@aws-cdk/cdk-build-tools';
import * as cdk from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import {
AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, Credentials, DatabaseCluster,
DatabaseClusterEngine, DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup, DatabaseSecret,
DatabaseInstanceEngine, SqlServerEngineVersion,
AuroraEngineVersion,
AuroraMysqlEngineVersion,
AuroraPostgresEngineVersion,
CfnDBCluster,
Credentials,
DatabaseCluster,
DatabaseClusterEngine,
DatabaseClusterFromSnapshot,
DatabaseInstanceEngine,
DatabaseSecret,
InstanceUpdateBehaviour,
ParameterGroup,
PerformanceInsightRetention,
SqlServerEngineVersion,
SubnetGroup,
} from '../lib';

describe('cluster', () => {
Expand Down Expand Up @@ -122,6 +134,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

0 comments on commit 5be1417

Please sign in to comment.