AWS DynamoDB Tables with Terraform: Keys, Indexes, TTL and Autoscaling
A DynamoDB table in Terraform #
Amazon DynamoDB is a managed NoSQL key-value and document database. You choose the key schema first, because it defines how you can query the data, then the capacity mode.
A table with partition and sort key #
resource "aws_dynamodb_table" "orders" {
name = "ditwl-orders"
billing_mode = "PAY_PER_REQUEST"
hash_key = "customer_id"
range_key = "order_id"
attribute {
name = "customer_id"
type = "S"
}
attribute {
name = "order_id"
type = "S"
}
point_in_time_recovery {
enabled = true
}
server_side_encryption {
enabled = true
kms_key_arn = aws_kms_key.dynamodb.arn
}
deletion_protection_enabled = true
tags = local.common_tags
}
resource "aws_kms_key" "dynamodb" {
description = "DynamoDB table encryption"
enable_key_rotation = true
}hash_keyis the partition key: items with the same value are stored together, and every query must specify it.range_keyis the optional sort key, which orders the items inside a partition and allows range queries.- You declare as
attributeonly the attributes that are part of a key or an index. DynamoDB does not need a schema for the rest. - Types are
S(string),N(number) andB(binary). - Point-in-time recovery (PITR) allows restoring to any second of the last 35 days. Enable it for data you care about.
- Omit
kms_key_arnto use the AWS-owned key at no cost. Use your KMS key for control and audit.
Capacity modes #
| Mode | Use | Terraform |
|---|---|---|
| On-demand | Unknown or spiky traffic, new applications | billing_mode = "PAY_PER_REQUEST" |
| Provisioned | Predictable traffic, lower unit cost | billing_mode = "PROVISIONED" with read_capacity and write_capacity |
On-demand is the simplest and the right start for most workloads.
Global secondary index #
An index lets you query by other attributes:
resource "aws_dynamodb_table" "orders" {
# ... same arguments as above
attribute {
name = "status"
type = "S"
}
attribute {
name = "created_at"
type = "S"
}
global_secondary_index {
name = "status-created_at-index"
hash_key = "status"
range_key = "created_at"
projection_type = "ALL"
}
}Projection ALL copies every attribute into the index (more storage and write cost). Use KEYS_ONLY or INCLUDE when you only need some of them. In provisioned mode, an index needs its own capacity.
Time to live (TTL) #
Delete expired items automatically, at no cost:
ttl {
attribute_name = "expires_at" # epoch seconds in a number attribute
enabled = true
}Autoscaling for provisioned tables #
resource "aws_appautoscaling_target" "read" {
service_namespace = "dynamodb"
resource_id = "table/${aws_dynamodb_table.orders.name}"
scalable_dimension = "dynamodb:table:ReadCapacityUnits"
min_capacity = 5
max_capacity = 100
}
resource "aws_appautoscaling_policy" "read" {
name = "dynamodb-read-scaling"
service_namespace = aws_appautoscaling_target.read.service_namespace
resource_id = aws_appautoscaling_target.read.resource_id
scalable_dimension = aws_appautoscaling_target.read.scalable_dimension
policy_type = "TargetTrackingScaling"
target_tracking_scaling_policy_configuration {
target_value = 70
predefined_metric_specification {
predefined_metric_type = "DynamoDBReadCapacityUtilization"
}
}
}If you use autoscaling, ignore the capacity that it changes: lifecycle { ignore_changes = [read_capacity, write_capacity] } in the table (lifecycle).
Allow an application to use it #
data "aws_iam_policy_document" "orders_access" {
statement {
actions = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
]
resources = [
aws_dynamodb_table.orders.arn,
"${aws_dynamodb_table.orders.arn}/index/*",
]
}
}Attach it to the role of a Lambda function or of an EC2 instance. See IAM roles.
Design tips #
- Model your access patterns first, then choose the keys. DynamoDB is not a relational database: there are no joins.
- Choose a partition key with many different values to spread the load. A key such as
statusconcentrates traffic ("hot partition"). - Use
Queryinstead ofScan, which reads the whole table. - Keep items under 400 KB.