AWS Database Blog

Zero-downtime DynamoDB construct migration: from Table to TableV2 with cdk orphan

When you define an Amazon DynamoDB table using the AWS Cloud Development Kit (AWS CDK), you have two construct options: Table and TableV2. Both create fully functional DynamoDB tables, and for single AWS Region workloads, Table is feature-rich and works well. However, when your application must go global with replicas, TableV2 becomes the better choice. It maps directly to the AWS::DynamoDB::GlobalTable AWS CloudFormation resource, which means replicas are managed natively by CloudFormation with per-replica configuration, drift detection, and no custom resource AWS Lambda functions.

The challenge is migrating. If you swap Table for TableV2 in your CDK code, CloudFormation interprets this as a resource replacement. It removes your existing table and creates a new one. Previously, avoiding this required a careful, multi-step manual process through the CloudFormation console. With cdk orphan, you can now perform this migration directly from the CLI.

In this post, we show you how to use the new cdk orphan command to safely migrate a DynamoDB table from the Table construct to TableV2 with zero downtime. Your data stays intact, streams keep flowing, and your application remains available throughout the process.

Why start with TableV2

The Table construct has served CDK users well and continues to be fully supported. For single-Region tables, it provides everything you need: on-demand or provisioned capacity, global secondary indexes, DynamoDB Streams, and more.

The difference becomes apparent when you need replicas. The Table construct uses a Lambda-backed custom resource to manage replicas, and the replicationRegions property only accepts a list of Region strings. You can’t configure properties like contributor insights, point-in-time recovery, or table class on a per-replica basis. These settings apply uniformly across all replicas.

TableV2 addresses this by mapping directly to AWS::DynamoDB::GlobalTable. It works for single-Region tables too, so starting with TableV2 future-proofs your infrastructure. If your application must go global, you can add replicas without changing constructs.

The following table summarizes the key differences when replicas are involved:

Capability Table (with replicas) TableV2
CloudFormation resource AWS::DynamoDB::Table + custom resource AWS::DynamoDB::GlobalTable
Replica management Lambda-backed custom resource Native CloudFormation
Per-replica configuration (PITR, table class, contributor insights, capacity) Not supported Supported
Drift detection for replicas Not supported Supported
Customer-managed KMS per replica Not supported Supported

The migration challenge

Consider a CDK stack with a DynamoDB table that stores order data, with a Lambda function consuming its stream through an event source mapping. This is a common pattern for event-driven architectures.

Whether your table is single-Region or already has replicas configured through the Table construct’s replicationRegions property, the migration path is the same. The cdk orphan workflow works in both cases. If you already have replicas, they remain intact throughout the process.

Your business is growing and you must switch to TableV2, either to add your first replica, or to gain per-replica configuration and native CloudFormation management for replicas that you already have. But if you replace the construct:

// Changing from Table to TableV2 causes CloudFormation to REPLACE the resource
const table = new dynamodb.TableV2(this, 'OrdersTable', {
  tableName: 'OrdersTable',
  partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
  sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
  billing: dynamodb.Billing.onDemand(),
  dynamoStream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
  replicas: [{ region: 'eu-west-1' }],
});

processor.addEventSource(new eventsources.DynamoEventSource(table, {
  startingPosition: lambda.StartingPosition.LATEST,
}));

Running cdk diff reveals the problem:

[-] AWS::DynamoDB::Table OrdersTable794EDED1 destroy
[+] AWS::DynamoDB::GlobalTable OrdersTable794EDED1

CloudFormation plans to delete the existing AWS::DynamoDB::Table and create a new AWS::DynamoDB::GlobalTable. Your data, stream ARN, and active event source mappings would be lost.

Solution overview

The cdk orphan command solves this by detaching a resource from its CloudFormation stack without deleting it. Combined with cdk import, you can re-import the same physical resource under a new construct. The workflow has three steps:

  1. Orphan – Detach the existing table from the stack. CloudFormation removes the resource from its state, but the table continues to serve traffic.
  2. Update your CDK code – Replace the Table construct with TableV2, pointing to the same physical table.
  3. Import – Import the existing table into the stack under the new TableV2 construct.

Throughout this process, the DynamoDB table remains fully operational. Reads, writes, and stream processing continue uninterrupted.

Prerequisites

To follow along with this walkthrough, you need:

  • An AWS account.
  • AWS CDK v2.x installed with the cdk orphan command available.
  • Node.js 20.x or later.
  • A bootstrapped CDK environment with bootstrap stack version 32 or later. The cdk orphan command requires additional AWS Identity and Access Management (IAM) permissions added in this version. If your environment is already bootstrapped, re-run cdk bootstrap to update.

Walkthrough

Set up the project

Before we begin, we must create a new CDK project and install dependencies. Create a directory, initialize a TypeScript CDK app, and then replace the generated stack code with our example:

mkdir orders-app && cd orders-app
cdk init app --language=typescript

Replace the contents of lib/orders-app-stack.ts with the following code. This creates a DynamoDB table with streams enabled and a Lambda function that processes stream events through an event source mapping:

import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as eventsources from 'aws-cdk-lib/aws-lambda-event-sources';
import { Construct } from 'constructs';

export class OrdersAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const table = new dynamodb.Table(this, 'OrdersTable', {
      tableName: 'OrdersTable',
      partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
    });

    const processor = new lambda.Function(this, 'StreamProcessor', {
      runtime: lambda.Runtime.NODEJS_LATEST,
      handler: 'index.handler',
      code: lambda.Code.fromInline('exports.handler = async (event) => { console.log(JSON.stringify(event)); };'),
    });

    processor.addEventSource(new eventsources.DynamoEventSource(table, {
      startingPosition: lambda.StartingPosition.LATEST,
    }));
  }
}

Step 1: Deploy the initial stack

Bootstrap your environment (or update to version 32 if already bootstrapped):

cdk bootstrap

Then deploy the stack:

cdk deploy OrdersAppStack

After the deployment completes, you should see the stack outputs:

✅  OrdersAppStack

Outputs:
OrdersAppStack.TableName = OrdersTable
OrdersAppStack.StreamArn = arn:aws:dynamodb:us-east-1:123456789012:table/OrdersTable/stream/...

Verify that the table is created:

aws dynamodb describe-table --table-name OrdersTable \
  --query 'Table.{TableName:TableName,StreamArn:LatestStreamArn,TableArn:TableArn}' \
  --output table

Now, add some data to the table. We will verify that this data is still accessible after the migration:

aws dynamodb put-item --table-name OrdersTable \
  --item '{"PK":{"S":"USER#123"},"SK":{"S":"ORDER#001"},"CurrentStatus":{"S":"SHIPPED"}}'

aws dynamodb put-item --table-name OrdersTable \
  --item '{"PK":{"S":"USER#123"},"SK":{"S":"ORDER#002"},"CurrentStatus":{"S":"PENDING"}}'

Step 2: Orphan the table

Before orphaning, we recommend creating an on-demand backup as a precaution:

aws dynamodb create-backup --table-name OrdersTable --backup-name pre-migration-backup

Use cdk orphan to detach the table from the CloudFormation stack:

cdk orphan OrdersAppStack/OrdersTable --unstable=orphan

The --unstable=orphan flag is required because this command is currently in developer preview. This means the API might change in future releases before it becomes stable.

If you have multiple tables to migrate, you can orphan them in a single command by passing multiple construct paths:

cdk orphan OrdersAppStack/OrdersTable OrdersAppStack/CustomersTable --unstable=orphan

The command performs three deployments behind the scenes:

  1. Resolve references – Temporarily adds CloudFormation outputs to capture Ref and Fn::GetAtt values that other resources depend on (such as the table name and stream ARN referenced by the Lambda event source mapping).
  2. Decouple – Replaces all cross-resource references with their resolved literal values, sets DeletionPolicy: Retain, and removes DependsOn entries. This isolates the resource from the rest of the stack.
  3. Remove – Removes the resource from the template entirely. CloudFormation drops it from its state, but because the deletion policy was set to Retain, the physical table remains untouched.

After the orphan completes, the command outputs a resource mapping that you will use for the import step:

✅ Resources orphaned from OrdersAppStack

Next steps:
  1. Update your CDK code to use the new resource type
  2. cdk import --resource-mapping-inline '{"OrdersTable794EDED1":{"TableName":"OrdersTable"}}'

Save this value. You will need it in Step 4.

Step 3: Update your CDK code

Replace the Table construct with TableV2 in lib/orders-app-stack.ts. The construct shape is slightly different, so you will need to adjust the properties to suit TableV2:

const table = new dynamodb.TableV2(this, 'OrdersTable', {
      tableName: 'OrdersTable',
      partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
      billing: dynamodb.Billing.onDemand(),
      dynamoStream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
    });

The Lambda function and event source mapping remain unchanged. Note that we haven’t added replicas yet. We first import the existing table under TableV2, then add replicas in a subsequent deployment.

Step 4: Import the table

Use cdk import with the resource mapping from Step 2 to bring the existing table into the stack under the new TableV2 construct:

cdk import OrdersAppStack --resource-mapping-inline '{"OrdersTable794EDED1":{"TableName":"OrdersTable"}}'

CloudFormation imports the existing physical table and associates it with the new AWS::DynamoDB::GlobalTable resource in the template. No data is moved or modified.

After the import completes, cdk import will detect drift between the imported resource and your template and prompt you to deploy. Accept the prompt to reconcile the template with the actual resource state, updating the Lambda event source mapping and IAM policies to reference the imported table’s attributes.

Step 5: Add replicas

Now that your table is managed by TableV2, you can add replicas. Update your CDK code:

const table = new dynamodb.TableV2(this, 'OrdersTable', {
  tableName: 'OrdersTable',
  partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
  sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
  billing: dynamodb.Billing.onDemand(),
  dynamoStream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
  replicas: [
    {
      region: 'eu-west-1',
      contributorInsights: true,
    },
  ],
});

Deploy:

cdk deploy OrdersAppStack

CloudFormation natively creates the replica with no custom resource Lambda, full drift detection, and per-replica configuration from day one.

Verifying the migration

After the migration, verify that everything is intact. First, confirm that the table exists and has the replica:

aws dynamodb describe-table --table-name OrdersTable \
  --query 'Table.{Status:TableStatus,Replicas:Replicas,StreamArn:LatestStreamArn}'

Then verify the data that we inserted before the migration is still accessible:

aws dynamodb get-item --table-name OrdersTable \
  --key '{"PK":{"S":"USER#123"},"SK":{"S":"ORDER#001"}}' \
  --consistent-read

You should see the item returned with CurrentStatus: SHIPPED, confirming that your data survived the migration. The stream ARN is preserved, and the Lambda event source mapping continues to process events. The table is now managed by TableV2 with a replica in eu-west-1.

Considerations

Keep the following in mind when performing this migration:

  • Import before adding new replicas – Import the table as a TableV2 first, then add new replicas in a subsequent deployment. If your table already has replicas, include them in your TableV2 definition at import time so CloudFormation recognizes them.
  • Stream consumers – If you have Lambda functions consuming the DynamoDB stream, the stream ARN is preserved during the migration. Event source mappings continue to work without modification.
  • Test in a non-production environment first – Validate the full workflow in a development or staging environment before performing the migration on production tables.

Cleaning up

If you created resources to follow along with this walkthrough, delete them to avoid ongoing charges:cdk destroy OrdersAppStack

Conclusion

Migrating from the CDK Table construct to TableV2 no longer requires extensive manual operations. It has always been possible to perform these steps manually through the CloudFormation console, but the process is time consuming and adds operational overhead. With cdk orphan, you can safely detach a DynamoDB table from its CloudFormation stack, update your CDK code to use TableV2, and re-import the same physical table, all while your application continues to serve traffic.

If you’re starting a new project, we recommend using TableV2 from the beginning. It works for single-Region tables and positions you to add replicas with native CloudFormation support whenever your application must go global.

To learn more, see the TableV2 construct documentation and the AWS CDK CLI reference.


About the authors

Lee Hannigan

Lee Hannigan

Lee is a Sr. Amazon DynamoDB Database Engineer based in Donegal, Ireland. He brings a wealth of expertise in distributed systems, with a strong foundation in big data and analytics technologies. In his role, Lee focuses on advancing the performance, scalability, and reliability of DynamoDB while helping customers and internal teams make the most of its capabilities.