Creating Terraform DynamoDB Tables and Configurations
- backlinksindiit
- 7 days ago
- 6 min read
Clicking through AWS console creating DynamoDB tables wastes time and creates inconsistency. Team members configure tables differently. Production and staging drift apart. Documentation becomes outdated immediately. Creating Terraform DynamoDB tables with infrastructure-as-code solves these problems through version-controlled, repeatable configurations.
Terraform Project Structure for DynamoDB
Organize Terraform files logically for maintainability:
terraform/
├── main.tf # Primary resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── backend.tf # State configuration
└── modules/
└── dynamodb/
├── main.tf
├── variables.tf
└── outputs.tf
This structure separates concerns. Variables define configuration inputs. Outputs expose table ARNs and names for other resources. Modules enable reusable table patterns across environments.
Choosing the Right Partition Key Design
Partition keys determine DynamoDB performance. Poor choices cause hot partitions where most traffic hits one physical server. Good choices distribute load evenly.
Bad partition key examples:
status field (most records have "active")
region field (uneven geographic distribution)
timestamp day (all today's records on same partition)
Good partition key examples:
userId (high cardinality, even distribution)
orderId (unique per order)
Composite: customerId#productId (combines multiple attributes)
Real-world table designs with proper keys:
# E-commerce Orders Table
resource "aws_dynamodb_table" "orders" {
name = "orders-${var.environment}"
billing_mode = "PAY_PER_REQUEST"
hash_key = "orderId"
range_key = "createdAt"
attribute {
name = "orderId"
type = "S"
}
attribute {
name = "createdAt"
type = "N"
}
attribute {
name = "customerId"
type = "S"
}
attribute {
name = "orderStatus"
type = "S"
}
global_secondary_index {
name = "CustomerOrdersIndex"
hash_key = "customerId"
range_key = "createdAt"
projection_type = "ALL"
}
global_secondary_index {
name = "StatusIndex"
hash_key = "orderStatus"
range_key = "createdAt"
projection_type = "KEYS_ONLY"
}
tags = {
Environment = var.environment
Purpose = "order-management"
}
}
# User Analytics Table
resource "aws_dynamodb_table" "analytics" {
name = "user-analytics-${var.environment}"
billing_mode = "PAY_PER_REQUEST"
hash_key = "userId"
range_key = "eventTimestamp"
attribute {
name = "userId"
type = "S"
}
attribute {
name = "eventTimestamp"
type = "N"
}
attribute {
name = "eventType"
type = "S"
}
global_secondary_index {
name = "EventTypeIndex"
hash_key = "eventType"
range_key = "eventTimestamp"
projection_type = "INCLUDE"
non_key_attributes = ["userId", "eventData"]
}
ttl {
attribute_name = "expirationTime"
enabled = true
}
tags = {
Environment = var.environment
DataRetention = "90-days"
}
}
Teams building applications for mobile app development houston clients benefit from TTL configurations automatically deleting old analytics data, reducing storage costs without manual cleanup scripts.
Pay-Per-Request vs Provisioned Capacity: The Real Math
Choosing billing modes impacts costs significantly. Here's actual pricing breakdown (us-east-1, 2025 rates):
Pay-Per-Request:
Write: $1.25 per million requests
Read: $0.25 per million requests
Provisioned Capacity:
Write: $0.00065 per WCU-hour ($0.47 per WCU-month)
Read: $0.00013 per RCU-hour ($0.09 per RCU-month)
Cost comparison at different scales:
Provisioned becomes cost-effective above 50 million combined operations monthly. Below that threshold, pay-per-request simplifies operations without capacity planning overhead.
Implementing Autoscaling for Provisioned Tables
Provisioned capacity requires autoscaling preventing throttling during traffic spikes:
resource "aws_dynamodb_table" "high_traffic" {
name = "high-traffic-table"
billing_mode = "PROVISIONED"
read_capacity = 10
write_capacity = 10
hash_key = "id"
attribute {
name = "id"
type = "S"
}
tags = {
Environment = "production"
}
}
resource "aws_appautoscaling_target" "read_target" {
max_capacity = 500
min_capacity = 10
resource_id = "table/${aws_dynamodb_table.high_traffic.name}"
scalable_dimension = "dynamodb:table:ReadCapacityUnits"
service_namespace = "dynamodb"
}
resource "aws_appautoscaling_target" "write_target" {
max_capacity = 500
min_capacity = 10
resource_id = "table/${aws_dynamodb_table.high_traffic.name}"
scalable_dimension = "dynamodb:table:WriteCapacityUnits"
service_namespace = "dynamodb"
}
resource "aws_appautoscaling_policy" "read_policy" {
name = "DynamoDBReadCapacityUtilization:${aws_appautoscaling_target.read_target.resource_id}"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.read_target.resource_id
scalable_dimension = aws_appautoscaling_target.read_target.scalable_dimension
service_namespace = aws_appautoscaling_target.read_target.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "DynamoDBReadCapacityUtilization"
}
target_value = 70.0
scale_in_cooldown = 60
scale_out_cooldown = 60
}
}
resource "aws_appautoscaling_policy" "write_policy" {
name = "DynamoDBWriteCapacityUtilization:${aws_appautoscaling_target.write_target.resource_id}"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.write_target.resource_id
scalable_dimension = aws_appautoscaling_target.write_target.scalable_dimension
service_namespace = aws_appautoscaling_target.write_target.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "DynamoDBWriteCapacityUtilization"
}
target_value = 70.0
scale_in_cooldown = 60
scale_out_cooldown = 60
}
}
The target_value = 70.0 maintains 70% utilization. When actual usage exceeds 70%, autoscaling adds capacity. Below 70% for sustained periods, capacity decreases. Cooldown periods prevent rapid scaling oscillations causing cost spikes.
Terraform State Locking with DynamoDB
DynamoDB provides distributed locking for Terraform state preventing concurrent modifications:
# backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/infrastructure.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-locks"
kms_key_id = "arn:aws:kms:us-east-1:123456789:key/abc-123"
}
}
# State lock table
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
point_in_time_recovery {
enabled = true
}
tags = {
Purpose = "terraform-state-locking"
Critical = "true"
}
}
When Terraform starts, it writes a lock entry. Concurrent runs wait for lock release. Crashes leave stale locks requiring manual removal via AWS console or CLI: aws dynamodb delete-item --table-name terraform-state-locks --key '{"LockID":{"S":"company-terraform-state/production/infrastructure.tfstate-md5"}}'
Production Security: Encryption and Backups
Production tables require encryption at rest and point-in-time recovery:
resource "aws_kms_key" "dynamodb" {
description = "DynamoDB encryption key"
deletion_window_in_days = 30
enable_key_rotation = true
tags = {
Purpose = "dynamodb-encryption"
}
}
resource "aws_kms_alias" "dynamodb" {
name = "alias/dynamodb-${var.environment}"
target_key_id = aws_kms_key.dynamodb.key_id
}
resource "aws_dynamodb_table" "production_data" {
name = "production-data"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
server_side_encryption {
enabled = true
kms_key_arn = aws_kms_key.dynamodb.arn
}
point_in_time_recovery {
enabled = true
}
lifecycle {
prevent_destroy = true
}
tags = {
Environment = "production"
Backup = "required"
Compliance = "pci-dss"
}
}
Point-in-time recovery costs $0.20 per GB-month but enables restoration to any second within the last 35 days. Customer-managed KMS keys add $1/month per key plus API request charges but satisfy compliance requirements requiring customer-controlled encryption.
Variables and Modules for Reusable Configuration
Extract common patterns into modules preventing duplication:
# modules/dynamodb/variables.tf
variable "table_name" {
description = "DynamoDB table name"
type = string
}
variable "hash_key" {
description = "Partition key attribute"
type = string
}
variable "range_key" {
description = "Sort key attribute (optional)"
type = string
default = null
}
variable "billing_mode" {
description = "Billing mode: PAY_PER_REQUEST or PROVISIONED"
type = string
default = "PAY_PER_REQUEST"
}
variable "attributes" {
description = "List of attribute definitions"
type = list(object({
name = string
type = string
}))
}
variable "global_secondary_indexes" {
description = "GSI configurations"
type = list(object({
name = string
hash_key = string
range_key = string
projection_type = string
non_key_attributes = list(string)
}))
default = []
}
# modules/dynamodb/main.tf
resource "aws_dynamodb_table" "this" {
name = var.table_name
billing_mode = var.billing_mode
hash_key = var.hash_key
range_key = var.range_key
dynamic "attribute" {
for_each = var.attributes
content {
name = attribute.value.name
type = attribute.value.type
}
}
dynamic "global_secondary_index" {
for_each = var.global_secondary_indexes
content {
name = global_secondary_index.value.name
hash_key = global_secondary_index.value.hash_key
range_key = global_secondary_index.value.range_key
projection_type = global_secondary_index.value.projection_type
non_key_attributes = global_secondary_index.value.non_key_attributes
}
}
}
# Usage
module "users_table" {
source = "./modules/dynamodb"
table_name = "users-production"
billing_mode = "PAY_PER_REQUEST"
hash_key = "userId"
range_key = "createdAt"
attributes = [
{ name = "userId", type = "S" },
{ name = "createdAt", type = "N" },
{ name = "email", type = "S" }
]
global_secondary_indexes = [
{
name = "EmailIndex"
hash_key = "email"
range_key = null
projection_type = "ALL"
non_key_attributes = []
}
]
}
Troubleshooting Terraform DynamoDB Issues
Issue: "Error creating DynamoDB Table: ResourceInUseException"
Table name already exists. Check AWS console or use terraform import:
terraform import aws_dynamodb_table.users users-production
Issue: "ValidationException: One or more parameter values were invalid"
Attribute definitions only include keys and index attributes. DynamoDB is schemaless - other attributes get added dynamically without declaration.
Issue: Terraform wants to replace table
Check for changes in hash_key or range_key. These require table recreation with data loss. Use terraform plan before applying to review changes.
Issue: "Error enabling streams: ResourceNotFoundException"
Streams enable only after table creation completes. Add depends_on if Lambda triggers depend on streams.
Implementation Checklist:
Organize Terraform files into logical structure with modules
Choose high-cardinality partition keys for even distribution
Design GSIs based on actual query patterns, not speculation
Calculate costs comparing pay-per-request vs provisioned for your scale
Implement autoscaling for provisioned tables with 70% target utilization
Enable point-in-time recovery for production tables
Use customer-managed KMS keys when compliance requires
Create DynamoDB table for Terraform state locking
Add lifecycle { prevent_destroy = true } protecting production tables
Extract common patterns into reusable Terraform modules
Use terraform import for existing tables before managing with Terraform
Monitor CloudWatch metrics: ConsumedCapacity, UserErrors, SystemErrors
Creating Terraform DynamoDB tables through infrastructure-as-code ensures consistency across environments, enables version control of database configurations, and prevents manual configuration errors. The initial learning curve pays dividends through reproducible, documented infrastructure.
Comments