Quick take — A reusable Terraform module for google_firestore_database on hashicorp/google ~> 5.0: Native vs Datastore mode, point-in-time recovery, scheduled backups, delete protection, and CMEK — all var-driven. 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 "firestore" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-firestore?ref=v1.0.0"
project_id = "..." # GCP project ID that will own the Firestore database.
location_id = "..." # Region (e.g. `asia-south1`) or multi-region (`nam5`, `e…
}
Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.
What this module is
Cloud Firestore is Google Cloud’s serverless, horizontally scalable document database. It stores data as collections of JSON-like documents, replicates synchronously across multiple zones (or regions, for nam5/eur3-style multi-region locations), and exposes real-time listeners, offline sync, and strongly consistent queries with automatic single-field indexing. There are no servers to provision and no capacity to size — you pay per document read/write/delete and per GiB stored.
The catch is that a Firestore database carries several decisions that are effectively permanent or destructive once made: the database type (Native mode vs Datastore mode), its location_id, and the moment you delete it you lose every document. On top of that, production-grade operation needs point-in-time recovery (PITR), a backup schedule, delete protection, and frequently a customer-managed encryption key (CMEK). Wiring all of that by hand in every project leads to drift — one team enables PITR, another forgets delete_protection_state, a third hardcodes the location.
This module wraps google_firestore_database plus its two most common production companions — google_firestore_backup_schedule (daily and weekly retention) and google_firestore_field (for TTL and index policy on a hot collection) — behind a small, validated variable surface. You get one consistent, reviewable way to stand up a Firestore database with sane safety defaults across every environment.
When to use it
- You are building serverless or mobile/web backends (Cloud Functions, Cloud Run, Firebase apps) that need a real-time, auto-scaling document store rather than a relational database.
- You want Native mode for real-time listeners, mobile SDK offline sync, and collection-group queries — or Datastore mode for high-throughput server-side workloads migrating off App Engine Datastore.
- You need defensible data protection: point-in-time recovery for the last 7 days, scheduled backups with separate daily/weekly retention, and
delete_protectionso a strayterraform destroycannot wipe production. - You are standardising Firestore across many GCP projects and want location, mode, PITR, backups, and CMEK to be policy, not per-project guesswork.
If you only need ephemeral key/value caching, or you need multi-row ACID transactions across a relational schema, reach for Memorystore or Cloud SQL/Spanner instead — Firestore’s transactions are document-oriented and its query model is index-driven, not ad-hoc SQL.
Module structure
terraform-module-gcp-firestore/
├── versions.tf # provider + required_version pins
├── main.tf # google_firestore_database + backup_schedule + field
├── variables.tf # var-driven inputs with validation
└── outputs.tf # database id/name + key attributes
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
main.tf
locals {
# PITR and backups are only meaningful for Native-mode databases.
enable_pitr = var.point_in_time_recovery_enabled && var.database_type == "FIRESTORE_NATIVE"
enable_backups = var.enable_backups && var.database_type == "FIRESTORE_NATIVE"
enable_ttl_field = var.ttl_field_path != null && var.ttl_collection != null
}
resource "google_firestore_database" "this" {
project = var.project_id
name = var.database_id
location_id = var.location_id
type = var.database_type
# Concurrency + consistency knobs (Native mode supports both).
concurrency_mode = var.concurrency_mode
app_engine_integration_mode = var.app_engine_integration_mode
# Point-in-time recovery: when enabled, retains 7 days of versioned data.
point_in_time_recovery_enablement = (
local.enable_pitr
? "POINT_IN_TIME_RECOVERY_ENABLED"
: "POINT_IN_TIME_RECOVERY_DISABLED"
)
# Guardrail against accidental deletion of the database.
delete_protection_state = (
var.delete_protection
? "DELETE_PROTECTION_ENABLED"
: "DELETE_PROTECTION_DISABLED"
)
# Deletion policy controls whether `terraform destroy` may remove the DB
# even when delete protection is off (ABANDON keeps it in GCP).
deletion_policy = var.deletion_policy
# Optional customer-managed encryption (CMEK). Omit the block for Google-managed keys.
dynamic "cmek_config" {
for_each = var.kms_key_name != null ? [1] : []
content {
kms_key_name = var.kms_key_name
}
}
labels = var.labels
}
# Daily backups with their own retention window.
resource "google_firestore_backup_schedule" "daily" {
count = local.enable_backups ? 1 : 0
project = var.project_id
database = google_firestore_database.this.name
retention = var.daily_backup_retention
daily_recurrence {}
}
# Weekly backups (kept longer) on a configurable day of week.
resource "google_firestore_backup_schedule" "weekly" {
count = local.enable_backups && var.enable_weekly_backups ? 1 : 0
project = var.project_id
database = google_firestore_database.this.name
retention = var.weekly_backup_retention
weekly_recurrence {
day = var.weekly_backup_day
}
}
# Optional TTL policy on a single field of one collection (e.g. expireAt).
# Setting ttl{} tells Firestore to auto-delete documents past that timestamp.
resource "google_firestore_field" "ttl" {
count = local.enable_ttl_field ? 1 : 0
project = var.project_id
database = google_firestore_database.this.name
collection = var.ttl_collection
field = var.ttl_field_path
ttl_config {}
# Leave automatic single-field indexing untouched for this field.
index_config {}
}
variables.tf
variable "project_id" {
type = string
description = "GCP project ID that will own the Firestore database."
}
variable "database_id" {
type = string
description = "Firestore database ID. Use \"(default)\" for the project's default database, or a custom ID (4-63 chars) for named databases."
default = "(default)"
validation {
condition = (
var.database_id == "(default)" ||
can(regex("^[a-z][a-z0-9-]{2,61}[a-z0-9]$", var.database_id))
)
error_message = "database_id must be \"(default)\" or 4-63 chars: lowercase letters, digits, hyphens; start with a letter and not end with a hyphen."
}
}
variable "location_id" {
type = string
description = "Location for the database: a region (e.g. us-central1, asia-south1) or a multi-region (nam5, eur3). Immutable after creation."
validation {
condition = length(var.location_id) > 0
error_message = "location_id must be set (e.g. asia-south1 or nam5)."
}
}
variable "database_type" {
type = string
description = "Database mode: FIRESTORE_NATIVE (real-time, mobile SDKs) or DATASTORE_MODE (server-side, high throughput). Immutable after creation."
default = "FIRESTORE_NATIVE"
validation {
condition = contains(["FIRESTORE_NATIVE", "DATASTORE_MODE"], var.database_type)
error_message = "database_type must be FIRESTORE_NATIVE or DATASTORE_MODE."
}
}
variable "concurrency_mode" {
type = string
description = "Transaction concurrency control: OPTIMISTIC, PESSIMISTIC, or OPTIMISTIC_WITH_ENTITY_GROUPS (Datastore-mode only)."
default = "OPTIMISTIC"
validation {
condition = contains(
["OPTIMISTIC", "PESSIMISTIC", "OPTIMISTIC_WITH_ENTITY_GROUPS"],
var.concurrency_mode
)
error_message = "concurrency_mode must be OPTIMISTIC, PESSIMISTIC, or OPTIMISTIC_WITH_ENTITY_GROUPS."
}
}
variable "app_engine_integration_mode" {
type = string
description = "Whether the database is tied to App Engine: ENABLED or DISABLED. Use DISABLED for standalone databases."
default = "DISABLED"
validation {
condition = contains(["ENABLED", "DISABLED"], var.app_engine_integration_mode)
error_message = "app_engine_integration_mode must be ENABLED or DISABLED."
}
}
variable "point_in_time_recovery_enabled" {
type = bool
description = "Enable point-in-time recovery (retains 7 days of versioned data). Native mode only; ignored for Datastore mode."
default = true
}
variable "delete_protection" {
type = bool
description = "Enable database-level delete protection to block accidental deletion via API/console."
default = true
}
variable "deletion_policy" {
type = string
description = "Terraform deletion behaviour: DELETE (allow destroy) or ABANDON (remove from state but keep the database in GCP)."
default = "ABANDON"
validation {
condition = contains(["DELETE", "ABANDON"], var.deletion_policy)
error_message = "deletion_policy must be DELETE or ABANDON."
}
}
variable "kms_key_name" {
type = string
description = "Optional CMEK key resource ID (projects/<p>/locations/<l>/keyRings/<r>/cryptoKeys/<k>). Must match the database location. Null = Google-managed encryption."
default = null
}
variable "enable_backups" {
type = bool
description = "Create a daily Firestore backup schedule. Native mode only."
default = true
}
variable "daily_backup_retention" {
type = string
description = "Retention for daily backups as a duration string (e.g. 604800s = 7 days). Max 14 days."
default = "604800s"
validation {
condition = can(regex("^[0-9]+s$", var.daily_backup_retention))
error_message = "daily_backup_retention must be a seconds duration string like \"604800s\"."
}
}
variable "enable_weekly_backups" {
type = bool
description = "Also create a weekly backup schedule (kept longer than daily)."
default = false
}
variable "weekly_backup_retention" {
type = string
description = "Retention for weekly backups as a duration string (e.g. 8467200s = 14 weeks-equivalent). Max 14 weeks."
default = "8467200s"
validation {
condition = can(regex("^[0-9]+s$", var.weekly_backup_retention))
error_message = "weekly_backup_retention must be a seconds duration string like \"8467200s\"."
}
}
variable "weekly_backup_day" {
type = string
description = "Day of week for weekly backups."
default = "SUNDAY"
validation {
condition = contains(
["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"],
var.weekly_backup_day
)
error_message = "weekly_backup_day must be a valid uppercase day name (e.g. SUNDAY)."
}
}
variable "ttl_collection" {
type = string
description = "Collection (collection group) on which to enable a TTL field. Null disables the TTL field resource."
default = null
}
variable "ttl_field_path" {
type = string
description = "Timestamp field used for TTL auto-deletion (e.g. expireAt). Requires ttl_collection. Null disables the TTL field resource."
default = null
}
variable "labels" {
type = map(string)
description = "Labels applied to the Firestore database."
default = {}
}
outputs.tf
output "id" {
description = "Fully qualified Firestore database ID (projects/<project>/databases/<database>)."
value = google_firestore_database.this.id
}
output "name" {
description = "Short database name (e.g. \"(default)\" or the custom database ID)."
value = google_firestore_database.this.name
}
output "location_id" {
description = "Location the database was created in."
value = google_firestore_database.this.location_id
}
output "database_type" {
description = "Mode of the database (FIRESTORE_NATIVE or DATASTORE_MODE)."
value = google_firestore_database.this.type
}
output "uid" {
description = "System-generated unique identifier (UUID) for the database."
value = google_firestore_database.this.uid
}
output "etag" {
description = "Current etag of the database resource (useful for optimistic-concurrency reads)."
value = google_firestore_database.this.etag
}
output "daily_backup_schedule_id" {
description = "ID of the daily backup schedule, or null when backups are disabled."
value = local.enable_backups ? google_firestore_backup_schedule.daily[0].id : null
}
How to use it
module "firestore" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-firestore?ref=v1.0.0"
project_id = "kloudvin-prod"
database_id = "orders"
location_id = "asia-south1" # Mumbai region — keep data close to users
database_type = "FIRESTORE_NATIVE"
point_in_time_recovery_enabled = true
delete_protection = true
deletion_policy = "ABANDON"
# Daily + weekly backups
enable_backups = true
daily_backup_retention = "604800s" # 7 days
enable_weekly_backups = true
weekly_backup_retention = "7257600s" # ~12 weeks
weekly_backup_day = "SUNDAY"
# Auto-expire session documents 30 days after expireAt
ttl_collection = "sessions"
ttl_field_path = "expireAt"
# Encrypt with a project CMEK key in the same region
kms_key_name = "projects/kloudvin-prod/locations/asia-south1/keyRings/firestore/cryptoKeys/orders-db"
labels = {
app = "orders-api"
environment = "prod"
managed_by = "terraform"
}
}
# Downstream: inject the database name into a Cloud Run service so the
# app connects to the right named Firestore database.
resource "google_cloud_run_v2_service" "orders_api" {
name = "orders-api"
location = "asia-south1"
project = "kloudvin-prod"
template {
containers {
image = "asia-south1-docker.pkg.dev/kloudvin-prod/apps/orders-api:1.4.0"
env {
name = "FIRESTORE_DATABASE_ID"
value = module.firestore.name
}
env {
name = "FIRESTORE_DATABASE"
value = module.firestore.id
}
}
}
}
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 config — live/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 config — live/prod/firestore/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-firestore?ref=v1.0.0"
}
inputs = {
project_id = "..."
location_id = "..."
}
3. Deploy one environment, or roll out all modules together:
cd live/prod/firestore && 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 that will own the Firestore database. |
database_id |
string |
"(default)" |
No | (default) or a custom named-database ID (4-63 chars, lowercase/digits/hyphens). |
location_id |
string |
— | Yes | Region (e.g. asia-south1) or multi-region (nam5, eur3). Immutable. |
database_type |
string |
"FIRESTORE_NATIVE" |
No | FIRESTORE_NATIVE or DATASTORE_MODE. Immutable. |
concurrency_mode |
string |
"OPTIMISTIC" |
No | OPTIMISTIC, PESSIMISTIC, or OPTIMISTIC_WITH_ENTITY_GROUPS. |
app_engine_integration_mode |
string |
"DISABLED" |
No | ENABLED or DISABLED. Use DISABLED for standalone databases. |
point_in_time_recovery_enabled |
bool |
true |
No | Enable PITR (7-day versioned recovery). Native mode only. |
delete_protection |
bool |
true |
No | Block accidental deletion at the database level. |
deletion_policy |
string |
"ABANDON" |
No | Terraform destroy behaviour: DELETE or ABANDON. |
kms_key_name |
string |
null |
No | CMEK key resource ID matching the DB location; null = Google-managed keys. |
enable_backups |
bool |
true |
No | Create a daily backup schedule. Native mode only. |
daily_backup_retention |
string |
"604800s" |
No | Daily backup retention duration (max 14 days). |
enable_weekly_backups |
bool |
false |
No | Also create a weekly backup schedule. |
weekly_backup_retention |
string |
"8467200s" |
No | Weekly backup retention duration (max 14 weeks). |
weekly_backup_day |
string |
"SUNDAY" |
No | Day of week for the weekly backup. |
ttl_collection |
string |
null |
No | Collection group to enable a TTL field on. |
ttl_field_path |
string |
null |
No | Timestamp field for TTL auto-deletion (e.g. expireAt). |
labels |
map(string) |
{} |
No | Labels applied to the database. |
Outputs
| Name | Description |
|---|---|
id |
Fully qualified database ID (projects/<project>/databases/<database>). |
name |
Short database name ((default) or the custom database ID). |
location_id |
Location the database was created in. |
database_type |
Database mode (FIRESTORE_NATIVE or DATASTORE_MODE). |
uid |
System-generated unique identifier (UUID) for the database. |
etag |
Current etag of the database resource. |
daily_backup_schedule_id |
ID of the daily backup schedule, or null when backups are disabled. |
Enterprise scenario
A logistics company runs a real-time parcel-tracking platform on Cloud Run in asia-south1, with each business domain (orders, tracking events, driver sessions) isolated in its own named Firestore Native-mode database inside a single production project. The platform team consumes this module once per database from a shared Terraform stack, enforcing delete_protection = true, PITR, daily-plus-weekly backups, and a CMEK key per database so that audit and key-rotation policies are uniform. The high-churn sessions database uses the module’s TTL field on expireAt to auto-purge expired driver sessions, keeping storage cost flat without a cleanup job — and because deletion_policy = "ABANDON", even a botched terraform destroy of the stack leaves the customer data intact in GCP.
Best practices
- Pick
typeandlocation_idonce. Both are immutable; changing them forces a destroy/recreate and total data loss. Default-database(default)mode is also locked the first time you create any Firestore or Datastore database in a project — decide Native vs Datastore deliberately. - Keep
delete_protection = trueanddeletion_policy = "ABANDON"in production. Delete protection blocks console/API deletes, andABANDONensures Terraform never deletes the database even if it leaves state — together they make accidental data loss require a deliberate, multi-step action. - Layer PITR and scheduled backups — they solve different problems. PITR gives fine-grained recovery for the last 7 days (great for “undo the bad migration we ran 20 minutes ago”); backup schedules give longer, point-in-time snapshots you can restore into a new database. Run both, with weekly retention longer than daily.
- Use CMEK that lives in the same location as the database and grant the Firestore service agent
roles/cloudkms.cryptoKeyEncrypterDecrypteron the key — a mismatched region or missing IAM binding makes the database creation fail. Rotate the key on a schedule; existing data is re-encrypted transparently. - Control cost with TTL and lean indexing. Firestore bills per read/write/delete and per stored GiB plus per-index storage; set TTL fields on ephemeral collections (sessions, OTPs, audit tail) so documents self-expire, and exempt large, never-queried fields from automatic single-field indexing to cut write cost and storage.
- Name databases by domain, not by environment. Prefer one project per environment with domain-named databases (
orders,tracking), and tag every database withapp,environment, andmanaged_bylabels so billing exports and policy tooling can attribute usage cleanly.