Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/db_instance: add backup_replication #23297

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 2 additions & 0 deletions internal/service/rds/enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ func StorageType_Values() []string {
}
}

const DefaultKmsKeyAlias = "alias/aws/rds"

// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/accessing-monitoring.html#Overview.DBInstance.Status.
const (
InstanceStatusAvailable = "available"
Expand Down
189 changes: 189 additions & 0 deletions internal/service/rds/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/aws/aws-sdk-go/service/rds"
awsbase "github.com/hashicorp/aws-sdk-go-base/v2"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand All @@ -18,6 +20,7 @@ import (
"github.com/hashicorp/terraform-provider-aws/internal/create"
"github.com/hashicorp/terraform-provider-aws/internal/flex"
tfiam "github.com/hashicorp/terraform-provider-aws/internal/service/iam"
tfkms "github.com/hashicorp/terraform-provider-aws/internal/service/kms"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
Expand Down Expand Up @@ -108,6 +111,29 @@ func ResourceInstance() *schema.Resource {
Computed: true,
ForceNew: true,
},
"backup_replication": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"backup_retention_period": {
Type: schema.TypeInt,
Optional: true,
},
"destination_region": {
Type: schema.TypeString,
Required: true,
},
"kms_key_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: verify.ValidARN,
},
},
},
},
"backup_retention_period": {
Type: schema.TypeInt,
Optional: true,
Expand Down Expand Up @@ -1421,6 +1447,57 @@ func resourceInstanceCreate(d *schema.ResourceData, meta interface{}) error {
}
}

if b, ok := d.GetOk("backup_replication"); ok {
v, err := FindDBInstanceByID(conn, d.Id())

if err != nil {
return fmt.Errorf("Unable to find database by id: %w", err)
}

backup_replication := b.([]interface{})[0].(map[string]interface{})

destinationRegion := backup_replication["destination_region"].(string)

if err := awsbase.ValidateRegion(destinationRegion); err != nil {
return fmt.Errorf("Invalid region for backup replication: %w", err)
}

opts := rds.StartDBInstanceAutomatedBackupsReplicationInput{
SourceDBInstanceArn: v.DBInstanceArn,
}

// Replication is initiated in the destination region.
session, err := conns.NewSessionForRegion(&conn.Config, destinationRegion, meta.(*conns.AWSClient).TerraformVersion)

if v, ok := backup_replication["kms_key_id"].(string); ok && v != "" {
opts.KmsKeyId = aws.String(backup_replication["kms_key_id"].(string))
} else {
if _, ok := d.GetOk("storage_encrypted"); ok {
kmsConn := kms.New(session)
keyMetadata, err := tfkms.FindKeyByID(kmsConn, DefaultKmsKeyAlias)
if err != nil {
return fmt.Errorf("Failed to describe default RDS KMS key (%s): %s", DefaultKmsKeyAlias, err)
}
opts.KmsKeyId = keyMetadata.Arn
}
}

if v, ok := backup_replication["backup_retention_period"].(string); ok && v != "" {
opts.BackupRetentionPeriod = aws.Int64(int64(backup_replication["backup_retention_period"].(int)))
}

if err != nil {
return fmt.Errorf("error creating AWS session: %w", err)
}

replicateConn := rds.New(session)

_, err = replicateConn.StartDBInstanceAutomatedBackupsReplication(&opts)
if err != nil {
return fmt.Errorf("Error enabling database snapshot replication: %w", err)
}
}

return resourceInstanceRead(d, meta)
}

Expand Down Expand Up @@ -1860,6 +1937,118 @@ func resourceInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
}
}

// separate connection required to enable replication of snapshots
if d.HasChange("backup_replication") {

b, ok := d.GetOk("backup_replication")

enableReplication := ok

var destinationRegion string

if enableReplication {
destinationRegion = b.([]interface{})[0].(map[string]interface{})["destination_region"].(string)
if err := awsbase.ValidateRegion(destinationRegion); err != nil {
return fmt.Errorf("Invalid region for backup replication: %w", err)
}
}

v, err := FindDBInstanceByID(conn, d.Id())

if err != nil {
return fmt.Errorf("Unable to find database by id: %w", err)
}

// Find existing region that is replicating and stop replication,
// to avoid errors starting a new replication
for _, r := range v.DBInstanceAutomatedBackupsReplications {
if arnParts := strings.Split(*r.DBInstanceAutomatedBackupsArn, ":"); len(arnParts) >= 4 {
region := arnParts[3]

log.Printf("[INFO] Stopping backup replication in region: %s", region)

session, err := conns.NewSessionForRegion(&conn.Config, region, meta.(*conns.AWSClient).TerraformVersion)

if err != nil {
return fmt.Errorf("error creating AWS session: %w", err)
}

replicateConn := rds.New(session)

stopOpts := rds.StopDBInstanceAutomatedBackupsReplicationInput{
SourceDBInstanceArn: v.DBInstanceArn,
}

_, err = replicateConn.StopDBInstanceAutomatedBackupsReplication(&stopOpts)

if err != nil && !tfawserr.ErrMessageContains(err, "InvalidDBInstanceState", "is not replicating to the current region") {
return fmt.Errorf("Error disabling database snapshot replication: %w", err)
}
}
}

if enableReplication {

if err != nil {
return fmt.Errorf("error creating AWS session: %w", err)
}

log.Printf("[INFO] Starting backup replication in region: %s", destinationRegion)

// Replication is initiated in the destination region.
session, err := conns.NewSessionForRegion(&conn.Config, destinationRegion, meta.(*conns.AWSClient).TerraformVersion)
replicateConn := rds.New(session)

backup_replication := b.([]interface{})[0].(map[string]interface{})

startOpts := rds.StartDBInstanceAutomatedBackupsReplicationInput{
SourceDBInstanceArn: v.DBInstanceArn,
}

if v, ok := backup_replication["kms_key_id"].(string); ok && v != "" {
startOpts.KmsKeyId = aws.String(backup_replication["kms_key_id"].(string))
} else {
if _, ok := d.GetOk("storage_encrypted"); ok {
kmsConn := kms.New(session)
keyMetadata, err := tfkms.FindKeyByID(kmsConn, DefaultKmsKeyAlias)
if err != nil {
return fmt.Errorf("Failed to describe default RDS KMS key (%s): %s", DefaultKmsKeyAlias, err)
}
startOpts.KmsKeyId = keyMetadata.Arn
log.Printf("[INFO] kms_key_id not provided for backup_replication, using: %s", *startOpts.KmsKeyId)
}
}

if v, ok := backup_replication["backup_retention_period"].(string); ok && v != "" {
startOpts.BackupRetentionPeriod = aws.Int64(int64(backup_replication["backup_retention_period"].(int)))
}

err = resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err = replicateConn.StartDBInstanceAutomatedBackupsReplication(&startOpts)
if err != nil {
log.Printf("[INFO] Error starting backup replication: %s", err)
if tfawserr.ErrMessageContains(err, "InvalidDBInstanceAutomatedBackupState", "is already replicating") {
return resource.RetryableError(err)
}
if tfawserr.ErrMessageContains(err, "InvalidDBInstanceState", "DB Instance is in an invalid state") {
return resource.RetryableError(err)
}
if tfawserr.ErrMessageContains(err, "InvalidDBInstanceState", "DB instance is already replicating backups to this region") {
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
}
return nil
})
if tfresource.TimedOut(err) {
_, err = replicateConn.StartDBInstanceAutomatedBackupsReplication(&startOpts)
}
if err != nil {
return fmt.Errorf("Error starting backup replication: %w", err)
}
}
}

if d.HasChange("tags_all") {
o, n := d.GetChange("tags_all")

Expand Down
47 changes: 47 additions & 0 deletions internal/service/rds/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,32 @@ func TestAccRDSInstance_s3Import_nameGenerated(t *testing.T) {
})
}

func TestAccRDSInstance_backupReplication_basic(t *testing.T) {
var dbInstance rds.DBInstance

rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_db_instance.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID),
Providers: acctest.Providers,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccInstanceConfig_backupReplication_basic(rName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckInstanceExists(resourceName, &dbInstance),
resource.TestCheckResourceAttr(resourceName, "backup_retention_period", "1"),
resource.TestCheckTypeSetElemNestedAttrs(resourceName, "backup_replication.*", map[string]string{
"destination_region": "us-east-2",
}),
),
},
},
})
}

func TestAccRDSInstance_SnapshotIdentifier_basic(t *testing.T) {
var dbInstance, sourceDbInstance rds.DBInstance
var dbSnapshot rds.DBSnapshot
Expand Down Expand Up @@ -4618,6 +4644,27 @@ resource "aws_db_instance" "test" {
`, bucketPrefix))
}

func testAccInstanceConfig_backupReplication_basic(rName string) string {
return acctest.ConfigCompose(
testAccInstanceConfig_orderableClass("postgres", "12.2", "postgresql-license"),
fmt.Sprintf(`
resource "aws_db_instance" "test" {
allocated_storage = 5
backup_retention_period = 1
engine = data.aws_rds_orderable_db_instance.test.engine
identifier = "%[1]s"
instance_class = data.aws_rds_orderable_db_instance.test.instance_class
password = "avoid-plaintext-passwords"
username = "tfacctest"
skip_final_snapshot = true

backup_replication {
destination_region = "us-east-2"
}
}
`, rName))
}

func testAccInstanceConfig_FinalSnapshotIdentifier(rInt int) string {
return acctest.ConfigCompose(testAccInstanceConfig_orderableClassMySQL(), fmt.Sprintf(`
resource "aws_db_instance" "snapshot" {
Expand Down
11 changes: 11 additions & 0 deletions website/docs/r/db_instance.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Defaults to true.
* `availability_zone` - (Optional) The AZ for the RDS instance.
* `backup_retention_period` - (Optional) The days to retain backups for. Must be
between `0` and `35`. Must be greater than `0` if the database is used as a source for a Read Replica. [See Read Replica][1].
* `backup_replication` - (Optional) Store automated snapshots in another region. When specifying `backup_replication`, `backup_retention_period` needs to be set to true. See [Replicating automated backups to another AWS Region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReplicateBackups.html#AutomatedBackups.Replicating.Enable).
* `backup_window` - (Optional) The daily time range (in UTC) during which
automated backups are created if they are enabled. Example: "09:46-10:16". Must
not overlap with `maintenance_window`.
Expand Down Expand Up @@ -253,6 +254,16 @@ resource "aws_db_instance" "db" {

This will not recreate the resource if the S3 object changes in some way. It's only used to initialize the database

### Backup Replication Options

You can configure the database to store the automated snapshots in a different region. Full details in the API Docs: [StartDBInstanceAutomatedBackupsReplication](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_StartDBInstanceAutomatedBackupsReplication.html).

The `backup_replication` block supports the following arguments:

* `destination_region` - (Required) The region to replicate the backups to (must be a different region than the database resides in)
* `backup_retention_period` - (Optional) How many days to to store backups for in the destination region
* `kms_key_id` - (Optional) If `storage_encrypted` is enabled and this is not provided, the default aws kms key for rds is used

### Timeouts

`aws_db_instance` provides the following
Expand Down