A best practice for the deployment of AWS resources is to use a configuration system that treats your infrastructure as code. Infrastructure as code is a key enabler of DevOps practices, which bring developers and operations together to collaborate on automating application delivery at scale. By modeling your entire infrastructure as code in AWS CloudFormation templates, you gain the following benefits:

The establishment of a single source of truth for your cloud resources

Deployment automation for development and test environments

Disaster recovery preparedness

Integration with code management tools such as version control

Using AWS CloudFormation templates in combination with a serverless database such as Amazon DynamoDB can greatly simplify operations for high-velocity development teams that have embraced an agile DevOps process. This post includes a real-world example and a walkthrough of how you can configure DynamoDB auto scaling by using AWS CloudFormation.

Note: I created a screencast in which I walk you through the solution in this blog post. Check it out!

DynamoDB auto scaling

DynamoDB is a fully managed, fast, highly scalable, and flexible cloud database service for applications that need consistent, single-digit millisecond latency at any scale. In 2017, AWS added auto scaling capabilities to DynamoDB. Auto scaling makes it even easier for you to manage applications that have unpredictable performance needs. Specifically, auto scaling enables you to configure the upper and lower bounds and also the target utilization for read and write capacity on tables and global secondary indexes.

AWS CloudFormation

With AWS CloudFormation, developers and administrators can define their cloud infrastructure by using text files, either in JSON or YAML format. Instead of using the AWS Management Console to create and configure resources such as DynamoDB tables, Amazon S3 buckets, and Amazon Simple Notification Service (Amazon SNS) topics, developers and administrators treat these resources like code. You can check AWS CloudFormation templates into source control and manage them in the same way as other code files. By creating all resources programmatically using template files, you can achieve repeatability and consistency across environments and limit your use of the console to discovery and troubleshooting.

The use case

For the purposes of this post, you create a single table to store user profile details for a web application. Users of the application will come from all over the globe. Let’s assume that your marketing department will be conducting a series of campaigns to drive signups, but you don’t have a good idea of just how many users you will have. It could be thousands—it could be millions. You need to make sure that the database can handle arbitrary increases and decreases based on the variable number of concurrent users. Users are grouped according to their city, and they are given priority for support requests according to their signup date. As a result, you must be able to sort by city and signup date.

The data model

The table you create has a primary partition key named userId and a single global secondary index with a partition key named city and a sort key named signupDate . The index allows developers to use the Query API to efficiently retrieve a sorted list of users by signup date within a given city.

The AWS CloudFormation template

Start the template with parameters for the table name ( userTableName ) and index name ( userIndexName ). This allows the person who creates the stack (a collection of AWS resources that you can manage as a single unit) to alter the names, if they want to.

AWSTemplateFormatVersion: "2010-09-09" Parameters: userTableName: Type: String Default: "myapp.User" Description: "Name of the user table" userIndexName: Type: String Default: "user-city-index" Description: "Name of the user index"

Now, begin configuring the table by creating the AWS CloudFormation resource, AWS::DynamoDB::Table.

AWS::DynamoDB::Table. Resources: tableMyAppUser: Type: "AWS::DynamoDB::Table" Properties: TableName: Ref: userTableName AttributeDefinitions: - AttributeName: userId AttributeType: S - AttributeName: city AttributeType: S - AttributeName: signupDate AttributeType: S

In the preceding code, you define the attributes that are a part of the primary key or a part of indexes. But you don’t have to define any of the other fields that you might store as a part of the user profile. This is because DynamoDB is schemaless, which allows non-key attributes to be added at runtime without modifying the definition of the table.

KeySchema: - AttributeName: userId KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5

The provisioned throughput settings ( ReadCapacityUnits and WriteCapacityUnits ) are the initial values that are set on the table as a starting point for auto scaling.

Next, define the global secondary index, which lets you query the data by city to retrieve a list of users sorted by signup date.

GlobalSecondaryIndexes: - IndexName: Ref: userIndexName KeySchema: - AttributeName: city KeyType: HASH - AttributeName: signupDate KeyType: RANGE Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5

By setting the ProjectionType to ALL , you tell DynamoDB to store all attributes on the index itself so that a secondary lookup is not necessary when you issue a Query command that uses the index.

Now comes the auto scaling portion of the template. First, configure the scalable target to set the upper and lower bounds for writes on the user table.

UserTableWriteCapacityScalableTarget: Type: "AWS::ApplicationAutoScaling::ScalableTarget" Properties: MaxCapacity: 100 MinCapacity: 5 ResourceId: !Sub table/${userTableName} RoleARN: !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_DynamoDBTable ScalableDimension: "dynamodb:table:WriteCapacityUnits" ServiceNamespace: dynamodb DependsON: tableMyAppUser

As you can see, the ScalableTarget is configured to scale between a minimum of 5 write capacity units and a maximum of 100 write capacity units.

The role that is referenced above is a service-linked role that is automatically created when you specify the role ARN. Note that this is a recent change to the ScalableTarget resource type. There is no need to create a role (“ ScalingRole ”) manually, as was described in a prior version of this blog post.

Then create a scaling policy, which references the scaling target and sets the target consumption.

UserTableWriteScalingPolicy: Type: "AWS::ApplicationAutoScaling::ScalingPolicy" Properties: PolicyName: WriteAutoScalingPolicy PolicyType: TargetTrackingScaling ScalingTargetId: Ref: UserTableWriteCapacityScalableTarget TargetTrackingScalingPolicyConfiguration: TargetValue: 70 ScaleInCooldown: 60 ScaleOutCooldown: 60 PredefinedMetricSpecification: PredefinedMetricType: DynamoDBWriteCapacityUtilization

The TargetValue is a percentage that indicates when DynamoDB should scale in or out. This is set to 70, which means 70 percent of the current provisioned throughput. For example, suppose that the current write capacity unit setting is 10 and you consume more than 7 write capacity units per second for 60 seconds. A scale-out event occurs because the consumption has exceeded 70 percent of the current setting. The newly provisioned capacity is set to a number that places the current consumption to 70 percent of the provisioned capacity. If you are now consuming 14 write capacity units, auto scaling sets the provisioned write capacity to 20, because 14 is 70 percent of 20.

For optimal scaling, you can tune the ScaleInCooldown and ScaleOutCooldown values to prevent DynamoDB from being too aggressive about changes that occur over short periods of time. Set the ScaleOutCooldown value as low as possible, but high enough that the scaling metric will have felt the impact of one scale-out event before another one begins.

Now you need to create a scalable target and scaling policy for the global secondary index. These parts of the AWS CloudFormation template look almost identical to the table configuration, so I don’t list them in their entirety here. Instead, I point out a few important differences.

UserIndexWriteCapacityScalableTarget: Type: "AWS::ApplicationAutoScaling::ScalableTarget" Properties: MaxCapacity: 100 MinCapacity: 5 ResourceId: !Sub table/${userTableName}/index/${userIndexName} RoleARN: !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_DynamoDBTable ScalableDimension: "dynamodb:index:WriteCapacityUnits" ServiceNamespace: dynamodb DependsON: tableMyAppUser

Note the ResourceId , which is the same as the ResourceId for the table, with /index/${userIndexName} appended, and the ScalableDimension , which is set to the write capacity units for the index.

Now that you have configured auto scaling for writes on the table and the index, repeat those configuration steps for reads. See the complete AWS CloudFormation template (JSON, YAML) for the read capacity details.

Create the stack

Now that you have a complete template, you can create the stack and verify that you get the expected resources.

Choose Launch Stack, which opens the AWS CloudFormation console.

On the Select Template page, choose Next. On the Specify Details page, you can specify different names for the stack, index, and table. Then choose Next. The Options page has a few optional configuration details that you can skip for the purposes of this post. Choose Next. Finally, you have to acknowledge that the template might create IAM resources in your account. Select the check box, and then choose Create.

Now that you have a complete template, you can create the stack and verify that you get the expected resources. The stack should take about two minutes to create.

After AWS CloudFormation creates the stack, navigate to the DynamoDB console, choose the newly created table, and then choose the Capacity tab to see the details about the auto scaling configuration, as shown in the following screenshot.

On the Indexes tab, you can see that auto scaling is enabled for both reads and writes (READ_AND_WRITE is displayed in the Auto Scaling column).

Auto scaling also created CloudWatch alarms related to consumed and provisioned throughput. You should see four alarms for each scaling policy, for a total of sixteen. To produce the following screenshot, I ran a script to add data to the table, and then I submitted a series of Scan and Query operations to provide enough usage for the alarms to display the OK state. If you navigate directly to CloudWatch after creating the stack but before adding items to the table, some of the alarms will be in the INSUFFICIENT_DATA state.

Summary

Congratulations, you have created your first DynamoDB table with auto scaling enabled by using an AWS CloudFormation template! You should be able to adapt this template for use with your own tables.

Remember to resist the temptation to make changes to your new tables using the console. Instead, modify your template and use AWS CloudFormation to create a change set. With change sets, you can view the modifications that will be made to a stack before executing the changes. AWS CloudFormation currently cannot detect drift in the configuration between the template and the actual resources that are defined by the template. So if you make manual changes and then later try to modify the template, the change set might fail.

If you have comments about this post, submit them in the “Comments” section below. If you have questions about the solution in this post, start a new thread on the DynamoDB forum.

About the Author

Eric Z. Beard is a partner solutions architect at Amazon Web Services. He works with the AWS customers to provide guidance and technical assistance on database projects, helping them improve the value of their solutions when using AWS.