Query icon
Log icon
If icon
SlackIncomingWebhook icon
Loop icon
Execute icon
Fail icon
Schedule icon

Cloud Spanner Schema Governance Guard

Find Cloud Spanner tables keeping timestamped data with no row deletion policy, alert Slack, add the policy with DDL and re-check INFORMATION_SCHEMA.

Categories
CloudData

Cloud Spanner can expire old rows for you with a row deletion policy (its TTL feature), but only on tables where someone declared one. A table that collects events or logs with a created_at timestamp and no policy grows without bound and nobody notices until storage and query cost climb. The policy is one line of DDL, so the gap is almost always an oversight.

This blueprint reads INFORMATION_SCHEMA and finds every base table that has the retention timestamp column but a null ROW_DELETION_POLICY_EXPRESSION. It alerts Slack and can add the missing policy with ALTER TABLE ADD ROW DELETION POLICY, one table at a time. It then reads INFORMATION_SCHEMA again, so the run only succeeds when each table carries a policy.

It runs against the Cloud Spanner emulator for a safe demo with no credentials and against managed Spanner in production.

This blueprint was created by zkasuran.

Findings

A table is in scope when it is a base table in the user schema and has a TIMESTAMP column named by retention_column (default created_at). A finding is such a table whose ROW_DELETION_POLICY_EXPRESSION is null. Tables with no retention column are never flagged, so reference and lookup tables are left alone.

Safety

Read-only by default. The ALTER runs only when auto_remediate is true and dry_run is false. The emulator accepts the policy DDL and reflects it in INFORMATION_SCHEMA but does not run the background deletion, so the re-check asserts the policy exists, never that rows were removed. On managed Spanner the background job then enforces it.

How it works

  1. detect (io.kestra.plugin.gcp.spanner.Query, read-only) lists base tables in the user schema that have the retention timestamp column and no row deletion policy. Each row carries the table, the column and a ready ALTER TABLE ADD ROW DELETION POLICY statement.
  2. summary logs every finding.
  3. route (io.kestra.plugin.core.flow.If):
    • clean: logs that every scoped table has a policy.
    • findings: alert posts to Slack.
      • remediate_gate runs only when auto_remediate is true and dry_run is false.
      • remediate (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) adds one policy per task.
      • reverify reads INFORMATION_SCHEMA again, then verify_result fails the run if any scoped table still has no policy.
  4. errors alerts Slack when a query or statement cannot run.

What you get

  • Every table that keeps timestamped data with no retention, read from the catalog itself.
  • The policy added with DDL, one table at a time, behind two safety inputs.
  • Proof from INFORMATION_SCHEMA that each table carries a policy after the run.

Who it's for

  • Teams running Cloud Spanner who want retention applied consistently across a database.
  • Platform engineers who review schema governance after migrations.
  • Anyone evaluating the Spanner plugin who wants a runnable example against the emulator.

Why orchestrate this with Kestra

The catalog query finds the gaps once. Kestra runs it on a schedule and after every migration, turning the result into a Slack alert. It adds the policies behind auto_remediate and dry_run, one per task so one failure stays isolated, verifies the result and keeps a history of what changed and when.

Tested end to end

On Kestra 2.0.5 OSS against the Cloud Spanner emulator (gcr.io/cloud-spanner-emulator/emulator), with a local HTTP sink standing in for Slack. The database held three base tables:

  • audit_events: created_at TIMESTAMP with a policy already (never flagged);
  • session_logs: created_at TIMESTAMP with no policy (the gap);
  • country_codes: no retention column (out of scope).
Run Inputs Result
1 defaults, degraded 1 finding (session_logs), alert sent, no change
2 auto_remediate: true, dry_run: true Same report, policy not added
3 auto_remediate: true, dry_run: false Policy added and verified, session_logs now OLDER_THAN(created_at, INTERVAL 90 DAY)
4 defaults, after the fix 0 findings, clean
5 unreachable database errors alert sent

Confirmed against the emulator's own INFORMATION_SCHEMA: session_logs had a null policy before run 3 and the OLDER_THAN expression after.

Prerequisites

  • A Cloud Spanner instance and database. For a local demo, run docker run -d --name spanner -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator, then create an instance and a database through the emulator admin API.
  • A table with a created_at TIMESTAMP column and no row deletion policy, so there is a gap to find.

Secrets

  • SLACK_WEBHOOK_URL: used by alert and the errors block.
  • GCP_SERVICE_ACCOUNT: a service account JSON for managed Spanner. Not read when emulator_host is set, because the emulator needs no credentials.
  • In Kestra OSS, provide each one as a base64-encoded environment variable prefixed SECRET_.

Inputs

Input Default Purpose
project_id test-project GCP project
instance_id w26-inst Spanner instance
database_id governance Spanner database
emulator_host localhost:9010 Emulator address. An empty value is not valid, see How to extend for managed Spanner
retention_column created_at Timestamp column that marks a table for retention
retention_days 90 OLDER_THAN interval used by the fix
auto_remediate false Allow the policy to be added
dry_run true Must be false for the ALTER to run

Quick start

  1. Add the SLACK_WEBHOOK_URL secret.
  2. Start the emulator and create an instance and a database with at least one table that has a timestamp column and no policy.
  3. Import the flow and run it with emulator_host set to the emulator address.
  4. Read the findings, then run it again with auto_remediate: true and dry_run: false.
  5. Enable the weekly trigger, which always runs in safe mode.

How to extend

  • For managed Cloud Spanner, delete the emulatorHost line from the three Spanner tasks (the plugin treats an empty value as a bad host, so the property must be absent) and set GCP_SERVICE_ACCOUNT.
  • Scope the scan to a table name pattern or a list of databases.
  • Chain this guard into a post-migration pipeline so a new table without retention is caught the day it ships.
  • Ask for approval with a Pause before adding policies on busy tables.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.