· 2 min read
Automated BigQuery table backups, defined in Terraform
Daily snapshots of critical BigQuery tables using scheduled queries, with retention and permissions managed as code.
- GCP
- BigQuery
- Terraform
- DevOps
During a platform engineering stint with the Prices team at the Office for National Statistics, one of my tasks was making sure key BigQuery tables could be recovered if something went wrong: a bad load, an accidental DELETE, a pipeline bug. BigQuery’s time travel only covers a short window (seven days at most), so we needed point-in-time copies that last longer, created automatically and owned by code rather than by someone’s memory.
Table snapshots are cheap insurance
A table snapshot is a read-only copy that only stores the bytes that differ from its base table, so daily snapshots of a mostly-append table cost very little. They can also expire automatically, which takes care of retention.
The scheduled query uses BigQuery scripting to build a dated snapshot name:
-- snapshot.sql
EXECUTE IMMEDIATE FORMAT("""
CREATE SNAPSHOT TABLE `${project}.${backup_dataset}.${table}_%s`
CLONE `${project}.${source_dataset}.${table}`
OPTIONS (expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL ${retention_days} DAY))
""", FORMAT_DATE('%Y%m%d', CURRENT_DATE()));
Everything else in Terraform
The backup dataset, a dedicated service account with the narrowest roles that work, and one scheduled query per table:
resource "google_bigquery_dataset" "backups" {
dataset_id = "backups"
location = "europe-west2"
}
resource "google_service_account" "backup" {
account_id = "bq-backup"
display_name = "BigQuery snapshot scheduler"
}
resource "google_bigquery_data_transfer_config" "snapshot" {
for_each = toset(var.tables)
display_name = "snapshot-${each.key}"
data_source_id = "scheduled_query"
location = "europe-west2"
schedule = "every day 02:00"
service_account_name = google_service_account.backup.email
params = {
query = templatefile("${path.module}/snapshot.sql", {
project = var.project_id
source_dataset = var.source_dataset
backup_dataset = google_bigquery_dataset.backups.dataset_id
table = each.key
retention_days = var.retention_days
})
}
}
Adding a table to the backup set is now a one-line change to var.tables, reviewed in a pull request like any other code.
Partitioned tables and longer retention
For large partitioned tables, the same pattern can copy only recent partitions. Where data needs to outlive the project itself, a small Cloud Function on a schedule can export snapshots to a Cloud Storage bucket with its own lifecycle and retention policy.
Test the restore, not just the backup
A backup you’ve never restored is a hope, not a plan. Restoring is a CREATE TABLE … CLONE from the snapshot. Rehearse it, write the steps in the runbook, and time it, so the first restore doesn’t happen during an incident.