---
title: "Fail before apply"
canonical_url: https://ruslanakchurin.dev/blog/making-iac-boring/04-fail-before-apply
description: "An IaC contract holds only if the release path enforces it: a resolver checks type and context, then refuses invalid composition before apply."
datePublished: 2026-06-07
dateModified: 2026-06-25
series: making-iac-boring
seriesName: "Making IaC boring"
tags: ["infrastructure as code","pulumi","release pipeline","validation","fail-fast"]
about: [{"name":"Infrastructure as code","@type":"Thing","sameAs":"https://www.wikidata.org/wiki/Q24964334"}]
mentions: [{"name":"Pulumi","@type":"SoftwareApplication","sameAs":"https://www.wikidata.org/wiki/Q138634162"},{"name":"Terraform","@type":"SoftwareApplication","sameAs":"https://www.wikidata.org/wiki/Q28957072"},{"name":"Domain Name System","@type":"Thing","sameAs":"https://www.wikidata.org/wiki/Q8767"}]
position: 4
---
# 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.

```svg path="making-iac-boring/diagrams/06-resolver-gate.svg"
producer tier
  publishes typed contract values
    v
resolver gate
  resolves producer / key / type in context
    |
    +-- resolved  ->  consumer apply continues
    +-- refused   ->  provider calls never run
```

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.

```text
binding rule: organisation/dns.workload-api-zone
  admits:  *.api.example.com
  permits: A | CNAME | TXT
  ops:     CREATE | UPDATE | DELETE
  holder:  workload-api/deploy
```

```svg path="making-iac-boring/diagrams/07-binding-rule.svg" width="550"
organisation
  DNS delegation       permission policy
        |                    |
        v                    v
              binding rule
          name pattern · allowed ops · bound principal
                    |
                    v
                workload apply
                    |
                    v
             record in delegated zone
```

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.
