Free tool

DynamoDB Terraform, CDK and CloudFormation Generator

Paste the output of aws dynamodb describe-table and get the table back as Terraform, AWS CDK or CloudFormation, global-table replicas included. It runs in your browser; nothing you paste is sent anywhere.

describe-table output
{
  "Table": {
    "TableName": "orders",
    "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/orders",
    "TableStatus": "ACTIVE",
    "KeySchema": [
      { "AttributeName": "PK", "KeyType": "HASH" },
      { "AttributeName": "SK", "KeyType": "RANGE" }
    ],
    "AttributeDefinitions": [
      { "AttributeName": "PK", "AttributeType": "S" },
      { "AttributeName": "SK", "AttributeType": "S" },
      { "AttributeName": "status", "AttributeType": "S" },
      { "AttributeName": "createdAt", "AttributeType": "N" }
    ],
    "BillingModeSummary": { "BillingMode": "PAY_PER_REQUEST" },
    "ProvisionedThroughput": {
      "NumberOfDecreasesToday": 0,
      "ReadCapacityUnits": 0,
      "WriteCapacityUnits": 0
    },
    "GlobalSecondaryIndexes": [
      {
        "IndexName": "by-status",
        "KeySchema": [
          { "AttributeName": "status", "KeyType": "HASH" },
          { "AttributeName": "createdAt", "KeyType": "RANGE" }
        ],
        "Projection": { "ProjectionType": "KEYS_ONLY" },
        "IndexStatus": "ACTIVE"
      }
    ],
    "StreamSpecification": { "StreamEnabled": true, "StreamViewType": "NEW_AND_OLD_IMAGES" },
    "GlobalTableVersion": "2019.11.21",
    "Replicas": [{ "RegionName": "us-west-2", "ReplicaStatus": "ACTIVE" }],
    "DeletionProtectionEnabled": true
  }
}

Paste the JSON that aws dynamodb describe-table prints. To include TTL, add the TimeToLiveDescription object from aws dynamodb describe-time-to-live next to Table.

Infrastructure code
# dynamodb-table-iac: Terraform for DynamoDB table "orders"
# Source: aws dynamodb describe-table, region us-east-1
#
# Not emitted (configure these yourself if the live table uses them):
#   - point-in-time recovery (DescribeTable does not return it; a replica declared without it has PITR off)
#   - tags (DescribeTable does not return them; applying removes live tags)
#   - auto-scaling policies (a fixed capacity snapshot is emitted instead; applying replaces the policy with that snapshot)
#   - warm throughput (DescribeTable reports the CURRENT value, which grows with traffic; emitting it would bill a pre-warm)
#   - contributor insights, Kinesis streaming destinations and resource policies (DescribeTable does not return them)
#   - the KMS key ARN of an SSE-encrypted table (DescribeTable cannot tell an AWS-managed key from a customer-managed one; the AWS-managed key is emitted and the live ARN is left in a comment)
#   - settings of non-home replicas that DynamoDB never synchronizes (their deletion protection, PITR and tags are only visible from their own region)
#
# Notes:
#   - TTL: not provided — include the output of `aws dynamodb describe-time-to-live` to add it.
#
# To adopt the live table instead of creating a new one, uncomment this import
# block, then run `terraform plan` and check that it reports no changes:
# import {
#   to = aws_dynamodb_table.orders
#   id = "orders"
# }

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.29"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_dynamodb_table" "orders" {
  name                        = "orders"
  billing_mode                = "PAY_PER_REQUEST"
  hash_key                    = "PK"
  range_key                   = "SK"
  stream_enabled              = true
  stream_view_type            = "NEW_AND_OLD_IMAGES"
  deletion_protection_enabled = true

  attribute {
    name = "PK"
    type = "S"
  }

  attribute {
    name = "SK"
    type = "S"
  }

  attribute {
    name = "status"
    type = "S"
  }

  attribute {
    name = "createdAt"
    type = "N"
  }

  global_secondary_index {
    name            = "by-status"
    projection_type = "KEYS_ONLY"

    key_schema {
      attribute_name = "status"
      key_type       = "HASH"
    }

    key_schema {
      attribute_name = "createdAt"
      key_type       = "RANGE"
    }
  }

  # Replica blocks default point_in_time_recovery, deletion_protection_enabled and
  # propagate_tags to false: applying this file turns them off on the replicas
  # below unless you set them here.
  replica {
    region_name      = "us-west-2"
    consistency_mode = "EVENTUAL"
  }
}

dynamodb-table-iac is the open-source (MIT) library behind this tool.

Bring a Console-built table under code

Most tables start in the Console or a one-off script. Writing the matching resource by hand means copying every key, index and capacity setting out of describe-table and hoping you missed nothing. This tool reads that same output and writes the definition for you.

Every generated file lists what it leaves out and what applying it over the live table would change, so you read the trade-offs before terraform plan does. The Terraform file also carries a commented import block for adopting the existing table.

The targets differ. The Terraform AWS provider has no vector-index support, so a vector index becomes a commented definition. CloudFormation always uses AWS::DynamoDB::GlobalTable, with a single-region table as one replica, and matches the CDK output resource for resource.

Worked example: a two-region table

The describe-table output below is for an on-demand table with a sort key, one GSI and a replica in us-west-2. The Terraform under it is exactly what the tool produces for it.

{
  "Table": {
    "TableName": "orders",
    "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/orders",
    "TableStatus": "ACTIVE",
    "KeySchema": [
      { "AttributeName": "PK", "KeyType": "HASH" },
      { "AttributeName": "SK", "KeyType": "RANGE" }
    ],
    "AttributeDefinitions": [
      { "AttributeName": "PK", "AttributeType": "S" },
      { "AttributeName": "SK", "AttributeType": "S" },
      { "AttributeName": "status", "AttributeType": "S" },
      { "AttributeName": "createdAt", "AttributeType": "N" }
    ],
    "BillingModeSummary": { "BillingMode": "PAY_PER_REQUEST" },
    "ProvisionedThroughput": {
      "NumberOfDecreasesToday": 0,
      "ReadCapacityUnits": 0,
      "WriteCapacityUnits": 0
    },
    "GlobalSecondaryIndexes": [
      {
        "IndexName": "by-status",
        "KeySchema": [
          { "AttributeName": "status", "KeyType": "HASH" },
          { "AttributeName": "createdAt", "KeyType": "RANGE" }
        ],
        "Projection": { "ProjectionType": "KEYS_ONLY" },
        "IndexStatus": "ACTIVE"
      }
    ],
    "StreamSpecification": { "StreamEnabled": true, "StreamViewType": "NEW_AND_OLD_IMAGES" },
    "GlobalTableVersion": "2019.11.21",
    "Replicas": [{ "RegionName": "us-west-2", "ReplicaStatus": "ACTIVE" }],
    "DeletionProtectionEnabled": true
  }
}
# dynamodb-table-iac: Terraform for DynamoDB table "orders"
# Source: aws dynamodb describe-table, region us-east-1
#
# Not emitted (configure these yourself if the live table uses them):
#   - point-in-time recovery (DescribeTable does not return it; a replica declared without it has PITR off)
#   - tags (DescribeTable does not return them; applying removes live tags)
#   - auto-scaling policies (a fixed capacity snapshot is emitted instead; applying replaces the policy with that snapshot)
#   - warm throughput (DescribeTable reports the CURRENT value, which grows with traffic; emitting it would bill a pre-warm)
#   - contributor insights, Kinesis streaming destinations and resource policies (DescribeTable does not return them)
#   - the KMS key ARN of an SSE-encrypted table (DescribeTable cannot tell an AWS-managed key from a customer-managed one; the AWS-managed key is emitted and the live ARN is left in a comment)
#   - settings of non-home replicas that DynamoDB never synchronizes (their deletion protection, PITR and tags are only visible from their own region)
#
# Notes:
#   - TTL: not provided — include the output of `aws dynamodb describe-time-to-live` to add it.
#
# To adopt the live table instead of creating a new one, uncomment this import
# block, then run `terraform plan` and check that it reports no changes:
# import {
#   to = aws_dynamodb_table.orders
#   id = "orders"
# }

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.29"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_dynamodb_table" "orders" {
  name                        = "orders"
  billing_mode                = "PAY_PER_REQUEST"
  hash_key                    = "PK"
  range_key                   = "SK"
  stream_enabled              = true
  stream_view_type            = "NEW_AND_OLD_IMAGES"
  deletion_protection_enabled = true

  attribute {
    name = "PK"
    type = "S"
  }

  attribute {
    name = "SK"
    type = "S"
  }

  attribute {
    name = "status"
    type = "S"
  }

  attribute {
    name = "createdAt"
    type = "N"
  }

  global_secondary_index {
    name            = "by-status"
    projection_type = "KEYS_ONLY"

    key_schema {
      attribute_name = "status"
      key_type       = "HASH"
    }

    key_schema {
      attribute_name = "createdAt"
      key_type       = "RANGE"
    }
  }

  # Replica blocks default point_in_time_recovery, deletion_protection_enabled and
  # propagate_tags to false: applying this file turns them off on the replicas
  # below unless you set them here.
  replica {
    region_name      = "us-west-2"
    consistency_mode = "EVENTUAL"
  }
}

What the generated code includes

The key schema and the attribute definitions those keys reference, billing mode and capacity for the table and each index, global and local secondary indexes, TTL, deletion protection, streams, encryption, table class, on-demand maximum throughput, vector indexes where the target supports them, and global-table replicas with their consistency mode.

What it leaves out

describe-table does not return everything a table carries. The header of every generated file lists these, with the reason:

  • point-in-time recovery (DescribeTable does not return it; a replica declared without it has PITR off)
  • tags (DescribeTable does not return them; applying removes live tags)
  • auto-scaling policies (a fixed capacity snapshot is emitted instead; applying replaces the policy with that snapshot)
  • warm throughput (DescribeTable reports the CURRENT value, which grows with traffic; emitting it would bill a pre-warm)
  • contributor insights, Kinesis streaming destinations and resource policies (DescribeTable does not return them)
  • the KMS key ARN of an SSE-encrypted table (DescribeTable cannot tell an AWS-managed key from a customer-managed one; the AWS-managed key is emitted and the live ARN is left in a comment)
  • settings of non-home replicas that DynamoDB never synchronizes (their deletion protection, PITR and tags are only visible from their own region)

Frequently asked questions

Is my table definition sent to a server?

No. The code is generated in your browser from the text you paste. Nothing is uploaded, and the page never calls AWS.

How do I include TTL?

TTL is not part of describe-table output. Run aws dynamodb describe-time-to-live for the table and paste its TimeToLiveDescription object next to Table in the same JSON. Without it, the generated file says TTL was not provided.

Which region does the generated code target?

The table’s home region, read from its TableArn. DescribeTable lists every other region as a replica, so run it in the region you manage the table from. Output captured from DynamoDB Local carries no real region, so set the region yourself.

Can I apply it to the table that already exists?

Yes, after reading the header. The Terraform file includes a commented import block: uncomment it and run terraform plan. The plan should change only what the header lists, such as removing live tags, so check each change it reports against that list.

Work with DynamoDB without the Console

A fast DynamoDB desktop client that runs the real SQL DynamoDB can’t — JOINs, GROUP BY, aggregates — with visual editing and an AI agent on your own Bedrock keys.

Free 30-day trial, no credit card — then the Free plan with no time limit.