From ea49a6540d9ad4171327bad6017aadd83255c4fc Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Fri, 14 Dec 2018 02:57:02 -0800 Subject: [PATCH 1/5] Add support for DynamodB PAY_PER_REQUEST billing mode --- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 73 +++- .../integ.dynamodb.ondemand.expected.json | 331 ++++++++++++++++++ .../test/integ.dynamodb.ondemand.ts | 121 +++++++ .../aws-dynamodb/test/test.dynamodb.ts | 155 ++++++++ .../lambda/src/requirements.txt | 2 +- 5 files changed, 679 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ondemand.expected.json create mode 100644 packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ondemand.ts diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 7fae70dfa3dbd..851b19716f5aa 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -41,16 +41,28 @@ export interface TableProps { /** * The read capacity for the table. Careful if you add Global Secondary Indexes, as * those will share the table's provisioned throughput. + * + * Can only be provided if billingMode is Provisioned. + * * @default 5 */ readCapacity?: number; /** * The write capacity for the table. Careful if you add Global Secondary Indexes, as * those will share the table's provisioned throughput. + * + * Can only be provided if billingMode is Provisioned. + * * @default 5 */ writeCapacity?: number; + /** + * Specify how you are charged for read and write throughput and how you manage capacity. + * @default Provisioned + */ + billingMode?: BillingMode; + /** * Enforces a particular physical table name. * @default @@ -134,12 +146,18 @@ export interface GlobalSecondaryIndexProps extends SecondaryIndexProps { /** * The read capacity for the global secondary index. + * + * Can only be provided if table billingMode is Provisioned or undefined. + * * @default 5 */ readCapacity?: number; /** * The write capacity for the global secondary index. + * + * Can only be provided if table billingMode is Provisioned or undefined. + * * @default 5 */ writeCapacity?: number; @@ -173,6 +191,7 @@ export class Table extends Construct { private tablePartitionKey?: Attribute; private tableSortKey?: Attribute; + private readonly billingMode: BillingMode; private readonly tableScaling: ScalableAttributePair = {}; private readonly indexScaling = new Map(); private readonly scalingRole: iam.IRole; @@ -180,6 +199,9 @@ export class Table extends Construct { constructor(parent: Construct, name: string, props: TableProps = {}) { super(parent, name); + this.billingMode = props.billingMode || BillingMode.Provisioned; + this.validateProvisioning(props); + this.table = new CfnTable(this, 'Resource', { tableName: props.tableName, keySchema: this.keySchema, @@ -187,7 +209,11 @@ export class Table extends Construct { globalSecondaryIndexes: new Token(() => this.globalSecondaryIndexes.length > 0 ? this.globalSecondaryIndexes : undefined), localSecondaryIndexes: new Token(() => this.localSecondaryIndexes.length > 0 ? this.localSecondaryIndexes : undefined), pointInTimeRecoverySpecification: props.pitrEnabled ? { pointInTimeRecoveryEnabled: props.pitrEnabled } : undefined, - provisionedThroughput: { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5 }, + billingMode: this.billingMode === BillingMode.PayPerRequest ? this.billingMode : undefined, + provisionedThroughput: props.billingMode === BillingMode.PayPerRequest ? undefined : { + readCapacityUnits: props.readCapacity || 5, + writeCapacityUnits: props.writeCapacity || 5 + }, sseSpecification: props.sseEnabled ? { sseEnabled: props.sseEnabled } : undefined, streamSpecification: props.streamSpecification ? { streamViewType: props.streamSpecification } : undefined, tags: new TagManager(this, { initialTags: props.tags }), @@ -246,6 +272,7 @@ export class Table extends Construct { throw new RangeError('a maximum number of global secondary index per table is 5'); } + this.validateProvisioning(props); this.validateIndexName(props.indexName); // build key schema and projection for index @@ -257,7 +284,10 @@ export class Table extends Construct { indexName: props.indexName, keySchema: gsiKeySchema, projection: gsiProjection, - provisionedThroughput: { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5 } + provisionedThroughput: this.billingMode === BillingMode.PayPerRequest ? undefined : { + readCapacityUnits: props.readCapacity || 5, + writeCapacityUnits: props.writeCapacity || 5 + } }); this.indexScaling.set(props.indexName, {}); @@ -301,6 +331,9 @@ export class Table extends Construct { if (this.tableScaling.scalableReadAttribute) { throw new Error('Read AutoScaling already enabled for this table'); } + if (this.billingMode === BillingMode.PayPerRequest) { + throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode'); + } return this.tableScaling.scalableReadAttribute = new ScalableTableAttribute(this, 'ReadScaling', { serviceNamespace: appscaling.ServiceNamespace.DynamoDb, @@ -320,6 +353,9 @@ export class Table extends Construct { if (this.tableScaling.scalableWriteAttribute) { throw new Error('Write AutoScaling already enabled for this table'); } + if (this.billingMode === BillingMode.PayPerRequest) { + throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode'); + } return this.tableScaling.scalableWriteAttribute = new ScalableTableAttribute(this, 'WriteScaling', { serviceNamespace: appscaling.ServiceNamespace.DynamoDb, @@ -336,6 +372,9 @@ export class Table extends Construct { * @returns An object to configure additional AutoScaling settings for this attribute */ public autoScaleGlobalSecondaryIndexReadCapacity(indexName: string, props: EnableScalingProps): IScalableTableAttribute { + if (this.billingMode === BillingMode.PayPerRequest) { + throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode'); + } const attributePair = this.indexScaling.get(indexName); if (!attributePair) { throw new Error(`No global secondary index with name ${indexName}`); @@ -359,6 +398,9 @@ export class Table extends Construct { * @returns An object to configure additional AutoScaling settings for this attribute */ public autoScaleGlobalSecondaryIndexWriteCapacity(indexName: string, props: EnableScalingProps): IScalableTableAttribute { + if (this.billingMode === BillingMode.PayPerRequest) { + throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode'); + } const attributePair = this.indexScaling.get(indexName); if (!attributePair) { throw new Error(`No global secondary index with name ${indexName}`); @@ -445,6 +487,19 @@ export class Table extends Construct { return errors; } + /** + * Validate read and write capacity are not specified for on-demand tables (billing mode PAY_PER_REQUEST). + * + * @param props read and write capacity properties + */ + private validateProvisioning(props: { readCapacity?: number, writeCapacity?: number}): void { + if (this.billingMode === BillingMode.PayPerRequest) { + if (props.readCapacity !== undefined || props.writeCapacity !== undefined) { + throw new Error('you cannot provision read and write capacity for a table with PAY_PER_REQUEST billing mode'); + } + } + } + /** * Validate index name to check if a duplicate name already exists. * @@ -575,6 +630,20 @@ export enum AttributeType { String = 'S', } +/** + * DyanmoDB's two Read/Write capacity mode. + */ +export enum BillingMode { + /** + * Pay only for what you use. You don't have to specify Read/Write capacity units. + */ + PayPerRequest = 'PAY_PER_REQUEST', + /** + * Explicitly specified Read/Write capacity units. + */ + Provisioned = 'PROVISIONED', +} + export enum ProjectionType { KeysOnly = 'KEYS_ONLY', Include = 'INCLUDE', diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ondemand.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ondemand.expected.json new file mode 100644 index 0000000000000..5e75e73fe82be --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ondemand.expected.json @@ -0,0 +1,331 @@ +{ + "Resources": { + "TableCD117FA1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + } + ] + } + }, + "TableWithGlobalAndLocalSecondaryIndexBC540710": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "sortKey", + "KeyType": "RANGE" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + }, + { + "AttributeName": "sortKey", + "AttributeType": "N" + }, + { + "AttributeName": "gsiHashKey", + "AttributeType": "S" + }, + { + "AttributeName": "gsiSortKey", + "AttributeType": "N" + }, + { + "AttributeName": "lsiSortKey", + "AttributeType": "N" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "GSI-PartitionKeyOnly", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "GSI-PartitionAndSortKeyWithReadAndWriteCapacity", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "gsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "GSI-ProjectionTypeKeysOnly", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "gsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "KEYS_ONLY" + } + }, + { + "IndexName": "GSI-ProjectionTypeInclude", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "gsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "NonKeyAttributes": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J" + ], + "ProjectionType": "INCLUDE" + } + }, + { + "IndexName": "GSI-InverseTableKeySchema", + "KeySchema": [ + { + "AttributeName": "sortKey", + "KeyType": "HASH" + }, + { + "AttributeName": "hashKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "LocalSecondaryIndexes": [ + { + "IndexName": "LSI-PartitionAndTableSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "LSI-PartitionAndSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "sortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "LSI-ProjectionTypeKeysOnly", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "KEYS_ONLY" + } + }, + { + "IndexName": "LSI-ProjectionTypeInclude", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "NonKeyAttributes": [ + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T" + ], + "ProjectionType": "INCLUDE" + } + } + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "SSEEnabled": true + }, + "StreamSpecification": { + "StreamViewType": "KEYS_ONLY" + }, + "Tags": [ + { + "Key": "Environment", + "Value": "Production" + } + ], + "TimeToLiveSpecification": { + "AttributeName": "timeToLive", + "Enabled": true + } + } + }, + "TableWithGlobalSecondaryIndexCC8E841E": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + }, + { + "AttributeName": "gsiHashKey", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "GlobalSecondaryIndexes": [ + { + "IndexName": "GSI-PartitionKeyOnly", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ] + } + }, + "TableWithLocalSecondaryIndex4DA3D08F": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "sortKey", + "KeyType": "RANGE" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + }, + { + "AttributeName": "sortKey", + "AttributeType": "N" + }, + { + "AttributeName": "lsiSortKey", + "AttributeType": "N" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "LocalSecondaryIndexes": [ + { + "IndexName": "LSI-PartitionAndSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ondemand.ts b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ondemand.ts new file mode 100644 index 0000000000000..e805abc7dd19e --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ondemand.ts @@ -0,0 +1,121 @@ +import { App, Stack } from '@aws-cdk/cdk'; +import { Attribute, AttributeType, BillingMode, ProjectionType, StreamViewType, Table } from '../lib'; + +// CDK parameters +const STACK_NAME = 'aws-cdk-dynamodb'; + +// DynamoDB table parameters +const TABLE = 'Table'; +const TABLE_WITH_GLOBAL_AND_LOCAL_SECONDARY_INDEX = 'TableWithGlobalAndLocalSecondaryIndex'; +const TABLE_WITH_GLOBAL_SECONDARY_INDEX = 'TableWithGlobalSecondaryIndex'; +const TABLE_WITH_LOCAL_SECONDARY_INDEX = 'TableWithLocalSecondaryIndex'; +const TABLE_PARTITION_KEY: Attribute = { name: 'hashKey', type: AttributeType.String }; +const TABLE_SORT_KEY: Attribute = { name: 'sortKey', type: AttributeType.Number }; + +// DynamoDB global secondary index parameters +const GSI_TEST_CASE_1 = 'GSI-PartitionKeyOnly'; +const GSI_TEST_CASE_2 = 'GSI-PartitionAndSortKeyWithReadAndWriteCapacity'; +const GSI_TEST_CASE_3 = 'GSI-ProjectionTypeKeysOnly'; +const GSI_TEST_CASE_4 = 'GSI-ProjectionTypeInclude'; +const GSI_TEST_CASE_5 = 'GSI-InverseTableKeySchema'; +const GSI_PARTITION_KEY: Attribute = { name: 'gsiHashKey', type: AttributeType.String }; +const GSI_SORT_KEY: Attribute = { name: 'gsiSortKey', type: AttributeType.Number }; +const GSI_NON_KEY: string[] = []; +for (let i = 0; i < 10; i++) { // 'A' to 'J' + GSI_NON_KEY.push(String.fromCharCode(65 + i)); +} + +// DynamoDB local secondary index parameters +const LSI_TEST_CASE_1 = 'LSI-PartitionAndSortKey'; +const LSI_TEST_CASE_2 = 'LSI-PartitionAndTableSortKey'; +const LSI_TEST_CASE_3 = 'LSI-ProjectionTypeKeysOnly'; +const LSI_TEST_CASE_4 = 'LSI-ProjectionTypeInclude'; +const LSI_SORT_KEY: Attribute = { name: 'lsiSortKey', type: AttributeType.Number }; +const LSI_NON_KEY: string[] = []; +for (let i = 0; i < 10; i++) { // 'K' to 'T' + LSI_NON_KEY.push(String.fromCharCode(75 + i)); +} + +const app = new App(); + +const stack = new Stack(app, STACK_NAME); + +// Provisioned tables +const table = new Table(stack, TABLE, {billingMode: BillingMode.PayPerRequest}); +table.addPartitionKey(TABLE_PARTITION_KEY); + +const tableWithGlobalAndLocalSecondaryIndex = new Table(stack, TABLE_WITH_GLOBAL_AND_LOCAL_SECONDARY_INDEX, { + pitrEnabled: true, + sseEnabled: true, + streamSpecification: StreamViewType.KeysOnly, + tags: { Environment: 'Production' }, + billingMode: BillingMode.PayPerRequest, + ttlAttributeName: 'timeToLive' +}); + +tableWithGlobalAndLocalSecondaryIndex.addPartitionKey(TABLE_PARTITION_KEY); +tableWithGlobalAndLocalSecondaryIndex.addSortKey(TABLE_SORT_KEY); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_1, + partitionKey: GSI_PARTITION_KEY, +}); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_2, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, +}); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_3, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, + projectionType: ProjectionType.KeysOnly, +}); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_4, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, + projectionType: ProjectionType.Include, + nonKeyAttributes: GSI_NON_KEY +}); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_5, + partitionKey: TABLE_SORT_KEY, + sortKey: TABLE_PARTITION_KEY, +}); + +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_2, + sortKey: LSI_SORT_KEY +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_1, + sortKey: TABLE_SORT_KEY +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_3, + sortKey: LSI_SORT_KEY, + projectionType: ProjectionType.KeysOnly +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_4, + sortKey: LSI_SORT_KEY, + projectionType: ProjectionType.Include, + nonKeyAttributes: LSI_NON_KEY +}); + +const tableWithGlobalSecondaryIndex = new Table(stack, TABLE_WITH_GLOBAL_SECONDARY_INDEX, {billingMode: BillingMode.PayPerRequest}); +tableWithGlobalSecondaryIndex.addPartitionKey(TABLE_PARTITION_KEY); +tableWithGlobalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_1, + partitionKey: GSI_PARTITION_KEY +}); + +const tableWithLocalSecondaryIndex = new Table(stack, TABLE_WITH_LOCAL_SECONDARY_INDEX, {billingMode: BillingMode.PayPerRequest}); +tableWithLocalSecondaryIndex.addPartitionKey(TABLE_PARTITION_KEY); +tableWithLocalSecondaryIndex.addSortKey(TABLE_SORT_KEY); +tableWithLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_1, + sortKey: LSI_SORT_KEY +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index e3cbc780c4d34..b5e5ab4c32e4b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -5,6 +5,7 @@ import { Test } from 'nodeunit'; import { Attribute, AttributeType, + BillingMode, GlobalSecondaryIndexProps, LocalSecondaryIndexProps, ProjectionType, @@ -388,6 +389,7 @@ export = { writeCapacity: 1337, pitrEnabled: true, sseEnabled: true, + billingMode: BillingMode.Provisioned, streamSpecification: StreamViewType.KeysOnly, tags: { Environment: 'Production' }, ttlAttributeName: 'timeToLive' @@ -427,6 +429,60 @@ export = { test.done(); }, + 'when specifying PAY_PER_REQUEST billing mode'(test: Test) { + const app = new TestApp(); + new Table(app.stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + billingMode: BillingMode.PayPerRequest, + partitionKey: TABLE_PARTITION_KEY + }); + const template = app.synthesizeTemplate(); + + test.deepEqual(template, { + Resources: { + MyTable794EDED1: { + Type: 'AWS::DynamoDB::Table', + Properties: { + KeySchema: [ + { AttributeName: 'hashKey', KeyType: 'HASH' }, + ], + BillingMode: 'PAY_PER_REQUEST', + AttributeDefinitions: [ + { AttributeName: 'hashKey', AttributeType: 'S' }, + ], + TableName: 'MyTable', + } + } + } + }); + + test.done(); + }, + + 'error when specifying read or write capacity with a PAY_PER_REQUEST billing mode'(test: Test) { + const app = new TestApp(); + test.throws(() => new Table(app.stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + billingMode: BillingMode.PayPerRequest, + partitionKey: TABLE_PARTITION_KEY, + readCapacity: 1 + })); + test.throws(() => new Table(app.stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + billingMode: BillingMode.PayPerRequest, + partitionKey: TABLE_PARTITION_KEY, + writeCapacity: 1 + })); + test.throws(() => new Table(app.stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + billingMode: BillingMode.PayPerRequest, + partitionKey: TABLE_PARTITION_KEY, + readCapacity: 1, + writeCapacity: 1 + })); + test.done(); + }, + 'when adding a global secondary index with hash key only'(test: Test) { const app = new TestApp(); new Table(app.stack, CONSTRUCT_NAME) @@ -623,6 +679,50 @@ export = { test.done(); }, + 'when adding a global secondary index on a table with PAY_PER_REQUEST billing mode'(test: Test) { + const app = new TestApp(); + new Table(app.stack, CONSTRUCT_NAME, { + billingMode: BillingMode.PayPerRequest, + partitionKey: TABLE_PARTITION_KEY, + sortKey: TABLE_SORT_KEY + }).addGlobalSecondaryIndex({ + indexName: GSI_NAME, + partitionKey: GSI_PARTITION_KEY, + }); + const template = app.synthesizeTemplate(); + + test.deepEqual(template, { + Resources: { + MyTable794EDED1: { + Type: 'AWS::DynamoDB::Table', + Properties: { + AttributeDefinitions: [ + { AttributeName: 'hashKey', AttributeType: 'S' }, + { AttributeName: 'sortKey', AttributeType: 'N' }, + { AttributeName: 'gsiHashKey', AttributeType: 'S' }, + ], + BillingMode: 'PAY_PER_REQUEST', + KeySchema: [ + { AttributeName: 'hashKey', KeyType: 'HASH' }, + { AttributeName: 'sortKey', KeyType: 'RANGE' } + ], + GlobalSecondaryIndexes: [ + { + IndexName: 'MyGSI', + KeySchema: [ + { AttributeName: 'gsiHashKey', KeyType: 'HASH' }, + ], + Projection: { ProjectionType: 'ALL' } + } + ] + } + } + } + }); + + test.done(); + }, + 'error when adding a global secondary index with projection type INCLUDE, but without specifying non-key attributes'(test: Test) { const app = new TestApp(); const table = new Table(app.stack, CONSTRUCT_NAME) @@ -712,6 +812,36 @@ export = { test.done(); }, + 'error when adding a global secondary index with read or write capacity on a PAY_PER_REQUEST table'(test: Test) { + const app = new TestApp(); + const table = new Table(app.stack, CONSTRUCT_NAME, { + partitionKey: TABLE_PARTITION_KEY, + billingMode: BillingMode.PayPerRequest + }); + + test.throws(() => table.addGlobalSecondaryIndex({ + indexName: GSI_NAME, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, + readCapacity: 1 + })); + test.throws(() => table.addGlobalSecondaryIndex({ + indexName: GSI_NAME, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, + writeCapacity: 1 + })); + test.throws(() => table.addGlobalSecondaryIndex({ + indexName: GSI_NAME, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, + readCapacity: 1, + writeCapacity: 1 + })); + + test.done(); + }, + 'when adding multiple global secondary indexes'(test: Test) { const app = new TestApp(); const table = new Table(app.stack, CONSTRUCT_NAME) @@ -1122,6 +1252,31 @@ export = { test.done(); }, + 'error when enabling AutoScaling on the PAY_PER_REQUEST table'(test: Test) { + // GIVEN + const app = new TestApp(); + const table = new Table(app.stack, CONSTRUCT_NAME, { billingMode: BillingMode.PayPerRequest }); + table.addPartitionKey(TABLE_PARTITION_KEY); + table.addGlobalSecondaryIndex({ + indexName: GSI_NAME, + partitionKey: GSI_PARTITION_KEY + }); + + // WHEN + test.throws(() => { + table.autoScaleReadCapacity({ minCapacity: 50, maxCapacity: 500 }); + }); + test.throws(() => { + table.autoScaleWriteCapacity({ minCapacity: 50, maxCapacity: 500 }); + }); + test.throws(() => table.autoScaleGlobalSecondaryIndexReadCapacity(GSI_NAME, { + minCapacity: 1, + maxCapacity: 5 + })); + + test.done(); + }, + 'error when specifying Read Auto Scaling with invalid scalingTargetValue < 10'(test: Test) { // GIVEN const app = new TestApp(); diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt b/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt index 4fca701665e3c..5bf81bdc138ea 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt @@ -1,2 +1,2 @@ -awscli==1.16.34 +awscli==1.16.71 From 3d5df7aa900c2e563a2ef37620ff6a8fe5cc0473 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Fri, 14 Dec 2018 03:08:51 -0800 Subject: [PATCH 2/5] Add documentation for dynamodb's billing mode --- packages/@aws-cdk/aws-dynamodb/README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index b5174f9501690..8eef2df0544d9 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -15,6 +15,24 @@ const table = new dynamodb.Table(stack, 'Table', { You can either specify `partitionKey` and/or `sortKey` when you initialize the table, or call `addPartitionKey` and `addSortKey` after initialization. +### Billing Mode + +DynamoDB supports two billing modes: +* PROVISIONED - the default mode where the table and global secondary indexes have configured read and write capacity. +* PAY_PER_REQUEST - on-demand pricing and scaling. You only pay for what you use and there is no read and write capacity for the table or its gloal secondary indexes. + +```ts +import dynamodb = require('@aws-cdk/aws-dynamodb'); + +const table = new dynamodb.Table(stack, 'Table', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.String }, + billingMode: dynamodb.BillingMode.PayPerRequest +}); +``` + +Further reading: +https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode. + ### Configure AutoScaling for your table You can have DynamoDB automatically raise and lower the read and write capacities @@ -24,6 +42,9 @@ times of the day: [Example of configuring autoscaling](test/integ.autoscaling.lit.ts) +Auto-scaling only works with the billing mode, PROVISIONED. + 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/ \ No newline at end of file +https://aws.amazon. +htmlcom/blogs/database/how-to-use-aws-cloudformation-to-configure-auto-scaling-for-amazon-dynamodb-tables-and-indexes/ From e73d9836d01005a4a1de4badbb51503acc15e43d Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Fri, 14 Dec 2018 03:14:27 -0800 Subject: [PATCH 3/5] Minor fixes to documentation --- packages/@aws-cdk/aws-dynamodb/README.md | 4 ++-- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index 8eef2df0544d9..2e504be94532b 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -40,9 +40,9 @@ of your table by setting up autoscaling. You can use this to either keep your tables at a desired utilization level, or by scaling up and down at preconfigured times of the day: -[Example of configuring autoscaling](test/integ.autoscaling.lit.ts) +Auto-scaling is only relevant for tables with the billing mode, PROVISIONED. -Auto-scaling only works with the billing mode, PROVISIONED. +[Example of configuring autoscaling](test/integ.autoscaling.lit.ts) Further reading: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.html diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 851b19716f5aa..8f282e8f9c958 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -631,11 +631,11 @@ export enum AttributeType { } /** - * DyanmoDB's two Read/Write capacity mode. + * DyanmoDB's Read/Write capacity modes. */ export enum BillingMode { /** - * Pay only for what you use. You don't have to specify Read/Write capacity units. + * Pay only for what you use. You don't configure Read/Write capacity units. */ PayPerRequest = 'PAY_PER_REQUEST', /** From a895ab811cb1c96035157acd84ad6abb08577654 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Fri, 14 Dec 2018 03:15:44 -0800 Subject: [PATCH 4/5] Revert accidental change to aws-s3-deployment --- packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt b/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt index 5bf81bdc138ea..4fca701665e3c 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt @@ -1,2 +1,2 @@ -awscli==1.16.71 +awscli==1.16.34 From 60421497721d693e5aa08b784a1ee87800943ba4 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Fri, 14 Dec 2018 03:16:59 -0800 Subject: [PATCH 5/5] Fix link in README - oops --- packages/@aws-cdk/aws-dynamodb/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index 2e504be94532b..a7af34f0f85ae 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -46,5 +46,4 @@ Auto-scaling is only relevant for tables with the billing mode, PROVISIONED. Further reading: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.html -https://aws.amazon. -htmlcom/blogs/database/how-to-use-aws-cloudformation-to-configure-auto-scaling-for-amazon-dynamodb-tables-and-indexes/ +https://aws.amazon.com/blogs/database/how-to-use-aws-cloudformation-to-configure-auto-scaling-for-amazon-dynamodb-tables-and-indexes/