Skip to content

Repository files navigation

AlloyDB PostgreSQL

This Nullstone module is used to create a GCP AlloyDB for PostgreSQL cluster and its primary instance. Additionally, this module creates resources that are necessary to securely connect apps via Nullstone.

AlloyDB is Google's PostgreSQL-compatible engine. It speaks the standard Postgres wire protocol on port 5432, so applications connect with an ordinary postgres:// URL and need no driver or query changes.

Which postgres module should I use?

This module is not a drop-in replacement for gcp-cloudsql-postgres.

gcp-cloudsql-postgres gcp-alloydb-postgres
Smallest instance db-f1-micro (shared core) 1 vCPU (dev/test only), 2 vCPU recommended
Storage You size the disk (allocated_storage) Auto-managed, no disk to size
Cost floor A few dollars/month Substantially higher — no shared-core tier
Best for Default choice, dev/preview environments Production workloads, analytics, vector/AI

Use Cloud SQL as the default. Reach for AlloyDB when you need its columnar engine, its vector/AI extensions (alloydb_scann, google_ml_integration), or its price/performance at larger scale.

⚠️ Cost

AlloyDB has no shared-core machine type. The smallest shape is a single vCPU, which Google documents as suitable for development and testing only, and the cluster carries always-on storage and backup charges independent of instance size. An environment that costs a few dollars/month on Cloud SQL will cost materially more here. Price a minimum cluster before adopting this for preview environments.

Sizing

Size the primary instance one of two ways:

  • cpu_count (default 2) — number of vCPUs. Set to 1 for dev/test to reduce cost.
  • machine_type — an explicit machine type such as n2-highmem-4 or c4a-highmem-4-lssd. When set, this takes precedence and cpu_count is ignored.

There is no storage variable. AlloyDB storage is disaggregated and grows automatically.

Connectivity: PSA vs PSC

There are two private ways to connect to this database. You pick one with the enable_psc variable. You do not need both — they are mutually exclusive, and on AlloyDB they are fixed at cluster creation.

Private Services Access (PSA) — the default

This is what you get out of the box (enable_psc = false). Google reserves a range of private IPs inside your VPC (via VPC peering) and gives the cluster one of them. Any app in the same VPC can reach it over that private IP, which becomes db_endpoint.

Choose PSA if your apps live in the same VPC as the database and you want the simplest setup.

Private Service Connect (PSC)

PSC publishes the database as a "service" reachable through endpoints (a reserved private IP + forwarding rule that this module creates in your VPC) rather than by stitching the database's network into yours. This module also registers an internal DNS name for that endpoint, and db_endpoint becomes that hostname instead of a raw IP.

Why you might want it:

  • Stronger network-level isolation. PSC does not use VPC peering, so nothing else in the producer network becomes reachable. Access is granted explicitly, one consumer endpoint at a time.
  • It's how managed Google Cloud services connect to your database — for example Datastream or Database Migration Service, which run in Google-managed projects.
  • Works across project and VPC boundaries without re-architecting.

When enable_psc = true:

  • Public access is forced off (enable_public_access is ignored — the two cannot be combined).
  • Apps connect using the internal DNS hostname exposed by db_endpoint.
  • The network connection must provide internal_domain_fqdn / internal_domain_zone_id, or the endpoint cannot be registered in DNS.

⚠️ Switching an existing database between PSA and PSC

PSA and PSC are set at cluster creation on AlloyDB. Flipping enable_psc on an existing cluster forces it to be replaced — which destroys its data. Do not toggle this in place on a database you care about. Take an export first, create the cluster with the new setting, and import your data into it.

Point-in-time recovery (PITR)

Set point_in_time_recovery_enabled = true to enable AlloyDB continuous backup. Once enabled, AlloyDB continuously archives write-ahead logs so you can recover the database to any moment within the recovery window, which this module ties to backup_retention_count (clamped to AlloyDB's 1–35 day limit).

Note: this module defaults PITR to disabled to match gcp-cloudsql-postgres. AlloyDB's own default is continuous backup enabled with a 14-day window. Turn this on for production.

A few things to know before you need it:

  • It must be enabled before the incident. PITR can only recover to points in time that occurred while it was already on.
  • Recovery creates a new cluster. AlloyDB restore never rewinds a cluster in place — see below.

Disaster recovery

AlloyDB's restore model differs from Cloud SQL's in one important way: there is no "restore a backup into an existing cluster" operation. Both backup restore and PITR always mint a new cluster. That means the Cloud SQL playbook of "let the module create the instance, then restore on top of it" does not apply.

The reliable path is logical: export from the source, let this module create a clean cluster, then import.

Strategy A — Export/import via Cloud Storage (recommended)

# 1. Export the source database to a GCS bucket.
#    Grant the source cluster's service account roles/storage.objectAdmin on the bucket first.
gcloud alloydb clusters export <SOURCE_CLUSTER> \
  --region=<REGION> \
  --database=<DB_NAME> \
  --gcs-uri="gs://<BUCKET>/dr/dump.sql" \
  --sql
# 2. Stand up the new cluster with the module. It comes up empty,
#    with a module-managed admin user and a fresh endpoint.
nullstone up --stack <stack> --block <block> --env <env>
# 3. Grant the NEW cluster's service account read access to the dump object, then import.
#    The target database must already exist.
gcloud alloydb clusters import <NEW_CLUSTER> \
  --region=<REGION> \
  --database=<DB_NAME> \
  --gcs-uri="gs://<BUCKET>/dr/dump.sql" \
  --user=<ADMIN_USERNAME> \
  --sql

Find <NEW_CLUSTER> with nullstone outputs --block <block> --env <env> -o json | jq -r .db_cluster_name.

The target database and its roles are normally created by the connected app's access capability (through the db-admin function), so make sure that block has applied before importing.

Strategy B — Restore to a new cluster, then export/import

If the source cluster is gone but its backups survive, or you need a specific point in time, first restore to a temporary cluster outside Terraform:

gcloud alloydb clusters restore <TEMP_CLUSTER> --region=<REGION> --backup=<BACKUP_ID>

Or, to a point in time (requires continuous backup to have been enabled):

gcloud alloydb clusters restore <TEMP_CLUSTER> --region=<REGION> --source-cluster=<SOURCE_CLUSTER> --point-in-time='2026-07-24T13:00:00.000Z'

Then treat that temporary cluster as the <SOURCE_CLUSTER> for Strategy A and delete it once the data is verified. This keeps the system of record on a module-managed cluster.

Migrating from Cloud SQL

Google provides a first-party migration path:

gcloud alloydb clusters migrate-cloud-sql <NEW_CLUSTER> --region=<REGION> --cloud-sql-project-id=<PROJECT_ID> --cloud-sql-instance-id=<CLOUDSQL_INSTANCE>

This creates a cluster outside Terraform, so it is a good way to evaluate AlloyDB but not to land on a module-managed cluster. For a managed result, use the export/import path in Strategy A above.

Two things to check in your application before migrating:

  • CREATE EXTENSION in migrations. Extension availability — and extension versions — differ between Cloud SQL and AlloyDB even on a matching Postgres major version.
  • Hardcoded cloudsqlsuperuser grants. AlloyDB's managed superuser role is alloydbsuperuser. This module publishes it as the db_superuser_role output so access capabilities pick it up automatically, but any grant written directly in your own migration SQL needs updating.

Monitoring

When a notification block is connected, this module creates alert policies for CPU utilization, available memory, and connection count.

The alert set intentionally differs from gcp-cloudsql-postgres, because AlloyDB's published metrics do:

  • Memory is an absolute threshold, not a percentage. AlloyDB publishes min_available_memory in bytes rather than a utilization ratio, so resource_thresholds.memory_available_mib alerts when free memory drops below the value.
  • There are no disk alerts. Storage is auto-managed and there is no disk-utilization metric to threshold.
  • There are no IOPS alerts. AlloyDB publishes no disk read/write ops metrics.

Verify before trusting the CPU alert: Google documents instance/cpu/average_utilization as "represented as a percentage", so this module applies the threshold directly (80 => 80). Cloud SQL's equivalent metric is a 0–1 fraction. Confirm the unit in Metrics Explorer against a live instance; if it is a fraction, divide the threshold by 100 in monitoring.tf.

Differences from gcp-cloudsql-postgres

If you are porting a config, these Cloud SQL variables have no AlloyDB equivalent and were omitted:

Cloud SQL variable Why it's gone
instance_class Replaced by machine_type / cpu_count
allocated_storage AlloyDB storage is auto-managed
edition AlloyDB has no ENTERPRISE / ENTERPRISE_PLUS split
enforce_secure_passwords AlloyDB has no password validation policy

And these changed shape:

Variable Change
resource_thresholds Now { cpu, memory_available_mib, connections }
ip_whitelist Only honored when enable_public_access is enabled
db_flags AlloyDB flag names, not Cloud SQL's (no cloudsql.*)

About

Creates a GCP AlloyDB postgres instance

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages