Application workloads fit Kubernetes well because their desired state is easy to describe: run this container image, expose this service, keep this many replicas healthy. Database schemas are harder. A table, index, constraint, or enum type evolves in place; the old state may contain production data; and the wrong change can silently break live traffic. Traditional migration jobs run a fixed sequence of SQL statements, but they usually do not behave like Kubernetes resources that can be inspected, reconciled, approved, retried, and observed.
Declarative database schema reconciliation applies the Kubernetes operator pattern to schema management. Instead of embedding migration commands in app startup or running ad hoc jobs during deployment, teams publish a custom resource that describes the desired database schema. A controller compares that desired schema with the actual database, plans the required DDL, applies safe changes, and reports status back through Kubernetes conditions.
The Atlas Kubernetes Operator is a concrete example of this pattern. It introduces AtlasSchema for declarative schema management and AtlasMigration for versioned migration workflows. In the declarative flow, an AtlasSchema resource can contain inline SQL, Atlas HCL, a ConfigMap reference, or a schema-registry reference. The controller connects to the target database, diffs actual and desired state, then plans and executes CREATE, ALTER, and DROP statements according to configured safety rules.
This topic teaches the transferable mechanism behind that design: how to model schema as Kubernetes state, how the reconciliation loop differs from one-shot migration scripts, and how to keep a declarative schema controller safe enough for production databases.
A declarative schema resource describes the database shape that should exist after reconciliation. The resource might say that a users table has an integer primary key, a unique email column, and an index on account id. It does not need to list every SQL step from the current state to the target state. The controller computes that transition by introspecting the live database and comparing it with the resource specification.
This is the same mental model as a Kubernetes Deployment. The user describes the target; the control loop decides whether it must create, update, or leave objects alone. The difference is that database state is durable and sometimes irreversible, so the schema controller needs stronger guardrails than a stateless workload controller.
Bundling migrations into application startup creates race conditions. Two pods can start at once, one pod may fail halfway through a migration, and the application rollout becomes coupled to a database change that might require approval or a maintenance window. An operator separates schema convergence from app process startup. The schema resource can be applied, reviewed, waited on, and rolled forward before new application replicas depend on it.
Declarative diffing is powerful because it can infer destructive operations. If the desired schema omits a column that exists in production, a naive controller might plan a DROP COLUMN. Safety policies constrain this behavior. Atlas supports diff policies such as skipping selected destructive operations, and its approval workflows let teams review or pre-approve planned changes before the controller executes them.
A production schema controller should be declarative about the target state but conservative about execution. Destructive changes, long-running locks, data backfills, and cross-service compatibility changes still need policy, review, and rollout sequencing.
The operator extends the Kubernetes API with CRDs for schema work. A minimal declarative resource includes a target database connection and a desired schema document. Credentials should come from Kubernetes Secrets or an external secret synchronizer; the schema may be inline for simple cases or referenced from a ConfigMap or registry for larger deployments.
apiVersion: db.atlasgo.io/v1alpha1
kind: AtlasSchema
metadata:
name: application-schema
spec:
urlFrom:
secretKeyRef:
name: postgres-credentials
key: url
schema:
sql: |
create table users (
id bigint primary key,
email text unique not null,
created_at timestamptz not null
);
Once this resource is applied, the controller receives an event, loads the target schema, connects to the database, and introspects the actual catalog. If the users table is missing, the plan includes a table creation. If the table already matches, the resource can become ready without changing the database.
A schema reconciler follows the same high-level controller flow as other Kubernetes operators: watch resources, read desired state, observe current state, compute a diff, execute actions, then update status. The difference is that the observed state lives partly outside Kubernetes inside the database catalog.
func Reconcile(schemaResource):
desired = loadDesiredSchema(schemaResource.spec.schema)
databaseURL = loadConnection(schemaResource.spec.urlFrom)
actual = inspectDatabaseCatalog(databaseURL)
plan = diff(actual, desired, schemaResource.spec.policy)
if plan.requiresApproval:
setCondition(schemaResource, "Ready", false, "ApprovalRequired")
return requeueAfterReview()
result = applyPlan(databaseURL, plan)
if result.failed:
setCondition(schemaResource, "Ready", false, result.reason)
return retryWithBackoff()
setCondition(schemaResource, "Ready", true, "Applied")
The status update is important. Platform tooling can wait for the resource to become ready before promoting an application version. Operators can inspect failed resources with familiar Kubernetes commands instead of hunting for a lost migration job log.
Declarative mode asks the controller to compute the path from actual state to desired state. Versioned mode treats migration files as the source of truth and applies unapplied versions in order. Both modes are useful. Declarative mode works well when the schema definition is authoritative and changes are relatively direct. Versioned mode is often better when the transition itself matters, such as data backfills, column splits, or multi-step compatibility migrations.
When schema resources live beside application manifests, pull requests can show both the app rollout and the database target state. A deployment pipeline can apply the schema resource, wait for the ready condition, and only then roll out app pods that depend on the new table or column. This gives database changes the same review and audit trail as other infrastructure changes.
Operators such as CloudNativePG manage PostgreSQL clusters as Kubernetes resources. A schema operator complements that by managing what lives inside the database. The database cluster operator keeps pods, storage, backups, and failover healthy; the schema operator keeps tables, indexes, and constraints aligned with the declared application contract.
Platform teams can install a schema controller with standard policies, approval flows, and credential patterns. Application teams then submit schema resources instead of writing custom migration jobs. This creates a consistent control plane for common concerns such as secret handling, destructive-change review, status reporting, and environment promotion.
Declarative schema resources are useful for short-lived environments because the same manifest can converge a fresh database from empty state or reconcile an existing preview database after changes. This reduces setup drift between local, preview, staging, and production environments.
Do not let every application pod attempt migrations on startup. Model schema convergence as its own resource and gate deployment on that resource becoming ready. In a simple pipeline, apply AtlasSchema, wait for its ready condition, then roll out the application Deployment.
kubectl apply -f atlas-schema.yaml
kubectl wait --for=condition=Ready atlasschema/application-schema --timeout=120s
kubectl rollout restart deployment/application-api
The schema resource should reference connection credentials rather than embedding passwords in the manifest. This keeps GitOps repositories free of database passwords and allows rotation without rewriting the schema definition.
apiVersion: v1
kind: Secret
metadata:
name: postgres-credentials
type: Opaque
stringData:
url: postgres://app:password@postgres.default:5432/app?sslmode=disable
Add a policy layer that treats additive and destructive changes differently. Creating a new table or nullable column is usually safe to apply automatically. Dropping a column, changing a type, adding a non-null constraint to a populated table, or rebuilding a large index should require review, a precomputed plan, or a versioned migration.
Declarative reconciliation should not bypass application compatibility discipline. For breaking changes, use an expand/contract sequence: first add the new schema shape, deploy code that can read and write both shapes, backfill data if needed, switch reads, then remove the old shape in a later release. The schema controller can manage each target state, but the rollout design still belongs to the engineering team.