An operator is a control loop with a domain in it. That is the whole definition. Everything else is implementation.

A factory is composed of operators, each one owning a seam. The seams are the spec, the build, the test, the deploy, the promote, the rollback. Each seam gets one operator. The operators do not call each other. The operators watch the cluster and react.

The spec operator

The spec operator owns a SoftwareSpec custom resource. A human writes the spec. The operator validates the spec. The operator emits a normalized form into the cluster, signed, with provenance attached.

The spec operator is the only operator that takes human input. Every other operator in the factory watches a spec-shaped resource. The spec operator is the auditor's first stop: what specs exist, who wrote them, what versions are live.

Keep it thin. The spec operator's job is to validate and emit. It does not run tests. It does not trigger builds. It does not know what a build is.

The build operator

The build operator watches a normalized spec. The build operator produces a BuildArtifact custom resource containing the hash, the SBOM, the provenance graph, and the signed manifest. The build operator does not know about clusters. The build operator does not know about deployment. The build operator's job is to take a spec and produce an artifact that other operators can verify.

The build operator is the operator that wants to know too much. Every time the build operator learns about tests, about deployment, about promotion, it becomes unmaintainable. The build operator that survives is the one that does one thing. Boring is what you want from a build operator.

The deploy operator

The deploy operator watches a signed BuildArtifact and a target environment. The deploy operator produces a Deployment custom resource. The deploy operator's job is to take a verified artifact and put it in a verified place.

The deploy operator is where most teams make their first mistake. They have the deploy operator trigger a kubectl apply, or call a Helm release, or invoke a serverless deploy. All of these are wrong. The deploy operator should produce a declarative resource, and a separate, generic Kubernetes primitive should reconcile to that resource. The deploy operator is the translator. The cluster is the executor.

When the deploy operator produces a resource, the resource can be inspected, audited, rolled back, frozen. When the deploy operator pushes to a remote API, none of those things are true. The remote API is the system. The remote API is what the auditor cannot see.

The policy operator

The policy operator is the seam. The policy operator watches deployments and consults a policy bundle. The policy bundle is a versioned, signed set of rules. The policy operator's job is to decide which deployments are allowed to promote to which environment.

The policy operator is the one that is hardest to write. The off-the-shelf policy engines (OPA, Cedar, Rego, the rest) cannot express what most factories need without a policy file that nobody on the team can read. The version that survives is a custom DSL, small enough to fit on a page, expressive enough to encode the rules the business actually needs.

The policy operator is where the human's authority lives in a dark factory. The policy is the human's signature on the system. The policy operator enforces that signature. Everything else in the factory can be fully dark. The policy operator is the part that cannot be.

The anti-pattern

One operator per application. One operator per microservice. The factory has many operators, and every one of them is a slightly different shape. The team spends more time learning the operators than they spend shipping product.

The mistake is treating the operator as a wrapper around the application. The operator is not a wrapper around the application. The operator is a wrapper around a type of change. The factory has one operator per type of change, regardless of how many applications use that type. The application is a parameter, not a peer.

The composition rule

Operators in a factory do not call each other. Operators watch resources. Operators emit resources. The composition happens in the cluster's etcd layer, not in operator code.

This rule sounds like bureaucracy. It is not. It is what makes the factory auditable. When operator A triggers operator B, the trigger is a side effect of state, and the state is in etcd, and the etcd is queryable. When operator A calls operator B's API, the trigger is hidden in operator A's code, and the audit is impossible. Choose the auditable version.