Create and Configure DynamoDB Tables

The aws:dynamodb/table:Table resource, part of the Pulumi AWS provider, defines a DynamoDB table: its key schema, indexes, capacity mode, and optional global replication. This guide focuses on three capabilities: primary key and Global Secondary Index configuration, TTL for automatic item cleanup, and Global Tables for multi-region replication.

Tables operate within a single AWS region by default; global replication requires stream configuration and replica definitions. The examples are intentionally small. Combine them with your own access patterns, backup strategies, and monitoring.

Create a table with hash and range keys

Most deployments start with a hash key for partitioning and a range key for sorting items within partitions.

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 sorting and range queries within a partition. The attributes array declares only the key attributes; DynamoDB is schemaless for non-key attributes. The billingMode controls whether you provision capacity (PROVISIONED) or pay per request (PAY_PER_REQUEST).

Add a Global Secondary Index for alternate queries

Applications often need to query by attributes other than the primary key. GSIs create alternate key structures for efficient queries.

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 globalSecondaryIndexes array defines alternate query patterns. Each GSI has its own hash and range keys drawn from the attributes array. The projectionType controls which attributes are copied to the index: INCLUDE copies specified nonKeyAttributes, ALL copies everything, KEYS_ONLY copies only keys. Here, querying by GameTitle and TopScore doesn’t require scanning the base table.

Enable automatic item expiration with TTL

Tables storing sessions or temporary records benefit from automatic cleanup without consuming write capacity.

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 ttl block enables Time To Live. Set attributeName to a numeric timestamp attribute (Unix epoch seconds). DynamoDB deletes expired items within 48 hours of expiration, freeing storage without write capacity charges.

Replicate tables across regions for global access

Applications serving worldwide users need low-latency access from multiple regions. Global Tables V2 replicate data automatically.

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 defines target regions. DynamoDB replicates writes to all regions with eventual consistency. The streamEnabled and streamViewType properties are required; streams capture changes for replication. Each replica maintains its own read and write capacity.

Configure strong consistency across three regions

Financial applications requiring immediate consistency can use Multi-Region Strong Consistency with three 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",
            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

Setting consistencyMode to STRONG on replicas enables strongly consistent reads from any region. This requires PAY_PER_REQUEST billing and at least three replicas (or two replicas plus a witness region). Strongly consistent reads always return the latest data regardless of which region you query.

Beyond these examples

These snippets focus on specific table-level features: primary key design and Global Secondary Indexes, TTL for automatic item expiration, and global replication with eventual and strong consistency. They’re intentionally minimal rather than full data models.

The examples assume pre-existing infrastructure such as an AWS provider with configured region(s). They focus on table configuration rather than provisioning surrounding infrastructure.

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

  • Point-in-time recovery (pointInTimeRecovery)
  • Encryption configuration (serverSideEncryption)
  • Local Secondary Indexes (localSecondaryIndexes)
  • On-demand throughput limits (onDemandThroughput)
  • Deletion protection (deletionProtectionEnabled)
  • Table restore operations (restoreSourceName, restoreDateTime)

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.

Let's create and Configure DynamoDB Tables

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

Try Pulumi Cloud for FREE

Frequently Asked Questions

Common Errors & Pitfalls
Why am I seeing an infinite loop during planning?
This happens when you define attributes that aren’t used as keys. Only define attributes used as table hash/range keys, LSI keys, or GSI keys. Defining unused attributes causes an infinite planning loop.
Why is my globalTableWitness configuration failing to provision?
globalTableWitness must be used alongside exactly one replica with consistencyMode set to STRONG. Other combinations will fail to provision.
Can I use both replica blocks and aws.dynamodb.TableReplica?
No. Choose one approach: either use replica configuration blocks in aws.dynamodb.Table OR use the aws.dynamodb.TableReplica resource, not both together.
Configuration & Capacity
Why should I use ignoreChanges for capacity settings with autoscaling?
When an autoscaling policy is attached to your table, it modifies readCapacity and writeCapacity outside of Pulumi, causing drift. Use ignoreChanges for these properties to prevent perpetual diffs.
How does billing mode affect capacity configuration?
billingMode defaults to PROVISIONED, which requires you to specify readCapacity and writeCapacity. With PAY_PER_REQUEST, capacity settings are not required.
What's the default for deletion protection?
Deletion protection is disabled by default (deletionProtectionEnabled defaults to false). Enable it explicitly to prevent accidental table deletion.
Attributes & Keys
Which attributes should I define in my table?
Only define attributes used as keys: table hash/range key, LSI hash/range key, or GSI hash/range key. Defining unused attributes causes an infinite planning loop.
What properties can't be changed after table creation?
hashKey, name, rangeKey, and localSecondaryIndexes are immutable. Changing these requires recreating the table. LSIs can only be allocated at creation time.
Do I need to define attributes for externally managed GSIs?
No. When using the aws.dynamodb.GlobalSecondaryIndex resource to manage GSIs separately, you don’t need to define those GSI attributes in the aws.dynamodb.Table resource.
Global Tables & Replication
How do I avoid drift when managing replicas?
If you’re using the aws.dynamodb.TableReplica resource, add ignoreChanges for the replica property in your table’s lifecycle configuration to prevent drift.
How do I set up Multi-Region Strong Consistency?
Configure replicas with consistencyMode set to STRONG. You can use three replicas, or two replicas plus one globalTableWitness in a different region.
Indexes
How do I configure a Global Secondary Index with multi-attribute keys?
Use the keySchema block instead of hashKey/rangeKey for GSIs. The hashKey and rangeKey arguments are deprecated in favor of keySchema.
How do I handle autoscaling drift for Global Secondary Indexes?
If autoscaling creates drift for your globalSecondaryIndex blocks, use the experimental aws.dynamodb.GlobalSecondaryIndex resource for more granular lifecycle management.

Using a different cloud?

Explore dynamodb guides for other cloud providers: