Your First Schema Change with the Ptah Kubernetes Operator
Publish a shipment schema with Ptah, inspect the operator's plan, approve that exact change, and verify the database has converged.
A fulfillment service needs a shipments table before it can accept orders.
The schema is in Git, but someone still needs to compare it with the database,
review the SQL, and apply the change.
The Ptah Kubernetes operator makes that process visible through Kubernetes resources. We’ll publish the desired schema with the Ptah CLI, inspect the operator’s plan, and approve it. Then we’ll check the database itself.
What the operator owns
Section titled “What the operator owns”The operator observes the database and runs Ptah in short-lived Jobs. Its resources separate the desired state from the decision to apply a change:
| Resource | What it records |
|---|---|
PtahSchema |
The target database, desired schema artifact, and apply policy |
PtahSchemaPlan |
The proposed change and the identity of its stored SQL |
PtahSchemaApproval |
Approval of one specific plan |
This example uses PtahSchema, which converges a database toward a declared
schema. If you need to execute an ordered migration directory, the operator’s
separate PtahMigration resource handles that workflow.
The commands below start with that operator installed. NAMESPACE names the
application namespace. It contains a database URL Secret, registry credentials,
and the immutable verification-policy ConfigMap included with the example.
The installation guide
explains the required image pins. The kubectl ptah plugin in this run was
built from the same operator commit.
Publish the shipment schema
Section titled “Publish the shipment schema”The desired schema is ordinary SQL:
CREATE TABLE public.shipments ( id bigint PRIMARY KEY, order_id bigint NOT NULL, status text NOT NULL DEFAULT 'queued');Publish it to the test registry, with credentials supplied through Ptah’s registry authentication settings:
ptah schema push "oci://$REGISTRY/schemas/blog-shipments:20260923-v1" \ --schema-file schema.sql --dialect postgres --plain-httpThe registry in this disposable lab uses HTTP on a private test network, so
this command opts into it explicitly. Use TLS for your normal registry.
The returned digest identifies the published content. Copy that digest into
desired.ociRef in shipments.yaml.
The resource also names the database Secret and sets apply: OnApproval.
Its coordinationKey identifies the physical database: every resource that
addresses that same database must use the same key, including through aliases.
Keep one resource responsible for this schema.
The verification policy in this example accepts Ptah schema artifacts. It does not require a publisher signature; registry access controls still matter.
Let it plan, then read the SQL
Section titled “Let it plan, then read the SQL”Apply the resource and wait for the approval condition:
kubectl -n "$NAMESPACE" apply -f shipments.yamlkubectl -n "$NAMESPACE" wait ptahschema/shipments \ --for=condition=ApprovalRequired=True --timeout=180sThe operator resolves and verifies the artifact, observes PostgreSQL, and
publishes a plan. At this point the phase is AwaitingApproval. Our database
check confirmed that public.shipments was still absent.
Read the SQL through the plugin:
kubectl ptah plan shipments --current -n "$NAMESPACE" -o sqlThe plan contains CREATE SCHEMA IF NOT EXISTS "public" followed by the
shipment table definition. The plugin verifies the stored plan’s chunks before
printing them. Reading the desired SQL file alone would miss how Ptah plans to
change the actual database.
Approve the plan you just inspected
Section titled “Approve the plan you just inspected”An approval binds the schema name and UID, the plan name and UID, and the plan fingerprint. Read those values from Kubernetes:
kubectl -n "$NAMESPACE" get ptahschema shipments -o yamlUse metadata.uid and the current status.plan fields to fill
approval.json. Its recorded identifiers belong to
this test run; a new installation needs the identifiers from its own plan.
The admission webhook supplies the remaining execution bindings and the
authenticated approver identity.
We first submitted an approval with the wrong fingerprint. Kubernetes rejected it with:
approval plan fingerprint does not match the immutable planThe shipment table remained absent. The valid approval then allowed the change:
kubectl -n "$NAMESPACE" create -f approval.jsonThis requires permission to create approvals and to read the plan’s stored SQL. In this disposable cluster we used its administrator identity. An application deployment identity does not need approval permission simply because it can update the desired schema.
Verify convergence after the Job finishes
Section titled “Verify convergence after the Job finishes”Wait for convergence and read the latest observed status:
kubectl -n "$NAMESPACE" wait ptahschema/shipments \ --for=condition=InSync=True --timeout=180skubectl ptah schema shipments -n "$NAMESPACE"The relevant output was:
Phase: InSyncDrift: noneInSync: True (ScopedConverged)JobCompleted means the apply Job finished. ScopedConverged means a database
observation found the managed scope matched the desired state. In this run,
the recorded observation was later than the apply completion, and both
referred to the current resource generation. The approval was marked
Consumed=True, with reason DispatchCommitted.
A converged schema has no pending plan. To inspect what was applied, select the applied plan explicitly:
kubectl ptah plan shipments --applied -n "$NAMESPACE" -o sqlFinally, we inserted shipment 1 for order 42. PostgreSQL returned the row
with its default status, queued. The full insert and output are in the
example files.
The useful result is a reviewable change with a recorded decision and a fresh database check. For the next schema change, publish a new artifact, update the resource’s digest, and review the new plan. The previous approval does not authorize it.
Example files
- application-query.sql
- approval.json
- expected/converged.txt
- expected/rejected.txt
- measured/application-query.txt
- measured/approval-consumed.txt
- measured/approve.txt
- measured/awaiting-approval.txt
- measured/commands.json
- measured/converged.txt
- measured/postgres-version.txt
- measured/push.txt
- measured/read-applied-plan.txt
- measured/read-status.txt
- measured/review-plan.txt
- measured/runtime.json
- measured/table-after-rejection.txt
- measured/table-before-approval.txt
- measured/wrong-approval.txt
- README.md
- schema.sql
- shipments.yaml
- verification-policy.yaml
- verified.json
- wrong-approval.json