Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4de9a40
feat: implement RDB engine upgrade using MajorUpgradeWorkflow to mini…
jremy42 Oct 16, 2025
c7b61a2
feat(rdb): add comprehensive tests and documentation for engine upgra…
jremy42 Oct 16, 2025
924440e
refactor(rdb): consolidate engine upgrade tests into single comprehen…
jremy42 Oct 16, 2025
87e6807
feat: ensure both old and new RDB instances reach stable states durin…
jremy42 Oct 16, 2025
0ac44c8
fix: resolve golangci-lint issues for RDB engine upgrade implementation
jremy42 Oct 16, 2025
5c6e49a
test: compress cassettes and remove orphan test data files
jremy42 Oct 16, 2025
3d2525a
docs: regenerate documentation after rebase on master
jremy42 Oct 16, 2025
c748bfe
refactor: replace interface{} with any and remove unnecessary comments
jremy42 Oct 17, 2025
6aca093
chore: bump sdk (#3411)
remyleone Oct 16, 2025
70553e5
chore: fix documentation and include a make docs (#3412)
remyleone Oct 17, 2025
1aac8ff
Merge branch 'master' into feat/rdb-engine-upgrade-management
jremy42 Oct 17, 2025
a611544
feat(rdb): clarify engine expects version name not ID
jremy42 Oct 21, 2025
5996778
fix(rdb): gofmt formatting
jremy42 Oct 21, 2025
40809a4
feat(rdb): accept only version.Name for engine (simplify validation)
jremy42 Oct 21, 2025
d376f65
docs: regenerate after engine description update
jremy42 Oct 21, 2025
980450b
Merge branch 'master' into feat/rdb-engine-upgrade-management
jremy42 Oct 21, 2025
0032d57
Merge branch 'master' into feat/rdb-engine-upgrade-management
jremy42 Oct 27, 2025
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
41 changes: 39 additions & 2 deletions docs/resources/rdb_instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,38 @@ resource "scaleway_rdb_instance" "main" {
}
```

### Example Engine Upgrade

```terraform
# Initial creation with PostgreSQL 14
resource "scaleway_rdb_instance" "main" {
name = "my-database"
node_type = "DB-DEV-S"
engine = "PostgreSQL-14"
is_ha_cluster = false
disable_backup = true
user_name = "my_user"
password = "thiZ_is_v&ry_s3cret"
}

# Check available versions for upgrade
output "upgradable_versions" {
value = scaleway_rdb_instance.main.upgradable_versions
}

# To upgrade to PostgreSQL 15, simply change the engine value
# This will trigger a blue/green upgrade with automatic endpoint migration
# resource "scaleway_rdb_instance" "main" {
# name = "my-database"
# node_type = "DB-DEV-S"
# engine = "PostgreSQL-15" # Changed from PostgreSQL-14
# is_ha_cluster = false
# disable_backup = true
# user_name = "my_user"
# password = "thiZ_is_v&ry_s3cret"
# }
```

### Examples of endpoint configuration

Database Instances can have a maximum of 1 public endpoint and 1 private endpoint. They can have both, or none.
Expand Down Expand Up @@ -141,9 +173,9 @@ interruption.

~> **Important** Once your Database Instance reaches `disk_full` status, if you are using `lssd` storage, you should upgrade the `node_type`, and if you are using `bssd` storage, you should increase the volume size before making any other changes to your Database Instance.

- `engine` - (Required) Database Instance's engine version (e.g. `PostgreSQL-11`).
- `engine` - (Required) Database Instance's engine version name (e.g. `PostgreSQL-16`, `MySQL-8`).

~> **Important** Updates to `engine` will recreate the Database Instance.
~> **Important** Updates to `engine` will perform a blue/green upgrade using `MajorUpgradeWorkflow`. This creates a new instance from a snapshot, migrates endpoints automatically, and updates the Terraform state with the new instance ID. The upgrade ensures minimal downtime but **any writes between the snapshot and the endpoint migration will be lost**. Use the `upgradable_versions` computed attribute to check available versions for upgrade.

- `volume_type` - (Optional, default to `lssd`) Type of volume where data are stored (`lssd`, `sbs_5k` or `sbs_15k`).

Expand Down Expand Up @@ -245,6 +277,11 @@ are of the form `{region}/{id}`, e.g. `fr-par/11111111-1111-1111-1111-1111111111
- `address` - The private IPv4 address.
- `certificate` - Certificate of the Database Instance.
- `organization_id` - The organization ID the Database Instance is associated with.
- `upgradable_versions` - List of available engine versions for upgrade. Each version contains:
- `id` - Version ID to use in upgrade requests.
- `name` - Engine version name (e.g., `PostgreSQL-15`).
- `version` - Version string (e.g., `15.5`).
- `minor_version` - Minor version string (e.g., `15.5.0`).

## Limitations

Expand Down
119 changes: 112 additions & 7 deletions internal/services/rdb/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ func ResourceInstance() *schema.Resource {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Database's engine version id",
Description: "Database's engine version name (e.g., 'PostgreSQL-16', 'MySQL-8'). Changing this value triggers a blue/green upgrade using MajorUpgradeWorkflow with automatic endpoint migration",
DiffSuppressFunc: dsf.IgnoreCase,
ConflictsWith: []string{
"snapshot_id",
Expand Down Expand Up @@ -327,6 +326,35 @@ func ResourceInstance() *schema.Resource {
Optional: true,
Description: "Enable or disable encryption at rest for the database instance",
},
"upgradable_versions": {
Type: schema.TypeList,
Computed: true,
Description: "List of available engine versions for upgrade",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
Description: "Version ID for upgrade requests",
},
"name": {
Type: schema.TypeString,
Computed: true,
Description: "Engine name",
},
"version": {
Type: schema.TypeString,
Computed: true,
Description: "Version string",
},
"minor_version": {
Type: schema.TypeString,
Computed: true,
Description: "Minor version string",
},
},
},
},
"private_ip": {
Type: schema.TypeList,
Computed: true,
Expand Down Expand Up @@ -671,6 +699,18 @@ func ResourceRdbInstanceRead(ctx context.Context, d *schema.ResourceData, m any)
_ = d.Set("encryption_at_rest", res.Encryption.Enabled)
}

upgradableVersions := make([]map[string]any, len(res.UpgradableVersion))
for i, version := range res.UpgradableVersion {
upgradableVersions[i] = map[string]any{
"id": version.ID,
"name": version.Name,
"version": version.Version,
"minor_version": version.MinorVersion,
}
}

_ = d.Set("upgradable_versions", upgradableVersions)

// set user and password
if user, ok := d.GetOk("user_name"); ok {
_ = d.Set("user_name", user.(string))
Expand Down Expand Up @@ -911,21 +951,86 @@ func ResourceRdbInstanceUpdate(ctx context.Context, d *schema.ResourceData, m an
})
}

// Carry out the upgrades
if d.HasChange("engine") {
oldEngine, newEngine := d.GetChange("engine")
newEngineStr := newEngine.(string)

targetVersionID := ""

var availableVersions []string
for _, version := range rdbInstance.UpgradableVersion {
availableVersions = append(availableVersions, version.Name)
if version.Name == newEngineStr {
targetVersionID = version.ID

break
}
}

if targetVersionID == "" {
return diag.FromErr(fmt.Errorf("engine version %s is not available for upgrade from %s. Available versions: %v",
newEngineStr, oldEngine.(string), availableVersions))
}

upgradeInstanceRequests = append(upgradeInstanceRequests,
rdb.UpgradeInstanceRequest{
Region: region,
InstanceID: ID,
MajorUpgradeWorkflow: &rdb.UpgradeInstanceRequestMajorUpgradeWorkflow{
UpgradableVersionID: targetVersionID,
WithEndpoints: true,
},
})
}

for i := range upgradeInstanceRequests {
_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
if err != nil && !httperrors.Is404(err) {
return diag.FromErr(err)
}

_, err = rdbAPI.UpgradeInstance(&upgradeInstanceRequests[i], scw.WithContext(ctx))
upgradedInstance, err := rdbAPI.UpgradeInstance(&upgradeInstanceRequests[i], scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
if err != nil && !httperrors.Is404(err) {
return diag.FromErr(err)
if upgradeInstanceRequests[i].MajorUpgradeWorkflow != nil && upgradedInstance.ID != ID {
tflog.Info(ctx, fmt.Sprintf("Engine upgrade created new instance, updating ID from %s to %s", ID, upgradedInstance.ID))
oldInstanceID := ID
ID = upgradedInstance.ID
d.SetId(regional.NewIDString(region, ID))

_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
if err != nil && !httperrors.Is404(err) {
return diag.FromErr(err)
}

_, err = waitForRDBInstance(ctx, rdbAPI, region, oldInstanceID, d.Timeout(schema.TimeoutUpdate))
if err != nil && !httperrors.Is404(err) {
tflog.Warn(ctx, fmt.Sprintf("Old instance %s not ready for deletion: %v", oldInstanceID, err))
} else {
_, err = rdbAPI.DeleteInstance(&rdb.DeleteInstanceRequest{
Region: region,
InstanceID: oldInstanceID,
}, scw.WithContext(ctx))
if err != nil && !httperrors.Is404(err) {
tflog.Warn(ctx, fmt.Sprintf("Failed to delete old instance %s: %v", oldInstanceID, err))
} else {
_, err = rdbAPI.WaitForInstance(&rdb.WaitForInstanceRequest{
Region: region,
InstanceID: oldInstanceID,
Timeout: scw.TimeDurationPtr(d.Timeout(schema.TimeoutUpdate)),
}, scw.WithContext(ctx))
if err != nil && !httperrors.Is404(err) {
tflog.Warn(ctx, fmt.Sprintf("Error waiting for old instance %s deletion: %v", oldInstanceID, err))
}
}
}
} else {
_, err = waitForRDBInstance(ctx, rdbAPI, region, ID, d.Timeout(schema.TimeoutUpdate))
if err != nil && !httperrors.Is404(err) {
return diag.FromErr(err)
}
}
}

Expand Down
Loading
Loading