---
title: "Start with the shape"
canonical_url: https://ruslanakchurin.dev/blog/making-iac-boring/01-shape
description: "By the time anyone reviews an IaC codebase, its shape is already decided."
datePublished: 2026-05-17
dateModified: 2026-06-25
series: making-iac-boring
seriesName: "Making IaC boring"
tags: ["infrastructure as code","software architecture","separation of concerns","ownership boundaries","tiering"]
about: [{"name":"Infrastructure as code","@type":"Thing","sameAs":"https://www.wikidata.org/wiki/Q24964334"},{"name":"Separation of concerns","@type":"Thing","sameAs":"https://www.wikidata.org/wiki/Q2465506"}]
position: 1
---
# Start with the shape

_Ownership and tier boundaries._

> [!PRELUDE]
> The hardest single part of building a software system is deciding precisely what to build.
>
> -- Frederick P. Brooks, "No Silver Bullet" · 1986

By the time anyone reviews an IaC codebase, its shape is already decided: what changes together, who can apply it, and which layer depends on which.

If that shape is not chosen on purpose, resource-first growth chooses it instead. State boundaries become hard to move, shared modules lose owners, and environments start mirroring account layouts, team boundaries, and whatever the first migration made convenient.

A common version is the environment stack becoming the place for workload exceptions. A service needs a one-off subnet route, an extra record, or a special role binding; the environment tier absorbs it because the resource lives nearby. Six months later, unrelated workload deploys need platform review, environment applies can break application behaviour, and the boundary has stopped matching ownership.

> [!NOTE]
> This series names the boundary decisions before they turn into migration work: where ownership lives, which direction dependencies can cross tiers, which resources belong to each tier, and how old paths retire. The examples are diagrams, contract shapes, and release-order checks; syntax is secondary once those choices are explicit.

## The two cuts that matter

Shape is mostly two decisions. **Ownership** decides who applies a tier, takes the page, and has authority to change the contract consumers rely on. **Tiers** decide what must exist before anything else can be applied, and which direction dependency is allowed to flow.

Ownership comes first because a tier no team owns cannot carry production safely. Applying day-to-day changes and deciding when a published contract can break are different authorities, and the second is the one usually missing. Without it, every consumer gets an informal veto and the tier freezes around its first shape.

The dependency graph sets the tier boundary. Lower tiers publish capabilities and apply first; higher tiers consume them through the published contract only. When foundation reads workload outputs, or environment accumulates workload exceptions, the boundary is already wrong. The repair is to re-cut the system so the thing that changes with the workload lives with the workload.

The symptom is review scope widening for the wrong reason: an application release drags an environment apply with it, or a foundation change has to account for workload-specific behaviour it should never have known about.

```svg path="making-iac-boring/diagrams/02-cut.svg"
wrong cut:
  environment
    network
    workload dns record
    workload role binding
  workload
    service

clean cut:
  environment
    network contract
  workload
    service
    dns record
    role binding
```

The clean cut moves workload-specific behaviour back to the owner responsible for releasing and repairing it; folder layout is only the visible symptom.

## Contracts make cuts durable

A consumer that did not change should remain valid when a lower tier changes internally. The interface between tiers is {{the contract | The public boundary between tiers: producer, key, context, type, and meaning. Consumers depend on that boundary instead of the producer's stack layout. | Contract}}: output names, subnet semantics, IAM primitives. The implementation behind that contract can move; the contract cannot quietly change underneath consumers. Renaming an output, repurposing a subnet under the same name, or changing a role's trust assumption invalidates downstream stacks without their consent. Breaking changes are allowed, but they need to be named.

Destructive cross-tier changes need staging. Consumers detach first, the provider removes the object second, and the gap has to be visible in review and release planning. The protection should be layered because any single guard can usually be edited by the same person performing the destroy.

Strictness depends on change cadence and blast radius. A foundation tier changes rarely and underpins many workloads, so its contracts should be nearly immutable and its migrations planned. A workload tier can move faster because its contracts are mostly internal to the team and its intended blast radius is narrow. Lifecycle and state size shape the internal split after that, but both are secondary.

Every stack shrinks the blast radius of an apply and adds orchestration across a new boundary. A new stack earns its place when the existing dependency graph cannot hold, or when ownership, cadence, and blast radius line up strongly enough to pay the cost. Folder size, resource importance, existing module boundaries, and cleaner-looking plans are weak reasons on their own.

## A default three-tier cut

The durable starting cut is three tiers: organisation, environment, workload.

```svg path="making-iac-boring/diagrams/01-tiers.svg" width="480"
apply order:

organisation
  publishes company-wide roots
    v
environment
  publishes landing-zone capabilities
    v
workload
  consumes the environment contract
```

**Organisation** contains things that exist once for the company before any environment exists: account roots, users, groups, source-control administration, SaaS configuration, domain ownership, bootstrap access, policy roots. The cloud provider's Organization object belongs here only when its lifecycle matches the tier; it is one resource, not the tier. Membership here requires something to exist once with everything downstream of it; resources that are merely important rather than genuinely company-wide do not belong here.

**Environment** describes the landing zone for services in a given environment. In cloud terms, that usually means the projects or accounts those services run inside, plus environment-scoped resources such as network and federated access. But the boundary is not limited to cloud resources: Vercel environments, GitHub environment configuration, and any other platform settings that exist once per environment belong here too. This tier changes less often than workloads and fails wider than any single application. Its contract says what a workload can attach to or consume, not how a specific workload happens to be deployed today. Workload exceptions either belong with the workload or prove the boundary needs to move.

**Workload** is application-owned. It consumes everything below it and should have the highest change cadence and the narrowest intended blast radius. A workload owns what it can break, repair, and migrate without forcing unrelated consumers to move. If applying a workload requires changing environment or organisation, the contract is missing or the cut is wrong.

### DNS tests the boundary

> [!NOTE]
> DNS made this lesson expensive for me. I spent a week fixing a shape where DNS lived in the tier that was convenient to write, not the tier that owned the change. The first migration took a day and looked clean enough; then the real boundary showed up, and I had to migrate the same names again into the proper shape. The lesson was not subtle: think through authority and cadence before moving records around.

DNS has more than one ownership surface. The domain at the registrar belongs with organisation because it exists before any environment and everything downstream depends on it. Parent delegation belongs there too: it is the organisation-owned contract that says which lower layer is authoritative for a zone.

Hosted zone authority and record authoring should follow cadence and ownership. A per-environment zone can live with environment when it mostly publishes landing-zone names. Service records, validation records, and routing records that change with releases belong closer to the workload, even when they are written into a delegated zone.

The boundary is wrong when organisation has to read live environment outputs before it can apply, or when environment becomes the dumping ground for {{service-specific records | DNS is the visible version. IAM bindings and release exceptions drift the same way when they change with one service but live in the environment tier. | Service-specific records}}. Delegation values are a reviewed contract between tiers, not a reverse dependency from organisation into environment.

Edge usually sits closer to workloads because listeners and routing policy change near application releases. It belongs with the workload when the same team owns it at the same cadence; a separate cut is warranted when it is shared, security-sensitive, or reused across workloads.

If an environment tier changes every time a workload ships, workload behaviour is stored in the wrong place. The next part moves from shape to contracts: what each tier publishes upward, and what makes that contract stable enough for release planning.
