AWS DynamoDB Tables with Terraform: Keys, Indexes, TTL and Autoscaling

· 2 min read · Terraform & OpenTofu Tutorials

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 #

dynamodb.tf
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_key is the partition key: items with the same value are stored together, and every query must specify it.
  • range_key is the optional sort key, which orders the items inside a partition and allows range queries.
  • You declare as attribute only 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) and B (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_arn to 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:

dynamodb.tf
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:

dynamodb.tf
  ttl {
    attribute_name = "expires_at"   # epoch seconds in a number attribute
    enabled        = true
  }

Autoscaling for provisioned tables #

autoscaling.tf
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 #

iam.tf
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 status concentrates traffic ("hot partition").
  • Use Query instead of Scan, which reads the whole table.
  • Keep items under 400 KB.

For relational workloads use RDS or Aurora.

#AWS #Dynamodb #Terraform #OpenTofu