diff --git a/README.md b/README.md index a3f7a07..82a44b9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,20 @@ Please review the README.md in the examples for all Pre-deployment and Post-Depl The default configuration is a large production-sized deployment. Please consider that when testing and adjust the configuration to use smaller sized resources. +## Cloud SQL machine types + +The module supports only Cloud SQL Enterprise Plus. +It selects the disk type from `postgres_machine_type`. +The default remains `db-perf-optimized-N-8` +A change to the database machine type causes downtime. + +| Machine type | Disk | +| --- | --- | +| `db-perf-optimized-N-*` | SSD | +| `db-c4a-highmem-*` | Hyperdisk Balanced | +| `db-perf-optimized-C4-*` | Hyperdisk Balanced | + + ## Development Setup This section is only relevant if you are a contributor who wants to make changes to this module. All others can skip this section. diff --git a/examples/braintrust-data-plane/main.tf b/examples/braintrust-data-plane/main.tf index ef00386..89c112a 100644 --- a/examples/braintrust-data-plane/main.tf +++ b/examples/braintrust-data-plane/main.tf @@ -57,8 +57,9 @@ module "braintrust-data-plane" { # postgres_machine_type = "db-perf-optimized-N-8" # postgres_availability_type = "REGIONAL" # postgres_disk_size = 1000 - # Does this auto expand? how do we handle that? - # How do we control disk perf IOPS/etc + # Optional Hyperdisk performance values when using C4A or C4 machine types. Null uses Cloud SQL defaults. + # postgres_disk_provisioned_iops = 12000 + # postgres_disk_provisioned_throughput = 500 ### Redis configuration # redis_version = "REDIS_7_2" diff --git a/examples/braintrust-data-plane/versions.tf b/examples/braintrust-data-plane/versions.tf index 7f79077..fcdba56 100644 --- a/examples/braintrust-data-plane/versions.tf +++ b/examples/braintrust-data-plane/versions.tf @@ -4,11 +4,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" + version = ">= 8.2.0, < 9.0.0" } google-beta = { source = "hashicorp/google-beta" - version = "~> 6.45" + version = ">= 8.2.0, < 9.0.0" } } } \ No newline at end of file diff --git a/main.tf b/main.tf index 576b262..45a22cb 100644 --- a/main.tf +++ b/main.tf @@ -25,18 +25,20 @@ module "kms" { module "database" { source = "./modules/database" - deployment_name = var.deployment_name - custom_labels = var.custom_labels - postgres_network = var.create_vpc ? module.vpc[0].network_self_link : var.existing_network_self_link - postgres_kms_cmek_id = module.kms.kms_key_id - postgres_version = var.postgres_version - postgres_availability_type = var.postgres_availability_type - postgres_machine_type = var.postgres_machine_type - postgres_disk_size = var.postgres_disk_size - postgres_enable_seqscan = var.postgres_enable_seqscan - postgres_backup_start_time = var.postgres_backup_start_time - postgres_maintenance_window = var.postgres_maintenance_window - postgres_deletion_protection = var.postgres_deletion_protection + deployment_name = var.deployment_name + custom_labels = var.custom_labels + postgres_network = var.create_vpc ? module.vpc[0].network_self_link : var.existing_network_self_link + postgres_kms_cmek_id = module.kms.kms_key_id + postgres_version = var.postgres_version + postgres_availability_type = var.postgres_availability_type + postgres_machine_type = var.postgres_machine_type + postgres_disk_provisioned_iops = var.postgres_disk_provisioned_iops + postgres_disk_provisioned_throughput = var.postgres_disk_provisioned_throughput + postgres_disk_size = var.postgres_disk_size + postgres_enable_seqscan = var.postgres_enable_seqscan + postgres_backup_start_time = var.postgres_backup_start_time + postgres_maintenance_window = var.postgres_maintenance_window + postgres_deletion_protection = var.postgres_deletion_protection depends_on = [module.vpc] } diff --git a/modules/database/main.tf b/modules/database/main.tf index 6de2935..4972949 100644 --- a/modules/database/main.tf +++ b/modules/database/main.tf @@ -1,4 +1,6 @@ locals { + postgres_uses_hyperdisk = can(regex("^db-(c4a-highmem|perf-optimized-C4)-[0-9]+$", var.postgres_machine_type)) + common_labels = merge(var.custom_labels, { braintrustdeploymentname = var.deployment_name }) @@ -53,11 +55,15 @@ resource "google_sql_database_instance" "braintrust" { deletion_protection = var.postgres_deletion_protection settings { - availability_type = var.postgres_availability_type - tier = var.postgres_machine_type - disk_type = "PD_SSD" - disk_size = var.postgres_disk_size - disk_autoresize = true + availability_type = var.postgres_availability_type + tier = var.postgres_machine_type + edition = "ENTERPRISE_PLUS" + disk_type = local.postgres_uses_hyperdisk ? "HYPERDISK_BALANCED" : "PD_SSD" + data_disk_provisioned_iops = var.postgres_disk_provisioned_iops + data_disk_provisioned_throughput = var.postgres_disk_provisioned_throughput + disk_size = var.postgres_disk_size + disk_autoresize = true + disk_autoresize_limit = 0 # Braintrust will create a high number of connections to the database. Setting this to an extremely high amount of connections as changing this requires a DB restart. database_flags { diff --git a/modules/database/variables.tf b/modules/database/variables.tf index ea21bce..982f638 100644 --- a/modules/database/variables.tf +++ b/modules/database/variables.tf @@ -35,14 +35,67 @@ variable "postgres_availability_type" { variable "postgres_machine_type" { type = string - description = "Machine size of Cloud SQL for PostgreSQL instance." + description = "Enterprise Plus machine type from the N2, C4A, or C4 series. The machine type selects compatible storage settings." default = "db-perf-optimized-N-8" + + validation { + condition = can(regex("^db-(perf-optimized-(N|C4)|c4a-highmem)-[0-9]+$", var.postgres_machine_type)) + error_message = "Use an Enterprise Plus machine type from the N2, C4A, or C4 series." + } + + validation { + condition = !contains(["db-c4a-highmem-2", "db-perf-optimized-C4-2"], var.postgres_machine_type) + error_message = "C4A and C4 machine types require at least four vCPUs." + } +} + +variable "postgres_disk_provisioned_iops" { + type = number + description = "Hyperdisk IOPS. Null uses the Cloud SQL default for the disk size and machine type." + default = null + + validation { + condition = var.postgres_disk_provisioned_iops == null ? true : var.postgres_disk_provisioned_iops >= 3000 && floor(var.postgres_disk_provisioned_iops) == var.postgres_disk_provisioned_iops + error_message = "Hyperdisk IOPS must be an integer of at least 3000." + } + + validation { + condition = var.postgres_disk_provisioned_iops == null || can(regex("^db-(c4a-highmem|perf-optimized-C4)-[0-9]+$", var.postgres_machine_type)) + error_message = "Custom IOPS require a C4A or C4 machine type with Hyperdisk Balanced." + } +} + +variable "postgres_disk_provisioned_throughput" { + type = number + description = "Hyperdisk throughput in MiB/s. Null uses the Cloud SQL default for the disk size and machine type." + default = null + + validation { + condition = var.postgres_disk_provisioned_throughput == null ? true : var.postgres_disk_provisioned_throughput >= 140 && floor(var.postgres_disk_provisioned_throughput) == var.postgres_disk_provisioned_throughput + error_message = "Hyperdisk throughput must be an integer of at least 140 MiB/s." + } + + validation { + condition = var.postgres_disk_provisioned_throughput == null || can(regex("^db-(c4a-highmem|perf-optimized-C4)-[0-9]+$", var.postgres_machine_type)) + error_message = "Custom throughput requires a C4A or C4 machine type with Hyperdisk Balanced." + } } variable "postgres_disk_size" { type = number - description = "Size in GB of PostgreSQL disk." + description = "Initial PostgreSQL disk size in GB. Terraform ignores later changes to this value." default = 1000 + nullable = false + + validation { + condition = var.postgres_disk_size >= 10 && floor(var.postgres_disk_size) == var.postgres_disk_size + error_message = "The initial disk size must be an integer of at least 10 GB." + } + + validation { + condition = !can(regex("^db-(c4a-highmem|perf-optimized-C4)-[0-9]+$", var.postgres_machine_type)) || var.postgres_disk_size >= 20 + error_message = "Hyperdisk Balanced requires a disk size of at least 20 GB." + } } variable "postgres_enable_seqscan" { diff --git a/modules/database/versions.tf b/modules/database/versions.tf index 0620503..f552106 100644 --- a/modules/database/versions.tf +++ b/modules/database/versions.tf @@ -4,11 +4,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" + version = ">= 8.2.0, < 9.0.0" } google-beta = { source = "hashicorp/google-beta" - version = "~> 6.45" + version = ">= 8.2.0, < 9.0.0" } random = { source = "hashicorp/random" diff --git a/modules/gke-cluster/main.tf b/modules/gke-cluster/main.tf index f80aebb..3e4b63c 100644 --- a/modules/gke-cluster/main.tf +++ b/modules/gke-cluster/main.tf @@ -123,13 +123,6 @@ resource "google_container_cluster" "braintrust_autopilot" { google_kms_crypto_key_iam_member.gke_compute_cmek ] - lifecycle { - # GKE Autopilot may report ALL_OBJECTS_ENCRYPTION_ENABLED after create, but - # the Terraform provider currently accepts only ENCRYPTED/DECRYPTED as input. - ignore_changes = [ - database_encryption[0].state, - ] - } } #---------------------------------------------------------------------------------------------- diff --git a/modules/gke-cluster/versions.tf b/modules/gke-cluster/versions.tf index 67e6567..0fdb7ab 100644 --- a/modules/gke-cluster/versions.tf +++ b/modules/gke-cluster/versions.tf @@ -4,11 +4,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" - } - random = { - source = "hashicorp/random" - version = ">= 3.7.2" + version = ">= 8.2.0, < 9.0.0" } } } diff --git a/modules/gke-iam/versions.tf b/modules/gke-iam/versions.tf index 0620503..0fdb7ab 100644 --- a/modules/gke-iam/versions.tf +++ b/modules/gke-iam/versions.tf @@ -4,15 +4,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" - } - google-beta = { - source = "hashicorp/google-beta" - version = "~> 6.45" - } - random = { - source = "hashicorp/random" - version = ">= 3.7.2" + version = ">= 8.2.0, < 9.0.0" } } } diff --git a/modules/kms/versions.tf b/modules/kms/versions.tf index 67e6567..7804b7b 100644 --- a/modules/kms/versions.tf +++ b/modules/kms/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" + version = ">= 8.2.0, < 9.0.0" } random = { source = "hashicorp/random" diff --git a/modules/redis/versions.tf b/modules/redis/versions.tf index 67e6567..0fdb7ab 100644 --- a/modules/redis/versions.tf +++ b/modules/redis/versions.tf @@ -4,11 +4,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" - } - random = { - source = "hashicorp/random" - version = ">= 3.7.2" + version = ">= 8.2.0, < 9.0.0" } } } diff --git a/modules/storage/versions.tf b/modules/storage/versions.tf index 67e6567..7804b7b 100644 --- a/modules/storage/versions.tf +++ b/modules/storage/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" + version = ">= 8.2.0, < 9.0.0" } random = { source = "hashicorp/random" diff --git a/modules/vpc/versions.tf b/modules/vpc/versions.tf index 0620503..0fdb7ab 100644 --- a/modules/vpc/versions.tf +++ b/modules/vpc/versions.tf @@ -4,15 +4,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" - } - google-beta = { - source = "hashicorp/google-beta" - version = "~> 6.45" - } - random = { - source = "hashicorp/random" - version = ">= 3.7.2" + version = ">= 8.2.0, < 9.0.0" } } } diff --git a/tests/sql_disk_options.tftest.hcl b/tests/sql_disk_options.tftest.hcl new file mode 100644 index 0000000..62b9438 --- /dev/null +++ b/tests/sql_disk_options.tftest.hcl @@ -0,0 +1,50 @@ +mock_provider "google" {} +mock_provider "google-beta" {} +mock_provider "random" {} + +variables { + deployment_name = "sql-disk-test" + postgres_network = "projects/test/global/networks/test" + postgres_kms_cmek_id = "projects/test/locations/us-central1/keyRings/test/cryptoKeys/test" +} + +run "default_growth" { + command = plan + module { source = "./modules/database" } + assert { + condition = google_sql_database_instance.braintrust.settings[0].disk_autoresize && google_sql_database_instance.braintrust.settings[0].disk_autoresize_limit == 0 + error_message = "Automatic disk growth must remain enabled without a custom limit." + } +} + +run "reject_small_disk" { + command = plan + module { source = "./modules/database" } + variables { + postgres_disk_size = 9 + } + expect_failures = [var.postgres_disk_size] +} + +run "n2_accepts_ten_gb_disk" { + command = plan + module { source = "./modules/database" } + variables { postgres_disk_size = 10 } + assert { + condition = google_sql_database_instance.braintrust.settings[0].disk_size == 10 + error_message = "N2 must retain support for a 10 GB SSD." + } +} + +run "c4a_accepts_twenty_gb_disk" { + command = plan + module { source = "./modules/database" } + variables { + postgres_machine_type = "db-c4a-highmem-8" + postgres_disk_size = 20 + } + assert { + condition = google_sql_database_instance.braintrust.settings[0].disk_size == 20 + error_message = "C4A must accept the 20 GB Hyperdisk minimum." + } +} diff --git a/tests/sql_machine_types.tftest.hcl b/tests/sql_machine_types.tftest.hcl new file mode 100644 index 0000000..9e1f6a3 --- /dev/null +++ b/tests/sql_machine_types.tftest.hcl @@ -0,0 +1,166 @@ +mock_provider "google" {} +mock_provider "google-beta" {} +mock_provider "random" {} + +variables { + deployment_name = "sql-test" + postgres_network = "projects/test/global/networks/test" + postgres_kms_cmek_id = "projects/test/locations/us-central1/keyRings/test/cryptoKeys/test" +} + +run "n2_defaults" { + command = plan + module { source = "./modules/database" } + assert { + condition = ( + google_sql_database_instance.braintrust.settings[0].edition == "ENTERPRISE_PLUS" && + google_sql_database_instance.braintrust.settings[0].disk_type == "PD_SSD" + ) + error_message = "The machine type must select compatible edition and storage settings." + } + assert { + condition = google_sql_database_instance.braintrust.settings[0].data_cache_config[0].data_cache_enabled == true + error_message = "The module must keep the data cache enabled." + } +} + +run "reject_c4a_two_cpu" { + command = plan + module { source = "./modules/database" } + variables { postgres_machine_type = "db-c4a-highmem-2" } + expect_failures = [var.postgres_machine_type] +} + +run "c4a_four_cpu" { + command = plan + module { source = "./modules/database" } + variables { postgres_machine_type = "db-c4a-highmem-4" } + assert { + condition = ( + google_sql_database_instance.braintrust.settings[0].edition == "ENTERPRISE_PLUS" && + google_sql_database_instance.braintrust.settings[0].disk_type == "HYPERDISK_BALANCED" + ) + error_message = "The machine type must select compatible edition and storage settings." + } + assert { + condition = google_sql_database_instance.braintrust.settings[0].data_cache_config[0].data_cache_enabled == true + error_message = "The module must keep the data cache enabled." + } +} + +run "reject_c4_two_cpu" { + command = plan + module { source = "./modules/database" } + variables { postgres_machine_type = "db-perf-optimized-C4-2" } + expect_failures = [var.postgres_machine_type] +} + +run "c4_four_cpu" { + command = plan + module { source = "./modules/database" } + variables { postgres_machine_type = "db-perf-optimized-C4-4" } + assert { + condition = ( + google_sql_database_instance.braintrust.settings[0].edition == "ENTERPRISE_PLUS" && + google_sql_database_instance.braintrust.settings[0].disk_type == "HYPERDISK_BALANCED" + ) + error_message = "The machine type must select compatible edition and storage settings." + } + assert { + condition = google_sql_database_instance.braintrust.settings[0].data_cache_config[0].data_cache_enabled == true + error_message = "The module must keep the data cache enabled." + } +} + +run "hyperdisk_performance" { + command = plan + module { source = "./modules/database" } + variables { + postgres_machine_type = "db-c4a-highmem-4" + postgres_disk_provisioned_iops = 12000 + postgres_disk_provisioned_throughput = 500 + } + assert { + condition = ( + google_sql_database_instance.braintrust.settings[0].data_disk_provisioned_iops == 12000 && + google_sql_database_instance.braintrust.settings[0].data_disk_provisioned_throughput == 500 + ) + error_message = "The resource must receive custom Hyperdisk performance settings." + } +} + +run "reject_ssd_performance" { + command = plan + module { source = "./modules/database" } + variables { + postgres_disk_provisioned_iops = 4000 + } + expect_failures = [var.postgres_disk_provisioned_iops] +} + +run "reject_small_hyperdisk" { + command = plan + module { source = "./modules/database" } + variables { + postgres_machine_type = "db-c4a-highmem-4" + postgres_disk_size = 10 + } + expect_failures = [var.postgres_disk_size] +} + +run "reject_unknown_series" { + command = plan + module { source = "./modules/database" } + variables { + postgres_machine_type = "db-unknown-2" + } + expect_failures = [var.postgres_machine_type] +} + +run "reject_low_iops" { + command = plan + module { source = "./modules/database" } + variables { + postgres_disk_provisioned_iops = 2999 + postgres_machine_type = "db-c4a-highmem-4" + } + expect_failures = [var.postgres_disk_provisioned_iops] +} + +run "reject_low_throughput" { + command = plan + module { source = "./modules/database" } + variables { + postgres_disk_provisioned_throughput = 139 + postgres_machine_type = "db-c4a-highmem-4" + } + expect_failures = [var.postgres_disk_provisioned_throughput] +} + +run "reject_enterprise_n4" { + command = plan + module { source = "./modules/database" } + variables { postgres_machine_type = "db-custom-N4-2-16384" } + expect_failures = [var.postgres_machine_type] +} + +run "reject_enterprise_custom" { + command = plan + module { source = "./modules/database" } + variables { postgres_machine_type = "db-custom-2-8192" } + expect_failures = [var.postgres_machine_type] +} + +run "reject_enterprise_shared_core" { + command = plan + module { source = "./modules/database" } + variables { postgres_machine_type = "db-f1-micro" } + expect_failures = [var.postgres_machine_type] +} + +run "reject_ssd_throughput" { + command = plan + module { source = "./modules/database" } + variables { postgres_disk_provisioned_throughput = 200 } + expect_failures = [var.postgres_disk_provisioned_throughput] +} diff --git a/variables.tf b/variables.tf index f230a7d..f285c84 100644 --- a/variables.tf +++ b/variables.tf @@ -186,8 +186,18 @@ variable "postgres_version" { variable "postgres_machine_type" { type = string - description = "Machine size of Cloud SQL for PostgreSQL instance." + description = "Enterprise Plus machine type from the N2, C4A, or C4 series. The machine type selects compatible storage settings." default = "db-perf-optimized-N-8" + + validation { + condition = can(regex("^db-(perf-optimized-(N|C4)|c4a-highmem)-[0-9]+$", var.postgres_machine_type)) + error_message = "Use an Enterprise Plus machine type from the N2, C4A, or C4 series." + } + + validation { + condition = !contains(["db-c4a-highmem-2", "db-perf-optimized-C4-2"], var.postgres_machine_type) + error_message = "C4A and C4 machine types require at least four vCPUs." + } } variable "postgres_availability_type" { @@ -196,10 +206,53 @@ variable "postgres_availability_type" { default = "REGIONAL" } +variable "postgres_disk_provisioned_iops" { + type = number + description = "Hyperdisk IOPS. Null uses the Cloud SQL default for the disk size and machine type." + default = null + + validation { + condition = var.postgres_disk_provisioned_iops == null ? true : var.postgres_disk_provisioned_iops >= 3000 && floor(var.postgres_disk_provisioned_iops) == var.postgres_disk_provisioned_iops + error_message = "Hyperdisk IOPS must be an integer of at least 3000." + } + + validation { + condition = var.postgres_disk_provisioned_iops == null || can(regex("^db-(c4a-highmem|perf-optimized-C4)-[0-9]+$", var.postgres_machine_type)) + error_message = "Custom IOPS require a C4A or C4 machine type with Hyperdisk Balanced." + } +} + +variable "postgres_disk_provisioned_throughput" { + type = number + description = "Hyperdisk throughput in MiB/s. Null uses the Cloud SQL default for the disk size and machine type." + default = null + + validation { + condition = var.postgres_disk_provisioned_throughput == null ? true : var.postgres_disk_provisioned_throughput >= 140 && floor(var.postgres_disk_provisioned_throughput) == var.postgres_disk_provisioned_throughput + error_message = "Hyperdisk throughput must be an integer of at least 140 MiB/s." + } + + validation { + condition = var.postgres_disk_provisioned_throughput == null || can(regex("^db-(c4a-highmem|perf-optimized-C4)-[0-9]+$", var.postgres_machine_type)) + error_message = "Custom throughput requires a C4A or C4 machine type with Hyperdisk Balanced." + } +} + variable "postgres_disk_size" { type = number - description = "Size in GB of PostgreSQL disk." + description = "Initial PostgreSQL disk size in GB. Terraform ignores later changes to this value." default = 1000 + nullable = false + + validation { + condition = var.postgres_disk_size >= 10 && floor(var.postgres_disk_size) == var.postgres_disk_size + error_message = "The initial disk size must be an integer of at least 10 GB." + } + + validation { + condition = !can(regex("^db-(c4a-highmem|perf-optimized-C4)-[0-9]+$", var.postgres_machine_type)) || var.postgres_disk_size >= 20 + error_message = "Hyperdisk Balanced requires a disk size of at least 20 GB." + } } variable "postgres_enable_seqscan" { diff --git a/versions.tf b/versions.tf index 4cb73ce..81f60dd 100644 --- a/versions.tf +++ b/versions.tf @@ -4,16 +4,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "~> 6.45" + version = ">= 8.2.0, < 9.0.0" } google-beta = { source = "hashicorp/google-beta" - version = "~> 6.45" + version = ">= 8.2.0, < 9.0.0" } - random = { - source = "hashicorp/random" - version = ">= 3.7.2" - } - } }