Skip to content

Commit

Permalink
Enhance configuration API (#8)
Browse files Browse the repository at this point in the history
* Support string or array for `table` property

* Refactoring

* Prepare basic index handling

* Draft for index support

* Add DependsOn for resource to wait for previous resources

* Fix lint errors

* Rewrite table/index handling

* Update README.md

* Update README.md

* Fix lint errors

* Update README.md

* Update README.md

* Update README.md and package description

* Update README.md

* Expand names.js function

* Add stage variable to names.js

* Pass stage to resource objects

* Use stage in names.js

* Remove arrow functions from names.js

* Reformat JSON for resources

* Fixed typos

* Minor tweaks

* Clean up process()
  • Loading branch information
sbstjn committed Jul 25, 2017
1 parent fa6b13f commit 2c21710
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 126 deletions.
58 changes: 42 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# ⚡️ Serverless Plugin for DynamoDB Auto Scaling

[![npm](https://img.shields.io/npm/v/serverless-dynamodb-autoscaling.svg)](https://www.npmjs.com/package/serverless-dynamodb-autoscaling)
[![CircleCI](https://img.shields.io/circleci/project/github/sbstjn/serverless-dynamodb-autoscaling.svg)](https://circleci.com/gh/sbstjn/serverless-dynamodb-autoscaling)
[![CircleCI](https://img.shields.io/circleci/project/github/sbstjn/serverless-dynamodb-autoscaling/master.svg)](https://circleci.com/gh/sbstjn/serverless-dynamodb-autoscaling)
[![license](https://img.shields.io/github/license/sbstjn/serverless-dynamodb-autoscaling.svg)](https://github.com/sbstjn/serverless-dynamodb-autoscaling/blob/master/LICENSE.md)
[![Coveralls](https://img.shields.io/coveralls/sbstjn/serverless-dynamodb-autoscaling.svg)](https://coveralls.io/github/sbstjn/serverless-dynamodb-autoscaling)

With this plugin for [serverless](https://serverless.com) you can set DynamoDB Auto Scaling configuratin in your `serverless.yml` file. The plugin supports multiple tables and separate configuration sets for `read` and `write` capacities using AWS [native DynamoDB Auto Scaling](https://aws.amazon.com/blogs/aws/new-auto-scaling-for-amazon-dynamodb/).
With this plugin for [serverless](https://serverless.com), you can enable DynamoDB Auto Scaling for tables and **Global Secondary Indexes** easily in your `serverless.yml` configuration file. The plugin supports multiple tables and indexes, as well as separate configuration for `read` and `write` capacities using Amazon's [native DynamoDB Auto Scaling](https://aws.amazon.com/blogs/aws/new-auto-scaling-for-amazon-dynamodb/).

## Usage

Expand All @@ -16,24 +16,26 @@ Add the [NPM package](https://www.npmjs.com/package/serverless-dynamodb-autoscal
$ yarn add serverless-dynamodb-autoscaling

# Via npm
$ npm install serverless-dynamodb-autoscaling --save-dev
$ npm install serverless-dynamodb-autoscaling
```

## Configuration

Add the plugin to your `serverless.yml`:

```yaml
plugins:
- serverless-dynamodb-autoscaling
```
Configure DynamoDB Auto Scaling in `serverless.yml` with references to your DynamoDB CloudFormation resources for the `table` property:
## Configuration
Configure DynamoDB Auto Scaling in `serverless.yml` with references to your DynamoDB CloudFormation resources for the `table` property. The `index` configuration is optional to apply Auto Scaling *Global Secondary Index*.

```yaml
custom:
capacities:
- table: CustomTable # DynamoDB Resource
index: # List or single index name
- custom-index-name
read:
minimum: 5 # Minimum read capacity
maximum: 1000 # Maximum read capacity
Expand All @@ -42,22 +44,36 @@ custom:
minimum: 40 # Minimum write capacity
maximum: 200 # Maximum write capacity
usage: 0.5 # Targeted usage percentage
- table: AnotherTable
read:
minimum: 5
maximum: 1000
# usage: 0.75 is the default
```

That's it! With the next deployment (`sls deploy`) serverless will add a CloudFormation configuration to enable Auto Scaling for the DynamoDB resources `CustomTable` and `AnotherTable`.
That's it! With the next deployment, [serverless](https://serverless.com) will add a CloudFormation configuration to enable Auto Scaling for the DynamoDB resources `CustomTable` and its *Global Secondary Index* called `custom-index-name`.

You must provide at least a configuration for `read` or `write` to enable Auto Scaling!

You must of course provide at least a configuration for `read` or `write` to enable Auto Scaling. The value for `usage` has a default of 75 percent.
### Defaults

**Notice:** *With the release of `v0.2.x` the plugin introduced a breaking change. Starting with `v0.2.0` you need to provide the CloudFormation reference for the `table` property. In `v0.1.x` the plugin used a `name` property with the DynamoDB table name.*
```yaml
maximum: 200
minimum: 5
usage: 0.75
```

### Index

If you only want to enable Auto Scaling for the index, use `indexOnly: true` to skip Auto Scaling for the general DynamoDB table.

### API Throtteling

CloudWatch has very strict [API rate limits](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_limits.html)! If you plan to configure Auto Scaling for multiple DynamoDB tables or *Global Secondary Indexes*, request an increase of the rate limits first! Otherwise, you might run into an error like this:

```
An error occurred while provisioning your stack: XYZ - Unable to create alarms for scaling policy XYZ due to reason:
Rate exceeded (Service: AmazonCloudWatch; Status Code: 400; Error Code: Throttling; Request ID: XYZ).
```

## DynamoDB

The configuration above works fine for a default DynamoDB table configuration.
The example serverless configuration above works fine for a DynamoDB table CloudFormation resource like this:

```yaml
resources:
Expand All @@ -75,6 +91,16 @@ resources:
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
GlobalSecondaryIndexes:
- IndexName: custom-index-name
KeySchema:
- AttributeName: key
KeyType: HASH
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
```

## License
Expand All @@ -90,4 +116,4 @@ Feel free to use the code, it's released using the [MIT license](LICENSE.md).

You are welcome to contribute to this project! 😘

To make sure you have a pleasant experience, please read the [code of conduct](CODE_OF_CONDUCT.md). It outlines core values and believes and will make working together a happier experience.
To make sure you have a pleasant experience, please read the [code of conduct](CODE_OF_CONDUCT.md). It outlines core values and beliefs and will make working together a happier experience.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "serverless-dynamodb-autoscaling",
"description": "Serverless Plugin for Amazon DynamoDB Auto Scaling configuration.",
"description": "Serverless Plugin for Amazon DynamoDB Auto Scaling.",
"version": "0.0.0",
"main": "src/plugin.js",
"scripts": {
Expand Down
82 changes: 74 additions & 8 deletions src/aws/names.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,78 @@
const util = require('util')

const clean = (input) => input.replace(/[^a-z0-9+]+/gi, '')
function clean (input) {
return input.replace(/[^a-z0-9+]+/gi, '')
}

const policyScale = (table, read) => clean(util.format('Table%sScalingPolicy-%s', read ? 'Read' : 'Write', table))
const policyRole = (table) => clean(util.format('DynamoDBAutoscalePolicy-%s', table))
const dimension = (read) => util.format('dynamodb:table:%sCapacityUnits', read ? 'Read' : 'Write')
const target = (table, read) => clean(util.format('AutoScalingTarget%s-%s', read ? 'Read' : 'Write', table))
const metric = (read) => clean(util.format('DynamoDB%sCapacityUtilization', read ? 'Read' : 'Write'))
const role = (table) => clean(util.format('DynamoDBAutoscaleRole-%s', table))
function policyScale (table, read, index, stage) {
return clean(
util.format(
'Table%sScalingPolicy-%s%s%s',
read ? 'Read' : 'Write',
table,
index || '',
stage || ''
)
)
}

module.exports = { dimension, metric, policyScale, policyRole, role, target, clean }
function policyRole (table, index, stage) {
return clean(
util.format(
'DynamoDBAutoscalePolicy-%s%s%s',
table,
index || '',
stage || ''
)
)
}

function dimension (read, index) {
return util.format(
'dynamodb:%s:%sCapacityUnits',
index ? 'index' : 'table',
read ? 'Read' : 'Write'
)
}

function target (table, read, index, stage) {
return clean(
util.format(
'AutoScalingTarget%s-%s%s%s',
read ? 'Read' : 'Write',
table,
index || '',
stage || ''
)
)
}

function metric (read) {
return clean(
util.format(
'DynamoDB%sCapacityUtilization',
read ? 'Read' : 'Write'
)
)
}

function role (table, index, stage) {
return clean(
util.format(
'DynamoDBAutoscaleRole-%s%s%s',
table,
index || '',
stage || ''
)
)
}

module.exports = {
clean,
dimension,
metric,
policyRole,
policyScale,
role,
target
}
22 changes: 14 additions & 8 deletions src/aws/policy.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
const names = require('./names')

class Policy {
constructor (table, value, read, scaleIn, scaleOut) {
constructor (table, value, read, scaleIn, scaleOut, index, stage) {
this.table = table
this.index = index
this.stage = stage
this.value = parseFloat(value, 10) * 100
this.read = !!read
this.scaleIn = parseInt(scaleIn, 10)
this.scaleOut = parseInt(scaleOut, 10)
this.dependencies = []
}

setDependencies (list) {
this.dependencies = list

return this
}

toJSON () {
return {
[names.policyScale(this.table, this.read)]: {
[names.policyScale(this.table, this.read, this.index, this.stage)]: {
'Type': 'AWS::ApplicationAutoScaling::ScalingPolicy',
'DependsOn': [
this.table,
names.target(this.table, this.read)
],
'DependsOn': [ this.table, names.target(this.table, this.read, this.index, this.stage) ].concat(this.dependencies),
'Properties': {
'PolicyName': names.policyScale(this.table, this.read),
'PolicyName': names.policyScale(this.table, this.read, this.index, this.stage),
'PolicyType': 'TargetTrackingScaling',
'ScalingTargetId': { 'Ref': names.target(this.table, this.read) },
'ScalingTargetId': { 'Ref': names.target(this.table, this.read, this.index, this.stage) },
'TargetTrackingScalingPolicyConfiguration': {
'PredefinedMetricSpecification': {
'PredefinedMetricType': names.metric(this.read)
Expand Down
21 changes: 14 additions & 7 deletions src/aws/role.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
const names = require('./names')

class Role {
constructor (table) {
constructor (table, index, stage) {
this.table = table
this.index = index
this.stage = stage
this.dependencies = []
}

setDependencies (list) {
this.dependencies = list

return this
}

toJSON () {
return {
[names.role(this.table)]: {
[names.role(this.table, this.index, this.stage)]: {
'Type': 'AWS::IAM::Role',
'DependsOn': [
this.table
],
'DependsOn': [ this.table ].concat(this.dependencies),
'Properties': {
'RoleName': names.role(this.table),
'RoleName': names.role(this.table, this.index, this.stage),
'AssumeRolePolicyDocument': {
'Version': '2012-10-17',
'Statement': [
Expand All @@ -28,7 +35,7 @@ class Role {
},
'Policies': [
{
'PolicyName': names.policyRole(this.table),
'PolicyName': names.policyRole(this.table, this.index, this.stage),
'PolicyDocument': {
'Version': '2012-10-17',
'Statement': [
Expand Down
30 changes: 21 additions & 9 deletions src/aws/target.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
const names = require('./names')

class Target {
constructor (table, min, max, read) {
constructor (table, min, max, read, index, stage) {
this.table = table
this.index = index
this.stage = stage
this.min = parseInt(min, 10)
this.max = parseInt(max, 10)
this.read = !!read
this.dependencies = []
}

setDependencies (list) {
this.dependencies = list

return this
}

toJSON () {
const resource = [ 'table/', { 'Ref': this.table } ]

if (this.index) {
resource.push('/index/', this.index)
}

return {
[names.target(this.table, this.read)]: {
[names.target(this.table, this.read, this.index, this.stage)]: {
'Type': 'AWS::ApplicationAutoScaling::ScalableTarget',
'DependsOn': [
this.table,
names.role(this.table)
],
'DependsOn': [ this.table, names.role(this.table, this.index, this.stage) ].concat(this.dependencies),
'Properties': {
'MaxCapacity': this.max,
'MinCapacity': this.min,
'ResourceId': { 'Fn::Join': [ '', [ 'table/', { 'Ref': this.table } ] ] },
'RoleARN': { 'Fn::GetAtt': [ names.role(this.table), 'Arn' ] },
'ScalableDimension': names.dimension(this.read),
'ResourceId': { 'Fn::Join': [ '', resource ] },
'RoleARN': { 'Fn::GetAtt': [ names.role(this.table, this.index, this.stage), 'Arn' ] },
'ScalableDimension': names.dimension(this.read, this.index),
'ServiceNamespace': 'dynamodb'
}
}
Expand Down
Loading

0 comments on commit 2c21710

Please sign in to comment.