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.

Table design Table design: DynamoDB table with 4 facets. DynamoDB table PK/SK access pattern GSI secondary query Billing on-demand PITR recovery
Keys and indexes are fixed at creation; capacity and recovery are configurable but easy to forget.

Prerequisites

Prerequisites Prerequisites: layered from UpdateTable down to AWS. UpdateTable Python AWS
Prerequisites: the building blocks this section assembles.
  • Python 3.9+ with pulumi>=3.0 and pulumi-aws>=6.0 pinned
  • 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.

Provision flow Provision flow: define table then pulumi preview then create table then create GSI then verify define table pulumi preview create table create GSI verify
Preview then apply; the GSI is created as part of the same table resource.
# 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.

Verification Verification: Test → Program → Mock/Cloud. Test Program Mock/Cloud invoke declare resolve assert
Verification: the test drives the program and asserts on resolved values.
# 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

Gotchas & Edge Cases Gotchas & Edge Cases: Where it breaks with 4 facets. Where it breaks attributes watch this boundary hash_key watch this boundary range_key watch this boundary Edge Cases watch this boundary
Gotchas & Edge Cases: the boundaries where things break and what to check.

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.