diff --git a/.changelog/12646.txt b/.changelog/12646.txt new file mode 100644 index 0000000000..f7c11dacb9 --- /dev/null +++ b/.changelog/12646.txt @@ -0,0 +1,6 @@ +```release-note:enhancement +sql: added `replication_cluster` field to `google_sql_database_instance` resource +``` +```release-note:enhancement +sql: added support of switchover for MySQL and PostgreSQL in `google_sql_database_instance` resource +``` \ No newline at end of file diff --git a/google-beta/services/sql/data_source_sql_database_instances.go b/google-beta/services/sql/data_source_sql_database_instances.go index d2dc9310c5..14ac1a4da2 100644 --- a/google-beta/services/sql/data_source_sql_database_instances.go +++ b/google-beta/services/sql/data_source_sql_database_instances.go @@ -155,6 +155,7 @@ func flattenDatasourceGoogleDatabaseInstancesList(fetchedInstances []*sqladmin.D } instance["replica_configuration"] = flattenReplicaConfigurationforDataSource(rawInstance.ReplicaConfiguration) + instance["replication_cluster"] = flattenReplicationClusterForDataSource(rawInstance.ReplicationCluster) ipAddresses := flattenIpAddresses(rawInstance.IpAddresses) instance["ip_address"] = ipAddresses @@ -200,3 +201,19 @@ func flattenReplicaConfigurationforDataSource(replicaConfiguration *sqladmin.Rep return rc } + +// flattenReplicationClusterForDataSource converts cloud SQL backend ReplicationCluster (proto) to +// terraform replication_cluster. We explicitly allow the case when ReplicationCluster +// is nil since replication_cluster is computed+optional. +func flattenReplicationClusterForDataSource(replicationCluster *sqladmin.ReplicationCluster) []map[string]interface{} { + data := make(map[string]interface{}) + data["failover_dr_replica_name"] = "" + if replicationCluster != nil && replicationCluster.FailoverDrReplicaName != "" { + data["failover_dr_replica_name"] = replicationCluster.FailoverDrReplicaName + } + data["dr_replica"] = false + if replicationCluster != nil { + data["dr_replica"] = replicationCluster.DrReplica + } + return []map[string]interface{}{data} +} diff --git a/google-beta/services/sql/resource_sql_database_instance.go b/google-beta/services/sql/resource_sql_database_instance.go index d6fb25a5d1..3f823c609c 100644 --- a/google-beta/services/sql/resource_sql_database_instance.go +++ b/google-beta/services/sql/resource_sql_database_instance.go @@ -927,6 +927,27 @@ is set to true. Defaults to ZONAL.`, }, Description: `The replicas of the instance.`, }, + "replication_cluster": { + Type: schema.TypeList, + Computed: true, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "failover_dr_replica_name": { + Type: schema.TypeString, + Optional: true, + Description: fmt.Sprintf(`If the instance is a primary instance, then this field identifies the disaster recovery (DR) replica. The standard format of this field is "your-project:your-instance". You can also set this field to "your-instance", but cloud SQL backend will convert it to the aforementioned standard format.`), + }, + "dr_replica": { + Type: schema.TypeBool, + Computed: true, + Description: `Read-only field that indicates whether the replica is a DR replica.`, + }, + }, + }, + Description: "A primary instance and disaster recovery replica pair. Applicable to MySQL and PostgreSQL. This field can be set only after both the primary and replica are created.", + }, "server_ca_cert": { Type: schema.TypeList, Computed: true, @@ -1721,6 +1742,11 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e if err := d.Set("replica_names", instance.ReplicaNames); err != nil { return fmt.Errorf("Error setting replica_names: %w", err) } + + // We always set replication_cluster because it is computed+optional. + if err := d.Set("replication_cluster", flattenReplicationCluster(instance.ReplicationCluster, d)); err != nil { + return fmt.Errorf("Error setting replication_cluster: %w", err) + } ipAddresses := flattenIpAddresses(instance.IpAddresses) if err := d.Set("ip_address", ipAddresses); err != nil { log.Printf("[WARN] Failed to set SQL Database Instance IP Addresses") @@ -1983,7 +2009,7 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsSqlOperationInProgressError}, }) if err != nil { - return fmt.Errorf("Error, failed to promote read replica instance as primary stand-alone %s: %s", instance.Name, err) + return fmt.Errorf("Error, failed to promote read replica instance as primary stand-alone %s: %s", d.Get("name"), err) } err = SqlAdminOperationWaitTime(config, op, project, "Promote Instance", userAgent, d.Timeout(schema.TimeoutUpdate)) if err != nil { @@ -2052,6 +2078,13 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) instance.DatabaseVersion = databaseVersion } + failoverDrReplicaName := d.Get("replication_cluster.0.failover_dr_replica_name").(string) + if failoverDrReplicaName != "" { + instance.ReplicationCluster = &sqladmin.ReplicationCluster{ + FailoverDrReplicaName: failoverDrReplicaName, + } + } + err = transport_tpg.Retry(transport_tpg.RetryOptions{ RetryFunc: func() (rerr error) { op, rerr = config.NewSqlAdminClient(userAgent).Instances.Update(project, d.Get("name").(string), instance).Do() @@ -2379,6 +2412,22 @@ func flattenDatabaseFlags(databaseFlags []*sqladmin.DatabaseFlags) []map[string] return flags } +// flattenReplicationCluster converts cloud SQL backend ReplicationCluster (proto) to +// terraform replication_cluster. We explicitly allow the case when ReplicationCluster +// is nil since replication_cluster is computed+optional. +func flattenReplicationCluster(replicationCluster *sqladmin.ReplicationCluster, d *schema.ResourceData) []map[string]interface{} { + data := make(map[string]interface{}) + data["failover_dr_replica_name"] = "" + if replicationCluster != nil && replicationCluster.FailoverDrReplicaName != "" { + data["failover_dr_replica_name"] = replicationCluster.FailoverDrReplicaName + } + data["dr_replica"] = false + if replicationCluster != nil { + data["dr_replica"] = replicationCluster.DrReplica + } + return []map[string]interface{}{data} +} + func flattenIpConfiguration(ipConfiguration *sqladmin.IpConfiguration, d *schema.ResourceData) interface{} { data := map[string]interface{}{ "ipv4_enabled": ipConfiguration.Ipv4Enabled, @@ -2661,11 +2710,6 @@ func isSwitchoverRequested(d *schema.ResourceData) bool { if !slices.Contains(newReplicaNames.([]interface{}), originalPrimaryName) { return false } - dbVersion := d.Get("database_version") - if !strings.HasPrefix(dbVersion.(string), "SQLSERVER") { - log.Printf("[WARN] Switchover is only supported for SQL Server %q", dbVersion) - return false - } return true } @@ -2683,10 +2727,6 @@ func isReplicaPromoteRequested(_ context.Context, oldInstanceType interface{}, n // Check if this resource change is the manual update done on old primary after a switchover. If true, no replacement is needed. func isSwitchoverFromOldPrimarySide(d *schema.ResourceDiff) bool { dbVersion := d.Get("database_version") - if !strings.HasPrefix(dbVersion.(string), "SQLSERVER") { - log.Printf("[WARN] Switchover is only supported for SQL Server %q", dbVersion) - return false - } oldInstanceType, newInstanceType := d.GetChange("instance_type") oldReplicaNames, newReplicaNames := d.GetChange("replica_names") _, newMasterInstanceName := d.GetChange("master_instance_name") @@ -2701,11 +2741,12 @@ func isSwitchoverFromOldPrimarySide(d *schema.ResourceDiff) bool { newMasterInOldReplicaNames := slices.Contains(oldReplicaNames.([]interface{}), newMasterInstanceName) newMasterNotInNewReplicaNames := !slices.Contains(newReplicaNames.([]interface{}), newMasterInstanceName) isCascadableReplica := cascadableReplicaFieldExists && cascadableReplica.(bool) + isSQLServer := strings.HasPrefix(dbVersion.(string), "SQLSERVER") return newMasterInstanceName != nil && instanceTypeChangedFromPrimaryToReplica && - newMasterInOldReplicaNames && newMasterNotInNewReplicaNames && - isCascadableReplica + newMasterInOldReplicaNames && newMasterNotInNewReplicaNames && (!isSQLServer || + isCascadableReplica) } func checkPromoteConfigurations(d *schema.ResourceData) error { diff --git a/google-beta/services/sql/resource_sql_database_instance_test.go b/google-beta/services/sql/resource_sql_database_instance_test.go index ea5ba8242e..85bb16fd8b 100644 --- a/google-beta/services/sql/resource_sql_database_instance_test.go +++ b/google-beta/services/sql/resource_sql_database_instance_test.go @@ -2585,6 +2585,158 @@ func TestAccSqlDatabaseInstance_SwitchoverSuccess(t *testing.T) { }) } +// Switchover for MySQL. +func TestAccSqlDatabaseInstance_MysqlSwitchoverSuccess(t *testing.T) { + t.Parallel() + primaryName := "tf-test-mysql-sw-primary-" + acctest.RandString(t, 10) + replicaName := "tf-test-mysql-sw-replica-" + acctest.RandString(t, 10) + project := envvar.GetTestProjectFromEnv() + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlDatabaseInstanceConfig_mysqlEplusWithReplica(project, primaryName, replicaName), + }, + { + ResourceName: "google_sql_database_instance.original-primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: ignoredReplicaConfigurationFields, + }, + { + ResourceName: "google_sql_database_instance.original-replica", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: ignoredReplicaConfigurationFields, + }, + // Let's make sure that setting and unsetting failover replica works. + { + Config: googleSqlDatabaseInstance_mysqlSetFailoverReplica(project, primaryName, replicaName), + }, + { + Config: googleSqlDatabaseInstance_mysqlUnsetFailoverReplica(project, primaryName, replicaName), + }, + { + Config: googleSqlDatabaseInstance_mysqlSetFailoverReplica(project, primaryName, replicaName), + }, + { + // Split into two configs because current TestStep implementation checks diff before refreshing. + Config: googleSqlDatabaseInstance_mysqlSwitchoverOnReplica(project, primaryName, replicaName), + // Original primary needs to be updated at the next step. + ExpectNonEmptyPlan: true, + }, + { + Config: googleSqlDatabaseInstance_mysqlUpdatePrimaryAfterSwitchover(project, primaryName, replicaName), + }, + { + RefreshState: true, + Check: resource.ComposeTestCheckFunc(resource.TestCheckTypeSetElemAttr("google_sql_database_instance.original-replica", "replica_names.*", primaryName), checkSwitchoverOriginalReplicaConfigurations("google_sql_database_instance.original-replica"), checkSwitchoverOriginalPrimaryConfigurations("google_sql_database_instance.original-primary", replicaName)), + }, + { + ResourceName: "google_sql_database_instance.original-primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: ignoredReplicaConfigurationFields, + }, + { + ResourceName: "google_sql_database_instance.original-replica", + ImportState: true, + ImportStateVerify: true, + // original-replica is no longer a replica, but replica_configuration is O + C and cannot be unset + ImportStateVerifyIgnore: []string{"replica_configuration", "deletion_protection", "root_password"}, + }, + { + // Delete replica first so PostTestDestroy doesn't fail when deleting instances which have replicas. We've already validated switchover behavior, the remaining steps are cleanup + Config: googleSqlDatabaseInstance_mysqlDeleteReplicasAfterSwitchover(project, primaryName, replicaName), + // We delete replica, but haven't updated the master's replica_names + ExpectNonEmptyPlan: true, + }, + { + // Remove replica from primary's resource + Config: googleSqlDatabaseInstance_mysqlRemoveReplicaFromPrimaryAfterSwitchover(project, replicaName), + }, + }, + }) +} + +// Switchover for PostgreSQL. +func TestAccSqlDatabaseInstance_PostgresSwitchoverSuccess(t *testing.T) { + t.Parallel() + primaryName := "tf-test-pg-sw-primary-" + acctest.RandString(t, 10) + replicaName := "tf-test-pg-sw-replica-" + acctest.RandString(t, 10) + project := envvar.GetTestProjectFromEnv() + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlDatabaseInstanceConfig_postgresEplusWithReplica(project, primaryName, replicaName), + }, + { + ResourceName: "google_sql_database_instance.original-primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: ignoredReplicaConfigurationFields, + }, + { + ResourceName: "google_sql_database_instance.original-replica", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: ignoredReplicaConfigurationFields, + }, + // Let's make sure that setting and unsetting failover replica works. + { + Config: googleSqlDatabaseInstance_postgresSetFailoverReplica(project, primaryName, replicaName), + }, + { + Config: googleSqlDatabaseInstance_postgresUnsetFailoverReplica(project, primaryName, replicaName), + }, + { + Config: googleSqlDatabaseInstance_postgresSetFailoverReplica(project, primaryName, replicaName), + }, + { + // Split into two configs because current TestStep implementation checks diff before refreshing. + Config: googleSqlDatabaseInstance_postgresSwitchoverOnReplica(project, primaryName, replicaName), + // Original primary needs to be updated at the next step. + ExpectNonEmptyPlan: true, + }, + { + Config: googleSqlDatabaseInstance_postgresUpdatePrimaryAfterSwitchover(project, primaryName, replicaName), + }, + { + RefreshState: true, + Check: resource.ComposeTestCheckFunc(resource.TestCheckTypeSetElemAttr("google_sql_database_instance.original-replica", "replica_names.*", primaryName), checkSwitchoverOriginalReplicaConfigurations("google_sql_database_instance.original-replica"), checkSwitchoverOriginalPrimaryConfigurations("google_sql_database_instance.original-primary", replicaName)), + }, + { + ResourceName: "google_sql_database_instance.original-primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: ignoredReplicaConfigurationFields, + }, + { + ResourceName: "google_sql_database_instance.original-replica", + ImportState: true, + ImportStateVerify: true, + // original-replica is no longer a replica, but replica_configuration is O + C and cannot be unset + ImportStateVerifyIgnore: []string{"replica_configuration", "deletion_protection", "root_password"}, + }, + { + // Delete replica first so PostTestDestroy doesn't fail when deleting instances which have replicas. We've already validated switchover behavior, the remaining steps are cleanup + Config: googleSqlDatabaseInstance_postgresDeleteReplicasAfterSwitchover(project, primaryName, replicaName), + // We delete replica, but haven't updated the master's replica_names + ExpectNonEmptyPlan: true, + }, + { + // Remove replica from primary's resource + Config: googleSqlDatabaseInstance_postgresRemoveReplicaFromPrimaryAfterSwitchover(project, replicaName), + }, + }, + }) +} + func TestAccSqlDatabaseInstance_updateSslOptionsForPostgreSQL(t *testing.T) { t.Parallel() @@ -3470,6 +3622,554 @@ resource "google_sql_database_instance" "original-replica" { `, replicaName) } +func testGoogleSqlDatabaseInstanceConfig_mysqlEplusWithReplica(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + deletion_protection = false + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = google_sql_database_instance.original-primary.name + deletion_protection = false + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } +} +`, project, primaryName, project, replicaName) +} + +func googleSqlDatabaseInstance_mysqlSetFailoverReplica(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = "%s" + deletion_protection = false + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } +} +`, project, primaryName, project, replicaName, project, replicaName, primaryName) +} + +func googleSqlDatabaseInstance_mysqlUnsetFailoverReplica(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = "%s" + deletion_protection = false + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } +} +`, project, primaryName, project, replicaName, primaryName) +} + +func googleSqlDatabaseInstance_mysqlSwitchoverOnReplica(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = ["%s"] + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} +`, project, primaryName, project, replicaName, project, replicaName, primaryName, project, primaryName) +} + +func googleSqlDatabaseInstance_mysqlUpdatePrimaryAfterSwitchover(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "MYSQL_8_0" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = "%s" + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = false + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = ["%s"] + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} +`, project, primaryName, replicaName, project, replicaName, primaryName, project, primaryName) +} + +// After a switchover, the original-primary is now the replica and must be removed first. +func googleSqlDatabaseInstance_mysqlDeleteReplicasAfterSwitchover(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = ["%s"] + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} +`, project, replicaName, primaryName, project, primaryName) +} + +// Update original-replica replica_names after deleting original-primary +func googleSqlDatabaseInstance_mysqlRemoveReplicaFromPrimaryAfterSwitchover(project, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = [] + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} +`, project, replicaName) +} + +func testGoogleSqlDatabaseInstanceConfig_postgresEplusWithReplica(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + deletion_protection = false + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = google_sql_database_instance.original-primary.name + deletion_protection = false + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } +} +`, project, primaryName, project, replicaName) +} + +func googleSqlDatabaseInstance_postgresSetFailoverReplica(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = "%s" + deletion_protection = false + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } +} +`, project, primaryName, project, replicaName, project, replicaName, primaryName) +} + +func googleSqlDatabaseInstance_postgresUnsetFailoverReplica(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = "%s" + deletion_protection = false + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } +} +`, project, primaryName, project, replicaName, primaryName) +} + +func googleSqlDatabaseInstance_postgresSwitchoverOnReplica(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = ["%s"] + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} +`, project, primaryName, project, replicaName, project, replicaName, primaryName, project, primaryName) +} + +func googleSqlDatabaseInstance_postgresUpdatePrimaryAfterSwitchover(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-primary" { + project = "%s" + name = "%s" + region = "us-east1" + database_version = "POSTGRES_12" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = "%s" + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = false + point_in_time_recovery_enabled = false + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = ["%s"] + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} +`, project, primaryName, replicaName, project, replicaName, primaryName, project, primaryName) +} + +// After a switchover, the original-primary is now the replica and must be removed first. +func googleSqlDatabaseInstance_postgresDeleteReplicasAfterSwitchover(project, primaryName, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = ["%s"] + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "%s:%s" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} +`, project, replicaName, primaryName, project, primaryName) +} + +// Update original-replica replica_names after deleting original-primary +func googleSqlDatabaseInstance_postgresRemoveReplicaFromPrimaryAfterSwitchover(project, replicaName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "original-replica" { + project = "%s" + name = "%s" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = [] + deletion_protection = false + + replication_cluster { + failover_dr_replica_name = "" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} +`, project, replicaName) +} + func testAccSqlDatabaseInstance_basicInstanceForPsc(instanceName string, projectId string, orgId string, billingAccount string) string { return fmt.Sprintf(` resource "google_project" "testproject" { diff --git a/website/docs/guides/sql_instance_switchover.html.markdown b/website/docs/guides/sql_instance_switchover.html.markdown index 07623519a9..eaa817f0de 100644 --- a/website/docs/guides/sql_instance_switchover.html.markdown +++ b/website/docs/guides/sql_instance_switchover.html.markdown @@ -7,7 +7,7 @@ description: |- # Performing a SQL Instance Switchover This page is a brief walkthrough of performing a switchover through terraform. - ~> **NOTE:** Only supported for SQL Server. +## SQL Server 1. Create a **cross-region** primary and cascadable replica. It is recommended to use deletion_protection to prevent accidental deletions. ``` @@ -83,4 +83,405 @@ resource "google_sql_database_instance" "original-primary" { - `terraform plan` does not say **"must be replaced"** for any resource - Every resource **"will be updated in-place"** - Only the 2 instances involved in switchover have planned changes -- (Recommended) Use `deletion_protection` on instances as a safety measure \ No newline at end of file +- (Recommended) Use `deletion_protection` on instances as a safety measure + +## MySQL + +1. Create a **cross-region, Enterprise Plus edition** primary and replica. The primary should have backup and binary log enabled. + +``` +resource "google_sql_database_instance" "original-primary" { + project = "your-project" + name = "your-original-primary" + # Can be any region. + region = "us-east1" + # Any database version that supports Enterprise Plus edition. + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + + settings { + # Any tier that supports Enterprise Plus edition. + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } + + # You can add more settings. +} + +resource "google_sql_database_instance" "original-replica" { + project = "your-project" + name = "your-original-replica" + # Can be any region, but must be different from the primary's region. + region = "us-west2" + # Must be same as the primary's database_version. + database_version = "MYSQL_8_0" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = google_sql_database_instance.original-primary.name + + settings { + # Any tier that supports Enterprise Plus edition. + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } + + # You can add more settings. +} +``` + +2. Designate the replica as DR replica of the primary by adding `replication_cluster.failover_dr_replica_name`. +```diff +resource "google_sql_database_instance" "original-primary" { + project = "your-project" + name = "your-original-primary" + region = "us-east1" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + ++ replication_cluster { ++ # Note that the format of the name is "project:instance". ++ # If you want to unset DR replica, put empty string in this field. ++ failover_dr_replica_name = "your-project:your-original-replica" ++ } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "your-project" + name = "your-original-replica" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = "your-original-primary" + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } +} +``` + +3. Invoke switchover on the original replica. + +* Change `instance_type` from `READ_REPLICA_INSTANCE` to `CLOUD_SQL_INSTANCE`. +* Remove `master_instance_name`. +* Add original primary's name to the original replica's `replica_names` list and `replication_cluster.failover_dr_replica_name`. +* Enable backup and binary log for original replica. + +```diff +resource "google_sql_database_instance" "original-primary" { + project = "your-project" + name = "your-original-primary" + region = "us-east1" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + + replication_cluster { + failover_dr_replica_name = "your-project:your-original-replica" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "your-project" + name = "your-original-replica" + region = "us-west2" + database_version = "MYSQL_8_0" +- instance_type = "READ_REPLICA_INSTANCE" ++ instance_type = "CLOUD_SQL_INSTANCE" +- master_instance_name = "your-original-primary" ++ replica_names = ["your-original-primary"] + ++ replication_cluster { ++ failover_dr_replica_name = "your-project:your-original-primary" ++ } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" ++ backup_configuration { ++ enabled = true ++ binary_log_enabled = true ++ } + } +} +``` + +4. Update the original primary and run `terraform plan`. +* Change `instance_type` from `CLOUD_SQL_INSTANCE` to `READ_REPLICA_INSTANCE`. +* Set `master_instance_name` to the new primary (original replica). +* (If `replica_names` is present) Remove original replica from `replica_names`. + * **NOTE**: Do **not** delete the `replica_names` field, even if it has no replicas remaining. Set `replica_names = [ ]` to indicate it having no replicas. +* Remove original replica from `replication_cluster.failover_dr_replica_name` by setting this field to the empty string. +* Disable backup for original primary (because it became a replica). +* Run `terraform plan` and verify that your configuration matches infrastructure. You should see a message like the following: + * **`No changes. Your infrastructure matches the configuration.`** + +```diff +resource "google_sql_database_instance" "original-primary" { + project = "your-project" + name = "your-original-primary" + region = "us-east1" + database_version = "MYSQL_8_0" +- instance_type = "CLOUD_SQL_INSTANCE" ++ instance_type = "READ_REPLICA_INSTANCE" ++ master_instance_name = "your-original-replica" + + replication_cluster { +- failover_dr_replica_name = "your-project:your-original-replica" ++ failover_dr_replica_name = "" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { +- enabled = true ++ enabled = false + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "your-project" + name = "your-original-replica" + region = "us-west2" + database_version = "MYSQL_8_0" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = ["your-original-primary"] + + replication_cluster { + failover_dr_replica_name = "your-project:your-original-primary" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} +``` + +## PostgreSQL + +1. Create a **cross-region, Enterprise Plus edition** primary and replica. The primary should have backup and PITR enabled. + +``` +resource "google_sql_database_instance" "original-primary" { + project = "your-project" + name = "your-original-primary" + # Can be any region. + region = "us-east1" + # Any database version that supports Enterprise Plus edition. + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + + settings { + # Any tier that supports Enterprise Plus edition. + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } + + # You can add more settings. +} + +resource "google_sql_database_instance" "original-replica" { + project = "your-project" + name = "your-original-replica" + # Can be any region, but must be different from the primary's region. + region = "us-west2" + # Must be same as the primary's database_version. + database_version = "POSTGRES_12" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = google_sql_database_instance.original-primary.name + + settings { + # Any tier that supports Enterprise Plus edition. + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } + + # You can add more settings. +} +``` + +2. Designate the replica as DR replica of the primary by adding `replication_cluster.failover_dr_replica_name`. +```diff +resource "google_sql_database_instance" "original-primary" { + project = "your-project" + name = "your-original-primary" + region = "us-east1" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + ++ replication_cluster { ++ # Note that the format of the name is "project:instance". ++ # If you want to unset DR replica, put empty string in this field. ++ failover_dr_replica_name = "your-project:your-original-replica" ++ } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "your-project" + name = "your-original-replica" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "READ_REPLICA_INSTANCE" + master_instance_name = "your-original-primary" + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + } +} +``` + +3. Invoke switchover on the original replica. + +* Change `instance_type` from `READ_REPLICA_INSTANCE` to `CLOUD_SQL_INSTANCE`. +* Remove `master_instance_name`. +* Add original primary's name to the original replica's `replica_names` list and `replication_cluster.failover_dr_replica_name`. +* Enable backup and PITR for original replica. + +```diff +resource "google_sql_database_instance" "original-primary" { + project = "your-project" + name = "your-original-primary" + region = "us-east1" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + + replication_cluster { + failover_dr_replica_name = "your-project:your-original-replica" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "your-project" + name = "your-original-replica" + region = "us-west2" + database_version = "POSTGRES_12" +- instance_type = "READ_REPLICA_INSTANCE" ++ instance_type = "CLOUD_SQL_INSTANCE" +- master_instance_name = "your-original-primary" ++ replica_names = ["your-original-primary"] + ++ replication_cluster { ++ failover_dr_replica_name = "your-project:your-original-primary" ++ } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" ++ backup_configuration { ++ enabled = true ++ point_in_time_recovery_enabled = true ++ } + } +} +``` + +4. Update the original primary and run `terraform plan`. +* Change `instance_type` from `CLOUD_SQL_INSTANCE` to `READ_REPLICA_INSTANCE`. +* Set `master_instance_name` to the new primary (original replica). +* (If `replica_names` is present) Remove original replica from `replica_names`. + * **NOTE**: Do **not** delete the `replica_names` field, even if it has no replicas remaining. Set `replica_names = [ ]` to indicate it having no replicas. +* Remove original replica from `replication_cluster.failover_dr_replica_name` by setting this field to the empty string. +* Disable backup and PITR for original primary (because it became a replica). +* Run `terraform plan` and verify that your configuration matches infrastructure. You should see a message like the following: + * **`No changes. Your infrastructure matches the configuration.`** + +```diff +resource "google_sql_database_instance" "original-primary" { + project = "your-project" + name = "your-original-primary" + region = "us-east1" + database_version = "POSTGRES_12" +- instance_type = "CLOUD_SQL_INSTANCE" ++ instance_type = "READ_REPLICA_INSTANCE" ++ master_instance_name = "your-original-replica" + + replication_cluster { +- failover_dr_replica_name = "your-project:your-original-replica" ++ failover_dr_replica_name = "" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { +- enabled = true ++ enabled = false +- point_in_time_recovery_enabled = true ++ point_in_time_recovery_enabled = false + } + } +} + +resource "google_sql_database_instance" "original-replica" { + project = "your-project" + name = "your-original-replica" + region = "us-west2" + database_version = "POSTGRES_12" + instance_type = "CLOUD_SQL_INSTANCE" + replica_names = ["your-original-primary"] + + replication_cluster { + failover_dr_replica_name = "your-project:your-original-primary" + } + + settings { + tier = "db-perf-optimized-N-2" + edition = "ENTERPRISE_PLUS" + backup_configuration { + enabled = true + point_in_time_recovery_enabled = true + } + } +} +``` diff --git a/website/docs/r/sql_database_instance.html.markdown b/website/docs/r/sql_database_instance.html.markdown index 6f6e2e668b..8992cfeadf 100644 --- a/website/docs/r/sql_database_instance.html.markdown +++ b/website/docs/r/sql_database_instance.html.markdown @@ -557,6 +557,12 @@ block during resource creation/update will trigger the restore action after the * `project` - (Optional) The full project ID of the source instance.` +The optional, computed `replication_cluster` block represents a primary instance and disaster recovery replica pair. Applicable to MySQL and PostgreSQL. This field can be set only after both the primary and replica are created. This block supports: + +* `failover_dr_replica_name`: (Optional) If the instance is a primary instance, then this field identifies the disaster recovery (DR) replica. The standard format of this field is "your-project:your-instance". You can also set this field to "your-instance", but cloud SQL backend will convert it to the aforementioned standard format. + +* `dr_replica`: Read-only field that indicates whether the replica is a DR replica. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are @@ -620,8 +626,8 @@ performing filtering in a Terraform config. * `server_ca_cert.0.sha1_fingerprint` - SHA Fingerprint of the CA Cert. -## Switchover (SQL Server Only) -Users can perform a switchover on any direct `cascadable` replica by following the steps below. +## Switchover +Users can perform a switchover on a replica by following the steps below. ~>**WARNING:** Failure to follow these steps can lead to data loss (You will be warned during plan stage). To prevent data loss during a switchover, please verify your plan with the checklist below. @@ -629,22 +635,26 @@ For a more in-depth walkthrough with example code, see the [Switchover Guide](.. ### Steps to Invoke Switchover -Create a `cascadable` replica in a different region from the primary (`cascadable_replica` is set to true in `replica_configuration`) +MySQL/PostgreSQL: Create a cross-region, Enterprise Plus edition primary and replica pair, then set the value of primary's `replication_cluster.failover_dr_replica_name` as the replica. + +SQL Server: Create a `cascadable` replica in a different region from the primary (`cascadable_replica` is set to true in `replica_configuration`) #### Invoking switchover in the replica resource: 1. Change instance_type from `READ_REPLICA_INSTANCE` to `CLOUD_SQL_INSTANCE` 2. Remove `master_instance_name` -3. Remove `replica_configuration` +3. (SQL Server) Remove `replica_configuration` 4. Add current primary's name to the replica's `replica_names` list +5. (MySQL/PostgreSQL) Add current primary's name to the replica's `replication_cluster.failover_dr_replica_name`. +6. (MySQL/PostgreSQL) Adjust `backup_configuration`. See [Switchover Guide](../guides/sql_instance_switchover.html.markdown) for details. #### Updating the primary resource: 1. Change `instance_type` from `CLOUD_SQL_INSTANCE` to `READ_REPLICA_INSTANCE` 2. Set `master_instance_name` to the original replica (which will be primary after switchover) -3. Set `replica_configuration` and set `cascadable_replica` to `true` +3. (SQL Server) Set `replica_configuration` and set `cascadable_replica` to `true` 4. Remove original replica from `replica_names` - - ~> **NOTE**: Do **not** delete the replica_names field, even if it has no replicas remaining. Set replica_names = [ ] to indicate it having no replicas. - + * **NOTE**: Do **not** delete the replica_names field, even if it has no replicas remaining. Set replica_names = [ ] to indicate it having no replicas. +5. (MySQL/PostgreSQL) Set `replication_cluster.failover_dr_replica_name` as the empty string. +6. (MySQL/PostgreSQL) Adjust `backup_configuration`. See [Switchover Guide](../guides/sql_instance_switchover.html.markdown) for details. #### Plan and verify that: - `terraform plan` outputs **"0 to add, 0 to destroy"** - `terraform plan` does not say **"must be replaced"** for any resource