DynamoDB Pulumi: TypeScript and Python Table Examples

Written by Lakindu Hewawasam
Last updated: October 23rd, 2026
Create a DynamoDB table with Pulumi's aws.dynamodb.Table in TypeScript or pulumi_aws.dynamodb.Table in Python. The current Pulumi AWS provider major is v7, and it supports on-demand capacity, indexes, backups, deletion protection, warm throughput and global replicas. Pulumi provisions infrastructure; application reads and writes use an AWS SDK separately.
Why use Pulumi for DynamoDB?
Pulumi lets your infrastructure use the same language, package management and abstractions as your application. You can export the table name, pass it to Lambda and construct scoped IAM policies from the table ARN. Resources can live in imported modules; they do not all have to be declared directly in index.ts.
Terraform provides similar infrastructure management through HCL. Choose based on the team's existing state management, review process and language preferences. Pulumi's TypeScript does not eliminate the need to understand table replacement, IAM or DynamoDB access patterns. SST adds application-oriented components and linking on top of Pulumi-based infrastructure.
The examples use the Pulumi AWS Table reference. Pulumi AWS v7 and Terraform AWS v6 are separate package version lines; do not assume their major numbers should match.
Step 1: initialize a TypeScript stack
Install the Pulumi CLI, Node.js and AWS CLI v2. Authenticate with your chosen Pulumi state backend and configure an AWS development profile:
Choose a project name and a dev stack. Keep the package lock file and review provider upgrades before running them against persistent data.


These screenshots illustrate the workflow; CLI versions and prompts have changed. Replace any generated sample bucket with the table code below.
Step 2: provision the users table
Create dynamo/users.ts. This complete definition keeps the original composite key, adds useful secondary indexes and enables backups:
Import it from index.ts:
The logical name users is not necessarily the physical table name: Pulumi can generate a unique suffix. Export Users.name rather than assuming a literal name. Values such as name and arn are Outputs; pass them to other resources or use pulumi.interpolate, rather than calling .get() during deployment.


The current preview includes the protections and settings above. Check changes before approving pulumi up, particularly anything marked replace or delete.
The same table in Python
Use this in __main__.py of a separate pulumi new aws-python project. Install a reviewed pulumi-aws v7 release in its virtual environment and set the AWS Region as above. This is an alternative implementation, not a second owner for the TypeScript table.
Python uses snake_case argument names and typed Args objects. TypeScript uses camelCase. Both definitions express the same DynamoDB settings; the application can use boto3 or SDK v3 independently of the infrastructure language.
Table settings and index behavior
TTL and point-in-time recovery
deleteAt is a Number containing Unix epoch seconds. JavaScript's Date.now() returns milliseconds, so divide by 1000 before adding a duration. TTL cleanup normally happens within a few days after expiry, not exactly at the expiry timestamp. Expired items remain readable until deleted unless the application filters or rejects them.
PITR supports a recovery window of 1 to 35 days, with 35 as the default. The example explicitly chooses 14. A shorter window does not reduce the PITR price, and restoration creates a new table rather than rolling the existing table backward. Test the restore and application cutover procedures.
Encryption
DynamoDB always encrypts data at rest. Omitting serverSideEncryption uses an AWS owned key. serverSideEncryption: { enabled: true } selects the AWS managed DynamoDB key, not the AWS owned key. Add kmsKeyArn to that object to choose a customer managed key.
Retain customer managed keys with the data and review their key policies. A table retained by Pulumi can still become unavailable if its encryption key is disabled or removed.
LSIs and GSIs
Only attributes used by table or index keys belong in attributes. The example's age and deleteAt are ordinary item fields and therefore absent. Key attributes are automatically projected into indexes; do not repeat them in nonKeyAttributes.
The ByName LSI shares the base table's id partition key and provides an alternate sort key. LSIs must be created with the table. Adding or changing one later requires replacement, so use a new-table migration rather than accepting a destructive preview.
The ByEmail GSI has a separate partition key. GSIs can be added later, but backfill must complete before the application relies on them. Their reads are eventually consistent. Indexes add storage and write costs; ALL projects every field, while INCLUDE narrows that copy.
On-demand caps, warm throughput and provisioned scaling
For a best-effort on-demand request-rate cap, add:
For planned pre-warming, add this separate optional setting:
Maximum throughput can throttle requests; it is not a hard monthly budget. Warm throughput prepares immediate capacity and proactive increases can incur a one-time charge. Do not configure it casually on a small development table.
For provisioned mode, set billingMode: "PROVISIONED", specify numeric readCapacity and writeCapacity, remove on-demand caps and set capacities on each GSI. Application Auto Scaling uses separate targets and policies. When it owns capacity, use Pulumi resource options such as ignoreChanges: ["readCapacity", "writeCapacity"] so updates do not continually restore stale capacity settings. Review GSI capacity drift separately.
Global-table replicas
For an eventually consistent global table, add a replica to the TypeScript table definition; Streams are already enabled:
Configure replica backups and deletion protection explicitly. Do not manage the same replica both inline and through a standalone replica resource. The table and index key schemas must remain consistent across Regions.
The provider also exposes consistencyMode: "STRONG" on replicas and globalTableWitness: { regionName: "us-east-2" }. These support MRSC creation with three Regions: three data replicas, or two plus a witness. The example table cannot simply switch to MRSC because it has TTL and an LSI, both unsupported by MRSC. Transactions are also unsupported. Plan a separate empty table and review the global tables guide.
Step 3: implement the user API with SDK v3
Keep application code separate from Pulumi code. Do not use the old aws.sdk.DynamoDB.DocumentClient helper. Create lambda/index.mjs and install its own SDK dependencies:
This handler serves all four original operations. Both key components travel in update and delete paths. The example is for a disposable demonstration dataset: its five-day TTL is deliberate, not an appropriate default for permanent user accounts.
The PATCH code checks whether fields are present instead of using truthiness, so an age of zero is not silently omitted. It aliases attribute names and rejects an empty update. The existence condition avoids accidentally inserting an incomplete item during an update.
The list operation returns a continuation token. Follow it until null; one Scan cannot fetch an arbitrarily large table. For an email lookup, use Query on ByEmail and grant index access. A GSI does not enforce email uniqueness. Add application authorization, field-length checks and an appropriate idempotency design before using this API beyond a development account.
Step 4: connect Lambda and API Gateway
Add this after the table import in index.ts. It uses explicit provider resources so the IAM and invocation permission are visible:
The archive includes index.mjs, its package files and installed dependencies at the ZIP root. Inspect it before deployment and keep unrelated files out of lambda. Runtime code receives the resolved table name through an environment variable; it does not need Pulumi installed.
These routes have no authorizer. Keep the demonstration isolated, or add authorization before real data is written. The Lambda role restricts AWS resource access but does not decide which caller owns an item.
Step 5: process table changes
Streams can contain keys only, new images, old images, or both. The table above chooses both. Create stream/index.mjs:
Add a separate role, function and event source mapping to index.ts:
The managed role policy grants stream-read and logging permissions broadly. Replace it with a stream-scoped policy if that scope is too wide for your environment. Process every record in the batch, not only Records[0]. Add a failure destination and idempotent side effects before using the consumer for important processing; the Streams guide covers those settings.
Deploy, test and retain the data
Use the returned URL to POST a user, then keep both id and numeric createdAt from the response. PATCH and DELETE use both in the path. GET /users returns a page; follow its token for more. Check CloudWatch for stream events after writes. A correctly declared auto-deploying API stage should not need an unexplained second deployment.



These screenshots illustrate preview and deployment only; the resources and routes above replace the older callback-function implementation. For local SDK tests, use DynamoDB Local with an explicit endpoint and dummy credentials. Provider deployment, IAM and global replication still require AWS validation.
Pulumi protect rejects deletion through Pulumi, while retainOnDelete preserves the physical resource if it is removed from state through a permitted deletion. DynamoDB deletion protection is a third, service-side guard. Review the protect and retainOnDelete semantics before cleanup. Disable protections only as part of a reviewed retention or migration plan, and remember that retained tables continue to incur charges.
Frequently Asked Questions
Which Pulumi resource creates a DynamoDB table?
aws.dynamodb.Table in TypeScript or pulumi_aws.dynamodb.Table in Python. Set PAY_PER_REQUEST explicitly for on-demand billing.Does Pulumi AWS v7 mean Terraform AWS v7?
Do I declare every DynamoDB field in Pulumi attributes?
Can I add a local secondary index after deployment?
Does enabled server-side encryption mean an AWS owned key?
Dynobase is a Professional GUI Client for DynamoDB
Start your 7-day free trial today
Product Features
DynamoDB Tools
- SSH GUI Client
- Chat With Cloud
- MongoDB GUI Client
- AWS Cost Monitoring
- DynamoDB Query Builder
- DynamoDB Code Examples
- DynamoDB Node.js Queries
- DynamoDB JSON Converter
- DynamoDB UUID Generator
- DynamoDB Local Admin GUI
- DynamoDB Pricing Calculator
- DynamoDB Single-Table Design
- Best DynamoDB GUI Clients
- Dynobase vs DynoTable