Create and Configure DynamoDB Tables

The aws:dynamodb/table:Table resource, part of the Pulumi AWS provider, defines a DynamoDB table: its key schema, capacity mode, indexes, and optional replication across regions. This guide focuses on three capabilities: primary key and index configuration, multi-region replication, and consistency modes for global tables.

A DynamoDB table often connects to other AWS services. Auto Scaling policies can manage capacity, and global tables require AWS provider configuration for additional regions. The examples are intentionally small and show feature-level configuration. Combine them with your own capacity planning and monitoring infrastructure.

Define a table with hash key, range key, and GSI

Most deployments start with a partition key for data distribution, a sort key for query flexibility, and global secondary indexes for alternate access patterns.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const basic_dynamodb_table = new aws.dynamodb.Table("basic-dynamodb-table", {
    name: "GameScores",
    billingMode: "PROVISIONED",
    readCapacity: 20,
    writeCapacity: 20,
    hashKey: "UserId",
    rangeKey: "GameTitle",
    attributes: [
        {
            name: "UserId",
            type: "S",
        },
        {
            name: "GameTitle",
            type: "S",
        },
        {
            name: "TopScore",
            type: "N",
        },
    ],
    ttl: {
        attributeName: "TimeToExist",
        enabled: true,
    },
    globalSecondaryIndexes: [{
        name: "GameTitleIndex",
        hashKey: "GameTitle",
        rangeKey: "TopScore",
        writeCapacity: 10,
        readCapacity: 10,
        projectionType: "INCLUDE",
        nonKeyAttributes: ["UserId"],
    }],
    tags: {
        Name: "dynamodb-table-1",
        Environment: "production",
    },
});
import pulumi
import pulumi_aws as aws

basic_dynamodb_table = aws.dynamodb.Table("basic-dynamodb-table",
    name="GameScores",
    billing_mode="PROVISIONED",
    read_capacity=20,
    write_capacity=20,
    hash_key="UserId",
    range_key="GameTitle",
    attributes=[
        {
            "name": "UserId",
            "type": "S",
        },
        {
            "name": "GameTitle",
            "type": "S",
        },
        {
            "name": "TopScore",
            "type": "N",
        },
    ],
    ttl={
        "attribute_name": "TimeToExist",
        "enabled": True,
    },
    global_secondary_indexes=[{
        "name": "GameTitleIndex",
        "hash_key": "GameTitle",
        "range_key": "TopScore",
        "write_capacity": 10,
        "read_capacity": 10,
        "projection_type": "INCLUDE",
        "non_key_attributes": ["UserId"],
    }],
    tags={
        "Name": "dynamodb-table-1",
        "Environment": "production",
    })
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/dynamodb"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := dynamodb.NewTable(ctx, "basic-dynamodb-table", &dynamodb.TableArgs{
			Name:          pulumi.String("GameScores"),
			BillingMode:   pulumi.String("PROVISIONED"),
			ReadCapacity:  pulumi.Int(20),
			WriteCapacity: pulumi.Int(20),
			HashKey:       pulumi.String("UserId"),
			RangeKey:      pulumi.String("GameTitle"),
			Attributes: dynamodb.TableAttributeArray{
				&dynamodb.TableAttributeArgs{
					Name: pulumi.String("UserId"),
					Type: pulumi.String("S"),
				},
				&dynamodb.TableAttributeArgs{
					Name: pulumi.String("GameTitle"),
					Type: pulumi.String("S"),
				},
				&dynamodb.TableAttributeArgs{
					Name: pulumi.String("TopScore"),
					Type: pulumi.String("N"),
				},
			},
			Ttl: &dynamodb.TableTtlArgs{
				AttributeName: pulumi.String("TimeToExist"),
				Enabled:       pulumi.Bool(true),
			},
			GlobalSecondaryIndexes: dynamodb.TableGlobalSecondaryIndexArray{
				&dynamodb.TableGlobalSecondaryIndexArgs{
					Name:           pulumi.String("GameTitleIndex"),
					HashKey:        pulumi.String("GameTitle"),
					RangeKey:       pulumi.String("TopScore"),
					WriteCapacity:  pulumi.Int(10),
					ReadCapacity:   pulumi.Int(10),
					ProjectionType: pulumi.String("INCLUDE"),
					NonKeyAttributes: pulumi.StringArray{
						pulumi.String("UserId"),
					},
				},
			},
			Tags: pulumi.StringMap{
				"Name":        pulumi.String("dynamodb-table-1"),
				"Environment": pulumi.String("production"),
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var basic_dynamodb_table = new Aws.DynamoDB.Table("basic-dynamodb-table", new()
    {
        Name = "GameScores",
        BillingMode = "PROVISIONED",
        ReadCapacity = 20,
        WriteCapacity = 20,
        HashKey = "UserId",
        RangeKey = "GameTitle",
        Attributes = new[]
        {
            new Aws.DynamoDB.Inputs.TableAttributeArgs
            {
                Name = "UserId",
                Type = "S",
            },
            new Aws.DynamoDB.Inputs.TableAttributeArgs
            {
                Name = "GameTitle",
                Type = "S",
            },
            new Aws.DynamoDB.Inputs.TableAttributeArgs
            {
                Name = "TopScore",
                Type = "N",
            },
        },
        Ttl = new Aws.DynamoDB.Inputs.TableTtlArgs
        {
            AttributeName = "TimeToExist",
            Enabled = true,
        },
        GlobalSecondaryIndexes = new[]
        {
            new Aws.DynamoDB.Inputs.TableGlobalSecondaryIndexArgs
            {
                Name = "GameTitleIndex",
                HashKey = "GameTitle",
                RangeKey = "TopScore",
                WriteCapacity = 10,
                ReadCapacity = 10,
                ProjectionType = "INCLUDE",
                NonKeyAttributes = new[]
                {
                    "UserId",
                },
            },
        },
        Tags = 
        {
            { "Name", "dynamodb-table-1" },
            { "Environment", "production" },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.dynamodb.Table;
import com.pulumi.aws.dynamodb.TableArgs;
import com.pulumi.aws.dynamodb.inputs.TableAttributeArgs;
import com.pulumi.aws.dynamodb.inputs.TableTtlArgs;
import com.pulumi.aws.dynamodb.inputs.TableGlobalSecondaryIndexArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var basic_dynamodb_table = new Table("basic-dynamodb-table", TableArgs.builder()
            .name("GameScores")
            .billingMode("PROVISIONED")
            .readCapacity(20)
            .writeCapacity(20)
            .hashKey("UserId")
            .rangeKey("GameTitle")
            .attributes(            
                TableAttributeArgs.builder()
                    .name("UserId")
                    .type("S")
                    .build(),
                TableAttributeArgs.builder()
                    .name("GameTitle")
                    .type("S")
                    .build(),
                TableAttributeArgs.builder()
                    .name("TopScore")
                    .type("N")
                    .build())
            .ttl(TableTtlArgs.builder()
                .attributeName("TimeToExist")
                .enabled(true)
                .build())
            .globalSecondaryIndexes(TableGlobalSecondaryIndexArgs.builder()
                .name("GameTitleIndex")
                .hashKey("GameTitle")
                .rangeKey("TopScore")
                .writeCapacity(10)
                .readCapacity(10)
                .projectionType("INCLUDE")
                .nonKeyAttributes("UserId")
                .build())
            .tags(Map.ofEntries(
                Map.entry("Name", "dynamodb-table-1"),
                Map.entry("Environment", "production")
            ))
            .build());

    }
}
resources:
  basic-dynamodb-table:
    type: aws:dynamodb:Table
    properties:
      name: GameScores
      billingMode: PROVISIONED
      readCapacity: 20
      writeCapacity: 20
      hashKey: UserId
      rangeKey: GameTitle
      attributes:
        - name: UserId
          type: S
        - name: GameTitle
          type: S
        - name: TopScore
          type: N
      ttl:
        attributeName: TimeToExist
        enabled: true
      globalSecondaryIndexes:
        - name: GameTitleIndex
          hashKey: GameTitle
          rangeKey: TopScore
          writeCapacity: 10
          readCapacity: 10
          projectionType: INCLUDE
          nonKeyAttributes:
            - UserId
      tags:
        Name: dynamodb-table-1
        Environment: production

The hashKey defines the partition key that distributes items across storage nodes. The rangeKey enables range queries within a partition. The attributes array must list all keys used in the table or indexes. The globalSecondaryIndexes array defines alternate query patterns, each with its own key structure and capacity settings. This example uses PROVISIONED billing, requiring explicit readCapacity and writeCapacity values.

Replicate tables across multiple AWS regions

Applications serving global users need low-latency access from multiple regions. DynamoDB replicates data automatically across configured replicas.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const example = new aws.dynamodb.Table("example", {
    name: "example",
    hashKey: "TestTableHashKey",
    billingMode: "PAY_PER_REQUEST",
    streamEnabled: true,
    streamViewType: "NEW_AND_OLD_IMAGES",
    attributes: [{
        name: "TestTableHashKey",
        type: "S",
    }],
    replicas: [
        {
            regionName: "us-east-2",
        },
        {
            regionName: "us-west-2",
        },
    ],
});
import pulumi
import pulumi_aws as aws

example = aws.dynamodb.Table("example",
    name="example",
    hash_key="TestTableHashKey",
    billing_mode="PAY_PER_REQUEST",
    stream_enabled=True,
    stream_view_type="NEW_AND_OLD_IMAGES",
    attributes=[{
        "name": "TestTableHashKey",
        "type": "S",
    }],
    replicas=[
        {
            "region_name": "us-east-2",
        },
        {
            "region_name": "us-west-2",
        },
    ])
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/dynamodb"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := dynamodb.NewTable(ctx, "example", &dynamodb.TableArgs{
			Name:           pulumi.String("example"),
			HashKey:        pulumi.String("TestTableHashKey"),
			BillingMode:    pulumi.String("PAY_PER_REQUEST"),
			StreamEnabled:  pulumi.Bool(true),
			StreamViewType: pulumi.String("NEW_AND_OLD_IMAGES"),
			Attributes: dynamodb.TableAttributeArray{
				&dynamodb.TableAttributeArgs{
					Name: pulumi.String("TestTableHashKey"),
					Type: pulumi.String("S"),
				},
			},
			Replicas: dynamodb.TableReplicaTypeArray{
				&dynamodb.TableReplicaTypeArgs{
					RegionName: pulumi.String("us-east-2"),
				},
				&dynamodb.TableReplicaTypeArgs{
					RegionName: pulumi.String("us-west-2"),
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var example = new Aws.DynamoDB.Table("example", new()
    {
        Name = "example",
        HashKey = "TestTableHashKey",
        BillingMode = "PAY_PER_REQUEST",
        StreamEnabled = true,
        StreamViewType = "NEW_AND_OLD_IMAGES",
        Attributes = new[]
        {
            new Aws.DynamoDB.Inputs.TableAttributeArgs
            {
                Name = "TestTableHashKey",
                Type = "S",
            },
        },
        Replicas = new[]
        {
            new Aws.DynamoDB.Inputs.TableReplicaArgs
            {
                RegionName = "us-east-2",
            },
            new Aws.DynamoDB.Inputs.TableReplicaArgs
            {
                RegionName = "us-west-2",
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.dynamodb.Table;
import com.pulumi.aws.dynamodb.TableArgs;
import com.pulumi.aws.dynamodb.inputs.TableAttributeArgs;
import com.pulumi.aws.dynamodb.inputs.TableReplicaArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var example = new Table("example", TableArgs.builder()
            .name("example")
            .hashKey("TestTableHashKey")
            .billingMode("PAY_PER_REQUEST")
            .streamEnabled(true)
            .streamViewType("NEW_AND_OLD_IMAGES")
            .attributes(TableAttributeArgs.builder()
                .name("TestTableHashKey")
                .type("S")
                .build())
            .replicas(            
                TableReplicaArgs.builder()
                    .regionName("us-east-2")
                    .build(),
                TableReplicaArgs.builder()
                    .regionName("us-west-2")
                    .build())
            .build());

    }
}
resources:
  example:
    type: aws:dynamodb:Table
    properties:
      name: example
      hashKey: TestTableHashKey
      billingMode: PAY_PER_REQUEST
      streamEnabled: true
      streamViewType: NEW_AND_OLD_IMAGES
      attributes:
        - name: TestTableHashKey
          type: S
      replicas:
        - regionName: us-east-2
        - regionName: us-west-2

The replicas array lists target regions for replication. Each replica receives a synchronized copy of table data. Setting streamEnabled to true and specifying streamViewType enables the change data capture that powers replication. Global tables require PAY_PER_REQUEST billing mode rather than provisioned capacity.

Enable strong consistency across global table replicas

Some workloads require strongly consistent reads across regions, guaranteeing the latest version regardless of which region serves the request.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const example = new aws.dynamodb.Table("example", {
    name: "example",
    hashKey: "TestTableHashKey",
    billingMode: "PAY_PER_REQUEST",
    streamEnabled: true,
    streamViewType: "NEW_AND_OLD_IMAGES",
    attributes: [{
        name: "TestTableHashKey",
        type: "S",
    }],
    replicas: [
        {
            regionName: "us-east-2",
            consistencyMode: "STRONG",
        },
        {
            regionName: "us-west-2",
            consistencyMode: "STRONG",
        },
    ],
});
import pulumi
import pulumi_aws as aws

example = aws.dynamodb.Table("example",
    name="example",
    hash_key="TestTableHashKey",
    billing_mode="PAY_PER_REQUEST",
    stream_enabled=True,
    stream_view_type="NEW_AND_OLD_IMAGES",
    attributes=[{
        "name": "TestTableHashKey",
        "type": "S",
    }],
    replicas=[
        {
            "region_name": "us-east-2",
            "consistency_mode": "STRONG",
        },
        {
            "region_name": "us-west-2",
            "consistency_mode": "STRONG",
        },
    ])
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/dynamodb"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := dynamodb.NewTable(ctx, "example", &dynamodb.TableArgs{
			Name:           pulumi.String("example"),
			HashKey:        pulumi.String("TestTableHashKey"),
			BillingMode:    pulumi.String("PAY_PER_REQUEST"),
			StreamEnabled:  pulumi.Bool(true),
			StreamViewType: pulumi.String("NEW_AND_OLD_IMAGES"),
			Attributes: dynamodb.TableAttributeArray{
				&dynamodb.TableAttributeArgs{
					Name: pulumi.String("TestTableHashKey"),
					Type: pulumi.String("S"),
				},
			},
			Replicas: dynamodb.TableReplicaTypeArray{
				&dynamodb.TableReplicaTypeArgs{
					RegionName:      pulumi.String("us-east-2"),
					ConsistencyMode: pulumi.String("STRONG"),
				},
				&dynamodb.TableReplicaTypeArgs{
					RegionName:      pulumi.String("us-west-2"),
					ConsistencyMode: pulumi.String("STRONG"),
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var example = new Aws.DynamoDB.Table("example", new()
    {
        Name = "example",
        HashKey = "TestTableHashKey",
        BillingMode = "PAY_PER_REQUEST",
        StreamEnabled = true,
        StreamViewType = "NEW_AND_OLD_IMAGES",
        Attributes = new[]
        {
            new Aws.DynamoDB.Inputs.TableAttributeArgs
            {
                Name = "TestTableHashKey",
                Type = "S",
            },
        },
        Replicas = new[]
        {
            new Aws.DynamoDB.Inputs.TableReplicaArgs
            {
                RegionName = "us-east-2",
                ConsistencyMode = "STRONG",
            },
            new Aws.DynamoDB.Inputs.TableReplicaArgs
            {
                RegionName = "us-west-2",
                ConsistencyMode = "STRONG",
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.dynamodb.Table;
import com.pulumi.aws.dynamodb.TableArgs;
import com.pulumi.aws.dynamodb.inputs.TableAttributeArgs;
import com.pulumi.aws.dynamodb.inputs.TableReplicaArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var example = new Table("example", TableArgs.builder()
            .name("example")
            .hashKey("TestTableHashKey")
            .billingMode("PAY_PER_REQUEST")
            .streamEnabled(true)
            .streamViewType("NEW_AND_OLD_IMAGES")
            .attributes(TableAttributeArgs.builder()
                .name("TestTableHashKey")
                .type("S")
                .build())
            .replicas(            
                TableReplicaArgs.builder()
                    .regionName("us-east-2")
                    .consistencyMode("STRONG")
                    .build(),
                TableReplicaArgs.builder()
                    .regionName("us-west-2")
                    .consistencyMode("STRONG")
                    .build())
            .build());

    }
}
resources:
  example:
    type: aws:dynamodb:Table
    properties:
      name: example
      hashKey: TestTableHashKey
      billingMode: PAY_PER_REQUEST
      streamEnabled: true
      streamViewType: NEW_AND_OLD_IMAGES
      attributes:
        - name: TestTableHashKey
          type: S
      replicas:
        - regionName: us-east-2
          consistencyMode: STRONG
        - regionName: us-west-2
          consistencyMode: STRONG

The consistencyMode property on each replica controls read consistency. Setting it to STRONG ensures reads always return the latest version, at the cost of higher latency compared to eventually consistent reads. This extends the basic global tables configuration with stronger guarantees but has specific AWS requirements and limitations.

Beyond These Examples

These snippets focus on specific table-level features: primary key structure (hash and range keys), global secondary indexes for alternate queries, and multi-region replication with eventual and strong consistency. They’re intentionally minimal rather than full database deployments.

The examples may reference pre-existing infrastructure such as AWS provider configured for the primary region, and additional AWS providers or region configuration for replicas. They focus on configuring the table rather than provisioning surrounding infrastructure.

To keep things focused, common table patterns are omitted, including:

  • Time-to-live (TTL) for automatic item expiration
  • Point-in-time recovery and backup configuration
  • Server-side encryption with customer-managed keys
  • Auto Scaling policies for capacity management
  • Local secondary indexes (LSIs)
  • Stream consumers and change data capture

These omissions are intentional: the goal is to illustrate how each table feature is wired, not provide drop-in database modules. See the DynamoDB Table resource reference for all available configuration options.

Frequently Asked Questions

Configuration & Common Pitfalls
Why am I getting an infinite planning loop with my DynamoDB table?
This happens when you define attributes that aren’t used as keys. Only define attributes that serve as the table’s hash key, range key, or GSI/LSI keys. DynamoDB doesn’t require you to define all table columns, just the key attributes.
Why do my read and write capacity changes keep showing up in diffs when I have autoscaling?
Autoscaling policies modify capacity outside of Pulumi, causing perpetual diffs. Use ignoreChanges for readCapacity and writeCapacity when you have an autoscaling policy attached.
Can I use both replica blocks and the TableReplica resource for the same table?
No, these two approaches conflict. Choose either replica configuration blocks on aws.dynamodb.Table OR separate aws.dynamodb.TableReplica resources. If using TableReplica, add ignoreChanges for the replica property.
What properties are immutable after table creation?
You cannot modify hashKey, rangeKey, name, localSecondaryIndexes, restoreDateTime, or restoreToLatestTime after creating the table. Changes to these require recreating the table.
Billing & Capacity
What's the difference between PROVISIONED and PAY_PER_REQUEST billing modes?
PROVISIONED (default) requires you to specify readCapacity and writeCapacity. PAY_PER_REQUEST scales automatically and doesn’t require capacity settings.
When are readCapacity and writeCapacity required?
These fields are required when billingMode is set to PROVISIONED. They’re not used with PAY_PER_REQUEST mode.
Global Tables & Replication
How do I create a Global Table with replicas in multiple regions?
Add replica blocks specifying the regionName for each region. You must enable streamEnabled: true and set streamViewType (typically NEW_AND_OLD_IMAGES) for Global Tables.
How do I enable Multi-Region Strong Consistency for my Global Table?
Set consistencyMode: STRONG on each replica block. This ensures strongly consistent reads across all regions, though it comes with restrictions detailed in AWS documentation.
How do I manage tags for Global Table replicas?
You can either set propagateTags: true on individual replicas to inherit table tags, or use the aws.dynamodb.Tag resource to manage replica tags separately.
Keys, Indexes & Attributes
What attributes should I define in my table schema?
Only define attributes used as the table’s hash key, range key, or GSI/LSI keys. DynamoDB is schemaless, you don’t need to define all columns, just the key attributes.
Can I modify Local Secondary Indexes after creating the table?
No, LSIs can only be allocated at table creation and cannot be changed afterward. Plan your LSI configuration carefully before creating the table.
How do I set up a Global Secondary Index?
Configure globalSecondaryIndexes with hashKey, optional rangeKey, capacity settings, and projectionType. Define the GSI key attributes in the table’s attributes list.
Features & Options
How do I enable Time to Live (TTL) on my table?
Configure the ttl block with attributeName (the column containing expiration timestamps) and enabled: true.
What are the options for DynamoDB Streams?
Set streamEnabled: true and choose a streamViewType: KEYS_ONLY (only key attributes), NEW_IMAGE (entire item after change), OLD_IMAGE (before change), or NEW_AND_OLD_IMAGES (both).
Is my DynamoDB table encrypted by default?
Yes, DynamoDB tables are automatically encrypted at rest with an AWS-owned Customer Master Key if you don’t specify serverSideEncryption. For cross-region restores, you must explicitly configure encryption.

Ready to get started?

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Create free account