dynobase-icon
Dynobase

DynamoDB Terraform: Tables, Replicas and State Locking

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

Use Terraform's aws_dynamodb_table resource to create a DynamoDB table, its indexes, backups and global-table replicas. Set billing_mode = "PAY_PER_REQUEST" explicitly for on-demand capacity: the AWS provider still defaults to provisioned capacity. For Terraform's own state, use native S3 locking with use_lockfile = true; DynamoDB-based locking is deprecated.

Why manage DynamoDB with Terraform?

Terraform records a table's configuration in version control and compares it with deployed infrastructure before making changes. It is useful when several applications share a table or when your team already manages networking, IAM and other services with Terraform.

Serverless Framework and AWS CDK can also manage long-lived tables safely. The important decision is resource ownership and deletion policy, not the language used to define it. Keep a shared database in a separately managed stack or state, and configure retention or deletion protection. Removing an application should not accidentally remove its data.

The examples below use the current AWS provider v6. The provider's DynamoDB resource reference documents table properties and update behavior. Commit your dependency lock file and review provider upgrades before applying them to production.

Step 1: configure Terraform and AWS credentials

Install Terraform and AWS CLI v2, configure an AWS profile, and check which account you will use:

Create main.tf:

The provider uses the normal AWS credential chain, including AWS_PROFILE. Do not hard-code access keys or a literal $HOME path in HCL. The archive provider is used later to package Lambda code; a table-only project does not need it.

Terraform initializing its AWS provider in a terminal

This historical screenshot illustrates initialization; installed versions and output will differ.

Step 2: create an on-demand DynamoDB table

Add this resource to main.tf. It keeps the original notes example, with a string noteId key and a userId index for listing one user's notes.

Declare attribute blocks only for table and index keys. text, createdAt and expiryPeriod are item attributes, not schema declarations. Adding every possible item field to the table resource is a common cause of validation errors or repeated plans.

A sort key uses range_key plus another attribute block. For a GSI, define its own keys and projection. ALL copies all item attributes; KEYS_ONLY keeps only keys; INCLUDE adds named non-key attributes. Index storage and writes add to your bill. A GSI can be added to an existing table, while an LSI must be defined at table creation.

Review and apply the table:

Historical Terraform plan for a DynamoDB table

Historical Terraform output while creating a DynamoDB table

These screenshots show an older provisioned-capacity example. The configuration above creates an on-demand table with deletion protection and PITR.

TTL, backups and encryption

expiryPeriod must contain a Number in Unix epoch seconds. In JavaScript, a seven-day expiry is Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60. TTL cleanup normally takes a few days after expiry, so applications must reject or filter expired records themselves when expiry affects access.

PITR keeps a configurable recovery window of 1 to 35 days and restores into a new table. Choosing 14 days does not halve its price. Test a restore and your application's table-name switch, rather than treating the enabled flag as a complete recovery plan.

DynamoDB always encrypts data at rest. Omitting server_side_encryption uses an AWS owned key. To use the AWS managed DynamoDB key, add this block inside the table:

For a customer managed key, also set kms_key_arn to its ARN. Its policy and availability become part of the database's operating requirements. Setting enabled = false selects the AWS owned key; it does not disable encryption.

Maximum on-demand throughput and warm throughput

An optional maximum throughput setting gives the table a best-effort request-rate cap. Add this inside the table resource if the resulting throttling is acceptable:

These are request units per second, not a monthly spending limit. Burst capacity can temporarily exceed the target. Table and GSI caps are independent; the same block is available inside global_secondary_index.

Warm throughput is different: it describes the traffic the table can immediately support. Before a planned traffic increase, you can request pre-warming:

Pre-warming can incur a one-time charge. Do not add it to a small development table merely because the property exists. Avoid setting a maximum cap below the traffic you are paying to prepare for, and review the warm-throughput plan independently from ordinary configuration changes.

Provisioned capacity and auto scaling

For predictable traffic, provisioned capacity may be appropriate. Change the table to billing_mode = "PROVISIONED", remove on_demand_throughput, and set numeric read_capacity and write_capacity values. Every GSI also needs its own provisioned capacities.

Application Auto Scaling manages capacity after creation. The following provisions both read and write targets for the base table. Its example limits are workload choices, not DynamoDB defaults:

Add lifecycle { ignore_changes = [read_capacity, write_capacity] } inside the table so subsequent applies do not undo scaling. GSI scaling needs separate targets using table/TABLE/index/INDEX and the index capacity dimensions. Review the Application Auto Scaling policy reference when adding index targets.

Using terraform-aws-modules/dynamodb-table/aws

The community DynamoDB table module wraps table configuration, replicas and auto scaling. Its registry source is exactly terraform-aws-modules/dynamodb-table/aws:

This is an alternative to the direct table resource, not an additional owner for tf-notes-table. Select and pin a reviewed module version before using it in a shared environment. Read its migration notes before toggling auto scaling: module implementations can use different resource addresses for scaled and unscaled tables, requiring state migration to avoid a replacement.

Global tables: replicas and strong consistency

For an eventually consistent global table, add Streams and a replica block to the original table:

Use replica on aws_dynamodb_table for current global tables. aws_dynamodb_global_table models the legacy version. Do not manage the same replica through both inline blocks and a standalone replica resource.

Multi-Region strong consistency (MRSC) is a different creation-time choice. It requires three Regions: three replicas, or two replicas and a witness. A separate example, with the provider still in us-east-1, is:

The witness does not serve reads or writes. MRSC does not support TTL, local secondary indexes or transactions, so it is not a drop-in conversion of the TTL-enabled notes example. Start with an empty table and review supported Regions and application semantics in the global tables guide.

Resource-based table policies

Application roles normally get identity-based IAM permissions. A resource-based policy is useful when the table itself must authorize another principal, including cross-account access. Supply the ARN of a real role to this variable:

The resource policy reference also supports stream ARNs. For cross-account requests, the caller needs an identity policy as well, and must address the table by its full ARN. Avoid wildcard principals for application access.

Step 3: deploy Lambda handlers for the notes table

Keep the table on-demand for this walkthrough. These three handlers create, delete and list notes; the original example did not deploy an HTTP gateway, so these are directly invokable Lambda functions. Add authentication and an API integration before exposing them to clients.

Create a lambdas directory and initialize a Node.js package there. Install @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb into that directory. Use .mjs files for ESM. Package these dependencies with the handlers to control their versions.

Execution role and permissions

Add the following to main.tf:

This grants the operations used below, rather than every DynamoDB action. For stricter isolation, give each function its own role and only its own operation.

Create a note

Save as lambdas/create-note.mjs. Direct invocations send an object with text and userId; the handler also accepts a JSON HTTP body. The user ID is a demonstration input, not authenticated identity.

Delete a note

Save as lambdas/delete-note.mjs. Keep the key a string, matching the table definition and the UUID returned by create:

List notes with pagination

Save as lambdas/get-all-notes.mjs. A single Scan does not return an unlimited table. Return a continuation key instead of loading every page into one Lambda response:

Call again with the returned nextKey until it is absent, even if a filtered page has no items. Scan is for this small administrative example. An application listing one user's notes should Query ByUser and receive dynamodb:Query on that index. Neither an index nor a client-supplied user ID enforces authorization on its own.

Package and deploy all three functions

The ZIP contains the handlers and node_modules at its root, so the handler names have no lambdas/ prefix. Review another plan and apply it, then invoke with AWS CLI v2:

Historical AWS CLI invocation of a Terraform-managed Lambda function

Check both the invocation status and output.json; a successful Lambda API call can still contain an application error. For local handler tests, use DynamoDB Local and an explicit local SDK endpoint.

Step 4: use native S3 state locking

Create the state bucket separately before configuring it as this project's backend, enable bucket versioning, and restrict access to the deployment identities. Replace the example name with your existing bucket:

Run terraform init -migrate-state when moving existing local state into this backend. Native locking uses a .tflock object: grant GetObject, PutObject and DeleteObject on that lock, alongside the backend's state-object and bucket-list permissions. See the S3 backend reference.

DynamoDB locking is deprecated. An existing installation may temporarily configure both mechanisms while older clients are upgraded. Legacy lock tables use a String partition key named LockID; retain them until every client has migrated. New projects do not need a DynamoDB lock table or S3 Object Lock for Terraform concurrency control.

Import, replacement and safe cleanup

For an existing table, write matching configuration and import its name with terraform import aws_dynamodb_table.tf_notes_table tf-notes-table. Review the next plan carefully: an import does not prove that every property matches. Dynobase can generate Terraform, CDK or CloudFormation table definitions as a starting point for that review.

Primary-key changes require replacement. Plan a new table, data migration and application cutover instead of accepting a destructive replacement on a populated table. Deletion protection makes a destroy fail until you deliberately disable it; it does not replace backups. To remove this demonstration, first decide what data to retain, disable protection in a reviewed apply, and only then destroy the disposable resources.

Frequently Asked Questions

How do I create a DynamoDB table with Terraform?

Use aws_dynamodb_table, define the partition key and its attribute type, and set billing_mode = "PAY_PER_REQUEST" for on-demand capacity. Review a plan before applying it.

Should I use the DynamoDB Terraform module or the resource?

The direct resource gives explicit control over the table. The community terraform-aws-modules/dynamodb-table/aws module packages common settings and auto scaling; pin its version and review migration behavior before changing options.

Does Terraform still need DynamoDB for state locking?

No. The S3 backend supports native locking with use_lockfile = true. DynamoDB locking is deprecated, although both can be configured during a client migration.

Why does Terraform reject my attribute definitions?

Only declare attributes used as table or index keys. Ordinary item fields and the TTL attribute do not belong in attribute blocks unless they are also keys.

Can Terraform create strongly consistent global tables?

Yes. The current AWS provider supports replica.consistency_mode = "STRONG" and an optional global_table_witness. MRSC requires three Regions and does not support TTL, transactions or local secondary indexes.

Dynobase is a Professional GUI Client for DynamoDB

Start your 7-day free trial today

Product Features

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