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.
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.
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.
Size the primary instance one of two ways:
cpu_count(default2) — number of vCPUs. Set to1for dev/test to reduce cost.machine_type— an explicit machine type such asn2-highmem-4orc4a-highmem-4-lssd. When set, this takes precedence andcpu_countis ignored.
There is no storage variable. AlloyDB storage is disaggregated and grows automatically.
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.
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.
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_accessis 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.
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.
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.
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.
# 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> \
--sqlFind <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.
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.
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 EXTENSIONin migrations. Extension availability — and extension versions — differ between Cloud SQL and AlloyDB even on a matching Postgres major version.- Hardcoded
cloudsqlsuperusergrants. AlloyDB's managed superuser role isalloydbsuperuser. This module publishes it as thedb_superuser_roleoutput so access capabilities pick it up automatically, but any grant written directly in your own migration SQL needs updating.
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_memoryin bytes rather than a utilization ratio, soresource_thresholds.memory_available_mibalerts 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_utilizationas "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 inmonitoring.tf.
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.*) |