Ruslan Akchurin · Sydney
AboutWriting

Fail before apply

Resolver enforcement for cross-tier contracts.

The resolver is the release-path refusal point. It takes the consumer's producer, key, context, and expected type, then returns a bound value or stops before any provider call changes infrastructure.

Contract addresses and ownership boundaries remain advisory while a consumer can reach into another tier's outputs and still get a plan. The apply path must reject that dependency; review alone leaves the shortcut executable.

Where the resolver runs

The resolver belongs in the earliest place that sees both sides of the contract: the consumer request and the producer's published values. Common hooks are CI before terraform plan, a generation step that writes stack inputs, or the IaC program before it instantiates provider resources.

The producer publishes typed values for consumers to resolve during a release through stack outputs, a registry, or a generated manifest. In Pulumi, StackReference reads those values from stack outputs. The reference is the transport, not the public contract: the consumer request is environment/network.primary as NetworkRef in prod, and the resolver maps that request to exactly one backing stack/output value or refuses.

Stack dependencies model sequencing when they are declared, but ordering alone does not prove composition. A StackReference wrapper becomes the resolver when it validates the value, producer, key, and context at runtime rather than casting the output to the expected type.

Resolver gate stops the apply A producer tier publishes typed contract values through a stack output. The resolver gate resolves the request across four dimensions - producer, key, type, context. A resolved request flows straight down into the consumer tier; a refused request terminates at a stop bar over a hatched void zone, where no provider call is ever made. CONTRACT RESOLUTION environment/network.primary → NetworkRef producer tier publishes typed contract values stack output resolver gate resolves the request in context producer key type context resolved refused consumer tier consumes the resolved value provider calls never run resolved - request flows to consumer refused - no provider call
Resolver gate stops the apply

Any backend still has to preserve producer namespace, key, context, type, and ownership; without those constraints, it is raw outputs behind another API.

What the gate refuses

A generic provider error tells the operator where the plan happened to break. A resolver error names the contract that could not be satisfied before anything changed.

The gate refuses five contract failures:

  • unknown producer: environment is not published in this context
  • missing key: environment/network.primary exists in staging but not prod
  • wrong type: the value resolves, but not as NetworkRef
  • ambiguous binding: more than one producer satisfies the same address in the same context
  • cycle: the producer cannot publish because it depends on the consumer

Unknown producer and missing key are cheap failures. The missing side either needs to ship, or the consumer needs to drop the dependency. A retry against the same published set should fail the same way.

Wrong type is a stronger warning because the address resolved. The value exists, but its shape does not carry the capability the consumer asked for. A string array where NetworkRef was expected cannot drift into a provider call because the values look syntactically usable.

Ambiguous binding means a migration leaked into the catalogue: two stacks publishing environment/network.primary, a copied stack that kept the original producer identity, or a context rewire that points two environments at the same surface. The resolver names both candidates and refuses to choose.

An environment tier cannot publish network.primary if it first needs workload-api/network-config, while the workload needs environment/network.primary to start. Graph tools catch many of these in the stack graph; the resolver still needs to refuse the same shape when the cycle appears through a catalogue or generated manifest.

A resolved contract can remain unusable while another cloud API converges, as with IAM propagation or DNS delegation. The release path needs a readiness check and backoff at that boundary. Weakening the contract would only hide a propagation problem as a dependency problem.

Shared surfaces need binding rules

Changing a shared surface needs more than a resolved value. A binding rule names the surface, the permitted request and operations, and the deploy identity allowed to make the change. DNS, listener attachments, and shared IAM surfaces need this contract before a consumer mutates them.

DNS makes the extra fields visible: delegation admits the name pattern, the permission policy allows the record operations and types, and the principal binding identifies who may exercise them. Delegation alone leaves permission scope and holder identity unchecked.

binding rule: organisation/dns.workload-api-zone
  admits:  *.api.example.com
  permits: A | CNAME | TXT
  ops:     CREATE | UPDATE | DELETE
  holder:  workload-api/deploy
Workload writes against the binding rule Organisation publishes delegation outputs and a permission policy. The binding rule binds the name pattern, the allowed operations, and the bound principal. Workload resolves the rule and writes a record into the delegated zone, drawn as the single accent edge. BINDING RULE organisation delegation outputs permission policy binding rule name pattern · allowed ops · bound principal workload record in delegated zone
Workload writes against the binding rule

Before calling the DNS provider, the resolver checks the requested name, record type, operation, and authenticated principal against the binding rule. Any mismatch stops the apply. The same check governs IAM changes on organisation-owned surfaces and listener changes on shared load balancers.

Existing systems need a migration path: publish the new contract beside the old output, move consumers, then remove the old path. The next part moves from enforcement to repair: the order for reaching this shape in a live system.

In the companion repoGitHublib/service-project/consumer.ts (opens in new tab)