Provision DynamoDB Tables with Pulumi Python
DynamoDB is deceptively simple to create and easy to misconfigure — wrong capacity mode, missing point-in-time recovery, unindexed access patterns. This guide, part of the AWS provider deep dive under Pulumi patterns and provider management, provisions a production-shaped table with a global secondary index in typed Python.
Context
A table's schema decisions are effectively permanent: the partition key and any global secondary index (GSI) are set at creation, and changing a key means a migration. Getting capacity mode, encryption, and recovery right up front — as with the other resources in the AWS provider deep dive — avoids expensive rework.
Prerequisites
- Python 3.9+ with
pulumi>=3.0andpulumi-aws>=6.0pinned - AWS credentials with
dynamodb:CreateTable,UpdateTable, and tagging permissions - A decided access pattern: the partition key and any GSI you will query by
# CLI: confirm the AWS provider plugin is installed at the pinned version
pulumi plugin ls | grep aws
Implementation
Declare the table with on-demand billing, server-side encryption, and point-in-time recovery, plus one GSI for a secondary query path.
# table.py — a production-shaped DynamoDB table with a GSI
# CLI: pulumi up
import pulumi_aws as aws
table = aws.dynamodb.Table(
"orders",
billing_mode="PAY_PER_REQUEST", # Provider note: no capacity to tune
hash_key="pk",
range_key="sk",
attributes=[
aws.dynamodb.TableAttributeArgs(name="pk", type="S"),
aws.dynamodb.TableAttributeArgs(name="sk", type="S"),
aws.dynamodb.TableAttributeArgs(name="gsi1pk", type="S"),
],
global_secondary_indexes=[aws.dynamodb.TableGlobalSecondaryIndexArgs(
name="gsi1", hash_key="gsi1pk", projection_type="ALL")],
point_in_time_recovery=aws.dynamodb.TablePointInTimeRecoveryArgs(enabled=True),
server_side_encryption=aws.dynamodb.TableServerSideEncryptionArgs(enabled=True),
tags={"owner": "platform", "env": "prod"},
)
# State implication: the table name and keys are recorded; changing a key replaces the table.
Verification
Confirm the table is active with the GSI and recovery enabled.
# CLI: table status ACTIVE and PITR enabled
aws dynamodb describe-table --table-name $(pulumi stack output tableName) \
--query 'Table.[TableStatus,GlobalSecondaryIndexes[0].IndexName]'
aws dynamodb describe-continuous-backups --table-name $(pulumi stack output tableName) \
--query 'ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus'
Gotchas & Edge Cases
Adding a GSI later throttles. Backfilling an index on a large table consumes capacity; on provisioned tables this can throttle production. Prefer on-demand during index changes.
Attribute list must match keys. Every key and index attribute must appear in attributes, or the apply fails with all attributes must be indexed.
Replacement on key change. Changing hash_key or range_key replaces the table and drops data; treat key design as a migration, using the discipline from migrating IaC state.
FAQ
On-demand or provisioned capacity?
Start on-demand (PAY_PER_REQUEST) for unpredictable traffic; switch to provisioned with autoscaling only when steady-state load makes it cheaper.
Can I add a GSI without downtime?
Yes — DynamoDB adds a GSI online, but the backfill consumes capacity. Plan it during low traffic and watch for throttling.
How do I secure the table?
Enable server-side encryption (shown above) and scope IAM to specific actions, following IAM least privilege.
Related
- Provisioning RDS Postgres with Pulumi Python — a sibling data-store guide with its own durability concerns.
- AWS Provider Deep Dive — the parent cluster covering core AWS services.
- Deploying AWS Lambda Functions with Pulumi Python — a common consumer of a DynamoDB table.