IaC GCP

Terraform Module: GCP Cloud SQL — a private-by-default Postgres/MySQL instance with PITR and replicas

Quick take — A reusable hashicorp/google ~> 5.0 module for google_sql_database_instance: private IP only, automated backups with point-in-time recovery, optional read replicas, databases, and IAM/built-in users. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.

Quickstart (copy-paste)

Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):

provider "google" {
  project = "my-project"
  region  = "us-central1"
}

module "cloud_sql" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-sql?ref=v1.0.0"

  project_id = "..."  # GCP project ID hosting the instance.
  name       = "..."  # Instance name; RFC1035, lowercase, <= 80 chars.
  region     = "..."  # Region for the primary, e.g. `asia-south1`.
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

Cloud SQL is GCP’s managed relational database service for PostgreSQL, MySQL, and SQL Server. You declare an engine, a version, a machine tier, and storage, and Google runs the database for you: provisioning, patching, failover to a standby in another zone, automated backups, and binary-log/WAL archiving for point-in-time recovery. The control surface is google_sql_database_instance, with companion resources for databases (google_sql_database), users (google_sql_user), and replicas (another google_sql_database_instance with a master_instance_name).

The catch is that a safe Cloud SQL instance is never just an engine and a tier. The defaults will happily give you a database with a public IPv4 address, no SSL enforcement, no deletion protection, backups you forgot to enable, and a root password living in plan output. Production-correct means: a private IP allocated from a Service Networking peering range (no public surface at all), SSL/TLS required, automated backups with PITR and a sensible retention window, a high-availability regional instance for prod, deletion protection, and credentials that come from Secret Manager rather than literals in HCL. Every team re-deriving that by hand will get at least one of those wrong.

This module wraps google_sql_database_instance into a single opinionated, variable-driven block. It defaults to private-IP-only with SSL required and PITR on, optionally creates a cross-region read replica, provisions the databases and users you list (built-in or Cloud SQL IAM users), and emits the connection name, private IP, and instance self-link as outputs so a Cloud Run service, a GKE workload, or a Secret Manager secret can consume them.

When to use it

Reach for AlloyDB or a self-managed VM instead when you need Postgres extensions Cloud SQL doesn’t support, Spanner when you need horizontal write scaling beyond a single primary, or BigQuery when the workload is analytical rather than transactional.

Module structure

terraform-module-gcp-cloud-sql/
├── versions.tf      # provider + required_version pins
├── main.tf          # instance, replica, databases, users
├── variables.tf     # var-driven inputs with validation
└── outputs.tf       # connection name, private IP, self-link, replica

versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.5"
    }
  }
}

main.tf

locals {
  # Engines that support a synchronous regional standby (HA).
  ha_capable      = !startswith(var.database_version, "SQLSERVER")
  availability    = var.high_availability && local.ha_capable ? "REGIONAL" : "ZONAL"

  # Point-in-time recovery uses transaction logs; the flag differs per engine.
  is_postgres     = startswith(var.database_version, "POSTGRES")

  # Built-in users get a generated password unless the caller supplies one.
  builtin_users   = { for u in var.users : u.name => u if u.type == "BUILT_IN" }
  iam_users       = { for u in var.users : u.name => u if u.type != "BUILT_IN" }
}

# Strong random passwords for built-in users that did not bring their own.
resource "random_password" "user" {
  for_each = { for k, u in local.builtin_users : k => u if u.password == null }

  length      = 24
  special     = true
  min_special = 2
  min_upper   = 2
  min_lower   = 2
  min_numeric = 2
  # Avoid characters that complicate URLs/shell quoting in connection strings.
  override_special = "!#%*-_=+"
}

resource "google_sql_database_instance" "this" {
  project             = var.project_id
  name                = var.name
  region              = var.region
  database_version    = var.database_version
  deletion_protection = var.deletion_protection

  settings {
    tier              = var.tier
    edition           = var.edition
    availability_type = local.availability
    disk_type         = var.disk_type
    disk_size         = var.disk_size_gb
    disk_autoresize   = var.disk_autoresize
    # Cap autogrowth so a runaway workload cannot balloon the bill unbounded.
    disk_autoresize_limit = var.disk_autoresize_limit_gb

    # GCE zone the primary runs in; the standby lands in another zone when REGIONAL.
    location_preference {
      zone = var.zone
    }

    ip_configuration {
      # Private IP only: no public IPv4 surface at all.
      ipv4_enabled                                  = var.public_ip_enabled
      private_network                               = var.private_network
      enable_private_path_for_google_cloud_services = var.private_path_for_google_services
      ssl_mode                                      = var.ssl_mode

      dynamic "authorized_networks" {
        for_each = var.public_ip_enabled ? var.authorized_networks : {}
        content {
          name  = authorized_networks.key
          value = authorized_networks.value
        }
      }
    }

    backup_configuration {
      enabled                        = var.backup_enabled
      start_time                     = var.backup_start_time
      location                       = var.backup_location
      point_in_time_recovery_enabled = var.backup_enabled && local.is_postgres ? true : var.backup_enabled
      transaction_log_retention_days = var.transaction_log_retention_days
      binary_log_enabled             = var.backup_enabled && !local.is_postgres

      backup_retention_settings {
        retained_backups = var.retained_backups
        retention_unit   = "COUNT"
      }
    }

    maintenance_window {
      day          = var.maintenance_day
      hour         = var.maintenance_hour
      update_track = var.maintenance_update_track
    }

    # Engine-level flags, e.g. { "log_min_duration_statement" = "1000" }.
    dynamic "database_flags" {
      for_each = var.database_flags
      content {
        name  = database_flags.key
        value = database_flags.value
      }
    }

    # Query Insights for application-level performance troubleshooting.
    insights_config {
      query_insights_enabled  = var.query_insights_enabled
      query_string_length     = var.query_insights_string_length
      record_application_tags = var.query_insights_record_application_tags
      record_client_address   = var.query_insights_record_client_address
    }

    user_labels = var.labels
  }

  lifecycle {
    # Generated/managed-elsewhere root password should not force diffs.
    ignore_changes = [settings[0].disk_size]
  }
}

# Optional cross-region read replica for read scaling or DR.
resource "google_sql_database_instance" "replica" {
  for_each = var.read_replicas

  project              = var.project_id
  name                 = each.key
  region               = each.value.region
  database_version     = var.database_version
  master_instance_name = google_sql_database_instance.this.name
  deletion_protection  = var.deletion_protection

  settings {
    tier              = coalesce(each.value.tier, var.tier)
    edition           = var.edition
    availability_type = "ZONAL" # replicas are single-zone by design
    disk_type         = var.disk_type
    disk_autoresize   = var.disk_autoresize

    ip_configuration {
      ipv4_enabled    = false
      private_network = var.private_network
      ssl_mode        = var.ssl_mode
    }

    insights_config {
      query_insights_enabled = var.query_insights_enabled
    }

    user_labels = var.labels
  }

  # A replica cannot exist without its primary.
  depends_on = [google_sql_database_instance.this]
}

# Application databases on the primary.
resource "google_sql_database" "this" {
  for_each = toset(var.databases)

  project   = var.project_id
  name      = each.value
  instance  = google_sql_database_instance.this.name
  charset   = var.database_charset
  collation = var.database_collation

  # Dropping a DB is destructive; require an explicit intent to remove it.
  deletion_policy = var.database_deletion_policy
}

# Built-in (password) users. Passwords come from the caller or are generated.
resource "google_sql_user" "builtin" {
  for_each = local.builtin_users

  project  = var.project_id
  name     = each.value.name
  instance = google_sql_database_instance.this.name
  password = coalesce(each.value.password, try(random_password.user[each.key].result, null))
  type     = "BUILT_IN"

  deletion_policy = var.user_deletion_policy
}

# Cloud SQL IAM database users (service accounts or IAM users). No password.
resource "google_sql_user" "iam" {
  for_each = local.iam_users

  project = var.project_id
  # IAM service-account users are named without the ".gserviceaccount.com" suffix.
  name     = each.value.type == "CLOUD_IAM_SERVICE_ACCOUNT" ? trimsuffix(each.value.name, ".gserviceaccount.com") : each.value.name
  instance = google_sql_database_instance.this.name
  type     = each.value.type

  deletion_policy = var.user_deletion_policy
}

variables.tf

variable "project_id" {
  description = "GCP project ID that hosts the Cloud SQL instance."
  type        = string
}

variable "name" {
  description = "Instance name (lowercase, RFC1035, <= 80 chars). Cannot be reused for ~1 week after deletion."
  type        = string

  validation {
    condition     = can(regex("^[a-z]([-a-z0-9]*[a-z0-9])?$", var.name)) && length(var.name) <= 80
    error_message = "name must be lowercase RFC1035 (start with a letter, hyphens allowed) and <= 80 chars."
  }
}

variable "region" {
  description = "Region for the primary instance, e.g. asia-south1."
  type        = string
}

variable "zone" {
  description = "Preferred zone for the primary, e.g. asia-south1-a. The HA standby lands in another zone automatically."
  type        = string
  default     = null
}

variable "database_version" {
  description = "Engine and version, e.g. POSTGRES_16, MYSQL_8_0, SQLSERVER_2022_STANDARD."
  type        = string
  default     = "POSTGRES_16"

  validation {
    condition     = can(regex("^(POSTGRES|MYSQL|SQLSERVER)_", var.database_version))
    error_message = "database_version must start with POSTGRES_, MYSQL_, or SQLSERVER_."
  }
}

variable "tier" {
  description = "Machine tier, e.g. db-custom-2-7680 (2 vCPU / 7.5GB) or a shared db-g1-small for non-prod."
  type        = string
  default     = "db-custom-2-7680"
}

variable "edition" {
  description = "Cloud SQL edition: ENTERPRISE or ENTERPRISE_PLUS (PITR-to-the-second, higher limits)."
  type        = string
  default     = "ENTERPRISE"

  validation {
    condition     = contains(["ENTERPRISE", "ENTERPRISE_PLUS"], var.edition)
    error_message = "edition must be ENTERPRISE or ENTERPRISE_PLUS."
  }
}

variable "high_availability" {
  description = "Run a REGIONAL HA instance with a synchronous standby in another zone (ignored for SQL Server)."
  type        = bool
  default     = true
}

variable "disk_type" {
  description = "Storage type: PD_SSD or PD_HDD."
  type        = string
  default     = "PD_SSD"

  validation {
    condition     = contains(["PD_SSD", "PD_HDD"], var.disk_type)
    error_message = "disk_type must be PD_SSD or PD_HDD."
  }
}

variable "disk_size_gb" {
  description = "Initial data disk size in GB (>= 10)."
  type        = number
  default     = 20

  validation {
    condition     = var.disk_size_gb >= 10
    error_message = "disk_size_gb must be at least 10 GB."
  }
}

variable "disk_autoresize" {
  description = "Automatically grow storage when it nears full. Storage can grow but never shrink."
  type        = bool
  default     = true
}

variable "disk_autoresize_limit_gb" {
  description = "Hard ceiling for autoresize in GB. 0 means no limit (not recommended for cost control)."
  type        = number
  default     = 500
}

variable "public_ip_enabled" {
  description = "Assign a public IPv4 address. Keep false: private-IP-only is the secure default."
  type        = bool
  default     = false
}

variable "private_network" {
  description = "Self-link of the VPC for Private Service Access (e.g. projects/<host>/global/networks/<vpc>). Required for private IP."
  type        = string
  default     = null

  validation {
    condition     = var.public_ip_enabled || var.private_network != null
    error_message = "private_network is required when public_ip_enabled is false (private IP only)."
  }
}

variable "private_path_for_google_services" {
  description = "Allow Google-managed services (e.g. BigQuery external connections) to reach the instance over its private IP."
  type        = bool
  default     = false
}

variable "ssl_mode" {
  description = "TLS enforcement: ENCRYPTED_ONLY (require TLS) or TRUSTED_CLIENT_CERTIFICATE_REQUIRED (also require a client cert)."
  type        = string
  default     = "ENCRYPTED_ONLY"

  validation {
    condition = contains([
      "ENCRYPTED_ONLY",
      "TRUSTED_CLIENT_CERTIFICATE_REQUIRED",
    ], var.ssl_mode)
    error_message = "ssl_mode must be ENCRYPTED_ONLY or TRUSTED_CLIENT_CERTIFICATE_REQUIRED (plaintext is not allowed)."
  }
}

variable "authorized_networks" {
  description = "Map of name => CIDR allowed to reach the public IP. Only used when public_ip_enabled is true."
  type        = map(string)
  default     = {}
}

variable "backup_enabled" {
  description = "Enable automated daily backups (and PITR transaction-log/binary-log retention)."
  type        = bool
  default     = true
}

variable "backup_start_time" {
  description = "Daily backup start time in UTC, HH:MM (24h). Choose a low-traffic window."
  type        = string
  default     = "18:00" # ~23:30 IST

  validation {
    condition     = can(regex("^([01][0-9]|2[0-3]):[0-5][0-9]$", var.backup_start_time))
    error_message = "backup_start_time must be HH:MM in 24-hour UTC."
  }
}

variable "backup_location" {
  description = "Backup storage location (region or multi-region, e.g. asia). Null uses the default for the instance region."
  type        = string
  default     = null
}

variable "retained_backups" {
  description = "Number of automated backups to retain."
  type        = number
  default     = 14

  validation {
    condition     = var.retained_backups >= 1 && var.retained_backups <= 365
    error_message = "retained_backups must be between 1 and 365."
  }
}

variable "transaction_log_retention_days" {
  description = "Days of transaction logs kept for point-in-time recovery (1-7 on ENTERPRISE; up to 35 on ENTERPRISE_PLUS)."
  type        = number
  default     = 7

  validation {
    condition     = var.transaction_log_retention_days >= 1 && var.transaction_log_retention_days <= 35
    error_message = "transaction_log_retention_days must be between 1 and 35."
  }
}

variable "maintenance_day" {
  description = "Day of week (1=Mon .. 7=Sun) for the maintenance window."
  type        = number
  default     = 7

  validation {
    condition     = var.maintenance_day >= 1 && var.maintenance_day <= 7
    error_message = "maintenance_day must be between 1 (Mon) and 7 (Sun)."
  }
}

variable "maintenance_hour" {
  description = "Hour (0-23, UTC) for the maintenance window."
  type        = number
  default     = 19

  validation {
    condition     = var.maintenance_hour >= 0 && var.maintenance_hour <= 23
    error_message = "maintenance_hour must be between 0 and 23."
  }
}

variable "maintenance_update_track" {
  description = "Maintenance track: stable (default), canary (early), or week5 (later)."
  type        = string
  default     = "stable"

  validation {
    condition     = contains(["stable", "canary", "week5"], var.maintenance_update_track)
    error_message = "maintenance_update_track must be stable, canary, or week5."
  }
}

variable "database_flags" {
  description = "Engine flags as a name => value map, e.g. { log_min_duration_statement = \"1000\" }."
  type        = map(string)
  default     = {}
}

variable "query_insights_enabled" {
  description = "Enable Cloud SQL Query Insights (per-query latency and plans in the console)."
  type        = bool
  default     = true
}

variable "query_insights_string_length" {
  description = "Max captured query string length for Query Insights (256-4500)."
  type        = number
  default     = 1024
}

variable "query_insights_record_application_tags" {
  description = "Record application tags (sqlcommenter) in Query Insights."
  type        = bool
  default     = true
}

variable "query_insights_record_client_address" {
  description = "Record client IP addresses in Query Insights."
  type        = bool
  default     = false
}

variable "databases" {
  description = "List of database names to create on the instance."
  type        = list(string)
  default     = []
}

variable "database_charset" {
  description = "Charset for created databases (e.g. UTF8 for Postgres, utf8mb4 for MySQL)."
  type        = string
  default     = "UTF8"
}

variable "database_collation" {
  description = "Collation for created databases (e.g. en_US.UTF8 for Postgres, utf8mb4_unicode_ci for MySQL)."
  type        = string
  default     = "en_US.UTF8"
}

variable "database_deletion_policy" {
  description = "What happens to a database on destroy: DELETE or ABANDON (leave it in place)."
  type        = string
  default     = "DELETE"

  validation {
    condition     = contains(["DELETE", "ABANDON"], var.database_deletion_policy)
    error_message = "database_deletion_policy must be DELETE or ABANDON."
  }
}

variable "users" {
  description = <<-EOT
    Users to create. Each: { name, type, password }.
      type = BUILT_IN                    -> password user; supply password or one is generated.
      type = CLOUD_IAM_USER              -> IAM user (human), no password.
      type = CLOUD_IAM_SERVICE_ACCOUNT   -> IAM service account, no password (name = SA email).
  EOT
  type = list(object({
    name     = string
    type     = optional(string, "BUILT_IN")
    password = optional(string)
  }))
  default = []

  validation {
    condition = alltrue([
      for u in var.users : contains(["BUILT_IN", "CLOUD_IAM_USER", "CLOUD_IAM_SERVICE_ACCOUNT"], u.type)
    ])
    error_message = "Each user type must be BUILT_IN, CLOUD_IAM_USER, or CLOUD_IAM_SERVICE_ACCOUNT."
  }
}

variable "user_deletion_policy" {
  description = "What happens to a user on destroy: ABANDON (default, safe) or DELETE."
  type        = string
  default     = "ABANDON"

  validation {
    condition     = contains(["ABANDON", "DELETE"], var.user_deletion_policy)
    error_message = "user_deletion_policy must be ABANDON or DELETE."
  }
}

variable "read_replicas" {
  description = "Read replicas keyed by replica instance name. Each: { region, tier }. tier defaults to the primary's tier."
  type = map(object({
    region = string
    tier   = optional(string)
  }))
  default = {}
}

variable "deletion_protection" {
  description = "Block deletion of the instance via the API/Terraform."
  type        = bool
  default     = true
}

variable "labels" {
  description = "Labels applied to the instance and replicas (user_labels)."
  type        = map(string)
  default     = {}
}

outputs.tf

output "id" {
  description = "Fully qualified Cloud SQL instance ID."
  value       = google_sql_database_instance.this.id
}

output "name" {
  description = "Name of the primary instance."
  value       = google_sql_database_instance.this.name
}

output "connection_name" {
  description = "Connection name (project:region:instance) for the Cloud SQL Auth Proxy / connectors."
  value       = google_sql_database_instance.this.connection_name
}

output "self_link" {
  description = "URI (self-link) of the primary instance."
  value       = google_sql_database_instance.this.self_link
}

output "private_ip_address" {
  description = "Private IP address of the primary instance (null if private IP is disabled)."
  value       = google_sql_database_instance.this.private_ip_address
}

output "public_ip_address" {
  description = "Public IP address of the primary instance (null when public IP is disabled)."
  value       = google_sql_database_instance.this.public_ip_address
}

output "service_account_email_address" {
  description = "Service account the instance uses (grant it KMS/CMEK or GCS access for imports/exports)."
  value       = google_sql_database_instance.this.service_account_email_address
}

output "database_names" {
  description = "Names of the databases created on the instance."
  value       = [for db in google_sql_database.this : db.name]
}

output "builtin_user_passwords" {
  description = "Generated passwords for built-in users without a supplied password, keyed by user name."
  value       = { for k, p in random_password.user : k => p.result }
  sensitive   = true
}

output "replica_connection_names" {
  description = "Map of read replica name to its connection name."
  value       = { for k, r in google_sql_database_instance.replica : k => r.connection_name }
}

output "replica_private_ip_addresses" {
  description = "Map of read replica name to its private IP address."
  value       = { for k, r in google_sql_database_instance.replica : k => r.private_ip_address }
}

How to use it

# Private Service Access must exist before a private-IP instance can be created:
# an allocated range peered to servicenetworking.googleapis.com on the VPC.
resource "google_compute_global_address" "psa_range" {
  project       = var.project_id
  name          = "cloudsql-psa-range"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16
  network       = var.vpc_self_link
}

resource "google_service_networking_connection" "psa" {
  network                 = var.vpc_self_link
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.psa_range.name]
}

module "cloud_sql" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-sql?ref=v1.0.0"

  project_id       = "kv-platform-prod"
  name             = "kv-orders-pg-prod"
  region           = "asia-south1"
  zone             = "asia-south1-a"
  database_version = "POSTGRES_16"
  edition          = "ENTERPRISE_PLUS"
  tier             = "db-perf-optimized-N-4" # 4 vCPU ENTERPRISE_PLUS perf tier

  # Private IP only, over the Shared VPC, with TLS required.
  high_availability = true
  private_network   = var.vpc_self_link
  ssl_mode          = "ENCRYPTED_ONLY"

  # 14 daily backups + 7 days of WAL for point-in-time recovery.
  backup_enabled                 = true
  backup_start_time              = "18:30" # 00:00 IST
  retained_backups               = 14
  transaction_log_retention_days = 7

  database_flags = {
    log_min_duration_statement = "1000" # log queries slower than 1s
    "cloudsql.iam_authentication" = "on"
  }

  databases = ["orders", "orders_outbox"]

  users = [
    # The orders service authenticates with its IAM service account (no password).
    {
      name = google_service_account.orders_api.email
      type = "CLOUD_IAM_SERVICE_ACCOUNT"
    },
    # A break-glass built-in admin; password is generated and stored in Secret Manager below.
    {
      name = "orders_admin"
      type = "BUILT_IN"
    },
  ]

  # Cross-region read replica for DR / read scaling.
  read_replicas = {
    "kv-orders-pg-prod-replica-sea" = {
      region = "asia-southeast1"
      tier   = "db-perf-optimized-N-2"
    }
  }

  deletion_protection = true

  labels = {
    team        = "payments"
    environment = "prod"
    data_class  = "restricted"
  }

  depends_on = [google_service_networking_connection.psa]
}

# Downstream: stash the generated admin password and the connection name in
# Secret Manager so apps and runbooks never see them in HCL or state output.
resource "google_secret_manager_secret" "orders_admin_pw" {
  project   = "kv-platform-prod"
  secret_id = "orders-admin-db-password"
  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "orders_admin_pw" {
  secret      = google_secret_manager_secret.orders_admin_pw.id
  secret_data = module.cloud_sql.builtin_user_passwords["orders_admin"] # <- module output
}

# A Cloud Run service reaching the DB privately via the Auth Proxy/connector.
resource "google_cloud_run_v2_service" "orders_api" {
  project  = "kv-platform-prod"
  name     = "orders-api"
  location = "asia-south1"

  template {
    service_account = google_service_account.orders_api.email
    containers {
      image = "asia-south1-docker.pkg.dev/kv-platform-prod/services/orders-api:1.8.2"
      env {
        name  = "INSTANCE_CONNECTION_NAME"
        value = module.cloud_sql.connection_name # <- module output wires the app to the DB
      }
    }
  }
}

With Terragrunt

Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.

1. Root configlive/terragrunt.hcl (inherited by every module):

remote_state {
  backend = "gcs"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    # ...gcs state bucket/container + key per path...
  }
}

2. Module configlive/prod/cloud_sql/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-sql?ref=v1.0.0"
}

inputs = {
  project_id = "..."
  name = "..."
  region = "..."
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/cloud_sql && terragrunt apply        # this module
terragrunt run-all apply                      # every module under live/prod

Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.

Inputs

Name Type Default Required Description
project_id string yes GCP project ID hosting the instance.
name string yes Instance name; RFC1035, lowercase, <= 80 chars.
region string yes Region for the primary, e.g. asia-south1.
zone string null no Preferred zone for the primary; standby auto-placed elsewhere.
database_version string POSTGRES_16 no Engine/version (POSTGRES_*, MYSQL_*, SQLSERVER_*).
tier string db-custom-2-7680 no Machine tier (vCPU/RAM).
edition string ENTERPRISE no ENTERPRISE or ENTERPRISE_PLUS.
high_availability bool true no REGIONAL HA with a synchronous standby (ignored for SQL Server).
disk_type string PD_SSD no PD_SSD or PD_HDD.
disk_size_gb number 20 no Initial storage size in GB (min 10).
disk_autoresize bool true no Auto-grow storage when nearly full.
disk_autoresize_limit_gb number 500 no Ceiling for autoresize; 0 = unlimited.
public_ip_enabled bool false no Assign a public IPv4 (keep false).
private_network string null no VPC self-link for Private Service Access (required when private).
private_path_for_google_services bool false no Allow Google services to reach the private IP.
ssl_mode string ENCRYPTED_ONLY no TLS enforcement mode (plaintext disallowed).
authorized_networks map(string) {} no name => CIDR for public IP (only if public enabled).
backup_enabled bool true no Daily automated backups + PITR log retention.
backup_start_time string 18:00 no Daily backup window start, UTC HH:MM.
backup_location string null no Backup storage location (region/multi-region).
retained_backups number 14 no Number of automated backups retained (1-365).
transaction_log_retention_days number 7 no PITR log retention in days (1-35).
maintenance_day number 7 no Maintenance day (1=Mon…7=Sun).
maintenance_hour number 19 no Maintenance hour (0-23 UTC).
maintenance_update_track string stable no stable, canary, or week5.
database_flags map(string) {} no Engine flags as name => value.
query_insights_enabled bool true no Enable Query Insights.
query_insights_string_length number 1024 no Captured query length (256-4500).
query_insights_record_application_tags bool true no Record sqlcommenter app tags.
query_insights_record_client_address bool false no Record client IPs.
databases list(string) [] no Databases to create on the instance.
database_charset string UTF8 no Charset for created databases.
database_collation string en_US.UTF8 no Collation for created databases.
database_deletion_policy string DELETE no DELETE or ABANDON databases on destroy.
users list(object) [] no Users {name, type, password} (built-in or IAM).
user_deletion_policy string ABANDON no ABANDON or DELETE users on destroy.
read_replicas map(object) {} no Read replicas {region, tier} keyed by name.
deletion_protection bool true no Block deletion via API/Terraform.
labels map(string) {} no Labels on the instance and replicas.

Outputs

Name Description
id Fully qualified Cloud SQL instance ID.
name Name of the primary instance.
connection_name project:region:instance for the Auth Proxy / connectors.
self_link URI (self-link) of the primary instance.
private_ip_address Private IP of the primary (null if disabled).
public_ip_address Public IP of the primary (null when disabled).
service_account_email_address Instance service account (for CMEK/GCS grants).
database_names Names of databases created on the instance.
builtin_user_passwords Generated built-in user passwords (sensitive), keyed by name.
replica_connection_names Map of replica name to connection name.
replica_private_ip_addresses Map of replica name to private IP.

Enterprise scenario

A payments platform runs its orders and ledger services on regional HA POSTGRES_16 instances in asia-south1, each provisioned by one call to this module. Every instance is private-IP-only over the Shared VPC with ssl_mode = "ENCRYPTED_ONLY", applications authenticate using Cloud SQL IAM database users tied to their workload service accounts (so there are no long-lived passwords to rotate), and a cross-region read replica in asia-southeast1 provides DR plus offloads heavy reporting reads. With ENTERPRISE_PLUS, 14 retained backups, and seven days of WAL, the team can restore any database to a chosen second within the window — and because deletion protection and the destructive DELETE database policy are governed by the module, a stray terraform destroy in a PR is blocked at plan time rather than wiping a production ledger.

Best practices

TerraformGCPCloud SQLModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading