@@ -26,6 +26,7 @@ import (
2626 sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
2727 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2828 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
29+
2930 "github.com/hashicorp/terraform-provider-aws/internal/conns"
3031 "github.com/hashicorp/terraform-provider-aws/internal/create"
3132 tfcty "github.com/hashicorp/terraform-provider-aws/internal/cty"
@@ -98,6 +99,7 @@ func resourceTable() *schema.Resource {
9899 }
99100 return nil
100101 },
102+ mrscReplicaTableDrift ,
101103 customdiff .ForceNewIfChange ("restore_source_name" , func (_ context.Context , old , new , meta any ) bool {
102104 // If they differ force new unless new is cleared
103105 // https://github.com/hashicorp/terraform-provider-aws/issues/25214
@@ -461,6 +463,16 @@ func resourceTable() *schema.Resource {
461463 Type : schema .TypeString ,
462464 Computed : true ,
463465 },
466+ // Per-replica stream state (from the replica table's
467+ // StreamSpecification), to detect MRSC replica stream drift.
468+ "stream_enabled" : {
469+ Type : schema .TypeBool ,
470+ Computed : true ,
471+ },
472+ "stream_view_type" : {
473+ Type : schema .TypeString ,
474+ Computed : true ,
475+ },
464476 },
465477 },
466478 },
@@ -1009,6 +1021,14 @@ func resourceTableCreate(ctx context.Context, d *schema.ResourceData, meta any)
10091021 if err := updateReplicaTags (ctx , conn , aws .ToString (output .TableArn ), v .List (), keyValueTags (ctx , getTagsIn (ctx ))); err != nil {
10101022 return create .AppendDiagError (diags , names .DynamoDB , create .ErrActionCreating , resNameTable , d .Id (), fmt .Errorf ("replica tags: %w" , err ))
10111023 }
1024+
1025+ // Streaming configuration is not automatically propagated to MRSC replica
1026+ // tables, so propagate the configuration explicitly.
1027+ if d .Get ("stream_enabled" ).(bool ) {
1028+ if err := updateReplicaStreams (ctx , conn , d .Id (), v .List (), true , d .Get ("stream_view_type" ).(string ), d .Timeout (schema .TimeoutCreate )); err != nil {
1029+ return create .AppendDiagError (diags , names .DynamoDB , create .ErrActionCreating , resNameTable , d .Id (), fmt .Errorf ("replica streams: %w" , err ))
1030+ }
1031+ }
10121032 }
10131033
10141034 return append (diags , resourceTableRead (ctx , d , meta )... )
@@ -1514,6 +1534,14 @@ func resourceTableUpdate(ctx context.Context, d *schema.ResourceData, meta any)
15141534 }
15151535 }
15161536
1537+ // Reconcile the DynamoDB Streams configuration of MRSC STRONG
1538+ // consistency-mode replicas to match the global-table level desired state.
1539+ if replicas := d .Get ("replica" ).(* schema.Set ); replicas .Len () > 0 {
1540+ if err := updateReplicaStreams (ctx , conn , d .Id (), replicas .List (), d .Get ("stream_enabled" ).(bool ), d .Get ("stream_view_type" ).(string ), d .Timeout (schema .TimeoutUpdate )); err != nil {
1541+ return create .AppendDiagError (diags , names .DynamoDB , create .ErrActionUpdating , resNameTable , d .Id (), err )
1542+ }
1543+ }
1544+
15171545 if d .HasChange ("point_in_time_recovery" ) {
15181546 if err := updatePITR (ctx , conn , d .Id (), d .Get ("point_in_time_recovery.0.enabled" ).(bool ), aws .Int32 (int32 (d .Get ("point_in_time_recovery.0.recovery_period_in_days" ).(int ))), meta .(* conns.AWSClient ).Region (ctx ), d .Timeout (schema .TimeoutUpdate )); err != nil {
15191547 return create .AppendDiagError (diags , names .DynamoDB , create .ErrActionUpdating , resNameTable , d .Id (), err )
@@ -1614,6 +1642,136 @@ func cycleStreamEnabled(ctx context.Context, conn *dynamodb.Client, id string, s
16141642 return nil
16151643}
16161644
1645+ // updateReplicaStreams propagates the global-table level streaming
1646+ // configuration to each MRSC replica of the table when the consistency mode
1647+ // is STRONG.
1648+ //
1649+ // MREC replicas are skipped as DynamoDB uses streams for replication with that
1650+ // consistency mode and hence streaming cannot be disabled/modified.
1651+ func updateReplicaStreams (ctx context.Context , conn * dynamodb.Client , tableName string , replicas []any , streamEnabled bool , streamViewType string , timeout time.Duration ) error {
1652+ for _ , tfMapRaw := range replicas {
1653+ tfMap , ok := tfMapRaw .(map [string ]any )
1654+ if ! ok {
1655+ continue
1656+ }
1657+
1658+ // Only MRSC (STRONG) replicas need explicit stream propagation.
1659+ if v , ok := tfMap ["consistency_mode" ].(string ); ! ok || awstypes .MultiRegionConsistency (v ) != awstypes .MultiRegionConsistencyStrong {
1660+ continue
1661+ }
1662+
1663+ region , ok := tfMap ["region_name" ].(string )
1664+ if ! ok || region == "" {
1665+ continue
1666+ }
1667+
1668+ curEnabled , curViewType , err := replicaStreamState (ctx , conn , tableName , region )
1669+ if err != nil {
1670+ return fmt .Errorf ("reading stream state for replica (%s): %w" , region , err )
1671+ }
1672+
1673+ switch {
1674+ case streamEnabled && ! curEnabled :
1675+ // Replica table has streaming disabled, enable it.
1676+ if err := updateReplicaStreamSpecification (ctx , conn , tableName , region , & awstypes.StreamSpecification {
1677+ StreamEnabled : aws .Bool (true ),
1678+ StreamViewType : awstypes .StreamViewType (streamViewType ),
1679+ }, timeout ); err != nil {
1680+ return fmt .Errorf ("enabling stream for replica (%s): %w" , region , err )
1681+ }
1682+ case streamEnabled && curEnabled && string (curViewType ) != streamViewType :
1683+ // Replica table has streaming enabled but is configured with the wrong
1684+ // view type, reconcile the drift.
1685+ if err := cycleReplicaStreamEnabled (ctx , conn , tableName , region , awstypes .StreamViewType (streamViewType ), timeout ); err != nil {
1686+ return fmt .Errorf ("changing stream view type for replica (%s): %w" , region , err )
1687+ }
1688+ case ! streamEnabled && curEnabled :
1689+ // Replica table has streaming enabled, but in the desired state,
1690+ // it should be disabled.
1691+ if err := updateReplicaStreamSpecification (ctx , conn , tableName , region , & awstypes.StreamSpecification {
1692+ StreamEnabled : aws .Bool (false ),
1693+ }, timeout ); err != nil {
1694+ return fmt .Errorf ("disabling stream for replica (%s): %w" , region , err )
1695+ }
1696+ }
1697+ // Else, replica table is already in the desired state.
1698+ }
1699+
1700+ return nil
1701+ }
1702+
1703+ // replicaStreamState returns the current DynamoDB streaming configuration
1704+ // for the specified replica table in the specified region.
1705+ // It returns whether streaming is actually enabled, and if it's enabled,
1706+ // the configured streaming view type for the replica table.
1707+ func replicaStreamState (ctx context.Context , conn * dynamodb.Client , tableName , region string ) (bool , awstypes.StreamViewType , error ) {
1708+ table , err := findTableByName (ctx , conn , tableName , func (o * dynamodb.Options ) { o .Region = region })
1709+ if err != nil {
1710+ return false , "" , err
1711+ }
1712+ if table .StreamSpecification == nil || ! aws .ToBool (table .StreamSpecification .StreamEnabled ) {
1713+ return false , "" , nil
1714+ }
1715+ return true , table .StreamSpecification .StreamViewType , nil
1716+ }
1717+
1718+ // cycleReplicaStreamEnabled disables and then re-enables the stream on a replica Region
1719+ // with streamViewType. It mirrors cycleStreamEnabled for the primary Region and is
1720+ // required to change the stream view type of an already-enabled MRSC replica stream.
1721+ func cycleReplicaStreamEnabled (ctx context.Context , conn * dynamodb.Client , tableName , region string , streamViewType awstypes.StreamViewType , timeout time.Duration ) error {
1722+ if err := updateReplicaStreamSpecification (ctx , conn , tableName , region , & awstypes.StreamSpecification {
1723+ StreamEnabled : aws .Bool (false ),
1724+ }, timeout ); err != nil {
1725+ return fmt .Errorf ("disabling stream: %w" , err )
1726+ }
1727+
1728+ if err := updateReplicaStreamSpecification (ctx , conn , tableName , region , & awstypes.StreamSpecification {
1729+ StreamEnabled : aws .Bool (true ),
1730+ StreamViewType : streamViewType ,
1731+ }, timeout ); err != nil {
1732+ return fmt .Errorf ("re-enabling stream: %w" , err )
1733+ }
1734+
1735+ return nil
1736+ }
1737+
1738+ // updateReplicaStreamSpecification issues a regional UpdateTable to set the stream
1739+ // specification on a single replica region and waits for that replica to become active.
1740+ //
1741+ // Global-table control-plane operations (e.g. an MRSC group still stabilizing after
1742+ // replica creation) can briefly leave the table in the UPDATING state, during which a
1743+ // further UpdateTable is rejected with ResourceInUseException. That error (and throttling)
1744+ // is therefore retried here, mirroring how createReplicas issues its UpdateTable calls.
1745+ func updateReplicaStreamSpecification (ctx context.Context , conn * dynamodb.Client , tableName , region string , streamSpec * awstypes.StreamSpecification , timeout time.Duration ) error {
1746+ input := & dynamodb.UpdateTableInput {
1747+ TableName : aws .String (tableName ),
1748+ StreamSpecification : streamSpec ,
1749+ }
1750+ optFn := func (o * dynamodb.Options ) { o .Region = region }
1751+
1752+ err := tfresource .Retry (ctx , max (replicaUpdateTimeout , timeout ), func (ctx context.Context ) * tfresource.RetryError {
1753+ if _ , err := conn .UpdateTable (ctx , input , optFn ); err != nil {
1754+ if tfawserr .ErrCodeEquals (err , errCodeThrottlingException ) {
1755+ return tfresource .RetryableError (err )
1756+ }
1757+ if errs.IsA [* awstypes.ResourceInUseException ](err ) {
1758+ return tfresource .RetryableError (err )
1759+ }
1760+ return tfresource .NonRetryableError (err )
1761+ }
1762+ return nil
1763+ })
1764+ if err != nil {
1765+ return err
1766+ }
1767+
1768+ if _ , err := waitReplicaActive (ctx , conn , tableName , region , timeout , replicaPropagationDelay ); err != nil {
1769+ return err
1770+ }
1771+
1772+ return nil
1773+ }
1774+
16171775func createReplicas (ctx context.Context , conn * dynamodb.Client , tableName string , tfList []any , globalTableWitnessRegionName string , create bool , timeout time.Duration ) error {
16181776 // Duplicating this for MRSC Adoption. If using MRSC and CreateReplicationGroupMemberAction list isn't initiated for at least 2 replicas
16191777 // then the update table action will fail with
@@ -2622,6 +2780,15 @@ func enrichReplicas(ctx context.Context, conn *dynamodb.Client, arn, tableName s
26222780 }
26232781 tfMap [names .AttrStreamARN ] = aws .ToString (table .LatestStreamArn )
26242782 tfMap ["stream_label" ] = aws .ToString (table .LatestStreamLabel )
2783+ // The streaming configuration for the replica table
2784+ // (whether streaming is enabled and the streaming view type).
2785+ if table .StreamSpecification != nil {
2786+ tfMap ["stream_enabled" ] = aws .ToBool (table .StreamSpecification .StreamEnabled )
2787+ tfMap ["stream_view_type" ] = string (table .StreamSpecification .StreamViewType )
2788+ } else {
2789+ tfMap ["stream_enabled" ] = false
2790+ tfMap ["stream_view_type" ] = ""
2791+ }
26252792
26262793 tfList [i ] = tfMap
26272794 }
@@ -3835,3 +4002,41 @@ func ctyValueLegacyEquals(lhs, rhs cty.Value) bool {
38354002
38364003 return false
38374004}
4005+
4006+ // mrscReplicaTableDrift detects MRSC replica stream drift when the consistency
4007+ // mode is STRONG. Each replica table's actual streaming state is stored in the
4008+ // stream_enabled and stream_view_type computed attributes in enrichReplicas,
4009+ // and if any replica table's streaming configuration does not match the
4010+ // global-table level configuration, a diff is enforced.
4011+ func mrscReplicaTableDrift (_ context.Context , diff * schema.ResourceDiff , _ any ) error {
4012+ if diff .Id () == "" {
4013+ return nil
4014+ }
4015+
4016+ oldReplicas , _ := diff .GetChange ("replica" )
4017+ replicas , ok := oldReplicas .(* schema.Set )
4018+ if ! ok {
4019+ return nil
4020+ }
4021+
4022+ wantEnabled , _ := diff .Get ("stream_enabled" ).(bool )
4023+ wantViewType , _ := diff .Get ("stream_view_type" ).(string )
4024+ for _ , tfMapRaw := range replicas .List () {
4025+ tfMap , ok := tfMapRaw .(map [string ]any )
4026+ if ! ok {
4027+ continue
4028+ }
4029+ if v , _ := tfMap ["consistency_mode" ].(string ); awstypes .MultiRegionConsistency (v ) != awstypes .MultiRegionConsistencyStrong {
4030+ continue
4031+ }
4032+ gotEnabled , _ := tfMap ["stream_enabled" ].(bool )
4033+ gotViewType , _ := tfMap ["stream_view_type" ].(string )
4034+ if gotEnabled != wantEnabled || (wantEnabled && gotEnabled && gotViewType != wantViewType ) {
4035+ if err := diff .SetNewComputed (names .AttrStreamARN ); err != nil {
4036+ return fmt .Errorf ("forcing diff for MRSC replica streaming configuration drift: %w" , err )
4037+ }
4038+ break
4039+ }
4040+ }
4041+ return nil
4042+ }
0 commit comments