Infrastructure-as-Code North Star
The durable statement of where OnRamp's infrastructure is heading and why — the reference to check intent against when building anything in infra/. Not an RFC (no single decision to ratify); a compass.
The one sentence. Write each piece of infrastructure once in Pulumi, prove it in a per-PR preview, then promote the same code to every environment — pulumi up per tier, driven by the environment — and have it deploy beautifully because it was already exercised downstream. Convention does the rote work (tagging, naming, scoping); humans describe only what's genuinely different.
TL;DR — the three pillars
One definition, N applies.
A resource is declared once as an env-agnostic component. Each tier is a Pulumi stack; pulumi up with the tier selected deploys it. No per-environment copies.
The Aspect, in Pulumi.
A stack transformation intercepts every resource and auto-applies the environment tag (and env-prefixes globally-unique names). Plus sensible defaults + derivation everywhere — deviations are the only thing spelled out.
Test it before it's real.
Previews are the proving ground. The destination: stage/prod run the same stack (CloudFront / S3 / Lambda / Neon) so what passes in a preview behaves the same in prod.
The ideal flow
flowchart LR
CODE["One Pulumi program<br/>(env-agnostic components)"] --> XF["Stack transformation<br/>auto environment tag + env-prefix"]
XF --> UP["pulumi up · stack = tier<br/>(via that tier's ABAC deploy role)"]
UP --> PV["preview-N"]
UP --> DEV["dev"]
UP --> STG["stage"]
UP --> PRD["prod / gdpr-prod"]
PV -.->|prove first, then promote| STG
style XF fill:#e6f3ff,color:#000This is the CDKTF-Aspect experience a few of us have shipped before — terraform apply with a new ENVIRONMENT and it deploys, because the code was identical and already validated downstream — rebuilt on Pulumi (which models it more cleanly).
The mechanisms that get us there
pulumi.runtime.registerStackTransformation intercepts every resource at synth and stamps environment=<tier> + env-prefixes globally-unique names — write the resource plain, the transform makes it env-correct. Quirks (non-taggable resources, etc.) are handled as by-type accommodations, not special-cased everywhere.
One reusable, env-scoped CI role per tier (boundary ∩ identity, self-scoped by the environment tag). Makes a per-tier pulumi up safe — a tier's role can only touch its own env's resources. See the Deploy Roles & Environments runbook.
Sensible defaults + derivation over bloated config: a new environment is a one-line registry entry; bucket / key / role / backend all derive from the tier name. Deviations are the only thing declared. (Codified in AGENTS.md.)
Reusable resource definitions (e.g. infra/components/agent-runtime.ts) that take args and are instantiated per tier — the unit of "write once." Features add a component; every tier picks it up.
The keystone synergy. The transform's auto-environment tag is exactly what the deploy role's ABAC boundary requires on create. So a feature that adds a new taggable resource (an SQS queue, say) is auto-tagged → automatically satisfies the boundary → deploys with zero manual tagging or per-resource IAM. Convention and security reinforce each other instead of fighting.
Today vs the north star
- Previews run the serverless stack (CloudFront / S3 / lambdalith / Neon), deployed by Pulumi via the per-PR deploy role.
- dev / stage / prod run on Duplo-managed Kubernetes, deployed via Duplo /
deploy.py— a different shape from previews. - Pulumi covers previews + (incoming) the env-scoped deploy roles; most prod infra is not yet Pulumi-managed.
- Net: you can prove infra in a preview, but there's no homogeneous prod stack to promote the same code into yet.
Maturity ladder — what's done, in-flight, far
| Capability | State |
|---|---|
| Pulumi for previews (lambdalith / CloudFront / S3 / Neon) | Done |
| Reusable env-scoped ABAC deploy roles + per-env state | In-flight (this PR's foundation) |
| Convention-over-config registry (one-line new env) | Done (this PR) |
| Stack transformation — auto env-tag + prefix (the Aspect) | Done (this PR — infra/components/env-conventions.ts, wired into the preview stack) |
| Boundary widened toward allow-broad + ABAC (near-zero per-service churn) | Done (this PR) — env-conventions now tags the shared stack too; boundary is the NotAction allow-all-except-iam shape, gated on the environment tag |
| Env-agnostic components for shared feature infra | Incremental — as features need them |
Non-preview tiers run pulumi up in their pipeline | Far — needs those envs on the serverless stack |
| stage / prod homogeneous with the preview stack | The long pole — the platform migration this is groundwork for. Compute shape then climbs the Domain Lambdaliths ladder (→ managed runtime + SnapStart); IaC homogeneity here is the Stage-0 baseline. |
The 95 / 5 rule
This worked in practice before: the transform + conventions cover 95%+ of AWS resources cleanly; a small tail needs accommodation. Plan for it, don't fight it.
- Non-taggable resources (Lambda permissions, policy attachments, KVS keys, …) — the transform skips them by type (a maintained denylist).
- Services that don't honor
aws:ResourceTag(e.g. bedrock-agentcore, whose create spawns un-ARNable sub-resources) — scoped by region + explicit Deny instead of ABAC, as a knowing exception. - Global-uniqueness names (S3 buckets) — env-prefixed by the transform.
The goal isn't 100% magic; it's that the common case is invisible and the exceptions are few, named, and local.
Risks & open questions
The homogeneity goal is gated on migrating stage/prod onto the serverless stack — a multi-month platform effort, not a config flip.
Mitigation: treat previews as the proving ground and build everything env-agnostic now, so the migration is adoption, not rework.
The boundary is now widened to the allow-broad NotAction shape gated on the environment tag. This removes per-service churn but relies on the transform tagging every taggable resource with environment=<tier>.
Resolved: the env-conventions transform now stamps environment=previews on the shared stack too (PR wiring confirmed), so tags are guaranteed on every taggable preview resource. The boundary's tag-on-create Deny enforces the gate at create time. Risk is accepted and the per-service enumeration is retired.
Open questions
Is dev a deploy tier? The registry is previews/stage/demo/prod/gdpr-prod. If the merge-to-main env gets Pulumi infra, add { env: "dev" } (one line).
Promotion trigger. How does the release flow drive per-tier pulumi up (preview → dev → stage → prod) — and how does it interleave with the existing trunk Promote-to-Staging / Promote-to-Production gates?
See also
- Deploy Roles & Environments — the env-scoped ABAC role runbook (the safe per-tier
pulumi upmechanism). - Serverless PR Previews — the preview stack this converges toward.
AGENTS.md→ Configuration shape: defaults over fields — the convention-over-configuration principle.