Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apis/v1alpha1/db_instance.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions config/crd/bases/rds.services.k8s.aws_dbinstances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,37 @@ spec:
* Can't be set to 0 for an RDS Custom for Oracle DB instance.
format: int64
type: integer
backupCrossRegionReplication:
default: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our CRDs are also generated and usually we want to leave any defaults up to the AWS API instead of defining them within ACK.

description: |-
BackupCrossRegionReplication enables cross-region automated backup replication.
When set to true, automated backups will be replicated to the specified destination region.
Default: false
type: boolean
backupCrossRegionReplicationDestinationRegion:
description: |-
BackupCrossRegionReplicationDestinationRegion specifies the AWS region where
automated backups should be replicated. Required when BackupCrossRegionReplication is true.
type: string
backupCrossRegionReplicationKMSKeyID:
description: |-
BackupCrossRegionReplicationKMSKeyID specifies the AWS KMS key identifier for encryption
of the replicated automated backups. The KMS key ID is the Amazon Resource Name (ARN) for
the KMS encryption key in the destination AWS Region.
type: string
backupCrossRegionReplicationRetentionPeriod:
default: 7
description: |-
BackupCrossRegionReplicationRetentionPeriod specifies the number of days to retain
replicated automated backups in the destination region.

Default: 7

Constraints:

* Must be a value from 0 to 35.
format: int64
type: integer
backupTarget:
description: |-
The location for storing automated backups and manual snapshots.
Expand Down
21 changes: 21 additions & 0 deletions generator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,27 @@ resources:
IOPS:
late_initialize:
skip_incomplete_check: {}
BackupCrossRegionReplication:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Does RDS limit a DB instance to only a single cross-region backup or can there be multiple cross-region destinations?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single cross-region

custom_field:
# This field controls whether to call Start/Stop operations
# It's not directly mapped to an AWS API field
documentation: |
Enables cross-region automated backup replication.
When true, calls StartDBInstanceAutomatedBackupsReplication.
When false, calls StopDBInstanceAutomatedBackupsReplication.
BackupCrossRegionReplicationDestinationRegion:
documentation: |
The AWS region where automated backups should be replicated.
Required when BackupCrossRegionReplication is true.
BackupCrossRegionReplicationRetentionPeriod:
documentation: |
Number of days to retain replicated automated backups.
Default: 7
BackupCrossRegionReplicationKMSKeyID:
documentation: |
The AWS KMS key identifier for encryption of the replicated automated backups.
The KMS key ID is the Amazon Resource Name (ARN) for the KMS encryption key
in the destination AWS Region.
Tags:
compare:
# We have a custom comparison function...
Expand Down
28 changes: 28 additions & 0 deletions pkg/resource/db_instance/delta.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

185 changes: 185 additions & 0 deletions pkg/resource/db_instance/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,3 +695,188 @@ func getCloudwatchLogExportsConfigDifferences(cloudwatchLogExportsConfigDesired
}
return logsTypesToEnable, logsTypesToDisable
}

// manageCrossRegionBackupReplication handles enabling/disabling cross-region backup replication
func (rm *resourceManager) manageCrossRegionBackupReplication(
ctx context.Context,
desired *resource,
latest *resource,
delta *ackcompare.Delta,
) (err error) {
rlog := ackrtlog.FromContext(ctx)
exit := rlog.Trace("rm.manageCrossRegionBackupReplication")
defer func(err error) { exit(err) }(err)

// Check if replication state changed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, we want to do delta comparisons in delta.go. If custom comparison logic is needed you instruct the code-generator to omit generating comparison logic and add the custom logic to the delta_pre_compare hook.

desiredEnabled := desired.ko.Spec.BackupCrossRegionReplication != nil &&
*desired.ko.Spec.BackupCrossRegionReplication
// Check status field because AWS doesn't populate the spec field
latestEnabled := latest.ko.Status.DBInstanceAutomatedBackupsReplications != nil &&
len(latest.ko.Status.DBInstanceAutomatedBackupsReplications) > 0
Comment on lines +714 to +715
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to assume that any existing DBInstanceAutomatedBackupsReplications is the same as the one specified in desired's spec?


// Enable replication
if desiredEnabled && !latestEnabled {
if desired.ko.Spec.BackupCrossRegionReplicationDestinationRegion == nil {
return fmt.Errorf("BackupCrossRegionReplicationDestinationRegion is required when BackupCrossRegionReplication is true")
}

if latest.ko.Status.ACKResourceMetadata == nil || latest.ko.Status.ACKResourceMetadata.ARN == nil {
return fmt.Errorf("DB instance ARN is required to enable cross-region backup replication")
}

// Check if automated backups are enabled on the source instance.
// AWS requires backupRetentionPeriod > 0 before enabling cross-region replication.
latestBackupRetentionPeriod := int64(-1)
if latest.ko.Spec.BackupRetentionPeriod != nil {
latestBackupRetentionPeriod = *latest.ko.Spec.BackupRetentionPeriod
}
desiredBackupRetentionPeriod := int64(-1)
if desired.ko.Spec.BackupRetentionPeriod != nil {
desiredBackupRetentionPeriod = *desired.ko.Spec.BackupRetentionPeriod
}

// Check for pending backup retention period changes
pendingBackupRetentionPeriod := int64(-1)
if latest.ko.Status.PendingModifiedValues != nil &&
latest.ko.Status.PendingModifiedValues.BackupRetentionPeriod != nil {
pendingBackupRetentionPeriod = *latest.ko.Status.PendingModifiedValues.BackupRetentionPeriod
}

// If backups status is unknown, wait for late-init/read to populate.
if latestBackupRetentionPeriod < 0 {
rlog.Info("BackupRetentionPeriod not yet observed; waiting before enabling cross-region backup replication")
return ackrequeue.NeededAfter(
errors.New("backup retention period not yet observed"),
ackrequeue.DefaultRequeueAfterDuration,
)
}

// If backups are not yet active, check if they're being enabled.
// If backupRetentionPeriod is being modified (pending), wait for that to complete.
// Otherwise, allow reconciliation to continue so Update() can call ModifyDBInstance.
if latestBackupRetentionPeriod == 0 {
if desiredBackupRetentionPeriod <= 0 {
return fmt.Errorf("automated backups must be enabled (backupRetentionPeriod > 0) before enabling cross-region backup replication")
}
// If backups are pending (being modified), wait for that to complete
if pendingBackupRetentionPeriod > 0 {
rlog.Info("Waiting for pending backup retention period modification to complete before enabling cross-region backup replication",
"pendingBackupRetentionPeriod", pendingBackupRetentionPeriod)
return ackrequeue.NeededAfter(
errors.New("backup retention period modification pending"),
ackrequeue.DefaultRequeueAfterDuration,
)
}
// Backups are not active and not pending - allow reconciliation to continue
// so Update() can be called to enable backups via ModifyDBInstance.
// Don't return an error here, as that would prevent Update() from being called.
// Note: If BackupRetentionPeriod is in the delta, Update() will call ModifyDBInstance.
// On the next reconciliation, PendingModifiedValues will be populated and we'll requeue.
rlog.Info("Automated backups not yet active; allowing reconciliation to continue so ModifyDBInstance can enable backups")
return nil
}

sourceARN := string(*latest.ko.Status.ACKResourceMetadata.ARN)
input := &svcsdk.StartDBInstanceAutomatedBackupsReplicationInput{
SourceDBInstanceArn: &sourceARN,
}

// Set retention period (default 7)
retentionPeriod := int32(7)
if desired.ko.Spec.BackupCrossRegionReplicationRetentionPeriod != nil {
retentionPeriod = int32(*desired.ko.Spec.BackupCrossRegionReplicationRetentionPeriod)
}
input.BackupRetentionPeriod = &retentionPeriod

// Set KMS key ID if specified
if desired.ko.Spec.BackupCrossRegionReplicationKMSKeyID != nil {
input.KmsKeyId = desired.ko.Spec.BackupCrossRegionReplicationKMSKeyID
}

// Create a client for the destination region
// The AWS SDK uses the client's configured region to determine where the API call targets
var apiClient *svcsdk.Client
if desired.ko.Spec.BackupCrossRegionReplicationDestinationRegion != nil {
destRegion := string(*desired.ko.Spec.BackupCrossRegionReplicationDestinationRegion)
destConfig := rm.clientcfg.Copy()
destConfig.Region = destRegion
apiClient = svcsdk.NewFromConfig(destConfig)
} else {
apiClient = rm.sdkapi
}

// Start must be called in the destination region
_, err := apiClient.StartDBInstanceAutomatedBackupsReplication(ctx, input)
rm.metrics.RecordAPICall("UPDATE", "StartDBInstanceAutomatedBackupsReplication", err)
if err != nil {
// Handle idempotent case: if replication is already enabled, treat as success
errMsg := err.Error()
if strings.Contains(errMsg, "already replicating") {
rlog.Info("Replication already enabled, treating as success")
return nil
}
return err
}
return nil
}

// Disable replication
if !desiredEnabled && latestEnabled {
// Check if there are active replications
if latest.ko.Status.DBInstanceAutomatedBackupsReplications == nil ||
len(latest.ko.Status.DBInstanceAutomatedBackupsReplications) == 0 {
rlog.Info("No active replication found to stop")
return nil
}

if latest.ko.Status.ACKResourceMetadata == nil || latest.ko.Status.ACKResourceMetadata.ARN == nil {
return fmt.Errorf("DB instance ARN is required to disable cross-region backup replication")
}

// Extract destination region from replication ARN in status
destRegion := ""
if len(latest.ko.Status.DBInstanceAutomatedBackupsReplications) > 0 &&
latest.ko.Status.DBInstanceAutomatedBackupsReplications[0].DBInstanceAutomatedBackupsARN != nil {
replicationARN := *latest.ko.Status.DBInstanceAutomatedBackupsReplications[0].DBInstanceAutomatedBackupsARN
arnParts := strings.Split(replicationARN, ":")
if len(arnParts) >= 4 {
destRegion = arnParts[3]
}
}

// Fallback to spec field if available
if destRegion == "" && desired.ko.Spec.BackupCrossRegionReplicationDestinationRegion != nil {
destRegion = string(*desired.ko.Spec.BackupCrossRegionReplicationDestinationRegion)
}

if destRegion == "" {
return fmt.Errorf("could not determine destination region for stopping replication")
}

sourceARN := string(*latest.ko.Status.ACKResourceMetadata.ARN)
input := &svcsdk.StopDBInstanceAutomatedBackupsReplicationInput{
SourceDBInstanceArn: &sourceARN,
}

// Stop must be called from the destination region
destConfig := rm.clientcfg.Copy()
destConfig.Region = destRegion
stopClient := svcsdk.NewFromConfig(destConfig)

_, err := stopClient.StopDBInstanceAutomatedBackupsReplication(ctx, input)
rm.metrics.RecordAPICall("UPDATE", "StopDBInstanceAutomatedBackupsReplication", err)
if err != nil {
// Handle idempotent case: if replication is not active, treat as success
errMsg := err.Error()
if strings.Contains(errMsg, "not replicating") {
rlog.Info("Replication already stopped or not active, treating as success")
return nil
}
return err
}
rlog.Info("Stopped cross-region backup replication")
return nil
}

return nil
}
25 changes: 25 additions & 0 deletions pkg/resource/db_instance/manager.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading