dynobase-icon
Dynobase

DynamoDB Pulumi: TypeScript and Python Table Examples

Lakindu Hewawasam

Written by Lakindu Hewawasam

Last updated: October 23rd, 2026

Writing DynamoDB code by hand? Dynobase turns supported data operations into application code or scripts, and generates CDK, CloudFormation or Terraform for an existing table. Code generation in Dynobase

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.

Historical Pulumi CLI version and AWS account checks

Historical Pulumi project initialization output

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.

Historical Pulumi preview of a DynamoDB table creation

Historical Pulumi output after creating the table

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.

Historical Pulumi preview after adding table settings

Historical Pulumi deployment progress

Historical Pulumi API Gateway deployment output

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?

Use 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?

No. They have independent version numbers. The current Pulumi AWS major is v7, while the Terraform AWS provider major is v6.

Do I declare every DynamoDB field in Pulumi attributes?

No. Declare only fields used by the table or index keys. Ordinary item fields, including the TTL field unless it is also a key, do not belong there.

Can I add a local secondary index after deployment?

No. LSIs are created with the table. Adding one requires a new table and a data migration; do not accept an unintended replacement of a populated table.

Does enabled server-side encryption mean an AWS owned key?

Not in this provider setting. Omitting serverSideEncryption uses an AWS owned key; enabled: true selects the AWS managed DynamoDB key unless kmsKeyArn identifies a customer managed key.

Dynobase is a Professional GUI Client for DynamoDB

Start your 7-day free trial today

Product Features

Download
/
Changelog
/
Pricing
/
Member Portal
/
Privacy
/
EULA
/
Twitter
© 2026 Dynobase