Skip to content

Commit

Permalink
feat(aws-dynamodb): support table application autoscaling (#637)
Browse files Browse the repository at this point in the history
Allows adding an application auto-scaling for a DynamoDB table
  • Loading branch information
SeekerWing authored and Elad Ben-Israel committed Sep 4, 2018
1 parent 45d0aa2 commit 85c4e64
Show file tree
Hide file tree
Showing 5 changed files with 1,163 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
- AWS::AppSync::DataSource HttpConfig (__added__)
- AWS::DAX::Cluster SSESpecification (__added__)
- AWS::DynamoDB::Table Stream (__added__)
- AWS::DynamoDB::Table AutoScalingSupport (__added__)
- AWS::EC2::VPCEndpoint IsPrivateDnsEnabled (__added__)
- AWS::EC2::VPCEndpoint SecurityGroupIds (__added__)
- AWS::EC2::VPCEndpoint SubnetIds (__added__)
Expand Down
59 changes: 59 additions & 0 deletions packages/@aws-cdk/aws-dynamodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,62 @@ const customTable = new dynamodb.Table(stack, 'CustomTable', {
tableName: 'MyTableName' // Default is CloudFormation-generated, which is the preferred approach
})
```

### Setup Auto Scaling for DynamoDB Table
further reading:
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.html
https://aws.amazon.com/blogs/database/how-to-use-aws-cloudformation-to-configure-auto-scaling-for-amazon-dynamodb-tables-and-indexes/

#### Setup via Constructor
```ts
import dynamodb = require('@aws-cdk/aws-dynamodb');

const customTable = new dynamodb.Table(stack, 'CustomTable', {
readCapacity: readUnits, // Default is 5
writeCapacity: writeUnits, // Default is 5
tableName: 'MyTableName', // Default is CloudFormation-generated, which is the preferred approach
readAutoScaling: {
minCapacity: 500,
maxCapacity: 5000,
targetValue: 75.0,
scaleInCooldown: 30,
scaleOutCooldown: 30,
scalingPolicyName: 'MyAwesomeReadPolicyName'
},
writeAutoScaling: {
minCapacity: 50,
maxCapacity: 500,
targetValue: 50.0,
scaleInCooldown: 10,
scaleOutCooldown: 10,
scalingPolicyName: 'MyAwesomeWritePolicyName'
},
});
```

#### Setup via addAutoScaling
```ts
import dynamodb = require('@aws-cdk/aws-dynamodb');

const customTable = new dynamodb.Table(stack, 'CustomTable', {
readCapacity: readUnits, // Default is 5
writeCapacity: writeUnits, // Default is 5
tableName: 'MyTableName' // Default is CloudFormation-generated, which is the preferred approach
});
table.addReadAutoScaling({
minCapacity: 500,
maxCapacity: 5000,
targetValue: 75.0,
scaleInCooldown: 30,
scaleOutCooldown: 30,
scalingPolicyName: 'MyAwesomeReadPolicyName'
});
table.addWriteAutoScaling({
minCapacity: 50,
maxCapacity: 500,
targetValue: 50.0,
scaleInCooldown: 10,
scaleOutCooldown: 10,
scalingPolicyName: 'MyAwesomeWritePolicyName'
});
```
169 changes: 163 additions & 6 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Construct } from '@aws-cdk/cdk';
import { cloudformation, TableArn, TableName, TableStreamArn } from './dynamodb.generated';
import { cloudformation as applicationautoscaling } from '@aws-cdk/aws-applicationautoscaling';
import { Role } from '@aws-cdk/aws-iam';
import { Construct, PolicyStatement, PolicyStatementEffect, ServicePrincipal } from '@aws-cdk/cdk';
import { cloudformation as dynamodb, TableArn, TableName, TableStreamArn } from './dynamodb.generated';

const HASH_KEY_TYPE = 'HASH';
const RANGE_KEY_TYPE = 'RANGE';
Expand Down Expand Up @@ -30,8 +32,58 @@ export interface TableProps {
* @default undefined, streams are disbaled
*/
streamSpecification?: StreamViewType;

/**
* AutoScalingProps configuration to configure Read AutoScaling for the DyanmoDB table.
* This field is optional and this can be achieved via addReadAutoScaling.
* @default undefined, read auto scaling is disabled
*/
readAutoScaling?: AutoScalingProps;

/**
* AutoScalingProps configuration to configure Write AutoScaling for the DyanmoDB table.
* This field is optional and this can be achieved via addWriteAutoScaling.
* @default undefined, write auto scaling is disabled
*/
writeAutoScaling?: AutoScalingProps;
}

/* tslint:disable:max-line-length */
export interface AutoScalingProps {
/**
* The minimum value that Application Auto Scaling can use to scale a target during a scaling activity.
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-applicationautoscaling-scalabletarget.html#cfn-applicationautoscaling-scalabletarget-mincapacity
*/
minCapacity: number;
/**
* The maximum value that Application Auto Scaling can use to scale a target during a scaling activity.
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-applicationautoscaling-scalabletarget.html#cfn-applicationautoscaling-scalabletarget-maxcapacity
*/
maxCapacity: number;
/**
* Application Auto Scaling ensures that the ratio of consumed capacity to provisioned capacity stays at or near this value. You define TargetValue as a percentage.
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration.html#cfn-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration-targetvalue
*/
targetValue: number;
/**
* The amount of time, in seconds, after a scale in activity completes before another scale in activity can start.
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration.html#cfn-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration-scaleincooldown
*/
scaleInCooldown: number;
/**
* The amount of time, in seconds, after a scale out activity completes before another scale out activity can start.
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration.html#cfn-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration-scaleoutcooldown
*/
scaleOutCooldown: number;
/**
* A name for the scaling policy.
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-applicationautoscaling-scalingpolicy.html#cfn-applicationautoscaling-scalingpolicy-policyname
* @default {TableName}[ReadCapacity|WriteCapacity]ScalingPolicy
*/
scalingPolicyName?: string;
}
/* tslint:enable:max-line-length */

/**
* Provides a DynamoDB table.
*/
Expand All @@ -40,18 +92,21 @@ export class Table extends Construct {
public readonly tableName: TableName;
public readonly tableStreamArn: TableStreamArn;

private readonly table: cloudformation.TableResource;
private readonly table: dynamodb.TableResource;

private readonly keySchema = new Array<dynamodb.TableResource.KeySchemaProperty>();
private readonly attributeDefinitions = new Array<dynamodb.TableResource.AttributeDefinitionProperty>();

private readonly keySchema = new Array<cloudformation.TableResource.KeySchemaProperty>();
private readonly attributeDefinitions = new Array<cloudformation.TableResource.AttributeDefinitionProperty>();
private readScalingPolicyResource?: applicationautoscaling.ScalingPolicyResource;
private writeScalingPolicyResource?: applicationautoscaling.ScalingPolicyResource;

constructor(parent: Construct, name: string, props: TableProps = {}) {
super(parent, name);

const readCapacityUnits = props.readCapacity || 5;
const writeCapacityUnits = props.writeCapacity || 5;

this.table = new cloudformation.TableResource(this, 'Resource', {
this.table = new dynamodb.TableResource(this, 'Resource', {
tableName: props.tableName,
keySchema: this.keySchema,
attributeDefinitions: this.attributeDefinitions,
Expand All @@ -64,6 +119,14 @@ export class Table extends Construct {
this.tableArn = this.table.tableArn;
this.tableName = this.table.ref;
this.tableStreamArn = this.table.tableStreamArn;

if (props.readAutoScaling) {
this.addReadAutoScaling(props.readAutoScaling);
}

if (props.writeAutoScaling) {
this.addWriteAutoScaling(props.writeAutoScaling);
}
}

public addPartitionKey(name: string, type: KeyAttributeType): this {
Expand All @@ -76,6 +139,14 @@ export class Table extends Construct {
return this;
}

public addReadAutoScaling(props: AutoScalingProps) {
this.readScalingPolicyResource = this.buildAutoScaling(this.readScalingPolicyResource, 'Read', props);
}

public addWriteAutoScaling(props: AutoScalingProps) {
this.writeScalingPolicyResource = this.buildAutoScaling(this.writeScalingPolicyResource, 'Write', props);
}

public validate(): string[] {
const errors = new Array<string>();
if (!this.findKey(HASH_KEY_TYPE)) {
Expand All @@ -84,6 +155,92 @@ export class Table extends Construct {
return errors;
}

private validateAutoScalingProps(props: AutoScalingProps) {
if (props.targetValue < 10 || props.targetValue > 90) {
throw new RangeError("scalingTargetValue for predefined metric type DynamoDBReadCapacityUtilization/"
+ "DynamoDBWriteCapacityUtilization must be between 10 and 90; Provided value is: " + props.targetValue);
}
if (props.scaleInCooldown < 0) {
throw new RangeError("scaleInCooldown must be greater than or equal to 0; Provided value is: " + props.scaleInCooldown);
}
if (props.scaleOutCooldown < 0) {
throw new RangeError("scaleOutCooldown must be greater than or equal to 0; Provided value is: " + props.scaleOutCooldown);
}
if (props.maxCapacity < 0) {
throw new RangeError("maximumCapacity must be greater than or equal to 0; Provided value is: " + props.maxCapacity);
}
if (props.minCapacity < 0) {
throw new RangeError("minimumCapacity must be greater than or equal to 0; Provided value is: " + props.minCapacity);
}
}

private buildAutoScaling(scalingPolicyResource: applicationautoscaling.ScalingPolicyResource | undefined,
scalingType: string,
props: AutoScalingProps) {
if (scalingPolicyResource) {
throw new Error(`${scalingType} Auto Scaling already defined for Table`);
}

this.validateAutoScalingProps(props);
const autoScalingRole = this.buildAutoScalingRole(`${scalingType}AutoScalingRole`);

const scalableTargetResource = new applicationautoscaling.ScalableTargetResource(
this, `${scalingType}CapacityScalableTarget`, this.buildScalableTargetResourceProps(
`dynamodb:table:${scalingType}CapacityUnits`, autoScalingRole, props));

return new applicationautoscaling.ScalingPolicyResource(
this, `${scalingType}CapacityScalingPolicy`,
this.buildScalingPolicyResourceProps(`DynamoDB${scalingType}CapacityUtilization`, `${scalingType}Capacity`,
scalableTargetResource, props));
}

private buildAutoScalingRole(roleResourceName: string) {
const autoScalingRole = new Role(this, roleResourceName, {
assumedBy: new ServicePrincipal('application-autoscaling.amazonaws.com')
});
autoScalingRole.addToPolicy(new PolicyStatement(PolicyStatementEffect.Allow)
.addActions("dynamodb:DescribeTable", "dynamodb:UpdateTable")
.addResource(this.tableArn));
autoScalingRole.addToPolicy(new PolicyStatement(PolicyStatementEffect.Allow)
.addActions("cloudwatch:PutMetricAlarm", "cloudwatch:DescribeAlarms", "cloudwatch:GetMetricStatistics",
"cloudwatch:SetAlarmState", "cloudwatch:DeleteAlarms")
.addAllResources());
return autoScalingRole;
}

private buildScalableTargetResourceProps(scalableDimension: string,
scalingRole: Role,
props: AutoScalingProps) {
return {
maxCapacity: props.maxCapacity,
minCapacity: props.minCapacity,
resourceId: `table/${this.tableName}`,
roleArn: scalingRole.roleArn,
scalableDimension,
serviceNamespace: 'dynamodb'
};
}

private buildScalingPolicyResourceProps(predefinedMetricType: string,
scalingParameter: string,
scalableTargetResource: applicationautoscaling.ScalableTargetResource,
props: AutoScalingProps) {
const scalingPolicyName = props.scalingPolicyName || `${this.tableName}${scalingParameter}ScalingPolicy`;
return {
policyName: scalingPolicyName,
policyType: 'TargetTrackingScaling',
scalingTargetId: scalableTargetResource.ref,
targetTrackingScalingPolicyConfiguration: {
predefinedMetricSpecification: {
predefinedMetricType
},
scaleInCooldown: props.scaleInCooldown,
scaleOutCooldown: props.scaleOutCooldown,
targetValue: props.targetValue
}
};
}

private findKey(keyType: string) {
return this.keySchema.find(prop => prop.keyType === keyType);
}
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-dynamodb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"pkglint": "^0.8.2"
},
"dependencies": {
"@aws-cdk/aws-applicationautoscaling": "^0.8.2",
"@aws-cdk/aws-iam": "^0.8.2",
"@aws-cdk/cdk": "^0.8.2"
},
Expand Down
Loading

0 comments on commit 85c4e64

Please sign in to comment.