AWS Database Blog
New in Terraform: Manage global secondary index drift in Amazon DynamoDB
If you’ve ever adjusted Amazon DynamoDB global secondary index capacity (GSI) outside Terraform, you know how Terraform detects drift and forces unwanted reverts. With Terraform’s new aws_dynamodb_global_secondary_index resource, you can address this problem.
The new aws_dynamodb_global_secondary_index resource treats each GSI as an independent resource with its own lifecycle management. You can use this feature to make capacity adjustments for GSI and tables outside of Terraform.
In this post, I demonstrate how to use Terraform’s new aws_dynamodb_global_secondary_index resource to manage GSI drift selectively. I walk you through the limitations of current approaches and guide you through implementing the solution.
The problem: Terraform drift and GSI management
Before diving into the solution, let’s establish what drift means in infrastructure management. In infrastructure as code (IaC), drift occurs when the actual state of your infrastructure differs from what’s defined in your Terraform configuration. Terraform detects drift by comparing the desired state (your .tf configuration files), the last known state (stored in terraform.tfstate), and the actual state (queried from AWS). When these don’t match, Terraform reports drift and proposes changes to reconcile the difference.
DynamoDB GSIs often require capacity adjustments for various operational reasons: load testing, capacity planning, emergency performance requirements, or managing warm throughput. Your DynamoDB capacity can also be changed by autoscaling events. Whenever you make these changes outside of Terraform, it creates drift between Terraform’s configuration and AWS reality.
For example, let’s assume your analytics team runs a daily report that queries a GSI heavily. The report runs at 2:00 AM and needs 50 read capacity units (RCUs), but during normal hours, 5 RCUs is sufficient. Your operations team manually increases capacity before the report runs to handle the load.
At 1:50 AM, your ops team increases capacity from 5 to 50 using AWS Command Line Interface (AWS CLI). The report runs from 2:00 AM to 3:00 AM with the higher capacity. Later that day, when you run terraform plan to deploy an unrelated change, Terraform detects drift because the actual capacity (50) doesn’t match your configuration (5). Terraform wants to revert the capacity back to 5, which would interfere with your operational capacity management.
The common workaround and its limitations
A common workaround is to use ignore_changes = [global_secondary_index] in your table’s lifecycle block. This prevents Terraform from detecting capacity drift. However, this approach is too broad—it ignores all GSI changes, not just capacity. Because global_secondary_index is a complex nested type, ignore_changes only works at the top-level, not at individual attributes. If someone accidentally deletes a GSI or modifies its key schema, Terraform won’t detect it. You can’t distinguish between intentional capacity tuning and accidental GSI deletions.
The solution: Separate GSI resources
The new aws_dynamodb_global_secondary_index resource treats each GSI as an independent resource with its own lifecycle management. This gives you granular control over which attributes to ignore for each GSI while still detecting important changes like deletions or schema modifications.
Prerequisites
Before you begin, verify you have:
- An AWS account with permissions to create and manage DynamoDB tables (needed to create the test resources that you will use in examples)
- An Amazon Elastic Compute Cloud (Amazon EC2) instance running Amazon Linux with an AWS Identity and Access Management (IAM) role that has DynamoDB permissions
- AWS Command Line Interface (AWS CLI) installed and configured
- AWS Terraform Provider version 6.28.0 or later (the
aws_dynamodb_global_secondary_indexresource was introduced in v6.28.0)
The aws_dynamodb_global_secondary_index resource is currently marked as experimental in the Terraform AWS provider. This means the schema or behavior might change without notice, and isn’t subject to the backwards compatibility guarantee of the provider.
You must set the environment variable TF_AWS_EXPERIMENT_dynamodb_global_secondary_index to enable this experimental resource. Without this environment variable, Terraform will return an error when attempting to use aws_dynamodb_global_secondary_index. Set it before running any Terraform commands:
Test thoroughly in non-production environments before using in production. You are welcome to provide feedback at GitHub Issue #45640.
If you’re upgrading from AWS Provider v5.x to v6.x, review the v6.0.0 upgrade guide for breaking changes before proceeding.
Install Terraform on Amazon Linux:
Using the new resource
Create a provisioned capacity table with two GSIs using the new separate resource method. You’ll create main.tf where the table and GSIs are defined as independent resources.
Table and GSI keys:
| Resource | Hash key | Range key | Capacity |
|---|---|---|---|
| Table | id | timestamp | 5/5 |
| StatusUserIndex | status | user_id | 5/5 |
| TimestampIndex | timestamp | – | 3/3 |
Configuration:
Deploy the resources:
Test selective ignore_changes by manually changing the capacity of StatusUserIndex (the one with ignore_changes):
Running terraform plan shows No changes even though StatusUserIndex capacity changed to 10/10 in AWS. This occurs because of ignore_changes = [provisioned_throughput].
Verify drift detection still works by manually changing TimestampIndex (the one without ignore_changes):
Running terraform plan detects the drift and proposes to change TimestampIndex capacity from 8 back to 3. This demonstrates that:
StatusUserIndexshows no changes (capacity ignored as intended)TimestampIndexshows drift detection (capacity changes detected)- Each GSI has independent lifecycle management
- You can selectively ignore specific attributes per GSI
- Terraform still detects important changes on GSIs without
ignore_changes.
The key differences from the traditional method are that the table defines attributes used by the table itself (id, timestamp), while GSI-specific attributes (status, user_id) are defined in the separate GSI resource’s key_schema blocks with their attribute_type (required in new resource). If a GSI reuses a table attribute, that attribute remains in the table’s attribute block. The GSI is a separate resource with its own lifecycle.
Benefits of the new resource
The new resource model provides several advantages. You can now ignore specific attributes of a GSI without affecting other GSIs and automated scripts can adjust capacity based on traffic patterns without creating Terraform drift. You still track important changes like key schema modifications, confirming no accidental GSI deletions or reconfigurations. Terraform state remains the source of truth for GSI structure, while DynamoDB APIs show actual runtime capacity.
Each GSI can have its own lifecycle rules, providing independent management. The new resource model follows Terraform best practices where each resource manages one logical infrastructure component, dependencies are explicit through resource references, and state management is more straightforward.
The new resource fully supports warm throughput configuration for on-demand tables. Warm throughput is a DynamoDB capability that you can use to specify baseline capacity for on-demand tables, helping you manage performance and costs more predictably. This is how you can test it.
Create ondemand.tf:
Deploy and test:
Terraform shows No changes because warm throughput changes are ignored as expected.
Before moving to the next section, destroy the on-demand test resources:terraform destroy
Migration example
Now that you’ve seen how the new resource works, let’s walk through a complete hands-on migration of existing infrastructure. Start with a table using the traditional nested GSI approach, then migrate it to the new separate resource method without any downtime.
Step 1: Create infrastructure with traditional method
Create a DynamoDB table with a GSI using the traditional nested block approach.
Create a file called migration-old.tf:
Deploy this infrastructure:
Verify the table and GSI were created:
Output:
Step 2: Prepare for migration
Before migrating, backup your Terraform state:
Set the required environment variable:
Step 3: Update your Terraform configuration
Create a new file called migration-new.tf with the updated configuration. Keep both files for now—you will remove the old one after import.
Step 4: Remove the old configuration
Now remove or rename the old file:
At this point, if you run terraform plan, you’ll see that Terraform wants to remove the GSI from the table (because the nested block is gone) and create a new separate GSI resource.
Don’t apply yet. This would cause downtime. Instead, import the existing GSI.
Step 5: Import the existing GSI
Import the existing GSI into the new resource’s state:
Output:
Step 6: Verify the migration
Run terraform plan to verify:
Expected output:
If you see No changes, the migration was successful. The GSI is now managed as a separate resource.
Migration summary
To complete a migration, you started with a traditional nested GSI configuration, which you then migrated to separate GSI resources without downtime using terraform import. You then verified the migration with terraform plan showing No changes, after which you successfully transitioned to the new resource model.
Key takeaways:
- Migration uses
terraform import - No AWS resources are modified or recreated
- The GSI continues to exist throughout the migration with zero downtime
- After migration, you have granular control over what to ignore with
ignore_changes - The migration process is safe and reversible
Migration considerations
Do not combine aws_dynamodb_global_secondary_index resources with global_secondary_index blocks on aws_dynamodb_table. Doing so might cause conflicts, perpetual differences, and GSIs being overwritten.
When migrating, follow these steps:
- Backup state:
terraform state pull > backup.tfstate - Set environment variable:
export TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1 - Update configuration: Remove the GSI block from table the table and create a new GSI resource
- Import existing GSI:
terraform import <resource> 'table_name,index_name' - Verify: Run
terraform plan, it should showNo changes - Test: Manually change capacity and verify that Terraform ignores the change
You won’t experience downtime during migration if done correctly using terraform import. The GSI continues to exist in AWS throughout the migration. The terraform import command only updates Terraform’s state file—it doesn’t modify AWS resources.
If your table has multiple GSIs, migrate them one at a time:
- Import the first GSI and verify with
terraform plan - Import the second GSI and verify with
terraform plan - Continue until all GSIs are migrated
This reduces risk and simplifies troubleshooting.
Comparison: Traditional compared to new method
The following table summarizes the key differences between the traditional nested block approach and the new separate resource method:
| Aspect | Traditional method (nested block) | New method (separate resource) |
|---|---|---|
| Resource enablement | No environment variable needed | Requires TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1 |
Granular ignore_changes |
Not supported | Supported |
| Independent GSI management | All GSIs managed together | Each GSI managed independently |
| Drift detection | All-or-nothing | Selective per GSI |
| Lifecycle rules | Applies to all GSIs | Per-GSI lifecycle rules |
| State management | Complex nested state | Straightforward flat state |
| Capacity configuration | Top-level attributes (read_capacity, write_capacity) |
Block syntax (provisioned_throughput block) |
| Projection configuration | Top-level attribute (projection_type) |
Block syntax (projection block) |
| Warm throughput support | Limited | Full support (attribute syntax: warm_throughput = { }) |
| Migration complexity | N/A | Requires import process |
| Backward compatibility | Existing method | Cannot mix with traditional method |
| Stability | Stable | Experimental (schema might change) |
Clean up
To avoid incurring future charges, delete the resources you created in this walkthrough:
If you created any resources manually during testing, make sure to delete those as well through the AWS Management Console or AWS CLI to avoid incurring future costs.
Conclusion
In this post, I showed you how the new aws_dynamodb_global_secondary_index resource solves the long-standing challenge of managing DynamoDB GSI drift in Terraform. The all-or-nothing nature of ignoring nested global_secondary_index blocks created a gap between operational flexibility and infrastructure governance.
By treating GSIs as first-class resources, you gain granular control with selective ignore_changes for specific GSI attributes, independent management where each GSI has its own lifecycle rules, better drift detection that tracks important changes while allowing operational adjustments, and a more straightforward architecture with separation of concerns between table and index configuration.
Remember that the aws_dynamodb_global_secondary_index resource is currently marked as experimental. While it provides powerful capabilities for managing GSI drift, be aware that:
- The schema or behavior might change in future provider versions
- You must set the environment variable
TF_AWS_EXPERIMENT_dynamodb_global_secondary_index=1to enable this resource - It’s not subject to the backwards compatibility guarantee of the provider
- You can’t mix this resource with traditional
global_secondary_indexblocks on the same table
Always test thoroughly in non-production environments and monitor provider release notes for updates. If you have feedback, provide it at GitHub Issue #45640 to help shape the future of this feature.