Two-way doors: escape hatches in Anvil
Secure defaults shouldn't be a one-way door. How Anvil's hatches, raw Pulumi resources and forward references let you step outside the guardrails when you need to.
Ever feel like you need an escape from life's rigid schedule? I can’t help with that, but I can help you avoid one-way-door decisions in your infrastructure-as-code. Jeff Bezos made the term one-way-doors famous at Amazon. A one-way-door decision is one you can't undo once you make it; once you walk that path, what goes in stays in forever.
Have you ever worked with someone who builds internal tools to solve a small problem in their domain, then tries to force-feed a company-wide initiative that slows everyone else down? Or they introduce an external tool for the same problem and it never really gets adoption? I never want Anvil to feel that way. I need it to feel safe and obvious, without permanent guardrails you can't step outside if need be.
A lot of the problems I see with people learning infrastructure as code from a traditional software engineering background is that they treat it like building software, with a need for abstraction and reusability in what they’re defining. Then, when they need to change one small thing, it becomes impossible to find, and only whatever almighty creator you believe in can fix it… maybe. Or they have to untangle it and flatten out the declared resources anyway, which they should have just done from the start.
I firmly believe your infrastructure should be self-documenting before anyone has a tantrum. I do believe in documentation. But if I need to work on something infrastructure-related that I haven’t created, I should be able to browse and tell you what it’s doing at a reasonable level; if I have to crack the Da Vinci Code just to figure out how this thing is actually deploying, it’s not good.
One of the most enjoyable things about using SST v3 was that components came with sensible defaults out of the box, but you could override them with the transform field. That let you change their defaults or add things their top-level API didn’t expose. Anvil uses the same concept; it’s just called a hatch, because it’s an escape hatch, and I thought that fit my theme well.
Anvil hatches deep-merge into the component’s defaults, so you only write the fields you want to change and everything else is kept. For example, every Anvil Lambda is tagged ManagedBy: anvil. Add your own tag through the hatch:
const fn = new anvil.aws.Lambda('processor', {
runtime: 'nodejs24.x',
handler: 'src/index.handler',
hatch: {
lambda: {
tags: { team: 'payments' },
},
},
});
The function ends up with both ManagedBy: anvil and team: payments. You don’t have to restate Anvil’s tag to keep it.
The one exception is lists, which replace rather than merge. Every Anvil Bucket comes with lifecycle rules that clean up abandoned uploads and move large files to cheaper storage. If you pass your own rules list, it replaces Anvil’s:
const bucket = new anvil.aws.Bucket('uploads', {
dataClassification: 'sensitive',
hatch: {
lifecycleRules: {
rules: [
{
id: 'delete-old-logs',
status: 'Enabled',
filter: { prefix: 'logs/' },
expiration: { days: 30 },
},
],
},
},
});
This bucket now only deletes old logs. Anvil’s cleanup and storage rules are gone. To keep them, add them to your list alongside your own. That’s deliberate: when you need to, you can always take over a list completely, and you never have to fight Anvil to do it.
Anvil strips deprecated fields from a component’s main hatch (the field on the top level-API that exposes hidden fields), so you can’t accidentally build on features AWS is phasing out. That said, you can still use standard Pulumi resources in Anvil, and they'll deploy with your Anvil resources and live in the same state file, but they don’t have the guardrails, so deprecated fields are still included. You own all decisions made with that resource; it’s a raw Pulumi resource. This is useful when Anvil doesn’t have a component for what you need, so you have an escape hatch and aren’t left hanging.
My favourite escape hatch in Anvil is the Forward Reference. This mainly comes from CDK pain points: it’s ordinary code, so you can’t reference something before the line that creates it, and you end up hardcoding strings, reshuffling code, or reaching for CDK tricks. In Anvil, ctx.ref('db') lets you reference a resource before it’s declared. Anvil hands back a placeholder that fills in once the resource is declared, and Pulumi works out the real creation order. Most people won’t need it often, but when you do, it saves a lot of hacks.
import { App } from '@anvil-cloud/sdk';
import * as anvil from '@anvil-cloud/sdk';
export default new App({
run(ctx) {
// The Lambda needs the database endpoint, but the database is declared below
const fn = new anvil.aws.Lambda('api', {
runtime: 'nodejs24.x',
handler: 'src/index.handler',
environment: {
DB_ENDPOINT: ctx.ref<anvil.aws.DSQL>('db').endpoint,
},
});
// Declared after the Lambda, and that's fine
const db = new anvil.aws.DSQL('db');
},
});
As I have said many times, it’s your infrastructure, not mine; I am just giving you a foundation to build on and the power to make decisions without feeling like I am building it for you and you have to do what I say. I don’t mind people having opinions, and I have a few of my own on the cloud and tech, but I don't like it when people think their word is gospel. Anvil is created with two-way doors in mind; adopting my framework shouldn’t be a decision you regret down the line.
Damien.
Reference docs for this topic: Overrides & Escape Hatch.