Provisions a
google_sql_database_instance(MySQL, PostgreSQL, or SQL Server) together with itsgoogle_sql_databaseandgoogle_sql_userchild records, targetinghashicorp/google ~> 7.0.
- 🗄️ Provisions one
google_sql_database_instance.this— MySQL, PostgreSQL, or SQL Server — as the keystone resource. - 📚 Manages any number of
google_sql_database.databasesviafor_eachovervar.databases. - 👤 Manages any number of
google_sql_user.usersviafor_eachovervar.users, with write-only, never-plaintext password handling. - 🔒 Public IP disabled, both deletion-protection guards enabled, and backups (with engine-appropriate point-in-time recovery) enabled — all by default.
- 🔑 Never stores a user password in Terraform state —
password_wo/password_wo_versiononly, nopasswordargument exposed anywhere in this module.
💡 Why it matters: Cloud SQL instances are long-lived, stateful, and expensive to get wrong — a name-reuse cooldown of up to one week, two independent deletion guards, and an engine/edition interaction that fails only at
applytime (notplan) are all real traps this module either closes at plan time or documents loudly rather than leaving the caller to discover them by reading an API error.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
This module has no confirmed downstream consumer module in the current catalog — it is consumed directly by application-layer compute (Cloud Run, GKE, Cloud SQL Proxy sidecars) that sit outside this Terraform library's scope. Two sibling modules feed into it optionally, and one real cross-module gap sits directly upstream of the private-IP path:
flowchart LR
VPC["terraform-google-vpc-network\n(VPC network)"]:::sibling
KMS["terraform-google-kms-keyring\n(CMEK crypto key)"]:::sibling
SNC(("google_service_networking_connection\nNOT in this catalog — caller-owned")):::gap
SQL["terraform-google-cloud-sql-instance"]:::this
NONE["No confirmed downstream\nconsumer module yet"]:::sibling
VPC -- "self_link (optional, private IP)" --> SQL
KMS -- "crypto key id (optional CMEK)" --> SQL
SNC -. "required prerequisite if private IP is used — out of scope, caller must create".-> SQL
SQL -- "connection_name / self_link / id" --> NONE
classDef this fill:#4285F4,color:#ffffff,stroke:#174EA6,stroke-width:2px;
classDef sibling fill:#e8eaed,color:#202124,stroke:#9aa0a6,stroke-width:1px;
classDef gap fill:#ffffff,color:#5f6368,stroke:#d93025,stroke-width:2px,stroke-dasharray: 5 5;
terraform-google-vpc-network's self_link output feeds settings.ip_configuration.private_network
only if the caller opts out of the public-IP default. terraform-google-kms-keyring's
crypto_key_ids output feeds encryption_key_name for CMEK. The
google_service_networking_connection node is dashed because it is a real, required prerequisite
for private IP that this library does not own or create — see SCOPE.md and the
Three resource types: one keystone, two independent for_each child collections.
flowchart TB
INSTANCE["google_sql_database_instance.this\n(keystone)"]:::keystone
DB["google_sql_database.databases\n(for_each over var.databases)"]:::this
USR["google_sql_user.users\n(for_each over var.users)"]:::this
INSTANCE -- "instance = google_sql_database_instance.this.name" --> DB
INSTANCE -- "instance = google_sql_database_instance.this.name" --> USR
classDef keystone fill:#174EA6,color:#ffffff,stroke:#174EA6,stroke-width:2px;
classDef this fill:#4285F4,color:#ffffff,stroke:#174EA6,stroke-width:2px;
| Resource | Cardinality | Notes |
|---|---|---|
google_sql_database_instance.this |
1 | Keystone; MySQL, PostgreSQL, or SQL Server |
google_sql_database.databases |
0..n (for_each over var.databases) |
Independent lifecycle, never inline |
google_sql_user.users |
0..n (for_each over var.users) |
Write-only password only, never plaintext |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/google |
~> 7.0 (resolved 7.39.0 at authoring time) |
| Provider block | None — the caller's root module configures google (ADC / WIF / service-account key) |
Schema notes that bite (all confirmed against the live, resolved 7.39.0 schema):
- Instance name-reuse cooldown is up to one week. Confirmed schema text: "after a name is used,
it cannot be reused for up to
one week." A
destroyimmediately followed by anapplyreusing the sameinstance_namecan fail at the API level for up to a week even thoughterraform planshows no conflict. - Three deletion-related mechanisms, not two.
deletion_protection(top-level, Terraform-only — blocksterraform destroy/applyinside Terraform only) andsettings.deletion_protection_enabled(API-level — blocks deletion across gcloud, Console, and the API too) are the two guards mandated by this module suite's house rule, bothtrueby default here. A confirmed THIRD field, top-leveldeletion_policy(enumDELETE/ABANDON/PREVENT, default"DELETE", also present ongoogle_sql_databaseandgoogle_sql_user), governs a different axis — whatterraform destroyactually does — and is left at its provider default rather than stacked as a third blocker. See Architecture Notes. ssl_modehas supersededrequire_ssl.require_ssldoes not exist anywhere in this provider version's schema.settings.ip_configuration.ssl_modeis a closed three-value enum:ALLOW_UNENCRYPTED_AND_ENCRYPTED,ENCRYPTED_ONLY(this module's default), andTRUSTED_CLIENT_CERTIFICATE_REQUIRED(not supported for SQL Server).point_in_time_recovery_enabledis engine-gated and restarts the database. Confirmed schema text: "Will restart database if enabled after instance creation. Valid only for PostgreSQL and SQL Server instances." MySQL usesbinary_log_enabledinstead — this module gates which one it sends to the API offdatabase_version's engine-family prefix, never both.encryption_key_name(CMEK) requires manual, non-Terraform setup. Confirmed schema text: "a special kind of service account must be created and granted permission on this key" — this module accepts the variable but cannot automate that grant.google_sql_userpasswords are stored in Terraform state as plaintext — if you usepassword. This module never exposes apasswordargument at all; only the write-onlypassword_wo/password_wo_versionpair is modeled, so there is no plaintext-password-in-state path through this module for any user it manages.google_sql_user.host/.name/.instanceare force-new.hostapplies only toBUILT_INusers on MySQL instances — never set it for PostgreSQL or SQL Server.- Timeouts are asymmetric across all three resources. Instance: create/update/delete, 90
minutes default, no
readtimeout. Database: create/update/delete, 20 minutes default. User: create/update/delete, 10 minutes default. Each is its owntimeouts/per-entry-timeoutssurface — never one shared block. - PostgreSQL 16+ auto-defaults to
ENTERPRISE_PLUSedition, which rejects shared-core/custom tiers. Confirmed schema text: "instances with database_version POSTGRES_16 or later default to ENTERPRISE_PLUS... shared-core and custom tiers such as db-g1-small, db-f1-micro, and db-custom-* require edition = "ENTERPRISE"." This module adds a plan-timevalidationonsettingsthat catches this combination beforeapply— see Architecture Notes. private_networkis documented two ways; this module follows the executable example. The schema's prose example is relative-id-shaped, but the resource's own worked "Private IP Instance" example setsprivate_network = google_compute_network.private_network.self_link. This module's README and variable description point callers at.self_link(fromterraform-google-vpc-network).
roles/cloudsql.admin— full lifecycle management (create/update/delete) of Cloud SQL instances, databases, and users on the target project. This is the only role confirmed for this module (see SCOPE.md).
sqladmin.googleapis.comAPI enabled on the target project (viaterraform-google-project-services).- If you opt into private IP (
settings.ip_configuration.ipv4_enabled = falsewithprivate_networkset): agoogle_service_networking_connection(private services access peering) must already exist in the target VPC. This module does not create or verify it —terraform plancannot catch a missing peering; onlyapplywill fail with an API-level error. The resource's own docs additionally note thatgoogle_sql_database_instancedoes not auto-interpolate a dependency on this peering — your composition must add an explicitdepends_on(see the⚠️ callout in Example 8 below). - Quota: Cloud SQL instance count quota per project/region, and disk size/IOPS quota appropriate to
settings.tier/settings.edition— enforced only atapply. - No specific org-policy constraint blocks this module's default configuration.
terraform-google-cloud-sql-instance/
├── providers.tf # required_providers + required_version — no provider {} block
├── variables.tf # instance/settings/databases/users object schemas, secure defaults
├── main.tf # google_sql_database_instance.this + 2 for_each child resources
├── outputs.tf # id, self_link, connection_name, name, region, IPs, child name maps
├── README.md # this file
├── SCOPE.md # cross-module contract
└── examples/
└── basic/ # smallest real call, matching Quick Start below
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "billing-primary"
database_version = "POSTGRES_15"
settings = {
tier = "db-custom-2-7680"
}
databases = {
"billing" = {}
}
users = {
"app_svc" = {
password_wo = var.app_svc_password
password_wo_version = 1
}
}
}The caller's root module configures the google provider (ADC, Workload Identity Federation, or a
service-account key) — this module never declares project/region/zone/credentials.
Consumes
| Input | Type | Source module |
|---|---|---|
settings.ip_configuration.private_network (optional) |
string (.self_link) |
terraform-google-vpc-network |
encryption_key_name (optional) |
string (full KMS crypto key resource path) |
terraform-google-kms-keyring |
| (none required) — runs standalone with public-IP-disabled, no-CMEK defaults | — | — |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Instance resource id | None confirmed yet in the catalog |
self_link |
Instance self_link (full API URL) |
None confirmed yet |
connection_name |
{project}:{region}:{name} — the form Cloud SQL Proxy/Cloud Run/GKE sidecars actually want |
App-layer compute outside this library's scope |
name |
Instance name | — |
region |
Effective region | — |
public_ip_address / private_ip_address |
Convenience computed IPs (null if not applicable) |
Firewall/connection-string composition |
database_names |
Map of database key → name | — |
user_names |
Map of user key → name (never the password) | — |
1 · Minimal MySQL instance
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "app-mysql"
database_version = "MYSQL_8_0"
settings = {
tier = "db-f1-micro"
}
}ℹ️ No
databases/userssupplied — a valid, empty-child-collection instance. Public IP stays disabled (ipv4_enabled = falsedefault) with noauthorized_networks, so nothing can connect until you add a private network or opt into public IP explicitly (Example 3).
2 · PostgreSQL with databases and an IAM-authenticated user
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "reporting-postgres"
database_version = "POSTGRES_15"
settings = {
tier = "db-custom-4-15360"
}
databases = {
"reporting" = { charset = "UTF8", collation = "en_US.UTF8" }
}
users = {
"svc-reporting@my-project.iam" = {
type = "CLOUD_IAM_SERVICE_ACCOUNT"
}
}
}💡
CLOUD_IAM_SERVICE_ACCOUNT/CLOUD_IAM_USERtypes need nopassword_wo— leave itnull(this module'susersvalidation rejects setting it for those two types).
3 · Explicit public IP with authorized networks
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "vendor-integration-mysql"
database_version = "MYSQL_8_0"
settings = {
tier = "db-f1-micro"
ip_configuration = {
ipv4_enabled = true
authorized_networks = {
"vendor-office" = { cidr_range = "203.0.113.0/24" }
"vendor-vpn" = { cidr_range = "198.51.100.4", expiration_time = "2027-01-01T00:00:00Z" }
}
}
}
}
⚠️ ipv4_enabled = trueis an explicit opt-out of this module's secure default. Everyauthorized_networksentry is an open inbound path from that CIDR — keep the list as narrow as possible and setexpiration_timefor anything temporary.
4 · SQL Server with a root write-only password
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "legacy-app-sqlserver"
database_version = "SQLSERVER_2022_STANDARD"
root_password_wo = var.sqlserver_root_password
root_password_wo_version = 1
settings = {
tier = "db-custom-4-15360"
edition = "ENTERPRISE"
ip_configuration = {
ssl_mode = "ENCRYPTED_ONLY"
}
}
}🔒
root_password_wois required for SQL Server and, like every password in this module, is write-only — never persisted to Terraform state. Incrementroot_password_wo_versionwhenever the value changes.
5 · Custom database flags (Cloud SQL IAM database authentication)
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "iam-auth-postgres"
database_version = "POSTGRES_15"
settings = {
tier = "db-custom-2-7680"
database_flags = {
"cloudsql.iam_authentication" = "on"
}
}
users = {
"engineer@example.com" = { type = "CLOUD_IAM_USER" }
}
}ℹ️
database_flagsis amap(string)(flag name → value) rather than a list — flag names are already unique identities, so a map gives stablefor_each-style behavior without a list-index diff when a flag is added or removed.
6 · High-availability PostgreSQL (REGIONAL) with maintenance window
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "core-ledger-postgres"
database_version = "POSTGRES_15"
settings = {
tier = "db-custom-8-30720"
availability_type = "REGIONAL"
maintenance_window = {
day = 7
hour = 3
update_track = "stable"
}
}
}💡
availability_type = "REGIONAL"requiressettings.backup_configuration.enabled = true(this module's default) — for PostgreSQL, also ensure PITR stays enabled (also this module's default for non-MySQL engines).
7 · PostgreSQL 16+ with a shared-core tier (edition trap, avoided)
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "budget-postgres16"
database_version = "POSTGRES_16"
settings = {
tier = "db-f1-micro"
edition = "ENTERPRISE" # required — see ⚠️ below
}
}
⚠️ PostgreSQL 16+ auto-defaults toedition = "ENTERPRISE_PLUS"wheneditionis omitted, andENTERPRISE_PLUSonly acceptsdb-perf-optimized-N-*/db-perf-optimized-C4A-*tiers. Omittingeditionhere withtier = "db-f1-micro"fails this module's plan-timevalidation— setedition = "ENTERPRISE"explicitly whenever you want a shared-core or custom tier on PostgreSQL 16 or later.
8 · Private IP (with the required peering prerequisite)
# In the SAME composition — NOT owned by terraform-google-cloud-sql-instance:
resource "google_compute_global_address" "private_ip_range" {
name = "cloud-sql-psa-range"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = module.vpc_network.id
}
resource "google_service_networking_connection" "private_vpc_connection" {
network = module.vpc_network.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.private_ip_range.name]
}
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "private-only-mysql"
database_version = "MYSQL_8_0"
settings = {
tier = "db-custom-2-7680"
ip_configuration = {
ipv4_enabled = false
private_network = module.vpc_network.self_link
enable_private_path_for_google_cloud_services = true
}
}
depends_on = [google_service_networking_connection.private_vpc_connection]
}
⚠️ google_service_networking_connectionis out of scope for this module and is not in the current catalog. You must create it yourself in the consuming composition, exactly as shown. Thegoogle_sql_database_instanceresource does not auto-interpolate a dependency on this peering — the explicitdepends_onshown above is required, per the resource's own documented caveat, or the instance creation can race the peering and fail.
9 · Multiple databases and users in one instance
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "shared-mysql"
database_version = "MYSQL_8_0"
settings = {
tier = "db-custom-4-15360"
}
databases = {
"orders" = {}
"invoices" = {}
"audit" = {}
}
users = {
"orders_svc" = { host = "10.0.%.%", password_wo = var.orders_pw, password_wo_version = 1 }
"invoices_svc" = { host = "10.0.%.%", password_wo = var.invoices_pw, password_wo_version = 1 }
"readonly_bi" = { host = "%", password_wo = var.bi_pw, password_wo_version = 1 }
}
}ℹ️
hostis only valid forBUILT_INusers on MySQL — each map key is independently force-new, so adding or removing one database/user entry never touches the others.
10 · Rotating a user's write-only password
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "app-mysql"
database_version = "MYSQL_8_0"
settings = { tier = "db-f1-micro" }
users = {
"app_svc" = {
password_wo = var.app_svc_password_v2 # new secret value
password_wo_version = 2 # incremented from 1 — this triggers the update
}
}
}🔒 Write-only arguments carry no value in state, so Terraform only detects a password rotation via the incremented
password_wo_versioncounter — changingpassword_wo's value alone, without bumping the version, produces no diff.
11 · Password validation policy for BUILT_IN users
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "hardened-postgres"
database_version = "POSTGRES_15"
settings = {
tier = "db-custom-2-7680"
password_validation_policy = {
enable_password_policy = true
min_length = 14
disallow_username_substring = true
reuse_interval = 5
complexity = "COMPLEXITY_DEFAULT"
}
}
users = {
"dba_admin" = {
password_wo = var.dba_admin_password
password_wo_version = 1
password_policy = {
enable_failed_attempts_check = true
allowed_failed_attempts = 5
}
}
}
}ℹ️
password_validation_policy(instance-level) andpassword_policy(per-user) are both only supported for MySQL and PostgreSQL — not SQL Server.
12 · Decommissioning: the two-step deletion-protection apply
# Step 1 — flip both guards to false and apply (writes the change to state,
# does NOT delete anything yet):
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "retiring-instance"
database_version = "MYSQL_8_0"
deletion_protection = false # was true (default)
settings = {
tier = "db-f1-micro"
deletion_protection_enabled = false # was true (default)
}
}
# Step 2 — after that apply succeeds, remove the module block (or run
# terraform destroy) to actually delete the instance.
⚠️ Skipping step 1 and going straight toterraform destroyfails:deletion_protectionblocks the destroy inside Terraform, and even if that were bypassed,settings.deletion_protection_enabledblocks it at the API level too. See Troubleshooting.
13 · Abandoning a user Terraform cannot delete (Postgres SQL roles)
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "roles-postgres"
database_version = "POSTGRES_15"
settings = { tier = "db-custom-2-7680" }
users = {
"role_holder" = {
password_wo = var.role_holder_password
password_wo_version = 1
deletion_policy = "ABANDON" # this user has been granted SQL roles; the API refuses DELETE
}
}
}ℹ️ Confirmed schema note on
google_sql_user.deletion_policy:"ABANDON"is "useful for Postgres, where users cannot be deleted from the API if they have been granted SQL roles." This removes the record from Terraform state without calling the delete API — the database role itself must still be revoked out-of-band if it needs to go away.
14 · CMEK-encrypted instance
module "kms" {
source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"
key_ring_name = "cloud-sql-cmek"
location = "us-east1"
crypto_keys = {
"cloud-sql-key" = { purpose = "ENCRYPT_DECRYPT" }
}
}
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "cmek-encrypted-mysql"
database_version = "MYSQL_8_0"
region = "us-east1" # must match the KMS key's location
encryption_key_name = module.kms.crypto_key_ids["cloud-sql-key"]
settings = {
tier = "db-custom-2-7680"
}
}🔒 The CMEK key must be in the same region as the instance. Manual, non-Terraform steps are still required (see ☁️ GCP Prerequisites) — this module accepts
encryption_key_namebut cannot grant the Cloud SQL service account access to the key on your behalf.
15 · 🏗️ End-to-end composition — VPC, CMEK, private IP, databases, and users
module "vpc_network" {
source = "git::https://github.com/microsoftexpert/terraform-google-vpc-network.git?ref=v1.0.0"
network_name = "data-tier-vpc"
subnetworks = {
"data-tier-use1" = { ip_cidr_range = "10.20.0.0/24", region = "us-east1" }
}
}
module "kms" {
source = "git::https://github.com/microsoftexpert/terraform-google-kms-keyring.git?ref=v1.0.0"
key_ring_name = "data-tier-cmek"
location = "us-east1"
crypto_keys = {
"cloud-sql-key" = { purpose = "ENCRYPT_DECRYPT" }
}
}
# google_service_networking_connection is a real, required prerequisite this
# library does not own — see ⚠️ Example 8 and SCOPE.md.
resource "google_compute_global_address" "private_ip_range" {
name = "data-tier-psa-range"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = module.vpc_network.id
}
resource "google_service_networking_connection" "private_vpc_connection" {
network = module.vpc_network.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.private_ip_range.name]
}
module "cloud_sql" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-sql-instance.git?ref=v1.0.0"
instance_name = "data-tier-postgres"
database_version = "POSTGRES_16"
region = "us-east1"
encryption_key_name = module.kms.crypto_key_ids["cloud-sql-key"]
settings = {
tier = "db-custom-4-15360"
edition = "ENTERPRISE" # required for a non-perf-optimized tier on PG16+, see Example 7
availability_type = "REGIONAL"
ip_configuration = {
ipv4_enabled = false
private_network = module.vpc_network.self_link
}
backup_configuration = {
enabled = true # point_in_time_recovery_enabled defaults true for PostgreSQL
}
}
databases = {
"app_primary" = { charset = "UTF8", collation = "en_US.UTF8" }
}
users = {
"app_svc" = {
password_wo = var.app_svc_password
password_wo_version = 1
}
"svc-account@my-project.iam" = {
type = "CLOUD_IAM_SERVICE_ACCOUNT"
}
}
labels = {
environment = "prod"
data_tier = "postgres"
}
depends_on = [google_service_networking_connection.private_vpc_connection]
}
output "cloud_sql_connection_name" {
value = module.cloud_sql.connection_name
}💡 This wires
terraform-google-vpc-network'sself_linkoutput intoprivate_network,terraform-google-kms-keyring'scrypto_key_idsoutput intoencryption_key_name, satisfies thegoogle_service_networking_connectionprerequisite explicitly (with the requireddepends_on), and avoids the PostgreSQL 16+ edition trap from Example 7 — the full shape a real production composition needs, not just the module call in isolation.
Top-level: instance_name (required) · database_version (required, engine-family validated)
· region (optional) · deletion_protection (default true) · deletion_policy (default
"DELETE") · encryption_key_name (optional CMEK) · final_backup_description (optional) ·
root_password_wo/root_password_wo_version (SQL Server only) · labels (default {}) ·
timeouts (instance-scoped only)
settings (required — only tier is mandatory): engine/machine configuration, ip_configuration
(secure default ipv4_enabled = false), backup_configuration (secure default enabled = true,
engine-gated PITR/binary-log), final_backup_config, database_flags (map(string)),
maintenance_window, insights_config (secure-ish default query_insights_enabled = true),
password_validation_policy, advanced_machine_features, location_preference,
data_cache_config.
databases (map(object(...)), default {}): charset, collation, deletion_policy,
timeouts.
users (map(object(...)), default {}): type (default "BUILT_IN"), host (MySQL
BUILT_IN only, force-new), password_wo/password_wo_version (write-only, never plaintext),
database_roles, deletion_policy, password_policy, timeouts.
Full object schemas
See variables.tf in full — every field is documented inline with its default, validation, and
(where relevant) force-new/engine-gating behavior. The file-level comment block at the top of
variables.tf documents six cross-cutting design decisions (the labels → settings.user_labels
mapping, the three deletion guards, the private_network self_link finding, the PostgreSQL
16+/edition validation, the full excluded-by-design block list, and the asymmetric timeouts) before
any individual variable.
| Output | Description |
|---|---|
id |
Instance Terraform-internal resource id |
self_link |
Instance self_link (full API URL) |
connection_name |
The form most consumers actually want — Cloud SQL Proxy/Cloud Run/GKE sidecar connection string |
name |
Instance name |
region |
Effective region |
public_ip_address |
First public IPv4, null if ipv4_enabled = false |
private_ip_address |
First private IPv4, null unless private IP is configured and the peering prerequisite exists |
maintenance_version |
Currently-applied maintenance version (computed) |
database_names |
Map of database key → name |
user_names |
Map of user key → name — never the password |
No output ever surfaces a password, write-only or otherwise — there is nothing to surface, since this module never reads one back.
connection_nameis elevated to a first-class identity output alongsideid/self_linkspecifically for this module: Cloud SQL Proxy, Cloud Run, and GKE sidecar integrations consume{project}:{region}:{name}far more often than either the Terraform-internalidor the APIself_link. Every other module in this library orders outputsid,self_link, then name/child-maps; this module insertsconnection_nameimmediately afterself_linkas a documented, deliberate exception.deletion_protectionvs.settings.deletion_protection_enabled— different scopes, same default.deletion_protectiononly stops Terraform itself (a bypass is trivial: remove it from state or flip it in config).settings.deletion_protection_enabledis enforced by the Cloud SQL API and blocks deletion from gcloud, the Console, and direct API calls too. Both default totruehere; destroying an instance requires flipping both tofalseand applying that change before the destroy — a single-stepterraform destroynever works against this module's defaults. Neither guard is the same aslifecycle { prevent_destroy = true }(a Terraform-only meta-argument bypassed by removing the block) nor the thirddeletion_policyfield (see below).deletion_policyis a third, independent mechanism governing whatterraform destroydoes (DELETEcalls the API normally,PREVENTfails the operation,ABANDONdrops Terraform management without an API call) rather than whether deletion is blocked. Left at its provider default ("DELETE") on the instance and both child resources — see variables.tf's file header comment #2 for the full reasoning against defaulting it to"PREVENT"here.- Engine-family gating is real, not cosmetic.
local.is_mysqlinmain.tf(derived fromdatabase_version's prefix) determines whetherbinary_log_enabledorpoint_in_time_recovery_enabledis sent to the API — sending the wrong one for the engine is rejected by Cloud SQL, andterraform validatecannot catch this (it depends on a runtime engine concept the type system doesn't model). - The PostgreSQL 16+/edition validation is a plan-time save, not a schema requirement. No
provider-level
validationenforces this — it is this module's own addition, added specifically because the interaction is confirmed in the live docs' prose but not enforced anywhere beforeapply. for_eachkey stability:databases/usersmap keys become each resource'snameargument — renaming a key destroys and recreates that specific entry (a force-newname), never the others.authorized_networksanddatabase_flagsare likewise keyed maps for the same reason.- IAM propagation lag applies if a composition grants
roles/cloudsql.adminand immediately applies this module in the same run — GCP IAM changes can take up to ~60 seconds to propagate.
| Concern | Secure default | Opt-out (explicit) |
|---|---|---|
| Public IP | settings.ip_configuration.ipv4_enabled = false |
Caller sets true and supplies authorized_networks |
| Deletion (Terraform) | deletion_protection = true |
Caller sets false |
| Deletion (API, all surfaces) | settings.deletion_protection_enabled = true |
Caller sets false — both must flip for a destroy to succeed |
| Backups | settings.backup_configuration.enabled = true |
Caller sets false |
| Point-in-time recovery (PostgreSQL/SQL Server) | point_in_time_recovery_enabled = true (engine-gated) |
Caller sets false in backup_configuration |
| Binary logging (MySQL, PITR-equivalent) | binary_log_enabled = true (engine-gated) |
Caller sets false in backup_configuration |
| Encryption at rest (CMEK) | Accepted via encryption_key_name, never defaulted to a specific key |
Caller supplies a key from terraform-google-kms-keyring |
| TLS enforcement | settings.ip_configuration.ssl_mode = "ENCRYPTED_ONLY" (module extension — not a house-table row verbatim) |
Caller sets "ALLOW_UNENCRYPTED_AND_ENCRYPTED" or "TRUSTED_CLIENT_CERTIFICATE_REQUIRED" |
| ExecuteSql (Data API) access | settings.data_api_access = "DISALLOW_DATA_API" (module extension) |
Caller sets "ALLOW_DATA_API" |
| Query Insights (observability) | settings.insights_config.query_insights_enabled = true (module extension of this module suite's audit-logging posture) |
Caller sets false |
| Query Insights client-address capture | record_client_address = false (left off — data-handling caution) |
Caller sets true |
| User passwords | Write-only password_wo/password_wo_version only — no plaintext password argument exists in this module |
N/A — by design |
cd terraform-google-cloud-sql-instance
terraform init -backend=false
terraform validate
terraform fmt -checkPin ?ref=v1.0.0 in every source = line — never a branch. This library is plan-only; a human
applies from CI with real Workload Identity Federation credentials against a specific project.
init -backend=false / validate / fmt -check confirm internal type/reference consistency and
canonical formatting only. Given this resource's schema is enforced heavily server-side (quota,
region/edition availability, the PostgreSQL 16+/edition interaction this module catches at plan
time notwithstanding, org policy, and the private-IP peering prerequisite this module cannot see),
a clean validate is not a guarantee that apply will succeed against a real project. The
examples/basic/ directory exists so a consuming GitHub Actions workflow can run a real
terraform plan with valid WIF credentials as part of its own review gate.
$ terraform output
connection_name = "my-project:us-east1:data-tier-postgres"
database_names = {
"app_primary" = "app_primary"
}
id = "projects/my-project/instances/data-tier-postgres"
name = "data-tier-postgres"
private_ip_address = "10.20.0.3"
public_ip_address = null
region = "us-east1"
self_link = "https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project/instances/data-tier-postgres"
user_names = {
"app_svc" = "app_svc"
"svc-account@my-project.iam" = "svc-account@my-project.iam"
}
| Symptom | Cause | Fix |
|---|---|---|
apply fails re-creating an instance with a name that was just destroyed |
Cloud SQL enforces a name-reuse cooldown of up to one week after deletion — confirmed schema text, not a vague "some delay" | Wait out the cooldown, or choose a different instance_name |
terraform destroy fails even after setting deletion_protection = false |
settings.deletion_protection_enabled (API-level) is still true — it is a separate guard from deletion_protection |
Set both deletion_protection = false AND settings.deletion_protection_enabled = false, apply that change first, then destroy |
apply fails with a private-IP-related API error even though plan showed no problem |
settings.ip_configuration.private_network is set but the required google_service_networking_connection peering does not exist (or the instance was created before the peering finished, due to a missing depends_on) |
Create the peering in the consuming composition (Example 8) and add the explicit depends_on — terraform plan cannot detect this gap |
apply fails with an "Invalid Tier... for (ENTERPRISE_PLUS) Edition" error |
database_version is PostgreSQL 16+, settings.tier is a shared-core/custom tier, and settings.edition was left unset — PG16+ auto-defaults to ENTERPRISE_PLUS |
This module's own validation on settings should catch this at plan time (Example 7) — if you see it at apply regardless, confirm you are on a provider version where this auto-default behavior still applies |
apply rejects binary_log_enabled/point_in_time_recovery_enabled |
Sent the wrong one for the engine family — binary_log_enabled is MySQL-only, point_in_time_recovery_enabled is PostgreSQL/SQL-Server-only |
Let this module's engine-gating handle it automatically; only override the field matching your actual database_version |
A google_sql_user won't delete |
The user has been granted PostgreSQL SQL roles, which the API refuses to delete out from under | Set that user's deletion_policy = "ABANDON" (Example 13) and revoke the role separately if needed |
google_sql_database_instancegoogle_sql_databasegoogle_sql_user- Cloud SQL database version policies
- Cloud SQL instance deletion
terraform-google-vpc-network,terraform-google-kms-keyring(sibling modules referenced above)- This module's
SCOPE.md