<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Riddam Jain — Engineering</title><description>Deep dives and write-ups from real work — cloud and application architecture, and the reasoning behind the decisions.</description><link>https://riddam.github.io/</link><language>en-us</language><item><title>Self-Service With Guardrails: Repositories and Pipelines as Data</title><link>https://riddam.github.io/engineering/self-service-with-guardrails/</link><guid isPermaLink="true">https://riddam.github.io/engineering/self-service-with-guardrails/</guid><description>How to let teams own their repositories and pipelines without the platform team becoming a ticket queue — declarative registries, validation that runs first, reconciliation with drift detection, and a lifecycle that includes retirement.</description><pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There&apos;s a stage every platform team hits. You&apos;ve built something good, teams want to use it, and now you spend your week creating repositories, adding people to groups, wiring webhooks, and setting up pipelines. You&apos;ve become a ticket queue with extra steps.&lt;/p&gt;
&lt;p&gt;The obvious fix is to give teams access and let them do it themselves. That works right up until you have a thousand-odd repositories, no two configured the same way, and no way to answer &quot;which of these have branch protection?&quot; without opening a thousand browser tabs.&lt;/p&gt;
&lt;p&gt;Self-service and consistency look like opposites. They aren&apos;t — but getting both means the thing teams edit has to be &lt;em&gt;data&lt;/em&gt;, not a UI.&lt;/p&gt;
&lt;p&gt;This is the layer above the infrastructure. &lt;a href=&quot;https://riddam.github.io/engineering/rebuilding-ci-cd-without-changing-platforms/&quot;&gt;Rebuilding the platform itself&lt;/a&gt; — agents, scaling, upgrades — is a separate story, and worth doing first: self-service on top of a platform you&apos;re afraid to touch just distributes the fear.&lt;/p&gt;
&lt;h2&gt;Two registries, one source of truth&lt;/h2&gt;
&lt;p&gt;Everything starts from declarations in version control rather than state in a web interface:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A pipeline registry&lt;/strong&gt; — one entry per repository that has a pipeline, naming the owning teams, the hosting platform it deploys to, and the grouping it belongs to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A repository registry&lt;/strong&gt; — the repositories themselves, their teams, their webhooks, their branch protection.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A team wanting a pipeline opens a pull request adding a few lines. That&apos;s the whole interface. The same reasoning I&apos;ve written about for &lt;a href=&quot;https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/&quot;&gt;configuration as data&lt;/a&gt; applies to organizational structure: the moment your source of truth is a UI, you have no history, no review, and no way to ask a question across the whole estate.&lt;/p&gt;
&lt;p&gt;What matters is that the registry is &lt;em&gt;the&lt;/em&gt; source of truth and not a description of one. Which means something has to continuously make reality match it — and something has to refuse entries that would break it.&lt;/p&gt;
&lt;p&gt;The scale this operates at is roughly 1,400 projects and around 2,400 build configurations. At that size, nobody is going to notice a hand-made exception, which is the real argument for the registry: not elegance, but the fact that no human review process survives four figures.&lt;/p&gt;
&lt;h2&gt;Validate before you touch anything&lt;/h2&gt;
&lt;p&gt;The validation step runs first, and it exits non-zero on the first problem. Nothing reaches the CI server until every check passes.&lt;/p&gt;
&lt;p&gt;Some checks are the obvious schema ones — every entry validates against a JSON Schema, and duplicate entries are rejected. Two others are worth calling out, because they&apos;re the ones I&apos;d port to any similar system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Entries must be in alphabetical order.&lt;/strong&gt; This sounds like fussiness and isn&apos;t. An unordered append-only list means every team adding a pipeline in the same week touches the same last line, and they all conflict. Enforced ordering distributes the edits across the file, so a class of merge conflict simply stops occurring. It also makes review trivial — a diff is one line in a predictable place rather than a reordering nobody can read.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The owning team must already exist.&lt;/strong&gt; A pipeline entry names the teams that own it, and validation checks those teams exist in the CI server before accepting the entry. Without this, the ownership field decays into free text — someone types a team name slightly wrong, nothing complains, and eighteen months later nobody can work out who owns a failing build. The check also tells the author exactly how to fix it and lists valid values, because a guardrail that blocks you without telling you what to do next is just an obstacle.&lt;/p&gt;
&lt;p&gt;The rest are conditional-shape rules: a mobile entry must use the mobile team field and not the general grouping field; a package entry must declare a package type. These are the rules that would otherwise live in a wiki page nobody reads.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Validation is where a convention becomes a guarantee. Anything you merely document, you don&apos;t have.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Reconcile, don&apos;t create&lt;/h2&gt;
&lt;p&gt;The naive version of this tooling creates things. The useful version makes reality match a declaration, which is a different job.&lt;/p&gt;
&lt;p&gt;For pipelines, the reconciliation is a set difference. Derive a deterministic identifier for each declared entry, list what actually exists on the server, and subtract: what&apos;s declared but missing gets created, and what exists in both gets synchronized. That&apos;s it — and because the identifier is derived from the declaration rather than stored, there&apos;s no mapping table to drift out of sync.&lt;/p&gt;
&lt;p&gt;For repositories it&apos;s more involved, because there are several independent concerns and each one needs its own answer to &quot;has this drifted?&quot; — team membership, webhooks, branch protection. Each concern gets a drift check before its sync, so the tooling can report &lt;em&gt;what&lt;/em&gt; changed rather than blindly reapplying everything.&lt;/p&gt;
&lt;p&gt;That distinction matters more than it sounds. Blind reapplication is safe only if every write is genuinely idempotent, and it makes your logs useless — every run reports doing everything. Checking first means an ordinary run is quiet and an interesting run is loud.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A drift check is the difference between infrastructure as code and infrastructure as a strongly worded suggestion.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Dry run is a feature, not a flag&lt;/h2&gt;
&lt;p&gt;Every operation supports a dry run, and it&apos;s the default rather than an afterthought.&lt;/p&gt;
&lt;p&gt;This is easy to dismiss as a nicety. It isn&apos;t — it&apos;s the reason anyone lets you run the thing at all. Tooling that reconciles an entire organization&apos;s repositories has a genuinely frightening blast radius: it can close pull requests, remove people from teams, and rewrite branch protection. Nobody should approve that on the promise that the logic is correct.&lt;/p&gt;
&lt;p&gt;With a dry run, the change becomes reviewable before it&apos;s real. You get the same output you&apos;d get from an apply, with every mutation described and none performed. It also makes the tooling &lt;em&gt;testable in production&lt;/em&gt;, which is where the surprising inputs live.&lt;/p&gt;
&lt;p&gt;The property worth designing for is that dry run isn&apos;t a separate code path. If it is, it will diverge from the real one and lie to you. It should be the same path with the writes suppressed at the boundary.&lt;/p&gt;
&lt;h2&gt;Compliance as a report before it&apos;s a gate&lt;/h2&gt;
&lt;p&gt;There&apos;s a strong temptation, once you can check things automatically, to start blocking on them. That&apos;s usually the wrong first move.&lt;/p&gt;
&lt;p&gt;The compliance checks run across every declared repository and answer a fixed set of questions: does it have a license file, does the default branch have protection, does it require at least one reviewer, has its team configuration drifted from the declaration, have its webhooks drifted, does it have unresolved critical dependency alerts, and has anyone committed to it in the last year. The output is a table for humans and JSON for anything else.&lt;/p&gt;
&lt;p&gt;Reporting first does two things. It tells you how bad the situation actually is, which is almost never what you assumed. And it gives teams a chance to fix things before non-compliance starts failing their builds — which is the difference between a platform team that raised the bar and a platform team that broke everyone&apos;s Tuesday.&lt;/p&gt;
&lt;p&gt;Once the report is mostly green, turning individual checks into gates is uncontroversial. Turning them into gates first is how platform tooling gets a reputation.&lt;/p&gt;
&lt;p&gt;The enforcement did arrive, and where it landed is the part I&apos;d repeat: inside the pipelines teams already own, rather than in a central gatekeeper. Policy-as-code for the rules that have to hold organization-wide, template linting for infrastructure definitions, language-specific linters and code-quality checks, and test pipelines that actually gate a merge.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The same check feels like part of your build when it runs in your pipeline, and like an audit when it runs in someone else&apos;s cron job. Identical logic, opposite reception.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Retirement is part of the lifecycle&lt;/h2&gt;
&lt;p&gt;Almost all platform tooling creates things. Very little of it retires them, which is why every organization accumulates repositories nobody has touched in three years and nobody is willing to delete.&lt;/p&gt;
&lt;p&gt;So the same tooling detects staleness — no commits for a configurable window, excluding already-archived repositories — and sorts by how long it&apos;s been, with a recommendation attached based on whether anything is still open against it. A repository with no commits in two years and no open work is a different case from one with no commits and a stack of open pull requests nobody closed.&lt;/p&gt;
&lt;p&gt;And when something is retired, there&apos;s an actual sequence rather than a single archive call: close the open issues, close the open pull requests, remove branch protection, and leave a note in the README explaining what happened and where the work went. Archiving without that leaves a repository that looks abandoned rather than deliberately closed — and the person who finds it in two years can&apos;t tell the difference.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Every tool that creates resources should know how to retire them. Otherwise you&apos;ve automated accumulation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The unglamorous parts that make it usable&lt;/h2&gt;
&lt;p&gt;Three things that no design document ever includes and every real system needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;An audit log.&lt;/strong&gt; Every action records who ran it, what it targeted, what changed, whether it was a dry run, and whether it succeeded — as structured JSON, with the notable actions also posted to a chat channel. When someone asks why a webhook changed last Thursday, you want an answer that isn&apos;t archaeology. Recording the dry-run flag matters too, so a rehearsal is never mistaken for the real thing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rate-limit awareness.&lt;/strong&gt; Anything that walks four figures of repositories through a provider API will hit rate limits. Knowing your remaining budget before starting a long reconciliation is the difference between a clean run and a job that dies two-thirds through having done two-thirds of the work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Account hygiene.&lt;/strong&gt; Enforcing multi-factor authentication and pruning outside collaborators are the least interesting features here and probably the highest-value per line of code. They&apos;re also the checks nobody performs manually, because doing so means reading a members list — which is exactly the sort of task that only ever happens if something scheduled does it.&lt;/p&gt;
&lt;h2&gt;What this actually buys&lt;/h2&gt;
&lt;p&gt;Teams change a few lines of YAML in a pull request and get a working pipeline, and the platform team isn&apos;t in that path at all. That&apos;s the visible win, and it&apos;s the one people ask for.&lt;/p&gt;
&lt;p&gt;The one worth more is that the estate becomes queryable. &quot;Which repositories lack branch protection?&quot; and &quot;who owns this pipeline?&quot; and &quot;what has nobody touched in a year?&quot; go from an afternoon of clicking to a command. You can&apos;t improve a property you can&apos;t measure across the whole estate, and a UI-shaped source of truth means you can never measure anything.&lt;/p&gt;
&lt;p&gt;If I were starting this again, the order I&apos;d build it in is: the registry, then validation, then dry run, then reconciliation, then reporting, and only then any gating. Every step in that order makes the next one safe. Done in the other direction, you get a tool that changes things nobody asked it to change — and once you&apos;ve done that once, you&apos;ll never be allowed to run it again.&lt;/p&gt;
</content:encoded><category>platform-engineering</category><category>developer-productivity</category><category>cicd</category><category>tooling</category></item><item><title>Rebuilding CI/CD Without Changing Platforms</title><link>https://riddam.github.io/engineering/rebuilding-ci-cd-without-changing-platforms/</link><guid isPermaLink="true">https://riddam.github.io/engineering/rebuilding-ci-cd-without-changing-platforms/</guid><description>What it takes to host a self-hosted CI platform properly — two agent tiers, queue-driven scaling, breaking the bootstrap dependency, stopping base-image drift, and making the server disposable.</description><pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When a CI/CD platform becomes painful, the instinct is to shop for a new one. Sometimes that&apos;s right. Often the pain has nothing to do with the product and everything to do with what has accumulated around it: build agents configured by hand years ago, pipelines assembled by clicking through a UI, a server nobody dares upgrade.&lt;/p&gt;
&lt;p&gt;Migrating platforms doesn&apos;t fix any of that. It relocates it, and adds a migration to the bill.&lt;/p&gt;
&lt;p&gt;I&apos;ve written about &lt;a href=&quot;https://riddam.github.io/leadership/leading-without-authority/&quot;&gt;the decision not to migrate&lt;/a&gt; and how you make that case credibly. This is the other half — what the rebuild actually consisted of, and which parts of it I&apos;d do the same way again.&lt;/p&gt;
&lt;p&gt;This post is about hosting the platform: agents, scaling, upgrades, and the things that quietly rot. The pipelines-as-code half of that list — how teams declare a pipeline instead of clicking one together — is &lt;a href=&quot;https://riddam.github.io/engineering/self-service-with-guardrails/&quot;&gt;its own post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For a sense of scale, what this now carries is roughly 1,400 projects and around 2,400 build configurations, with a few dozen more archived. That&apos;s worth holding in mind through the rest of this, because most of the decisions below only start to matter somewhere in the high hundreds.&lt;/p&gt;
&lt;h2&gt;Two agent tiers, and why&lt;/h2&gt;
&lt;p&gt;The instinct is one kind of build agent, uniformly configured. That breaks on the first job that needs Docker inside Docker.&lt;/p&gt;
&lt;p&gt;Nested container workloads need privileges a managed container platform won&apos;t grant you, and shouldn&apos;t. So there are two tiers, and the split is a rule rather than a preference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VM agents&lt;/strong&gt; for anything needing Docker-in-Docker, privileged operations, or a full machine. They cost more and start slower.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container agents&lt;/strong&gt; for everything else, which is the large majority of builds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The container tier runs framework-specific images — one for Python, one for TypeScript, one for .NET — rather than a single image carrying every toolchain any team might want. A fat image is appealing for about a month, until the pull time starts dominating short builds and every team&apos;s dependency upgrade becomes everyone&apos;s problem. Narrow images fail independently, which is the property you want.&lt;/p&gt;
&lt;p&gt;The VM tier is deliberately heterogeneous too: general-purpose x86 for most work, ARM instances where the workload benefits, compute-optimized shapes for the heavy jobs, and Mac metal for mobile builds, because iOS builds leave you no choice.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A single build image is a shared-fate decision disguised as a simplification.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Scale on the queue, not on CPU&lt;/h2&gt;
&lt;p&gt;Build capacity is bursty in a way that defeats ordinary autoscaling. Nothing for two hours, then dozens of jobs land at once because a release branch merged.&lt;/p&gt;
&lt;p&gt;CPU utilization is the wrong signal here — it&apos;s a lagging proxy for demand. The build queue &lt;em&gt;is&lt;/em&gt; the demand signal: it knows exactly how much work is waiting and what kind of agent each job needs. So agents provision in response to queue depth, and scale back down after an idle period.&lt;/p&gt;
&lt;p&gt;Two things follow from this that are worth stating plainly. Idle cost approaches zero, because at three in the morning there are no agents running at all. And the scale-down timer is a real tuning decision, not a default to accept: too aggressive and you pay the cold-start cost on every commit during working hours; too lax and you&apos;re paying for idle agents all afternoon. An hour of idle tolerance turned out to be a reasonable balance, but it&apos;s the kind of number worth revisiting against your own commit patterns rather than inheriting from a blog post.&lt;/p&gt;
&lt;p&gt;Build capacity also runs on spot, and &lt;em&gt;which&lt;/em&gt; workload gets it is the decision that matters. Pull request inspection is where it points: that generates by far the deepest queue, every run is short, and every run is safe to repeat.&lt;/p&gt;
&lt;p&gt;Which makes interruption handling much less interesting than it sounds, and deliberately so. When spot capacity gets reclaimed, the container tier absorbs the work — runners spin up there and the queue drains a little slower rather than stalling. That&apos;s a better answer than clever retry logic, because it requires no coordination and no new moving parts: the fallback is a capacity type that was already running everything else.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The cheapest way to handle a failure mode is to arrange for something else to already be able to do the work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Don&apos;t let the CI system deploy itself&lt;/h2&gt;
&lt;p&gt;This is the piece I&apos;d argue hardest for, and the one I see skipped most often.&lt;/p&gt;
&lt;p&gt;The platform&apos;s infrastructure — the server, the agent fleets, the networking, the roles — is defined as code. The question is what applies that code. The tempting answer is the CI platform itself: it&apos;s right there, teams already know it, and it can reach everything.&lt;/p&gt;
&lt;p&gt;Don&apos;t. If the system that rebuilds your pipeline system &lt;em&gt;is&lt;/em&gt; your pipeline system, you have a circular dependency. It costs you nothing on an ordinary day and everything on the day the server won&apos;t start, which is precisely the day you need to deploy a change to it.&lt;/p&gt;
&lt;p&gt;So that pipeline lives somewhere else entirely — in our case GitHub Actions, deliberately not the platform under management:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A pull request runs a plan&lt;/strong&gt; against every environment and posts the diff. The diff is the review artifact; you approve a described change, not an intention.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Merging applies it&lt;/strong&gt;, environment by environment, with concurrency pinned to one so two environments can never converge on the same state simultaneously.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environments go in order&lt;/strong&gt;, test first. The blast radius of a mistake is a test environment, and you learn about it before production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The test for whether you&apos;ve actually broken the dependency is uncomfortable and worth asking out loud: &lt;em&gt;if the CI server were gone right now, could I rebuild it?&lt;/em&gt; If the answer routes through the CI server, you haven&apos;t broken anything — you&apos;ve just written the circular dependency down.&lt;/p&gt;
&lt;p&gt;There&apos;s a subtlety worth naming. Not everything that touches the platform has to avoid it. A reconciliation job that calls the server&apos;s API to keep configuration current legitimately depends on the server being up, because it has nothing to do when the server is down. Bootstrap and steady-state are different problems, and only bootstrap needs the separation.&lt;/p&gt;
&lt;h2&gt;The base image will drift unless something stops it&lt;/h2&gt;
&lt;p&gt;This is the section I&apos;d most like people to take seriously, because I learned it the expensive way.&lt;/p&gt;
&lt;p&gt;I once estimated two weeks to move a fleet from Amazon Linux 2 to Amazon Linux 2023 and spent five. The operating system migration was never the problem. The problem was that the applications running on it had been quietly held together by an old base image for years — runtimes several versions behind, OpenSSL moving from the 1.0.2 and 1.1.1 era to 3.0, Python 2 gone entirely. I hadn&apos;t estimated a migration. I&apos;d estimated an unmeasured maintenance backlog.&lt;/p&gt;
&lt;p&gt;Drift isn&apos;t an event. It&apos;s the default state of any long-lived image that nothing actively updates. So something has to actively update it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;golden image&lt;/strong&gt; is built by a pipeline and its identifier published to a parameter store, so there is exactly one answer to &quot;what should agents be running?&quot;&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;scheduled reconciler&lt;/strong&gt; reads that parameter, resolves the image and its snapshot, and rewrites the agent fleet configuration to match. Agents replaced after that point come up on the current image.&lt;/li&gt;
&lt;li&gt;The schedule is &lt;strong&gt;staggered across environments&lt;/strong&gt; — test on one day, then the others through the week. A bad image surfaces in test before it reaches anything that matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The staggering is the part that&apos;s easy to skip and shouldn&apos;t be. Refreshing everything at once converts a routine hygiene job into a simultaneous fleet-wide outage the one time the new image is broken.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Nothing about this is clever. It&apos;s a scheduled job that keeps a number in sync with another number. That&apos;s exactly why it works — and why the five-week version of my week-two estimate never has to happen again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Scan the images, and mean it&lt;/h2&gt;
&lt;p&gt;The VM side of the fleet has drift handled. The container side has a different problem: an agent image is a supply chain, and it&apos;s one you&apos;re pulling into a system that holds deployment credentials.&lt;/p&gt;
&lt;p&gt;Container agent images come out of a managed image-build pipeline into a registry with scanning enabled, and they&apos;re exercised in a test phase &lt;em&gt;after&lt;/em&gt; the build rather than trusted on the strength of having built successfully. A green build says the Dockerfile is valid. It says nothing about whether the result works or what it now contains.&lt;/p&gt;
&lt;p&gt;The part that matters is what happens on a critical finding: the image is removed before it is used for any deployment. Not flagged for triage, not added to a backlog with a due date — deleted, under a policy that doesn&apos;t negotiate about it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A scanner whose findings you triage at leisure is a compliance artifact. A scanner that can delete an artifact before it ships is a control.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That distinction is worth being honest about, because scanning is one of the easiest things to install and one of the easiest to render meaningless. Almost everyone has scanning. Far fewer have a rule that stops a flagged image from being used, and the rule is the entire value.&lt;/p&gt;
&lt;h2&gt;Make the server disposable&lt;/h2&gt;
&lt;p&gt;The server was the thing nobody wanted to touch, which is how you end up years behind on versions.&lt;/p&gt;
&lt;p&gt;The fix isn&apos;t making the server more robust. It&apos;s making it disposable, which means getting the state off it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The database&lt;/strong&gt; moves to a managed cluster — more than one instance, encrypted, with a real backup retention window and a defined maintenance window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The data directory&lt;/strong&gt; moves to shared encrypted network storage, so it survives the instance entirely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups&lt;/strong&gt; go to object storage on top of that, because a managed cluster protects you from hardware failure, not from a bad decision.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With state elsewhere, upgrading stops being surgery on a precious box and becomes replacing an instance. It runs in an autoscaling group behind a load balancer, replaced one node at a time, and — the important detail — &lt;strong&gt;the rollout waits on a success signal from the new instance and requires it before proceeding.&lt;/strong&gt; A server that fails to come up blocks its own rollout rather than taking the platform down with it. The group also always tracks the newest launch template version, so an image change propagates on the next replacement instead of needing anyone to remember.&lt;/p&gt;
&lt;p&gt;One honest note, because this gets oversold: that is not a highly available server. It&apos;s a single node, and there&apos;s a window during replacement when it isn&apos;t serving. The availability lives in the state tier — the database cluster and the shared storage — while the compute is deliberately cheap to throw away. For an internal build platform that&apos;s the right trade, and it&apos;s worth being precise about which property you actually bought.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Highly available and disposable are different goals. Disposable is usually the one that makes upgrades boring, and boring upgrades are how you stop drifting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Credentials that expire, scoped by environment&lt;/h2&gt;
&lt;p&gt;The legacy setup&apos;s worst habit was credentials living in pipeline configuration. Two changes fixed the shape of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Credentials are issued, not stored.&lt;/strong&gt; A pipeline declares that it needs cloud access and receives a short-lived, scoped credential at build time. Nothing durable sits in the pipeline definition, so nothing leaks out of it in a screenshot or a config export.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The agent pool is the boundary.&lt;/strong&gt; Each environment has its own agent pool, and the pool a pipeline requests determines which environment&apos;s credentials it can obtain at all. A pipeline that asks for a test agent cannot get production credentials, because the association is structural rather than a naming convention someone has to respect. Environment isolation you can enforce beats environment isolation you have to remember — and it fails closed, which convention never does.&lt;/p&gt;
&lt;p&gt;The direction to keep heading is federated identity, where the CI system proves who it is to the cloud provider and no long-lived key exists anywhere in the chain. Any credential with no expiry is a credential you will eventually find in a place you didn&apos;t put it.&lt;/p&gt;
&lt;h2&gt;What&apos;s still unfinished&lt;/h2&gt;
&lt;p&gt;One honest edge, and it&apos;s the one with our name on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Image builds aren&apos;t self-service yet.&lt;/strong&gt; Agent images are built centrally, so a team wanting a new toolchain files a request and waits for us. The scanning and deletion policy above is exactly the sort of guardrail that makes handing that over safe — teams build their own images against a policy rather than asking permission, which is the pattern that already works for &lt;a href=&quot;https://riddam.github.io/engineering/self-service-with-guardrails/&quot;&gt;pipelines and repositories&lt;/a&gt;. The guardrail exists. The self-service hasn&apos;t been built on top of it yet.&lt;/p&gt;
&lt;h2&gt;What I&apos;d keep&lt;/h2&gt;
&lt;p&gt;If I were doing this again on a different platform, four things would come along unchanged: &lt;strong&gt;two agent tiers with a hard rule about which is which&lt;/strong&gt;, &lt;strong&gt;scaling on queue depth rather than a proxy metric&lt;/strong&gt;, &lt;strong&gt;the platform&apos;s own deployment living outside the platform&lt;/strong&gt;, and &lt;strong&gt;something scheduled that keeps the base image current whether or not anyone is paying attention.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;None of that is specific to a product. All of it is what the legacy setup was missing — which is why replacing the product would have solved nothing.&lt;/p&gt;
</content:encoded><category>cicd</category><category>platform-engineering</category><category>infrastructure-as-code</category><category>aws-cdk</category></item><item><title>Safe Rollouts for Stateful Cloud Infrastructure</title><link>https://riddam.github.io/engineering/safe-rollouts-for-stateful-cloud-infrastructure/</link><guid isPermaLink="true">https://riddam.github.io/engineering/safe-rollouts-for-stateful-cloud-infrastructure/</guid><description>A production-safe rollout checklist for infrastructure that cannot be recreated quickly, including retention defaults, staged validation, and manual approval points.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There is a particular flavor of dread that only comes from stateful infrastructure. With a stateless service, a bad deploy is a shrug — roll back, redeploy, move on. With a database cluster that took hours to provision and holds the only copy of something important, a bad change is not an inconvenience. It is an incident with a recovery time measured in hours and a blast radius measured in trust.&lt;/p&gt;
&lt;p&gt;So the discipline changes. For stateless systems, rollout speed is a feature. For stateful systems, &lt;em&gt;recoverability&lt;/em&gt; is the feature, and speed is something you trade away on purpose. This post is the rollout chapter of a series that starts with &lt;a href=&quot;https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/&quot;&gt;config-as-data&lt;/a&gt; and &lt;a href=&quot;https://riddam.github.io/engineering/path-filtered-ci-cd-for-infra-monorepos/&quot;&gt;path-filtered CI/CD&lt;/a&gt; — here I&apos;m assuming you have those, and focusing on the principles that keep a stateful change from becoming a story people tell for years.&lt;/p&gt;
&lt;h2&gt;Principle 1: Optimize for Recoverability, Not Speed&lt;/h2&gt;
&lt;p&gt;For anything stateful, the defaults should lean toward &quot;hard to destroy by accident.&quot; Concretely, that means retention policies and termination protection baked into the code, not left to a runbook:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# CDK — retain data-bearing resources even if removed from the stack
database.apply_removal_policy(RemovalPolicy.RETAIN)

# Pulumi — protect a critical resource from direct deletion
Database(&quot;primary&quot;, ..., opts=ResourceOptions(protect=True))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If recreation takes hours or days, &quot;just redeploy&quot; is not a recovery strategy — it is the incident. Make the safe default the &lt;em&gt;lazy&lt;/em&gt; default, so an engineer under pressure has to go out of their way to do the dangerous thing.&lt;/p&gt;
&lt;h2&gt;Principle 2: Separate Validation from Deployment&lt;/h2&gt;
&lt;p&gt;Every change earns its way to production through two distinct phases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Render/preview&lt;/strong&gt; in CI, for each target environment — the machine proves the change is valid.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment&lt;/strong&gt;, executed only by pipeline automation after approval — never an ad hoc local &lt;code&gt;deploy&lt;/code&gt;/&lt;code&gt;up&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;a href=&quot;https://riddam.github.io/engineering/path-filtered-ci-cd-for-infra-monorepos/&quot;&gt;path-filtered pipeline&lt;/a&gt; is what makes phase one cheap and phase two auditable.&lt;/p&gt;
&lt;h2&gt;Principle 3: Use Environment Promotion Discipline&lt;/h2&gt;
&lt;p&gt;Promote through environments in order, never in parallel:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;development / testing&lt;/li&gt;
&lt;li&gt;staging / acceptance&lt;/li&gt;
&lt;li&gt;production&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each stage catches a different class of problem: testing surfaces logic and config errors, staging surfaces integration and data-shape surprises, and by production you want no surprises left. If you run more than one production footprint for resilience (see Principle 6), roll out to them one at a time — never to both at once.&lt;/p&gt;
&lt;h2&gt;Principle 4: Treat Config Changes as High Signal&lt;/h2&gt;
&lt;p&gt;When infrastructure is config-driven, the config diff &lt;em&gt;is&lt;/em&gt; the change. That is a gift to reviewers — but only if they know to read it that way. Require a reviewer to check, on every diff:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;capacity and sizing fields&lt;/li&gt;
&lt;li&gt;network allocations&lt;/li&gt;
&lt;li&gt;identity and sharing principals&lt;/li&gt;
&lt;li&gt;feature-enablement flags&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A one-line config change can trigger a resource replacement. The &lt;a href=&quot;https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/&quot;&gt;config-as-data post&lt;/a&gt; is about making these diffs legible; this principle is about making sure someone actually reads them.&lt;/p&gt;
&lt;h2&gt;Principle 5: Enforce Manual Gates for High-Risk Changes&lt;/h2&gt;
&lt;p&gt;Manual approval earns its keep when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;replacement of a stateful resource is on the table&lt;/li&gt;
&lt;li&gt;a maintenance window is required&lt;/li&gt;
&lt;li&gt;cross-team coordination is necessary&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automated quality gates and manual production gates are not in tension — the machine proves &lt;em&gt;valid&lt;/em&gt;, the human decides &lt;em&gt;safe to release now&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Principle 6: Decide Your Availability Target Before You Design the Rollout&lt;/h2&gt;
&lt;p&gt;For production, the biggest rollout decision is not &lt;em&gt;how&lt;/em&gt; you deploy — it is &lt;em&gt;how many production footprints you keep alive&lt;/em&gt;. That is really a disaster recovery (DR) decision, and it drives both your resilience and your bill.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The ideal setup: active-active across two failure domains.&lt;/strong&gt; Run two independent production stacks in separate regions or availability zones, both serving traffic, both kept in lockstep. If one fails — an AZ outage, a bad regional change, a corrupted stack — the other already carries the load. You roll out to one footprint at a time, so a single bad release can never take down both. For a stateful platform that cannot tolerate downtime or data loss, this is the gold standard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The trade-off: it roughly doubles your infrastructure cost&lt;/strong&gt; and adds real operational weight. Data has to stay in sync, routing has to fail over cleanly (the &lt;a href=&quot;https://riddam.github.io/engineering/network-connectivity-for-managed-database-platforms/&quot;&gt;connectivity post&lt;/a&gt; covers the redundant-path side of this), and every change now runs twice. For an expensive-to-provision platform — a large managed database engine, for example — the second footprint is often the single biggest line item on the bill.&lt;/p&gt;
&lt;p&gt;So the honest choice is rarely &quot;do the ideal thing.&quot; It is &quot;how far down from the ideal can we responsibly go?&quot; The options I actually weigh:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Availability need&lt;/th&gt;
&lt;th&gt;Realistic setup&lt;/th&gt;
&lt;th&gt;Cost vs. single stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No downtime, no data loss&lt;/td&gt;
&lt;td&gt;Active-active, staged one side at a time&lt;/td&gt;
&lt;td&gt;~2x, highest complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short recovery time acceptable&lt;/td&gt;
&lt;td&gt;Active-passive: warm standby, promote on failure&lt;/td&gt;
&lt;td&gt;~1.3–1.6x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rebuild window acceptable&lt;/td&gt;
&lt;td&gt;Single production + rehearsed backup/restore&lt;/td&gt;
&lt;td&gt;~1x, cheapest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;My rule of thumb: design the ideal on paper, then deliberately spend down from it based on what an outage on &lt;em&gt;this specific system&lt;/em&gt; would actually cost the business. Sometimes active-active is obviously worth it. Just as often, active-passive with a &lt;strong&gt;rehearsed&lt;/strong&gt; failover is the better return. The worst outcome is paying for a second footprint you have never actually failed over to — that is cost without the reliability you bought it for.&lt;/p&gt;
&lt;h2&gt;Principle 7: Rotate the Credentials You Depend On&lt;/h2&gt;
&lt;p&gt;A stateful platform you cannot afford to lose is also one whose credentials you cannot afford to leak or let go stale. Two habits keep this boring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Store secrets in a secrets manager — never in state or config.&lt;/strong&gt; IaC state files are the classic accidental leak; keep database credentials out of them entirely (see &lt;a href=&quot;https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/&quot;&gt;where state and secrets live&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotate on a schedule, and rehearse it.&lt;/strong&gt; Rotation that has never been exercised is just another failover you are &lt;em&gt;assuming&lt;/em&gt; works. Automate it, and run it through the same testing → staging → production promotion as any other change, so a rotation can never surprise production.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;A Lightweight Risk Matrix&lt;/h2&gt;
&lt;p&gt;The point of the principles above is to make decisions consistent &lt;em&gt;under pressure&lt;/em&gt;, when judgment is worst. A small matrix in the pull request template does most of that work:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change Type&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Approval Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tag-only metadata updates&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Standard reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route and IAM changes&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Senior reviewer + preview diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateful sizing/replacement changes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Senior reviewer + manual release gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;It turns &quot;is this scary?&quot; from a gut call into a lookup — which is exactly what you want at 4pm on a Friday.&lt;/p&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;p&gt;Authoritative references on the DR strategies and retention defaults above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-workloads-on-aws.html&quot;&gt;AWS — Disaster Recovery of Workloads on AWS&lt;/a&gt; — the canonical spectrum from backup/restore through pilot light, warm standby, and multi-site active-active, framed around RTO/RPO. AWS&apos;s four-strategy vocabulary maps directly onto the trade-off table above.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/welcome.html&quot;&gt;AWS Well-Architected — Reliability Pillar&lt;/a&gt; — change management and failure-recovery best practices.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html&quot;&gt;AWS CDK — Best practices&lt;/a&gt; — &quot;Define removal policies and log retention&quot; and keeping stateful resources in their own protected stack.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.pulumi.com/docs/concepts/options/protect/&quot;&gt;Pulumi — the &lt;code&gt;protect&lt;/code&gt; resource option&lt;/a&gt; — preventing accidental deletion of critical resources.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Final Takeaway&lt;/h2&gt;
&lt;p&gt;For stateful cloud infrastructure, safety comes from process design, not heroics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;conservative defaults that make the safe path the easy path&lt;/li&gt;
&lt;li&gt;strict preview validation, separated from deployment&lt;/li&gt;
&lt;li&gt;phased promotion, one footprint at a time&lt;/li&gt;
&lt;li&gt;explicit production approvals matched to blast radius&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Get the process right and incidents become rare and boring. That scales far better than being brilliant during the ones you could have prevented.&lt;/p&gt;
</content:encoded><category>reliability</category><category>devops</category><category>infrastructure-as-code</category><category>cloud</category></item><item><title>Network Connectivity Patterns for Managed Database Platforms</title><link>https://riddam.github.io/engineering/network-connectivity-for-managed-database-platforms/</link><guid isPermaLink="true">https://riddam.github.io/engineering/network-connectivity-for-managed-database-platforms/</guid><description>How to choose between transit routing and direct peering for managed database access, with practical decision criteria and routing guardrails.</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The hardest part of putting a managed database platform into a large organization is almost never the database. It is answering one deceptively simple question: &lt;em&gt;how does everything that needs the database actually reach it?&lt;/em&gt; A dozen application teams, a couple of legacy accounts, an on-prem network still running the systems of record — each one needs a path in, and each path is a decision about ownership, latency, blast radius, and cost.&lt;/p&gt;
&lt;p&gt;I have watched that question get answered badly in both directions. Route everything through one central hub and you get a tidy diagram and a single point of failure that no one team feels responsible for. Let every team build its own path and you get flexibility plus a slow drift into a routing mess nobody can reason about. The useful framing sits in between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transit path&lt;/strong&gt; — for centralized ingress/egress, on-prem, and shared inspection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct peering&lt;/strong&gt; — for low-friction, east-west application access.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post is the connectivity chapter of a series on running stateful platforms safely; it leans on the same &lt;a href=&quot;https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/&quot;&gt;config-as-data discipline&lt;/a&gt; for CIDRs and shares its DR thinking with &lt;a href=&quot;https://riddam.github.io/engineering/safe-rollouts-for-stateful-cloud-infrastructure/&quot;&gt;safe rollouts&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Two Connectivity Models&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  subgraph Transit[Model A: Transit-Centric]
    T1[Consumer A] --&amp;gt; H((Transit Hub))
    T2[Consumer B] --&amp;gt; H
    OP[On-prem / VPN] --&amp;gt; H
    H --&amp;gt; DBa[(Database network)]
  end
  subgraph Direct[Model B: Direct Peering]
    C1[Consumer A] --&amp;gt; DBb[(Database network)]
    C2[Consumer B] --&amp;gt; DBb
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Model A: Transit-Centric&lt;/h3&gt;
&lt;p&gt;All traffic passes through a transit hub.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; centralized routing controls, a natural place for shared inspection, easy global policy enforcement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; more route dependencies, a larger blast radius when the hub changes, and often a longer path for what should be a short app-to-database hop.&lt;/p&gt;
&lt;h3&gt;Model B: Direct Peering by Consumer Domain&lt;/h3&gt;
&lt;p&gt;Each consumer domain owns a direct peering to the database network.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; clear ownership boundaries, simpler troubleshooting (one path, two ends), no central bottleneck.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; more peerings to govern, and route hygiene now lives in each domain rather than one place.&lt;/p&gt;
&lt;h2&gt;Decision Criteria&lt;/h2&gt;
&lt;p&gt;Choose &lt;strong&gt;direct peering&lt;/strong&gt; when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;consumer domains are mature enough to own their own routes&lt;/li&gt;
&lt;li&gt;low-latency application flows are the primary use case&lt;/li&gt;
&lt;li&gt;decentralized ownership is a feature, not a risk&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose &lt;strong&gt;transit-centric&lt;/strong&gt; when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;centralized traffic inspection is mandatory&lt;/li&gt;
&lt;li&gt;consumer domains cannot safely manage network routes&lt;/li&gt;
&lt;li&gt;a hub-and-spoke operating model already exists and works&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice most mature setups end up hybrid, and deliberately so: direct peering carries the high-volume east-west application traffic, while the transit path is reserved for on-prem and shared ingress — the flows that genuinely benefit from a central chokepoint. The mistake is drifting into a hybrid by accident, with no clear rule for which flow belongs where.&lt;/p&gt;
&lt;h2&gt;A Migration That Paid for Itself&lt;/h2&gt;
&lt;p&gt;My own route to that hybrid was not a clean design — it was a correction. The first version I built routed &lt;em&gt;everything&lt;/em&gt; through a central transit hub, because a hub is the easy thing to reason about: one place for routes, one place for policy, one diagram. It worked. It was also needlessly expensive.&lt;/p&gt;
&lt;p&gt;A transit hub typically bills two ways: an hourly charge per attachment &lt;em&gt;and&lt;/em&gt; a per-gigabyte data-processing charge on everything that flows through it. All that east-west application traffic to the database was paying a toll on a path that added nothing for it — those flows never needed central inspection, they just needed to reach the database. Moving the consumer traffic onto direct peerings, and leaving only on-prem and shared ingress on the hub, took a recurring line item down substantially with no loss of function.&lt;/p&gt;
&lt;p&gt;The lesson I took: a transit hub is a great home for the traffic that benefits from centralization and a quietly expensive one for the traffic that doesn&apos;t. Route by need, not by habit — and revisit the bill once real traffic is flowing, because the cheapest topology on day one is rarely the cheapest at scale.&lt;/p&gt;
&lt;h2&gt;The Ideal Path vs. the One You Can Afford&lt;/h2&gt;
&lt;p&gt;The ideal connectivity design is fully redundant: every consumer reaches the database over at least two independent paths, in two failure domains, so no single peering, route table, or region can cut access. Pair that with an active-active database footprint and you have a platform that shrugs off most infrastructure failures.&lt;/p&gt;
&lt;p&gt;That resilience is not free. Every extra path is another peering to create, another set of routes to keep clean, and — for active-active — a duplicate of the most expensive resource you run. The trade-offs I weigh in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redundant paths everywhere&lt;/strong&gt; — highest resilience, but the most route-governance overhead and cost. Justified only when downtime on this flow is genuinely unacceptable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single primary path plus a tested failover&lt;/strong&gt; — where most projects should live. You accept a short, rehearsed recovery step in exchange for far less standing cost and complexity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single path, no failover&lt;/strong&gt; — fine for environments you can afford to lose for a while (testing, internal tooling), reckless for anything else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My rule of thumb mirrors how I think about DR generally: design the ideal on paper, then deliberately spend down from it based on what an outage on &lt;em&gt;this specific flow&lt;/em&gt; would actually cost. A redundant path you never fail over to is just complexity with a bill attached. (I develop the same idea, applied to the database itself, in the &lt;a href=&quot;https://riddam.github.io/engineering/safe-rollouts-for-stateful-cloud-infrastructure/&quot;&gt;safe rollouts post&lt;/a&gt;.)&lt;/p&gt;
&lt;h2&gt;Routing Guardrails That Prevent Incidents&lt;/h2&gt;
&lt;p&gt;Whichever model you pick, the failure mode is the same — ambiguity about who owns which route. Most outages I have seen came from that, not from a missing cloud feature. The guardrails that hold it back:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Declare route ownership per domain, explicitly.&lt;/li&gt;
&lt;li&gt;Document the allowed prefixes in &lt;em&gt;both&lt;/em&gt; directions.&lt;/li&gt;
&lt;li&gt;Enforce non-overlapping CIDR allocations (validate them as data — see the &lt;a href=&quot;https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/&quot;&gt;config-as-data guardrails&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Test bidirectional reachability &lt;em&gt;before&lt;/em&gt; cutover, not after.&lt;/li&gt;
&lt;li&gt;Keep DNS forwarding design explicit and versioned, not tribal knowledge.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A minimal connectivity matrix, kept in the repo, is worth more than any diagram:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;app-domain-a&lt;/td&gt;
&lt;td&gt;database-net&lt;/td&gt;
&lt;td&gt;direct peering&lt;/td&gt;
&lt;td&gt;app-domain-a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;on-prem&lt;/td&gt;
&lt;td&gt;database-net&lt;/td&gt;
&lt;td&gt;transit hub&lt;/td&gt;
&lt;td&gt;network team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;The Prerequisite That Blocks Everything&lt;/h2&gt;
&lt;p&gt;Before any of this works, the managed-database integration has to be &lt;em&gt;enabled in every account that will consume it&lt;/em&gt;. It is easy to switch it on in the account that owns the database, prove connectivity there, and then watch a consumer account fail weeks later with an opaque error — because the integration was never enabled on that side. Make &quot;enable the integration in this account&quot; an explicit, checked step in account bootstrap, not tribal knowledge. When you run many accounts it belongs in the same &lt;a href=&quot;https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/&quot;&gt;config-as-data inventory&lt;/a&gt; that already tracks your CIDRs and route ownership.&lt;/p&gt;
&lt;h2&gt;A Two-Phase Rollout That Avoids the Route Race&lt;/h2&gt;
&lt;p&gt;The subtle trap with peering is ordering — and it gets worse when the database platform&apos;s control plane lives outside the cloud you are deploying from. Creating the peering is not instant: the request has to reconcile across that provider boundary, and until it reports &lt;em&gt;active&lt;/em&gt; on both sides, any route that references it will fail. A change that creates the peering and writes the routes in a single shot will race that propagation and leave a network half-wired. Split it into two phases with a real barrier between:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Phase 1  reserve CIDRs, validate no overlaps
         create the connectivity primitive (peering / attachment)
         --- confirm the primitive is ACTIVE ---
Phase 2  apply routes in the producer network
         apply routes in each consumer network
         validate DNS + the connectivity matrix
         enable application traffic gradually
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make that barrier a poll, not a guessed sleep: wait until the peering actually reports &lt;em&gt;active&lt;/em&gt;, then apply the routes. Treating peering creation and route updates as &lt;em&gt;separate&lt;/em&gt; approvals also keeps the blast radius small — a bad route change can be rolled back without tearing down the peering underneath it.&lt;/p&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;p&gt;Official documentation for the primitives behind these patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/vpc/latest/tgw/what-is-transit-gateway.html&quot;&gt;AWS — What is a transit gateway?&lt;/a&gt; — route tables, associations, and propagation for the transit model.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html&quot;&gt;AWS — What is VPC peering?&lt;/a&gt; — direct, non-transitive connectivity between networks.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/welcome.html&quot;&gt;AWS Well-Architected — Reliability Pillar&lt;/a&gt; — designing for redundant paths and failure recovery.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Final Takeaway&lt;/h2&gt;
&lt;p&gt;Connectivity architecture is mostly an ownership problem expressed through routes. The cloud gives you transit gateways and peerings; whether they add up to a reliable platform or a fragile one comes down to explicit boundaries, validated CIDRs, and disciplined rollout ordering — not which primitive you picked.&lt;/p&gt;
</content:encoded><category>networking</category><category>cloud-architecture</category><category>database</category><category>platform-engineering</category></item><item><title>Multi-Cloud, Done Deliberately: Strategy, and Choosing Between Terraform and Pulumi</title><link>https://riddam.github.io/engineering/multi-cloud-terraform-vs-pulumi/</link><guid isPermaLink="true">https://riddam.github.io/engineering/multi-cloud-terraform-vs-pulumi/</guid><description>When multi-cloud is worth it (and when it isn&apos;t), and how to pick the tool that automates it — a practitioner&apos;s take on Terraform, OpenTofu, and Pulumi in 2026.</description><pubDate>Fri, 17 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most &quot;multi-cloud strategies&quot; I&apos;ve seen weren&apos;t strategies at all. They were the accumulated residue of an acquisition, a team that liked BigQuery, another that lived in AWS, and a CFO who read an article about vendor lock-in. Multi-cloud arrived; nobody chose it.&lt;/p&gt;
&lt;p&gt;That&apos;s the honest starting point. Multi-cloud can be a genuine advantage or a self-inflicted tax, and the difference is whether you did it &lt;em&gt;deliberately&lt;/em&gt; — with a reason, a boundary, and the automation to keep it from sprawling. This post is about both halves: deciding whether (and how) to go multi-cloud, and then picking the infrastructure-as-code tool that makes it survivable. Since the IaC landscape shifted hard over the last two years, I&apos;ll be specific about where things actually stand in 2026.&lt;/p&gt;
&lt;h2&gt;Part 1 — Is multi-cloud even the right call?&lt;/h2&gt;
&lt;p&gt;Start by being ruthless about &lt;em&gt;why&lt;/em&gt;. There are good reasons and expensive-cargo-cult reasons.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reasons that hold up:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best-of-breed by workload.&lt;/strong&gt; BigQuery for analytics, a specific AWS service for something else, Azure because your enterprise agreements and AD already live there. You&apos;re picking the best tool per job, not spreading one app across clouds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data residency and regulation.&lt;/strong&gt; You genuinely need workloads in a region or jurisdiction where only one provider is a good fit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilience against provider-level failure.&lt;/strong&gt; Real, but rarer and more expensive than people think — true active-active across clouds is a serious engineering commitment, not a checkbox.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mergers and acquisitions.&lt;/strong&gt; You inherited another cloud and consolidation isn&apos;t worth it yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Negotiating leverage / avoiding lock-in.&lt;/strong&gt; Legitimate, but it&apos;s a business hedge, not an architecture — and it has a running cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Reasons that usually don&apos;t survive scrutiny:&lt;/strong&gt; &quot;in case we ever want to switch,&quot; and &quot;so we&apos;re not locked in&quot; — when nobody has costed what that portability actually requires.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The uncomfortable truth:&lt;/strong&gt; the more portable you force your architecture to be, the more you give up the managed services that made each cloud worth using. A lowest-common-denominator design that runs anywhere often runs &lt;em&gt;worse everywhere&lt;/em&gt;. Portability is a real cost you pay in capability — spend it on purpose.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The patterns, honestly&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;When it&apos;s worth it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Portfolio / siloed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Different apps live on different clouds; each is single-cloud internally&lt;/td&gt;
&lt;td&gt;The common, sane default — best-of-breed without cross-cloud complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best-of-breed composition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One system spans clouds to use a specific service (e.g. app on one, analytics on another)&lt;/td&gt;
&lt;td&gt;When a specific managed service is genuinely differentiated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Portable / abstracted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workloads built to run on any cloud (often on Kubernetes)&lt;/td&gt;
&lt;td&gt;Strong regulatory or exit requirements — and you&apos;ve accepted the capability tax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Active-active DR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same workload live across clouds for provider-level resilience&lt;/td&gt;
&lt;td&gt;Rarely; only when an outage cost genuinely exceeds the (large) engineering cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;What this looks like in practice&lt;/h3&gt;
&lt;p&gt;A deliberate best-of-breed split might look like this — illustrative, not a prescription:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Cloud&lt;/th&gt;
&lt;th&gt;Why there&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BigQuery and the data stack around it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Azure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;where the enterprise directory already lives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application tier&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AWS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the broadest service catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary database&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;a dedicated database cloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;placed where the engine runs best — kept separate from the app tier on purpose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Several clouds, but not chaos: each workload is placed where it runs best, and each stays coherent inside its own provider. That&apos;s the &quot;best-of-breed&quot; pattern working as intended — multi-cloud by deliberate selection, not by accident. The cost is that you now need &lt;em&gt;one&lt;/em&gt; consistent way to provision and observe all of them, which is the whole reason the IaC choice below matters so much.&lt;/p&gt;
&lt;p&gt;My default advice: &lt;strong&gt;aim for &quot;portfolio&quot; and reach for the heavier patterns only when a concrete requirement forces you there.&lt;/strong&gt; Whatever you pick, the thing that keeps multi-cloud from rotting into chaos is a single, consistent way to define and change infrastructure — which is where IaC earns its keep.&lt;/p&gt;
&lt;h2&gt;Part 2 — Why IaC matters more in multi-cloud&lt;/h2&gt;
&lt;p&gt;In a single cloud you can &lt;em&gt;almost&lt;/em&gt; get away with clicking around the console. Across clouds you can&apos;t: you now have two or three sets of primitives, IAM models, and networking quirks, and no human holds all of it. Infrastructure-as-code gives you one control plane, reviewable diffs, reproducible environments, and drift detection across every provider. In multi-cloud, IaC isn&apos;t a nice-to-have; it&apos;s the only thing standing between you and untracked, un-auditable sprawl.&lt;/p&gt;
&lt;p&gt;The 2026 question isn&apos;t &lt;em&gt;whether&lt;/em&gt; to use IaC — it&apos;s &lt;em&gt;which&lt;/em&gt; tool. The realistic contenders are &lt;strong&gt;Terraform&lt;/strong&gt;, its fork &lt;strong&gt;OpenTofu&lt;/strong&gt;, and &lt;strong&gt;Pulumi&lt;/strong&gt;. (Cloud-specific tools like AWS CDK or Bicep are excellent but single-cloud by design, so they&apos;re out for a genuinely multi-cloud estate.)&lt;/p&gt;
&lt;h2&gt;Part 3 — Terraform and OpenTofu: the fork you need to understand&lt;/h2&gt;
&lt;p&gt;You can&apos;t choose Terraform in 2026 without understanding what happened to it.&lt;/p&gt;
&lt;p&gt;In August 2023, HashiCorp relicensed Terraform from the open-source MPL to the &lt;strong&gt;Business Source License (BUSL)&lt;/strong&gt; — still source-available, but no longer OSI-approved, and it restricts building competing products. The community forked the last MPL version into &lt;strong&gt;OpenTofu&lt;/strong&gt;, now a Linux Foundation / CNCF project. Then in April 2024 &lt;strong&gt;IBM announced it would acquire HashiCorp&lt;/strong&gt;, a deal that &lt;strong&gt;closed on February 27, 2025&lt;/strong&gt; (~$6.4B). The license did not revert after the acquisition — Terraform remains BUSL.&lt;/p&gt;
&lt;p&gt;Two years on, the fork has genuinely &lt;strong&gt;diverged&lt;/strong&gt; — this is the part people miss:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Terraform (BUSL, IBM/HashiCorp)&lt;/th&gt;
&lt;th&gt;OpenTofu (MPL-2.0, CNCF)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Business Source License — source-available, restricted&lt;/td&gt;
&lt;td&gt;MPL-2.0, fully open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leads on&lt;/td&gt;
&lt;td&gt;Enterprise platform: HCP Terraform, Stacks, Sentinel policy, the provider registry, Infragraph&lt;/td&gt;
&lt;td&gt;Open-source CLI features: &lt;strong&gt;native state encryption&lt;/strong&gt;, provider &lt;code&gt;for_each&lt;/code&gt;, &lt;code&gt;-exclude&lt;/code&gt;, early variable evaluation, OCI registries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Vendor-controlled&lt;/td&gt;
&lt;td&gt;Community / foundation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best when&lt;/td&gt;
&lt;td&gt;You want the managed enterprise platform and don&apos;t mind the license&lt;/td&gt;
&lt;td&gt;You want open governance, no license risk, and state encryption in the CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both still speak &lt;strong&gt;HCL&lt;/strong&gt;, share the state format, and have the deepest provider ecosystem in the industry. Day to day they look nearly identical — but they are no longer the same tool, and &quot;which Terraform&quot; is now a real decision. For a licensing- or compliance-sensitive enterprise, OpenTofu is often the safer default; for teams already invested in HCP Terraform&apos;s platform features, Terraform proper still leads.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you&apos;re starting fresh on HCL in 2026,&lt;/strong&gt; I&apos;d default to OpenTofu unless you specifically need HashiCorp&apos;s commercial platform — you get an open license, state encryption out of the box, and an easy path, without betting on a vendor-controlled license.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Part 4 — Pulumi: infrastructure in a language you already know&lt;/h2&gt;
&lt;p&gt;Pulumi takes the other road. Instead of a DSL, you define infrastructure in &lt;strong&gt;general-purpose languages&lt;/strong&gt; — TypeScript, Python, Go, C#, Java, or YAML — against the same underlying cloud providers (GCP, Azure, AWS, and others), so it genuinely spans a multi-cloud estate. It&apos;s &lt;strong&gt;Apache-2.0 open source&lt;/strong&gt;. State can live in &lt;strong&gt;Pulumi Cloud&lt;/strong&gt; (managed, versioned, locked, with per-value secret encryption), or in a &lt;strong&gt;self-managed backend&lt;/strong&gt; — for example an object-storage bucket for state, with a cloud secrets manager holding the secrets. You&apos;re not forced onto a hosted service to use it.&lt;/p&gt;
&lt;p&gt;What that buys you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Real language constructs&lt;/strong&gt; — loops, conditionals, functions, classes, types — without HCL&apos;s expression gymnastics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lower onboarding cost.&lt;/strong&gt; Engineers who already write Python or TypeScript can read, understand, and contribute to infrastructure without first learning a separate DSL. In a multi-cloud estate that&apos;s a genuine force-multiplier: the app and data engineers who need to touch infra can &lt;em&gt;understand the setup&lt;/em&gt; because it&apos;s written in a language they already think in, so onboarding someone onto the infra is a matter of hours, not weeks spent learning HCL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testability&lt;/strong&gt; — you can unit-test infrastructure with the frameworks you already use (pytest, Jest, Go&apos;s testing), mock provider calls, and run integration tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedding IaC in software&lt;/strong&gt; — the &lt;strong&gt;Automation API&lt;/strong&gt; lets you drive Pulumi programmatically, which is why it&apos;s a strong fit for building an internal developer platform.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secrets and config&lt;/strong&gt; — &lt;strong&gt;Pulumi ESC&lt;/strong&gt; (Environments, Secrets, Configuration), GA since 2024, centralizes secrets across AWS Secrets Manager, Vault, Azure Key Vault, GCP Secret Manager, and more, with logged access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A hedge on the HCL question&lt;/strong&gt; — as of January 2026 Pulumi added &lt;strong&gt;native HCL support via a Terraform bridge&lt;/strong&gt;, so it can interpret existing HCL. That lowers the switching cost if you&apos;re coming from Terraform.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trade-off: general-purpose languages give you power &lt;em&gt;and&lt;/em&gt; enough rope to build genuinely over-engineered infrastructure. HCL&apos;s constraints are sometimes a feature — they keep configuration boring and legible. Pulumi shines when your team is application-developer-heavy or you&apos;re building a platform; it can feel like overkill for a small, stable estate a couple of HCL modules would cover.&lt;/p&gt;
&lt;h2&gt;Part 5 — How I&apos;d choose&lt;/h2&gt;
&lt;p&gt;There&apos;s no universal winner. The decision comes down to your team, your license tolerance, and whether infrastructure is a &lt;em&gt;product&lt;/em&gt; you build on or a &lt;em&gt;config&lt;/em&gt; you maintain.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
  A[&quot;Choosing an IaC tool for multi-cloud&quot;] --&amp;gt; B{&quot;Team writes app code&amp;lt;br/&amp;gt;daily and wants tests,&amp;lt;br/&amp;gt;loops, an IDP?&quot;}
  B --&amp;gt;|&quot;Yes&quot;| C[&quot;Pulumi&amp;lt;br/&amp;gt;real languages, testable,&amp;lt;br/&amp;gt;Automation API + ESC&quot;]
  B --&amp;gt;|&quot;No — infra config,&amp;lt;br/&amp;gt;HCL is fine&quot;| D{&quot;Need HashiCorp&apos;s&amp;lt;br/&amp;gt;enterprise platform?&amp;lt;br/&amp;gt;(HCP, Stacks, Sentinel)&quot;}
  D --&amp;gt;|&quot;Yes&quot;| E[&quot;Terraform (BUSL)&quot;]
  D --&amp;gt;|&quot;No — want open license&amp;lt;br/&amp;gt;+ state encryption&quot;| F[&quot;OpenTofu (MPL-2.0)&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A head-to-head, condensed:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Terraform / OpenTofu&lt;/th&gt;
&lt;th&gt;Pulumi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;HCL (declarative DSL)&lt;/td&gt;
&lt;td&gt;TypeScript / Python / Go / C# / Java / YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;BUSL (Terraform) · MPL-2.0 (OpenTofu)&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider ecosystem&lt;/td&gt;
&lt;td&gt;Largest in the industry&lt;/td&gt;
&lt;td&gt;Broad (bridges Terraform providers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Limited (plan checks, terratest)&lt;/td&gt;
&lt;td&gt;Native unit/integration testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;Vault / cloud stores; OpenTofu adds state encryption&lt;/td&gt;
&lt;td&gt;ESC + per-value state encryption by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Ops/platform teams standardizing on config&lt;/td&gt;
&lt;td&gt;App-dev teams, platform engineering, IDPs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where I land:&lt;/strong&gt; for a multi-cloud estate whose team lives in application code, I reach for &lt;strong&gt;Pulumi in a general-purpose language&lt;/strong&gt; (Python or TypeScript), with a &lt;strong&gt;self-managed state backend&lt;/strong&gt; and a &lt;strong&gt;dedicated secrets manager&lt;/strong&gt;. Real languages let the data and application engineers contribute to infrastructure in the languages they already use — onboarding a developer onto the infra takes hours rather than weeks of learning a DSL, and they can genuinely &lt;em&gt;understand&lt;/em&gt; how the setup works because it&apos;s written in a language they know. One tool spans every provider without a separate dialect per cloud to context-switch between. It&apos;s not the only right answer — but for a team that lives in code, it&apos;s the one that tends to stick.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Part 6 — The part that matters more than the tool&lt;/h2&gt;
&lt;p&gt;Here&apos;s what I&apos;d tell anyone going multi-cloud: &lt;strong&gt;the tool is the smallest decision.&lt;/strong&gt; I&apos;ve watched teams agonize over Terraform-vs-Pulumi and then lose a weekend to a corrupted state file, or ship an IAM wildcard to three clouds at once. What actually determines whether multi-cloud IaC succeeds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State strategy.&lt;/strong&gt; Remote, locked, encrypted, and &lt;em&gt;segmented&lt;/em&gt; — not one monolithic state file for your whole estate. Blast radius is a design decision.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Module/component boundaries per cloud.&lt;/strong&gt; Resist the urge to build a grand homegrown abstraction that &quot;unifies&quot; the clouds. Share &lt;em&gt;patterns and conventions&lt;/em&gt;, not a leaky lowest-common-denominator wrapper — those wrappers become the thing nobody can maintain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy-as-code.&lt;/strong&gt; Sentinel (Terraform) or OPA/Conftest (anywhere) to enforce least-privilege, tagging, and region rules &lt;em&gt;before&lt;/em&gt; apply — across every provider.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secrets, centralized.&lt;/strong&gt; ESC or Vault, never in state you can read, never in the repo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD with plan-review gates.&lt;/strong&gt; Every change is a reviewed diff; nobody applies from a laptop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability as code, and centralized.&lt;/strong&gt; Telemetry can&apos;t live in separate cloud consoles per provider. Standardize on one observability plane — Datadog, for instance — and manage its monitors, dashboards, and SLOs &lt;em&gt;as code&lt;/em&gt; too. The major observability platforms ship both a &lt;strong&gt;Pulumi provider and a Terraform provider&lt;/strong&gt;, so the same review-and-apply flow that provisions the infrastructure also provisions what watches it, across every cloud.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Get those right and Terraform, OpenTofu, and Pulumi all work. Get them wrong and no tool saves you.&lt;/p&gt;
&lt;h2&gt;Part 7 — From image to running infra: the delivery pipeline&lt;/h2&gt;
&lt;p&gt;Choosing the IaC tool is only half of &quot;automation.&quot; The other half is how images get built and how changes actually ship — and in a multi-cloud estate, that pipeline is what keeps things reproducible instead of hand-crafted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Immutable images, built once, deployed as versions.&lt;/strong&gt; Rather than mutating running servers, bake versioned images and replace instances wholesale. Two common ways to build those images:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Packer&lt;/strong&gt; for golden images across clouds — one set of templates produces the AMI, the Azure image, the GCP image, and so on from a shared definition, which is exactly the leverage you want when you support several providers. (Worth knowing: Packer, like Terraform, is now BUSL-licensed — the same license consideration applies.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud-native image builders&lt;/strong&gt; (EC2 Image Builder, Azure VM Image Builder, and equivalents) where a provider-managed pipeline fits a workload better than a portable one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A configuration-management tool&lt;/strong&gt; (Salt, Ansible, or similar) for the &quot;what&apos;s actually inside the image&quot; layer: packages, hardening, and config applied at build time, so a running instance is fully defined rather than hand-tweaked after boot.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That gives a clean separation of stages: &lt;strong&gt;build the image (Packer / image builder + config management) → version it → let the IaC tool reference the image ID → provision and deploy.&lt;/strong&gt; Each stage is independently versioned and reviewable, which is what makes a change traceable across clouds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CI/CD ties it together.&lt;/strong&gt; The IaC runs live in CI — often a mix of self-hosted runners and a hosted CI service. Self-hosted runners earn their place for pipelines that need to sit inside the network or reach systems that shouldn&apos;t touch the public internet; the hosted service handles the rest. Either way the flow is the same: &lt;strong&gt;&lt;code&gt;pulumi preview&lt;/code&gt; (or &lt;code&gt;terraform plan&lt;/code&gt;) on the pull request&lt;/strong&gt; — the reviewed diff — and &lt;strong&gt;apply on merge&lt;/strong&gt;, pulling secrets from a secrets manager and state from the backend, with policy checks gating the apply. Image builds run on the same backbone, publishing new versioned images that later infra changes pick up.&lt;/p&gt;
&lt;p&gt;The through-line: &lt;strong&gt;image build, configuration, and provisioning are three distinct, versioned, reviewable stages&lt;/strong&gt; — not one big manual dance. That discipline is what lets a multi-cloud, multi-language estate stay reproducible.&lt;/p&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;Go multi-cloud because a specific requirement makes it the better architecture — best-of-breed services, real regulatory needs, genuine resilience economics — not because &quot;lock-in&quot; sounds scary in the abstract. Then automate it with the tool that fits your team: &lt;strong&gt;OpenTofu&lt;/strong&gt; if you want open, HCL-based IaC without license risk; &lt;strong&gt;Terraform&lt;/strong&gt; if you&apos;re buying into HashiCorp&apos;s enterprise platform; &lt;strong&gt;Pulumi&lt;/strong&gt; if your people think in code and you&apos;re building infrastructure as a product. And whichever you pick, spend your real effort on state, boundaries, policy, and secrets — the discipline outlives the tool.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What&apos;s your multi-cloud setup — deliberate, or accidental? I&apos;d love to compare notes — &lt;a href=&quot;https://www.linkedin.com/in/riddam/&quot;&gt;find me on LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>multi-cloud</category><category>terraform</category><category>pulumi</category><category>infrastructure-as-code</category></item><item><title>Path-Filtered CI/CD for Infrastructure Monorepos</title><link>https://riddam.github.io/engineering/path-filtered-ci-cd-for-infra-monorepos/</link><guid isPermaLink="true">https://riddam.github.io/engineering/path-filtered-ci-cd-for-infra-monorepos/</guid><description>A practical CI/CD design for monorepos with multiple infrastructure runtimes, focused on selective pipelines, safer promotion, and clean ownership boundaries.</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An infrastructure monorepo is a lovely idea right up until the first time you change a README and watch three cloud pipelines spin up to tell you nothing changed. Multiply that by every pull request and you get the quiet tax of the naive monorepo: slow feedback, plans nobody reads because they&apos;re always green-for-the-wrong-reason, and eventually a team that stops trusting CI because it cries wolf on every commit.&lt;/p&gt;
&lt;p&gt;The fix is not to split the repo. It is to &lt;strong&gt;run only the pipeline that owns the files you touched&lt;/strong&gt; — path-filtered delivery chains. One repository, one place to review cross-cutting changes, but execution scoped tightly to what actually changed.&lt;/p&gt;
&lt;p&gt;This is the delivery chapter of a series on running stateful infrastructure safely. It assumes the &lt;a href=&quot;https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/&quot;&gt;config-as-data&lt;/a&gt; layout (the path filters map onto those directories) and feeds directly into &lt;a href=&quot;https://riddam.github.io/engineering/safe-rollouts-for-stateful-cloud-infrastructure/&quot;&gt;safe rollouts&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;In a monorepo with no path awareness:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a networking change triggers the database pipeline&lt;/li&gt;
&lt;li&gt;an IAM tweak triggers unrelated application checks&lt;/li&gt;
&lt;li&gt;slow, always-running plans bury the one failure that mattered in noise&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is longer feedback loops and — worse — lower confidence in every green check.&lt;/p&gt;
&lt;h2&gt;The Design&lt;/h2&gt;
&lt;p&gt;Split pipelines by directory ownership. A representative mapping:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cloud-a/**&lt;/code&gt; → Cloud A IaC chain&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloud-b/**&lt;/code&gt; → Cloud B IaC chain&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs/**&lt;/code&gt; → docs checks only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each chain owns its own runtime bootstrapping, lint/test commands, preview/synth step, and release/version policy. The repo stays unified; the &lt;em&gt;execution&lt;/em&gt; stays scoped.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  A[Pull Request] --&amp;gt; B{Changed Paths}
  B --&amp;gt;|cloud-a/**| C[Cloud A Chain]
  B --&amp;gt;|cloud-b/**| D[Cloud B Chain]
  B --&amp;gt;|docs/**| E[Docs Chain]
  C --&amp;gt; F[Preview or Synth]
  D --&amp;gt; G[Preview]
  F --&amp;gt; H[Manual Prod Gate]
  G --&amp;gt; H
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What a Path Filter Actually Looks Like&lt;/h2&gt;
&lt;p&gt;The mechanism is boring, which is the point — every major CI system ships it. The shape, in a generic pipeline config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs:
  cloud-a:
    trigger:
      paths: [&quot;cloud-a/**&quot;]
    steps:
      - install-deps
      - lint-and-test
      - render-plan        # synth / preview / equivalent
      - publish-artifact

  cloud-b:
    trigger:
      paths: [&quot;cloud-b/**&quot;]
    steps: [install-deps, lint-and-test, render-plan, publish-artifact]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On GitHub Actions that&apos;s &lt;code&gt;on.push.paths&lt;/code&gt;; on GitLab it&apos;s &lt;code&gt;rules: changes:&lt;/code&gt;; most other systems have a direct equivalent. Two guardrails worth setting from the start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Make filters mutually exclusive.&lt;/strong&gt; Overlapping globs (&lt;code&gt;cloud-a/**&lt;/code&gt; and &lt;code&gt;**/config&lt;/code&gt;) will double-trigger and quietly waste runners.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decide what a shared-file change does.&lt;/strong&gt; A change to a root lockfile or CI config arguably touches &lt;em&gt;every&lt;/em&gt; chain — be explicit about whether it fans out to all of them or fails closed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why Manual Production Gates Still Matter&lt;/h2&gt;
&lt;p&gt;Automated checks should be strict. But for stateful infrastructure, the deploy step itself often deserves a human. This is not nostalgia for manual ops — it is matching the approval to the blast radius.&lt;/p&gt;
&lt;p&gt;Use a manual gate when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;replacing the resource has a high blast radius&lt;/li&gt;
&lt;li&gt;the change window is controlled&lt;/li&gt;
&lt;li&gt;the deploy depends on coordination with another team&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automated quality gates and manual production gates are complementary: the machine proves the change is &lt;em&gt;valid&lt;/em&gt;, the human decides it is &lt;em&gt;safe to release now&lt;/em&gt;. I unpack that division in the &lt;a href=&quot;https://riddam.github.io/engineering/safe-rollouts-for-stateful-cloud-infrastructure/&quot;&gt;safe rollouts post&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Suggested Stage Layout&lt;/h2&gt;
&lt;p&gt;For each infrastructure chain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install dependencies (from a committed lockfile — see failure modes).&lt;/li&gt;
&lt;li&gt;Static checks and unit tests, including config guardrails.&lt;/li&gt;
&lt;li&gt;Render the plan (&lt;code&gt;synth&lt;/code&gt;, &lt;code&gt;preview&lt;/code&gt;, or equivalent).&lt;/li&gt;
&lt;li&gt;Publish the artifact and review the diff.&lt;/li&gt;
&lt;li&gt;Manual approval for production.&lt;/li&gt;
&lt;li&gt;Deploy by pipeline identity only — &lt;strong&gt;never&lt;/strong&gt; a local laptop.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last point matters more than it looks: a local deploy skips every gate above it and leaves no artifact to audit. The pipeline identity is the only principal that should hold deploy permissions in production.&lt;/p&gt;
&lt;h2&gt;Ownership Model&lt;/h2&gt;
&lt;p&gt;Path filters pay off only when each folder has a clear owner:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one team owns the pipeline &lt;em&gt;and&lt;/em&gt; the folder it deploys&lt;/li&gt;
&lt;li&gt;one review group approves that component&apos;s production releases&lt;/li&gt;
&lt;li&gt;one changelog and version stream per component&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anything ambiguous here shows up later as a handoff nobody owns.&lt;/p&gt;
&lt;h2&gt;Failure Modes to Watch&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Overlapping path rules&lt;/strong&gt; that trigger duplicate chains.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared files with unclear ownership&lt;/strong&gt; — the root config that belongs to everyone and no one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-deterministic previews&lt;/strong&gt; from a missing or stale lockfile. Pin dependencies or your &quot;no-op&quot; diffs will lie.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hidden runtime dependencies&lt;/strong&gt; on a specific engineer&apos;s machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A toolchain too old to know the resource types.&lt;/strong&gt; Brand-new cloud services often ship &lt;em&gt;L1-only&lt;/em&gt; — the raw, CloudFormation-level constructs — and only in recent releases of your IaC library. Pin a recent version (and, if you lint CloudFormation separately, a recent linter too). An older toolchain fails synth or validation on an &quot;unknown resource type&quot; it has simply never heard of — a confusing failure if you don&apos;t know to check versions first.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fix these before you scale up contributors, not after.&lt;/p&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;p&gt;How the major CI systems implement path-based triggering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions&quot;&gt;GitHub Actions — Workflow syntax (&lt;code&gt;paths&lt;/code&gt; / &lt;code&gt;paths-ignore&lt;/code&gt;)&lt;/a&gt; — trigger workflows only when matching files change.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#ruleschanges&quot;&gt;GitLab CI/CD — &lt;code&gt;rules:changes&lt;/code&gt;&lt;/a&gt; — run jobs conditionally based on changed paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Final Takeaway&lt;/h2&gt;
&lt;p&gt;Path filtering is one of the highest-leverage changes you can make to an infrastructure monorepo. It cuts noise, tightens feedback, and — most importantly — keeps delivery responsibility aligned with code ownership, so every green check means something again.&lt;/p&gt;
</content:encoded><category>cicd</category><category>platform-engineering</category><category>monorepo</category><category>infrastructure</category></item><item><title>Config as Data: A Safer Pattern for Multi-Environment Infrastructure</title><link>https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/</link><guid isPermaLink="true">https://riddam.github.io/engineering/config-as-data-for-infrastructure-repos/</guid><description>How to keep infrastructure stacks generic while all environment-specific values live in validated config files, reducing drift and risky one-off changes.</description><pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Almost every infrastructure incident I have had to clean up traced back to the same root cause, and it was never an exotic cloud bug. It was a human editing stack code under pressure: a CIDR copied from the wrong environment, an availability zone hardcoded &quot;just for now,&quot; a feature flag that defaulted to &lt;em&gt;on&lt;/em&gt; because nobody wrote down that it shouldn&apos;t. The cloud primitive worked perfectly. The change did exactly what the code said. The code was just wrong in a way no review caught, because the risky value was buried in the middle of a hundred lines of resource wiring.&lt;/p&gt;
&lt;p&gt;One pattern fixed most of that class of problem for me: &lt;strong&gt;treat configuration as data, and infrastructure code as a generic engine that consumes it.&lt;/strong&gt; The stacks stop knowing anything about &quot;production&quot; or &quot;testing.&quot; They know how to &lt;em&gt;build a network&lt;/em&gt; or &lt;em&gt;build a database&lt;/em&gt;, and they read every environment-specific number from a validated config file.&lt;/p&gt;
&lt;p&gt;This post is the anchor for a small series on the practices that grew out of running this pattern in anger — it pairs closely with &lt;a href=&quot;https://riddam.github.io/engineering/safe-rollouts-for-stateful-cloud-infrastructure/&quot;&gt;safe rollouts for stateful infrastructure&lt;/a&gt; and &lt;a href=&quot;https://riddam.github.io/engineering/path-filtered-ci-cd-for-infra-monorepos/&quot;&gt;path-filtered CI/CD for infra monorepos&lt;/a&gt;. The whole point is that the &lt;em&gt;pattern&lt;/em&gt; is the reusable part.&lt;/p&gt;
&lt;h2&gt;The Core Rule&lt;/h2&gt;
&lt;p&gt;Keep stack logic generic. Put every mutable, environment-specific value in a versioned config file. The dividing line is simple: &lt;strong&gt;if a value can differ between environments, it is data.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What lives in config:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CIDR blocks and subnet allocations&lt;/li&gt;
&lt;li&gt;Availability zone choices&lt;/li&gt;
&lt;li&gt;Environment toggles for optional resources&lt;/li&gt;
&lt;li&gt;Principals and cross-account sharing targets&lt;/li&gt;
&lt;li&gt;Sizing and capacity values&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What stays in code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validation rules&lt;/li&gt;
&lt;li&gt;Resource composition logic&lt;/li&gt;
&lt;li&gt;Security defaults (retention, encryption, permission boundaries)&lt;/li&gt;
&lt;li&gt;Naming and tagging conventions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mental model is a pure function. Config is the input, the stack is the function, and the rendered plan is the output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  A[(config/&amp;lt;br/&amp;gt;environments.py&amp;lt;br/&amp;gt;capacity.py)] --&amp;gt; B[Generic stack code&amp;lt;br/&amp;gt;network / database / identity]
  B --&amp;gt; C[testing plan]
  B --&amp;gt; D[acceptance plan]
  B --&amp;gt; E[production plan]
  A -.validated first.-&amp;gt; V{{Guardrail tests}}
  V -.block on failure.-&amp;gt; B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same function, different inputs, deterministic outputs. That single property is where all the benefits come from.&lt;/p&gt;
&lt;h2&gt;Why This Works&lt;/h2&gt;
&lt;p&gt;You gain three things that compound:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Predictability.&lt;/strong&gt; A change diff is almost entirely config, so intent is obvious at a glance. &quot;We grew the acceptance database by two nodes&quot; reads as a two-line diff, not a hunt through stack logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusability.&lt;/strong&gt; The same code path serves every environment, so testing exercises the exact code that runs in production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety.&lt;/strong&gt; Because config is just data, you can validate it &lt;em&gt;before&lt;/em&gt; the provider ever sees it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also collapses code review into one question: &lt;strong&gt;did we change data, or did we change behavior?&lt;/strong&gt; Data changes get scrutinized for values; behavior changes get scrutinized for blast radius. Reviewers stop having to do both at once on every line.&lt;/p&gt;
&lt;h2&gt;A Practical Structure&lt;/h2&gt;
&lt;p&gt;A layout that has held up well for me:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;infra/
  config/
    environments.py     # per-env CIDRs, AZs, principals, toggles
    capacity.py         # per-env sizing, gated by an `enabled` flag
  stacks/
    network_stack.py
    database_stack.py
    identity_stack.py
  tests/
    test_config_guardrails.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stacks load config by environment key and never reach for a constant of their own:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;env_cfg = ENVIRONMENTS[env_name]
network_cfg = env_cfg.network
capacity_cfg = CAPACITY[env_name]

# The stack composes resources from cfg — it never hardcodes an environment.
VpcNetwork(self, &quot;network&quot;, cidr=network_cfg.cidr, azs=network_cfg.azs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rule I hold the line on: &lt;strong&gt;grep the &lt;code&gt;stacks/&lt;/code&gt; directory for an environment name and you should find zero hits.&lt;/strong&gt; The moment &lt;code&gt;if env == &quot;production&quot;&lt;/code&gt; appears in stack code, the pattern has started to leak.&lt;/p&gt;
&lt;h2&gt;Guardrails You Should Add Early&lt;/h2&gt;
&lt;p&gt;The payoff of config-as-data is that you can validate the data. Do it before synth/preview runs, as ordinary unit tests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def test_subnets_within_supernet():
    for name, env in ENVIRONMENTS.items():
        assert env.network.cidr in env.network.supernet, (
            f&quot;{name}: client subnet escapes its supernet&quot;
        )

def test_no_overlapping_cidrs():
    seen = []
    for env in ENVIRONMENTS.values():
        assert not any(env.network.cidr.overlaps(s) for s in seen)
        seen.append(env.network.cidr)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A starter set of checks worth having on day one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every subnet sits within its environment&apos;s supernet.&lt;/li&gt;
&lt;li&gt;No two environments overlap CIDRs. (This one has saved me more than once — it&apos;s the same discipline that keeps &lt;a href=&quot;https://riddam.github.io/engineering/network-connectivity-for-managed-database-platforms/&quot;&gt;database connectivity&lt;/a&gt; sane across peerings.)&lt;/li&gt;
&lt;li&gt;Every environment defines all required keys.&lt;/li&gt;
&lt;li&gt;Account and tenant identifiers appear only in config, never in code.&lt;/li&gt;
&lt;li&gt;Optional features require an explicit flag; nothing dangerous defaults on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These run in milliseconds and catch bad inputs before any provider API call — which means bad config fails a pull request, not a deployment window.&lt;/p&gt;
&lt;h2&gt;CI/CD Integration&lt;/h2&gt;
&lt;p&gt;The guardrails only help if they run automatically. In every pull request:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Lint and unit tests.&lt;/li&gt;
&lt;li&gt;Config guardrail tests.&lt;/li&gt;
&lt;li&gt;Render plans/previews for the changed environments.&lt;/li&gt;
&lt;li&gt;Block merge on any failure.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because the config is the diff, reviewers get a plan that maps one-to-one to the values that changed. That fast, legible feedback loop is exactly what makes &lt;a href=&quot;https://riddam.github.io/engineering/path-filtered-ci-cd-for-infra-monorepos/&quot;&gt;path-filtered pipelines&lt;/a&gt; worth building.&lt;/p&gt;
&lt;h2&gt;Where State and Secrets Live&lt;/h2&gt;
&lt;p&gt;Config-as-data answers &lt;em&gt;what&lt;/em&gt; to deploy. Two adjacent questions decide how safely you can operate it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;State.&lt;/strong&gt; Tools like Pulumi and Terraform keep a state file describing reality. You can self-host that backend on object storage you own, or use the vendor&apos;s managed service. Self-hosting keeps the data in your own account and gives you full control; the managed option hands you locking, history, and encryption without running one more piece of stateful infrastructure. Neither is wrong — self-host when control or data residency matters, reach for the managed service when you would rather not babysit a backend. Pick deliberately, because migrating state later is a chore.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Secrets.&lt;/strong&gt; Whatever you choose, database credentials and other secrets do &lt;strong&gt;not&lt;/strong&gt; belong in that state file, in config, or in the repo — the state file is the classic accidental leak. Keep secrets in a dedicated secrets manager, reference them by name, and rotate them on a schedule (more on rotation in &lt;a href=&quot;https://riddam.github.io/engineering/safe-rollouts-for-stateful-cloud-infrastructure/&quot;&gt;safe rollouts&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Common Anti-Patterns&lt;/h2&gt;
&lt;p&gt;Even when time is tight, these are the ones that come back to bite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &quot;quick fix&quot; value hardcoded directly into stack logic.&lt;/li&gt;
&lt;li&gt;Environment-name checks (&lt;code&gt;if env == ...&lt;/code&gt;) scattered across files.&lt;/li&gt;
&lt;li&gt;Feature switches with implicit defaults.&lt;/li&gt;
&lt;li&gt;Copy-pasted config with no schema validation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every one of them reintroduces the exact drift the pattern exists to kill.&lt;/p&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;p&gt;The official guidance below backs up this pattern if you want to go deeper:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html&quot;&gt;AWS CDK — Best practices for developing and deploying cloud infrastructure&lt;/a&gt; — see &quot;Model all production stages in code&quot; and &quot;Commit &lt;code&gt;cdk.context.json&lt;/code&gt; to avoid non-deterministic behavior&quot;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.pulumi.com/docs/concepts/config/&quot;&gt;Pulumi — Configuration&lt;/a&gt; — per-stack configuration and secrets.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://12factor.net/config&quot;&gt;The Twelve-Factor App — III. Config&lt;/a&gt; — the original argument for separating config from code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;re still choosing the tool that will consume this config, my &lt;a href=&quot;https://riddam.github.io/engineering/multi-cloud-terraform-vs-pulumi/&quot;&gt;take on Terraform vs. Pulumi&lt;/a&gt; covers that decision.&lt;/p&gt;
&lt;h2&gt;Final Takeaway&lt;/h2&gt;
&lt;p&gt;If you adopt one rule from this post, make it this: &lt;strong&gt;environment-specific values are data, never stack logic.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It speeds reviews, kills a whole category of drift, and makes multi-environment infrastructure easier to reason about the larger your platform grows. Everything else in this series is a consequence of taking that one rule seriously.&lt;/p&gt;
</content:encoded><category>infrastructure-as-code</category><category>aws-cdk</category><category>pulumi</category><category>platform-engineering</category></item><item><title>Making AI Coding Assistants Reliable: My Playbook</title><link>https://riddam.github.io/engineering/ai-assisted-coding-playbook/</link><guid isPermaLink="true">https://riddam.github.io/engineering/ai-assisted-coding-playbook/</guid><description>The working discipline I&apos;ve settled on for getting dependable, secure, low-regret results from AI coding assistants — prompting frameworks, model selection, context hygiene, and safety rules, learned the hard way.</description><pubDate>Thu, 04 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Over the last couple of years I&apos;ve gone from occasionally accepting an autocomplete to running multi-file agents against real codebases every day. Along the way these tools have saved me genuine hours — and, on the days I got sloppy, handed me confident, subtly wrong code that cost me more hours than they saved. This post is the working discipline I&apos;ve settled on to get the upside without the regret. It&apos;s the guide I&apos;d give a version of myself two years ago.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;My examples lean on the stack I use daily — Python (uv, pytest, click) and TypeScript (AWS CDK) — but every principle transfers. It&apos;s written for cloud engineers and early-career developers.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The goal is simple: make AI coding assistance &lt;strong&gt;reliable, secure, and low-regret&lt;/strong&gt;. These assistants are spectacular accelerators and confident liars, often in the same minute. In my experience the difference between the days they make me faster and the days they quietly make me slower isn&apos;t the model — it&apos;s the discipline around it.&lt;/p&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Defaults&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Default model: &lt;strong&gt;Claude Sonnet 5&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Default workflow: &lt;strong&gt;Ask → Plan → Agent → Review → Test&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Rule: small diffs, explicit constraints, always add or verify tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Model ladder&lt;/strong&gt; (as of 2026 — revisit quarterly, this changes fast)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quick tiny edits → &lt;strong&gt;Claude Haiku 4.5&lt;/strong&gt; or your provider&apos;s fast tier (e.g. Gemini Flash)&lt;/li&gt;
&lt;li&gt;Most coding tasks → &lt;strong&gt;Claude Sonnet 5&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Multi-file patch + lots of tests → &lt;strong&gt;GPT Codex&lt;/strong&gt; (current tier)&lt;/li&gt;
&lt;li&gt;Deep debugging / security review / hardest refactors → &lt;strong&gt;Claude Opus 4.8&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Second opinion on architecture or algorithms → &lt;strong&gt;Gemini Pro&lt;/strong&gt; (current tier)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. What an AI Assistant Is (and Isn&apos;t)&lt;/h2&gt;
&lt;p&gt;Think of Copilot not as a replacement for a developer, but as a highly skilled, tireless intern with a photographic memory and no common sense.&lt;/p&gt;
&lt;h3&gt;What it&apos;s good at&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accelerating mechanical coding&lt;/strong&gt; — boilerplate, wiring up DTOs and interfaces, repetitive edits across large blocks of code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic multi-file scaffolding&lt;/strong&gt; — drafting entire feature skeletons: a new API endpoint, its service layer, and its database model in one pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generating test matrices&lt;/strong&gt; — edge-case matrices, pytest parameterization, and mock setups for legacy code that no human wants to write by hand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code forensics&lt;/strong&gt; — explaining legacy modules, tracing call graphs, finding hidden side effects (&quot;Which functions in this module write to S3?&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constraint-based refactoring&lt;/strong&gt; — safe refactors (&quot;rewrite this without changing the public API&quot;) when you provide a &quot;golden example&quot; from the repo to mimic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-the-fly documentation&lt;/strong&gt; — README updates, docstrings, and architectural summaries derived directly from code.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What it&apos;s not&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An independent architect.&lt;/strong&gt; It cannot make high-level design decisions or understand business requirements without explicit context and human steering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A source of truth.&lt;/strong&gt; It doesn&apos;t &quot;know&quot; your system state — it predicts from what it sees in your open tabs. Outdated tabs produce outdated advice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A replacement for quality gates.&lt;/strong&gt; It doesn&apos;t replace code review, integration tests, or threat modeling. AI-generated code needs &lt;em&gt;more&lt;/em&gt; rigorous review, not less.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A security expert.&lt;/strong&gt; It&apos;s a pattern mimic. It will happily suggest insecure patterns — hardcoded credentials, permissive IAM wildcards — because those patterns are everywhere in public training data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An autopilot for high-risk code.&lt;/strong&gt; IAM policies, encryption logic, and auth handlers require deep human verification plus specialized tooling (Snyk, Checkov, bandit).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The golden rule:&lt;/strong&gt; trust the AI for the syntax, trust yourself for the logic and security. If you wouldn&apos;t merge a junior developer&apos;s code without reading it, don&apos;t merge the AI&apos;s.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. Modes &amp;amp; Sessions: Choosing How It Thinks and Where It Works&lt;/h2&gt;
&lt;p&gt;Modern Copilot is no longer a chat box — it&apos;s a tiered workflow system. Picking the right &lt;strong&gt;mode&lt;/strong&gt; (how it thinks) and &lt;strong&gt;session&lt;/strong&gt; (where it works) is the difference between a clean PR and a messy refactor.&lt;/p&gt;
&lt;h3&gt;Modes — the &quot;brain&quot; selection&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Ask (analyze &amp;amp; explore).&lt;/strong&gt; For forensics (&quot;trace the call graph of this function&quot;), strategy (&quot;brainstorm test approaches for this module&quot;), and gut checks (&quot;is this IAM policy too permissive?&quot;). Avoid it when you want code written and applied — that&apos;s Agent mode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plan (architect &amp;amp; align).&lt;/strong&gt; Produces a Markdown plan and checklist of files to touch and tests to add &lt;em&gt;before&lt;/em&gt; any code changes. Best for multi-file work where you want to review the &quot;Step 1, Step 2…&quot; logic first, and for surfacing side effects early.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent (autonomous execution).&lt;/strong&gt; Implements features spanning multiple layers, applies patterns consistently across folders, and self-heals: it runs tests and linters, reads the errors, and fixes its own code until green.&lt;/p&gt;
&lt;p&gt;Guardrails I include in &lt;em&gt;every&lt;/em&gt; agent prompt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;No new dependencies.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Limit scope to [specific folder].&quot;&lt;/li&gt;
&lt;li&gt;&quot;Stop and ask for confirmation after each step.&quot;&lt;/li&gt;
&lt;li&gt;&quot;Run &lt;code&gt;uv run pytest&lt;/code&gt; after changes.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sessions — the &quot;workspace&quot; selection&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New chat&lt;/strong&gt; — start fresh for each ticket, or whenever the context feels polluted with old logs. Stale context measurably degrades reasoning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local (interactive)&lt;/strong&gt; — short tasks where you watch every edit in real time. Best for small fixes and tests in the file you&apos;re already in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background (asynchronous)&lt;/strong&gt; — delegated tasks of five-plus minutes (&quot;write 20 integration tests&quot;). The agent works in an isolated Git worktree while you keep coding; you review the diff and apply when done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud (remote / PR agent)&lt;/strong&gt; — large refactors and repo-wide tasks. Runs on remote infrastructure, opens a draft pull request, and assigns you as reviewer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The workflow I use for 90% of non-trivial tasks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  A[&quot;Ask&amp;lt;br/&amp;gt;understand the code&quot;] --&amp;gt; B[&quot;Plan&amp;lt;br/&amp;gt;design and align&quot;]
  B --&amp;gt; C[&quot;Background Agent&amp;lt;br/&amp;gt;execute in isolation&quot;]
  C --&amp;gt; D[&quot;Review the diff&quot;]
  D --&amp;gt; E[&quot;Run the tests&quot;]
  E --&amp;gt;|&quot;pass&quot;| F[&quot;Apply and commit&quot;]
  E --&amp;gt;|&quot;fail&quot;| C
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Context Window: The AI&apos;s Working Memory&lt;/h2&gt;
&lt;p&gt;AI reliability isn&apos;t about model size anymore — it&apos;s about &lt;strong&gt;context engineering&lt;/strong&gt;. Most tools now show real-time token usage (e.g. &lt;code&gt;15K / 128K&lt;/code&gt;); learn to read it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Too little context&lt;/strong&gt; → the AI guesses your repo patterns: hallucinated internal APIs, generic boilerplate that doesn&apos;t fit your stack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Too much noisy context&lt;/strong&gt; → &quot;context rot.&quot; Models get measurably less accurate when the window fills with irrelevant code, dead logs, and failed attempts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The 80% rule&lt;/strong&gt; → when usage climbs past roughly 80% of the limit, compact the conversation or start a new chat.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Practical context hygiene&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Curate your workspace.&lt;/strong&gt; The AI weights your active tab and open editors heavily. Keep open: the file you&apos;re editing, its test file (the &quot;contract&quot; for success), one golden example of the pattern you want, and config files only when relevant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Use explicit references.&lt;/strong&gt; Don&apos;t let it guess which files matter:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Implement the logic in #&lt;a href=&quot;http://UserService.py&quot;&gt;UserService.py&lt;/a&gt; following the pattern in #&lt;a href=&quot;http://AuthService.py&quot;&gt;AuthService.py&lt;/a&gt;.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;3. Run the &quot;inferred convention&quot; check.&lt;/strong&gt; Before letting it write 100 lines, verify its mental model:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Before you begin coding, summarize the architectural conventions, naming patterns, and testing libraries you&apos;ve inferred from my open files. If any convention is unclear, ask me instead of guessing.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;4. Manage session history.&lt;/strong&gt; Use &lt;code&gt;/compact&lt;/code&gt; to summarize the conversation and prune dead context. Switching goals (CLI work → infrastructure work)? New chat. Mixing goals leads to logic leaks.&lt;/p&gt;
&lt;p&gt;Think of the context window as working memory: if &lt;em&gt;you&apos;d&lt;/em&gt; be overwhelmed by 30 open tabs, so is the model.&lt;/p&gt;
&lt;h2&gt;4. Which Model to Use When&lt;/h2&gt;
&lt;p&gt;The current generation of models is optimized for agentic workflows and long-horizon coding, not just chat. Use a ladder to balance cost, speed, and accuracy — and expect this table to age; revisit whatever your tool offers every quarter.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;My pick (2026)&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default daily driver&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Sonnet 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Near-frontier quality on coding and agentic work at mid-tier cost. Handles most planning and implementation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quick tasks / tiny edits&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Haiku 4.5&lt;/strong&gt; / fast tier&lt;/td&gt;
&lt;td&gt;Surgical for scoped edits, boilerplate tests, renames, and docs — at a fraction of the cost.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-file patch + heavy tests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GPT Codex&lt;/strong&gt; (current tier)&lt;/td&gt;
&lt;td&gt;Strong instruction-following for structured code output; usefully rigid about provided signatures.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep debugging / security review / huge refactors&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Opus 4.8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The heavy artillery: long-horizon autonomy, strongest first-try rate on complex tasks, 1M-token context for sprawling codebases.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second opinion / logic check&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gemini Pro&lt;/strong&gt; (current tier)&lt;/td&gt;
&lt;td&gt;A genuinely different model family — useful cross-check on architectural trade-offs and math-heavy algorithms.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;The escalation strategy&lt;/h3&gt;
&lt;p&gt;Don&apos;t sit on one model all day — switch based on the risk profile of the task:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
  A[&quot;Task arrives&quot;] --&amp;gt; B{&quot;Risk and complexity?&quot;}
  B --&amp;gt;|&quot;mechanical: renames,&amp;lt;br/&amp;gt;boilerplate, docs&quot;| C[&quot;Downgrade&amp;lt;br/&amp;gt;Haiku 4.5 / fast tier&quot;]
  B --&amp;gt;|&quot;normal feature work,&amp;lt;br/&amp;gt;planning, tests&quot;| D[&quot;Default&amp;lt;br/&amp;gt;Claude Sonnet 5&quot;]
  B --&amp;gt;|&quot;high ambiguity: security audit,&amp;lt;br/&amp;gt;15+ interdependent files,&amp;lt;br/&amp;gt;race conditions&quot;| E[&quot;Upgrade&amp;lt;br/&amp;gt;Claude Opus 4.8&quot;]
  D --&amp;gt;|&quot;stuck or hallucinating&amp;lt;br/&amp;gt;an internal API&quot;| F[&quot;Cross-check&amp;lt;br/&amp;gt;Codex or Gemini Pro&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; if your default model keeps hallucinating a specific internal library, try a different family — specialized code models are often more rigid (in a good way) about sticking to the signatures you provide.&lt;/p&gt;
&lt;h2&gt;5. The Contract Model: Why Assistants Become Reliable&lt;/h2&gt;
&lt;p&gt;Copilot gets &quot;buggy&quot; when it has to guess your architecture. The fix is a &lt;strong&gt;contract-first&lt;/strong&gt; approach: bind the AI to the same truth sources humans follow.&lt;/p&gt;
&lt;h3&gt;5.1 System rules (the always-on layer)&lt;/h3&gt;
&lt;p&gt;Bake non-negotiables into &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; (or &lt;code&gt;AGENTS.md&lt;/code&gt;) so they attach to every request:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dependency discipline&lt;/strong&gt; — no new libraries unless explicitly requested.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security hygiene&lt;/strong&gt; — never log secrets or print tokens.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atomic changes&lt;/strong&gt; — small, incremental diffs; no drive-by refactors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test-driven execution&lt;/strong&gt; — every logic change ships with a test or verification checklist.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; — least privilege always; IAM wildcards (&lt;code&gt;*&lt;/code&gt;) require a comment explaining why, or they get rejected in review.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 Repo signals (defining the truth)&lt;/h3&gt;
&lt;p&gt;Keep these open or reference them with &lt;code&gt;@&lt;/code&gt; to ground the AI in your actual stack:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Python (uv)&lt;/th&gt;
&lt;th&gt;TypeScript (CDK)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build &amp;amp; deps&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;uv.lock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;tsconfig.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality gates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ruff.toml&lt;/code&gt;, &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eslint.config.js&lt;/code&gt;, &lt;code&gt;.prettierrc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test logic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tests/&lt;/code&gt;, &lt;code&gt;conftest.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tests/&lt;/code&gt;, &lt;code&gt;jest.config.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conventions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;docs/architecture.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;5.3 The task prompt (ticket-level contract)&lt;/h3&gt;
&lt;p&gt;When handing a ticket to an agent, include four pillars:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Anchor&lt;/strong&gt; — specific file paths and a golden example to mimic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Behavior&lt;/strong&gt; — clear inputs and outputs (&quot;the CLI takes &lt;code&gt;--json&lt;/code&gt; and returns a flat object&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constraints&lt;/strong&gt; — explicit don&apos;ts (&quot;don&apos;t call boto3 directly; use the existing storage wrapper&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Acceptance&lt;/strong&gt; — how to verify (&quot;&lt;code&gt;uv run pytest tests/unit&lt;/code&gt; passes&quot;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The truth-check prompt&lt;/strong&gt;, for when you suspect a hallucinated API:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Stop. Check the definitions in #pyproject.toml and the actual module source. Are you using a real method or guessing? If guessing, ask me for the correct import.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;6. The 4-Part Prompt: A.T.C.D.&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Anchor, Task, Constraints, Done-means.&lt;/strong&gt; Without an anchor the AI defaults to &quot;generic internet style&quot;; without exit criteria an agent doesn&apos;t know when to stop.&lt;/p&gt;
&lt;p&gt;Copy-paste template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Context: You are acting as a senior engineer in this repository.

### ANCHOR
- Primary file: @src/cli/deploy.py
- Reference pattern: follow the argument parsing style in @src/cli/auth.py

### TASK
- Goal: [describe the feature or fix]
- Signature: add_record(id: str, data: dict) -&amp;gt; bool
- Business rules: [rule 1], [rule 2]

### CONSTRAINTS
- Dependencies: no new libraries — existing uv / npm packages only
- Security: no PII or secrets in logs; least-privilege IAM
- Style: follow the ruff / eslint config in the repo root
- Diff: minimal changes only; do not refactor unrelated logic

### DONE MEANS
- [ ] Logic implemented in [path]
- [ ] Unit tests added/updated in [path]
- [ ] Verification: uv run pytest
- [ ] Edge cases handled: empty input, timeout, 403
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For complex tasks, add a chain-of-thought trigger at the bottom:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Before writing any code, explain your reasoning and list the files you intend to modify. Wait for my GO to proceed.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;7. Prompt Profiles: Choosing Reasoning Intensity&lt;/h2&gt;
&lt;p&gt;Match the instruction set to the task, like choosing a gear.&lt;/p&gt;
&lt;h3&gt;Generate (fast)&lt;/h3&gt;
&lt;p&gt;For atomic edits and one-liners. Ideal model: Haiku 4.5 / fast tier.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Minimal diff. Change ONLY the logic inside this function. Do not refactor, do not add comments, do not rename variables. If the fix is more than 5 lines, stop and ask.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Safe (default)&lt;/h3&gt;
&lt;p&gt;For daily feature work. Ideal model: Sonnet 5. The rhythm: &lt;strong&gt;plan → you approve → implement one step → verify with tests&lt;/strong&gt;. Plan twice, code once.&lt;/p&gt;
&lt;h3&gt;Forensics (legacy &amp;amp; infrastructure)&lt;/h3&gt;
&lt;p&gt;For high-stakes areas — legacy modules, complex CDK stacks, IAM logic. Ideal model: Opus 4.8 with thinking. The discovery prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Before suggesting any changes, analyze this module: 1) &lt;strong&gt;System map&lt;/strong&gt; — key responsibilities. 2) &lt;strong&gt;Side effects&lt;/strong&gt; — does this touch S3, a database, or external APIs? 3) &lt;strong&gt;Safe seams&lt;/strong&gt; — where is the safest place to inject new logic? 4) &lt;strong&gt;Blast radius&lt;/strong&gt; — if this fails, what breaks downstream? Return a bulleted report before proposing code.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;The profile switcher rule:&lt;/strong&gt; if a &quot;fast&quot; task starts getting complicated, abort and restart in Safe mode. Hallucinations cluster exactly where a complex problem meets a fast instruction set.&lt;/p&gt;
&lt;h2&gt;8. Prompt Cookbook: Battle-Tested Recipes&lt;/h2&gt;
&lt;h3&gt;Module forensics&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Analyze this module for a new engineer. Return bullets covering: core mission, the 3 most critical functions and their callers, any AWS / database / filesystem / network touches, which env vars or config files dictate behavior, and the lowest-risk seams for injecting new logic.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The plan-act handoff&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Propose a 4-step implementation plan for [feature] following the patterns in #file. Constraints: no new dependencies, minimal diff, pytest verification. Provide the plan first — do not write code. Once I approve, implement step 1 only.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Pytest matrix &amp;amp; parameterization&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Given this function, propose a pytest test matrix covering happy paths, boundaries (max/min, empty strings, None), and error propagation (403s, timeouts). Write the tests with @pytest.mark.parametrize and fixtures from #&lt;a href=&quot;http://conftest.py&quot;&gt;conftest.py&lt;/a&gt;. Strictly no network calls — mock all AWS and external APIs.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Click CLI testing&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Write pytest tests using click.testing.CliRunner for [command]. Cover exit codes (0 vs non-zero), stdout/stderr, and the --dry-run and --json flags. For --json, assert the output dictionary schema.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;CDK fine-grained assertions&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Write unit tests using the aws-cdk-lib assertions library for this construct. Verify resource properties in the synthesized template: encryption settings, retention policies, mandatory tags. Do not use snapshots — use Template.has_resource_properties for targeted assertions.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The safe refactor&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Refactor this function to reduce cyclomatic complexity. Constraints: zero behavior change, add type hints and docstrings, keep the public signature identical. Propose the refactor plus pytest tests that would protect existing behavior during the swap.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Facts-only documentation&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Update the docs for [feature]. Document ONLY behavior and flags explicitly present in the provided code. If unsure about a side effect, omit it. Include 3 usage examples and common failure modes with their error messages.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The self-critique pass&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Critique your own solution: 1) Security — hardcoded secrets, IAM wildcards? 2) Performance — O(n²) loops, excessive API calls? 3) Constraints — did you stick to #pyproject.toml dependencies? 4) Edge cases — network down, malformed input? Return a fix list before I merge.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;9. VS Code Surfaces: What to Use When&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Inline suggestions&lt;/strong&gt; — the flow state. Micro-edits, repetitive wiring, next-edit predictions. Steer with a comment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Create an SQS queue with a 14-day DLQ and KMS encryption
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avoid inline for multi-file logic or anything security-sensitive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chat (Ask mode)&lt;/strong&gt; — the researcher. &lt;code&gt;@workspace /explain&lt;/code&gt; for repo architecture (in current VS Code, &lt;code&gt;#codebase&lt;/code&gt; is the newer inline equivalent), stack-trace root-causing from &lt;code&gt;#terminal&lt;/code&gt;, planning before touching code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent &amp;amp; Plan modes&lt;/strong&gt; — the executors. Plan researches and drafts a step-by-step TODO list; Agent applies edits, runs commands, and fixes its own errors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Session types in practice:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Interactive tasks, real-time Keep/Undo review&lt;/td&gt;
&lt;td&gt;Edits your working tree directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background&lt;/td&gt;
&lt;td&gt;Long tasks (bulk test-writing, folder-wide refactors)&lt;/td&gt;
&lt;td&gt;Isolated Git worktree; review and apply when done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;Repo-wide migrations, &quot;document everything&quot;&lt;/td&gt;
&lt;td&gt;Remote infrastructure; opens a draft PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Watch the context indicator.&lt;/strong&gt; Red means the AI is about to start forgetting your early instructions — compact or start fresh.&lt;/p&gt;
&lt;h2&gt;10. The Agent Workflow, Step by Step&lt;/h2&gt;
&lt;h3&gt;Step 1: Local agent (interactive scaffolding)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Ensure a clean Git state so you can diff everything.&lt;/li&gt;
&lt;li&gt;Open Chat → Mode: Agent, with your default model.&lt;/li&gt;
&lt;li&gt;Prompt with the A.T.C.D. structure.&lt;/li&gt;
&lt;li&gt;Review with the Keep/Undo UI, cherry-picking suggestions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example: &lt;em&gt;&quot;Create a CLI skeleton using click with a command group and one subcommand. No new dependencies; use uv. Done means: pytest tests using CliRunner and the exact uv run command.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Step 2: Plan + background agent (delegated execution)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Commit first&lt;/strong&gt; — create a clean baseline.&lt;/li&gt;
&lt;li&gt;Switch to Plan mode and prompt for a roadmap.&lt;/li&gt;
&lt;li&gt;Approve the plan, then &quot;Continue in Background.&quot;&lt;/li&gt;
&lt;li&gt;Keep coding while the agent works in its isolated worktree.&lt;/li&gt;
&lt;li&gt;Review the completed diff and apply.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note: background agents don&apos;t see unsaved changes — save everything before delegating.&lt;/p&gt;
&lt;h3&gt;Step 3: Cloud agent (PR-scale work)&lt;/h3&gt;
&lt;p&gt;For repo-wide changes and migrations: the agent runs remotely, creates a branch, opens a draft PR, and your CI runs against it. Review it exactly like a human&apos;s PR — because that&apos;s the contract that keeps quality up.&lt;/p&gt;
&lt;h2&gt;11. Instruction Files: Reducing Bugs Through Grounding&lt;/h2&gt;
&lt;p&gt;The most common cause of hallucination is a lack of grounding. Instruction files are the AI&apos;s law books.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What counts as an instruction file:&lt;/strong&gt; anything that defines the rules of the repo — &lt;code&gt;pyproject.toml&lt;/code&gt; and lockfiles (which libraries are allowed), linter configs (style and strictness), CI workflows (how code is actually tested and deployed), and &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; (conventions and philosophy).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Three habits:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The open-tab rule.&lt;/strong&gt; Keep &lt;code&gt;pyproject.toml&lt;/code&gt; open in a background tab when dependency decisions matter — open files carry more weight in the AI&apos;s reasoning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit grounding.&lt;/strong&gt; &quot;Write this test using the fixtures defined in @conftest.py.&quot; Don&apos;t assume it read your configs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forbidden invention.&lt;/strong&gt; Models love suggesting popular libraries (&lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;pandas&lt;/code&gt;) whether or not they&apos;re in your stack: &lt;em&gt;&quot;Strictly use only the dependencies listed in #pyproject.toml. Do not suggest new libraries.&quot;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The repo-level instruction file.&lt;/strong&gt; Create &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; (Copilot) or &lt;code&gt;AGENTS.md&lt;/code&gt; (the emerging cross-tool standard) at the repo root with your hard rules: &quot;We use uv for all Python tasks.&quot; &quot;CDK tests use fine-grained assertions, never snapshots.&quot; &quot;CLI output must support --json.&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If a rule isn&apos;t in a file, it doesn&apos;t exist for the AI. Document your conventions to automate your conventions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;12. Safe Usage Rules (Security &amp;amp; Privacy)&lt;/h2&gt;
&lt;p&gt;AI-assisted coding is now a real attack vector — both for leaking your data and for pulling malicious code in.&lt;/p&gt;
&lt;h3&gt;Data privacy&lt;/h3&gt;
&lt;p&gt;Treat every prompt as a record that may be stored and logged.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No secrets.&lt;/strong&gt; Never paste API keys, tokens, or credentials. Use placeholders like &lt;code&gt;&amp;lt;MY_API_KEY&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redact PII.&lt;/strong&gt; No customer names, emails, or production data — use synthetic data (&lt;code&gt;test_user_123&lt;/code&gt;) for debugging.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt hygiene.&lt;/strong&gt; Don&apos;t describe proprietary algorithms in detail; use abstract descriptions. If you&apos;re doing this at work, know your employer&apos;s AI usage policy before pasting anything.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Infrastructure &amp;amp; IAM&lt;/h3&gt;
&lt;p&gt;AI agents lack common sense about permissions. They suggest wildcards (&lt;code&gt;*&lt;/code&gt;) because wildcards always work — and always create holes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Least privilege:&lt;/strong&gt; S3 access means a policy for &lt;em&gt;one bucket&lt;/em&gt;, not &lt;code&gt;s3:*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &quot;why&quot; rule:&lt;/strong&gt; any AI-suggested wildcard needs a manual comment justifying it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit trail:&lt;/strong&gt; tag AI-generated infrastructure code — &lt;code&gt;# Generated with AI — reviewed by &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Human-in-the-loop verification&lt;/h3&gt;
&lt;p&gt;Never blind-merge AI code. A meaningful share of AI suggestions still contains subtle vulnerabilities — SQL injection, insecure deserialization, over-broad permissions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Security-critical code (auth, IAM, encryption, data handling) gets human senior review, full stop.&lt;/li&gt;
&lt;li&gt;Run scanners in pre-commit: &lt;code&gt;ruff&lt;/code&gt;, &lt;code&gt;bandit&lt;/code&gt;, &lt;code&gt;checkov&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check for phantom dependencies.&lt;/strong&gt; Verify every new import actually exists in your lockfile. Attackers register packages under names AI models commonly hallucinate (&quot;slopsquatting&quot;) — an import that &quot;looks right&quot; can be a supply-chain attack.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;13. Definition of Done for AI-Assisted Changes&lt;/h2&gt;
&lt;p&gt;Because AI is probabilistic, &quot;done&quot; needs a verification loop. My PR checklist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Small diff, scoped to the ticket&lt;/strong&gt; — no drive-by refactors.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Contract alignment&lt;/strong&gt; — verified against &lt;code&gt;pyproject.toml&lt;/code&gt; (deps) and &lt;code&gt;conftest.py&lt;/code&gt; (test patterns).&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;No phantom dependencies&lt;/strong&gt; — every new import exists in the lockfile.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Tests added/updated&lt;/strong&gt; — happy path plus at least two edge cases.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Local verification passed&lt;/strong&gt; — &lt;code&gt;uv run pytest&lt;/code&gt; / &lt;code&gt;npm test&lt;/code&gt; ran and passed.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Lint &amp;amp; type checks pass.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Facts-only docs&lt;/strong&gt; — updated from the final code, not from intentions.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Security gut-check&lt;/strong&gt; — no secrets, no unjustified wildcards, no PII in logs.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;The explain-back test&lt;/strong&gt; — can &lt;em&gt;you&lt;/em&gt; explain why the AI chose this logic? No black-box code allowed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automate the tail end by ending agent prompts with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;When finished, provide a verification report: 1) files changed, 2) the exact command you ran to test, 3) confirmation that no new libraries were added.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;14. Going Deeper&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Learn to verify, not just to prompt.&lt;/strong&gt; The skills that matter most for auditing AI output:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Advanced pytest&lt;/strong&gt; — fixtures, &lt;code&gt;parametrize&lt;/code&gt;, &lt;code&gt;monkeypatch&lt;/code&gt;. The AI writes test logic well; humans must design the test &lt;em&gt;strategy&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fine-grained CDK assertions&lt;/strong&gt; — snapshots are brittle and AI defaults to them; targeted assertions make infrastructure tests survive refactors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secure-by-design IAM&lt;/strong&gt; — you need to spot an over-privileged suggestion at a glance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legacy refactoring patterns&lt;/strong&gt; — seams, the strangler-fig pattern, working effectively with untested code. Large AI refactors need stepwise human strategy.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;References worth your time:&lt;/strong&gt; the &lt;a href=&quot;https://code.visualstudio.com/docs/copilot/overview&quot;&gt;VS Code Copilot documentation&lt;/a&gt; and &lt;a href=&quot;https://code.visualstudio.com/docs/copilot/copilot-coding-agent&quot;&gt;agents guide&lt;/a&gt;, plus &lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview&quot;&gt;Anthropic&apos;s prompt-engineering docs&lt;/a&gt; — most of it transfers directly to any coding assistant.&lt;/p&gt;
&lt;h2&gt;15. Expert-Level Pitfalls&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Avoid vibe coding.&lt;/strong&gt; Never accept code because it looks clean and the AI sounds confident. Run the tests. Confidence is a UI feature, not a correctness signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combat context rot.&lt;/strong&gt; Models get less sharp as the window fills. &lt;code&gt;/compact&lt;/code&gt; often; new chat per ticket.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use cross-repo awareness deliberately.&lt;/strong&gt; &lt;code&gt;@workspace&lt;/code&gt; can trace how service A&apos;s schema change breaks service B — ask for the blast radius before you change shared contracts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Appendix: Quick Prompt Snippets&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Diff control&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Minimal diff:&lt;/strong&gt; &quot;Change ONLY the code inside the target function. No unrelated refactors, no reformatting, no import changes unless strictly required.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;No new dependencies:&lt;/strong&gt; &quot;Use only libraries already in @pyproject.toml / @package.json. If a task requires a new one, stop and ask.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask before guessing:&lt;/strong&gt; &quot;If unsure about a repo convention or internal API, ask a clarifying question. I prefer a question over a hallucination.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Python &amp;amp; CLI&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Type-strict:&lt;/strong&gt; &quot;Implement with full type hints; explicit return types everywhere.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Click hygiene:&lt;/strong&gt; &quot;Every @click.option gets a help string and a defined type, matching the existing help-text style.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Least-privilege IAM:&lt;/strong&gt; &quot;Generate the IAM policy with no wildcards for actions or resources. Use specific ARNs.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CDK assertions:&lt;/strong&gt; &quot;Test with the assertions library, focusing has_resource_properties on encryption and public-access settings. No snapshots.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quality &amp;amp; debugging&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Manual verification checklist:&lt;/strong&gt; &quot;Give me a checklist to verify manually: happy-path test, an error state to trigger, and logs to monitor.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Log forensics:&lt;/strong&gt; &quot;Analyze this stack trace against #file. Give the most likely failing line and 3 ranked hypotheses.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Blast radius:&lt;/strong&gt; &quot;If I change the return type of get_user_auth in @auth.py, trace all downstream dependencies across @workspace and list every file needing a signature update.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This playbook is a snapshot of 2026. The models will change; the discipline — anchor, constrain, verify, never blind-merge — won&apos;t.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>ai-assisted-coding</category><category>github-copilot</category><category>prompting</category><category>developer-productivity</category></item><item><title>uv: One Tool to Rule Your Python Workflow</title><link>https://riddam.github.io/engineering/uv-one-tool-to-rule-your-python/</link><guid isPermaLink="true">https://riddam.github.io/engineering/uv-one-tool-to-rule-your-python/</guid><description>How uv replaces pyenv, venv, pip, pip-tools, pipx, and most of Poetry with a single fast tool — with a quick-start, a migration path, and a replacement cheat sheet.</description><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every Python developer eventually accumulates the same tool zoo: pyenv to install Python versions, venv to isolate projects, pip to install packages, pip-tools to lock them, pipx to run CLI tools, and maybe Poetry to hold the whole thing together. Each tool is fine. The &lt;em&gt;pile&lt;/em&gt; is not — six tools, six config styles, six ways for CI to differ from your laptop.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/astral-sh/uv&quot;&gt;uv&lt;/a&gt;, from Astral (the makers of ruff), replaces the pile with one tool, written in Rust, and it is &lt;em&gt;fast&lt;/em&gt; — dependency resolution and installs are routinely 10–100× quicker than pip with a warm cache. This post covers what it replaces, how to start, and how to migrate incrementally. (It pairs well with my tour of &lt;a href=&quot;https://riddam.github.io/engineering/python-313-314-whats-new/&quot;&gt;what&apos;s new in Python 3.13 and 3.14&lt;/a&gt;.)&lt;/p&gt;
&lt;h2&gt;What uv actually is&lt;/h2&gt;
&lt;p&gt;One binary that can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Install and manage Python versions&lt;/strong&gt; — no more pyenv, no more system-Python roulette.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create and manage isolated environments&lt;/strong&gt; per project, automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manage dependencies&lt;/strong&gt; with a universal lockfile — add, remove, lock, sync.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run tools in isolation&lt;/strong&gt; — linters and formatters without polluting your project env.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speak pip&lt;/strong&gt; — a &lt;code&gt;uv pip&lt;/code&gt; interface for incremental adoption with zero workflow change.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The kitchen analogy.&lt;/strong&gt; If your development environment is a kitchen, the classic setup has a separate gadget for every job — one machine to chop, one to time, one to weigh. uv is the multifunction appliance: one interface, every job, and it happens to work faster than the gadgets it replaced.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
  A[&quot;pyenv&amp;lt;br/&amp;gt;Python versions&quot;] --&amp;gt; U[&quot;uv&amp;lt;br/&amp;gt;one fast binary&quot;]
  B[&quot;venv&amp;lt;br/&amp;gt;environments&quot;] --&amp;gt; U
  C[&quot;pip&amp;lt;br/&amp;gt;packages&quot;] --&amp;gt; U
  D[&quot;pip-tools&amp;lt;br/&amp;gt;lockfiles&quot;] --&amp;gt; U
  E[&quot;pipx&amp;lt;br/&amp;gt;isolated CLI tools&quot;] --&amp;gt; U
  F[&quot;Poetry&amp;lt;br/&amp;gt;project management&quot;] --&amp;gt;|&quot;mostly&quot;| U
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Quick start&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Install and pin a Python version for your project
uv python install 3.14
uv python pin 3.14

# Initialize a project, add dependencies, lock, sync, test
uv init myproj &amp;amp;&amp;amp; cd myproj
uv add fastapi uvicorn
uv lock
uv sync
uv run pytest -q

# Run tools without touching your project environment
uvx ruff check .
uv tool install black
uv tool list

# Or use pip-style commands for incremental adoption
uv venv .venv &amp;amp;&amp;amp; source .venv/bin/activate
uv pip install -r requirements.txt
uv pip freeze &amp;gt; requirements.lock.txt
uv pip sync requirements.lock.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two commands do most of the daily work: &lt;code&gt;uv add &amp;lt;package&amp;gt;&lt;/code&gt; (updates &lt;code&gt;pyproject.toml&lt;/code&gt;, resolves, locks, and syncs in one step) and &lt;code&gt;uv run &amp;lt;command&amp;gt;&lt;/code&gt; (runs anything inside the project environment, creating it on demand — you may never type &lt;code&gt;source .venv/bin/activate&lt;/code&gt; again).&lt;/p&gt;
&lt;h2&gt;The replacement cheat sheet&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Traditional tool&lt;/th&gt;
&lt;th&gt;What it did&lt;/th&gt;
&lt;th&gt;uv equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pyenv&lt;/td&gt;
&lt;td&gt;Install/switch Python versions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv python install&lt;/code&gt;, &lt;code&gt;uv python pin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;venv / virtualenv&lt;/td&gt;
&lt;td&gt;Create isolated environments&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv venv&lt;/code&gt; (or implicit via &lt;code&gt;uv run&lt;/code&gt;/&lt;code&gt;uv sync&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pip&lt;/td&gt;
&lt;td&gt;Install and uninstall packages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv pip install&lt;/code&gt;, &lt;code&gt;uv pip uninstall&lt;/code&gt;, or &lt;code&gt;uv add&lt;/code&gt;/&lt;code&gt;uv remove&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pip-tools&lt;/td&gt;
&lt;td&gt;Lock and synchronize dependencies&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv lock&lt;/code&gt;, &lt;code&gt;uv sync&lt;/code&gt;, &lt;code&gt;uv pip sync&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pipx&lt;/td&gt;
&lt;td&gt;Run CLI tools in isolation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uvx&lt;/code&gt;, &lt;code&gt;uv tool install&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poetry (mostly)&lt;/td&gt;
&lt;td&gt;Project deps, lockfiles, scripts, publishing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv init&lt;/code&gt;, &lt;code&gt;uv add&lt;/code&gt;, &lt;code&gt;uv lock&lt;/code&gt;, &lt;code&gt;uv run&lt;/code&gt;, &lt;code&gt;uv build&lt;/code&gt;, &lt;code&gt;uv publish&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A few notes on the edges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pipx → uvx&lt;/strong&gt; is the easiest win: &lt;code&gt;uvx ruff check .&lt;/code&gt; downloads, isolates, caches, and runs in one shot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poetry &quot;mostly&quot;&lt;/strong&gt;: uv covers dependency management, lockfiles, scripts, workspaces, building, and publishing. If you rely on Poetry plugins, check for equivalents before switching.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The lockfile&lt;/strong&gt; (&lt;code&gt;uv.lock&lt;/code&gt;) is cross-platform and universal — one lockfile resolves for macOS, Linux, and Windows simultaneously, which ends a classic CI-versus-laptop drift.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Who gets what out of it&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Platform / infrastructure teams&lt;/strong&gt;: reproducible, predictable builds — the lockfile plus pinned Python version means &quot;works on my machine&quot; finally means something.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DevOps / CI&lt;/strong&gt;: fewer tools to install in pipelines, and dramatically faster installs. A cold &lt;code&gt;uv sync&lt;/code&gt; frequently turns a minutes-long pip step into seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SREs&lt;/strong&gt;: isolated tool execution (&lt;code&gt;uvx&lt;/code&gt;) means diagnostic tools never contaminate an app environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developers&lt;/strong&gt;: one mental model, one config file, no environment-activation rituals.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;An incremental migration path&lt;/h2&gt;
&lt;p&gt;You don&apos;t need a big-bang migration. This is the sequence I recommend:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pilot&lt;/strong&gt; — pick one small project. &lt;code&gt;uv init&lt;/code&gt;, &lt;code&gt;uv add&lt;/code&gt; its dependencies, run its tests with &lt;code&gt;uv run pytest&lt;/code&gt;. You&apos;ll learn the model in an hour.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace pipx first&lt;/strong&gt; — switch your global tools (&lt;code&gt;ruff&lt;/code&gt;, &lt;code&gt;black&lt;/code&gt;, &lt;code&gt;pre-commit&lt;/code&gt;) to &lt;code&gt;uvx&lt;/code&gt;/&lt;code&gt;uv tool install&lt;/code&gt;. Zero risk, immediate speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adopt pip-compatibly&lt;/strong&gt; — keep your existing &lt;code&gt;requirements.txt&lt;/code&gt; workflow, but run it through &lt;code&gt;uv pip install&lt;/code&gt;/&lt;code&gt;uv pip sync&lt;/code&gt;. Same files, much faster, no team retraining.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Move to lockfile workflows&lt;/strong&gt; — when the team is comfortable, migrate to &lt;code&gt;pyproject.toml&lt;/code&gt; + &lt;code&gt;uv lock&lt;/code&gt; + &lt;code&gt;uv sync&lt;/code&gt; as the source of truth, and update CI to &lt;code&gt;uv sync --frozen&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pin Python per project&lt;/strong&gt; — &lt;code&gt;uv python pin&lt;/code&gt; ends the &quot;which Python is this using?&quot; class of bugs.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;uv is developed at a rapid pace — pin the uv version in CI (&lt;code&gt;uv self version&lt;/code&gt; locally, a pinned installer version in pipelines) so a tool upgrade never surprises a deploy.&lt;/li&gt;
&lt;li&gt;Some Poetry-specific workflows (custom plugins, some dynamic-versioning setups) need rework rather than translation.&lt;/li&gt;
&lt;li&gt;Corporate proxies and private indexes work well (&lt;code&gt;UV_DEFAULT_INDEX&lt;/code&gt; / &lt;code&gt;UV_INDEX&lt;/code&gt;, or &lt;code&gt;[[tool.uv.index]]&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt;), but test them in your pilot before rolling out broadly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The bottom line: uv consolidates pyenv, venv, pip, pip-tools, pipx, and most of Poetry into a single fast, coherent utility. Of everything that&apos;s happened in Python tooling in the past few years, this is the change with the highest payoff-to-effort ratio — the pilot costs you an afternoon, and it&apos;s the rare tool that&apos;s simultaneously simpler &lt;em&gt;and&lt;/em&gt; faster than what it replaces.&lt;/p&gt;
&lt;p&gt;For the full documentation, see the &lt;a href=&quot;https://github.com/astral-sh/uv&quot;&gt;official uv repository&lt;/a&gt; and &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>python</category><category>uv</category><category>tooling</category><category>developer-productivity</category></item><item><title>What&apos;s Actually New in Python 3.13 and 3.14</title><link>https://riddam.github.io/engineering/python-313-314-whats-new/</link><guid isPermaLink="true">https://riddam.github.io/engineering/python-313-314-whats-new/</guid><description>A plain-language tour of the two most consequential Python releases in years — free-threading, JIT compilation, subinterpreters, t-strings, deferred annotations, and what to do about them.</description><pubDate>Sat, 10 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Python 3.13 (October 2024) and 3.14 (October 2025) are the two most consequential releases the language has had in years. Between them, they touch the thing people have complained about for two decades — the Global Interpreter Lock — and quietly modernize half of the daily developer experience: the REPL, error messages, type annotations, string templating, debugging, and compression.&lt;/p&gt;
&lt;p&gt;This post is a plain-language tour of what changed and what it means in practice. If you also want to modernize your &lt;em&gt;tooling&lt;/em&gt;, read the companion post on &lt;a href=&quot;https://riddam.github.io/engineering/uv-one-tool-to-rule-your-python/&quot;&gt;uv, the tool that replaces pyenv, pip, venv, and pipx&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The ten things worth knowing&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Python 3.13 shipped a &lt;strong&gt;redesigned interactive shell&lt;/strong&gt; — colors, multi-line editing, sane shortcuts — and even &lt;strong&gt;friendlier error messages&lt;/strong&gt; with &quot;did you mean…?&quot; suggestions.&lt;/li&gt;
&lt;li&gt;An experimental 3.13 build &lt;strong&gt;removes the GIL&lt;/strong&gt; (free-threading), enabling real parallelism across CPU cores; in &lt;strong&gt;3.14 free-threading became officially supported&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JIT compilation&lt;/strong&gt; landed in 3.13 as an experimental, off-by-default foundation for future speedups.&lt;/li&gt;
&lt;li&gt;3.14 makes &lt;strong&gt;type annotations lazy&lt;/strong&gt; (deferred evaluation), improving startup time and killing a whole class of annotation workarounds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subinterpreters&lt;/strong&gt; — multiple isolated Python interpreters in one process — are now usable from Python code, giving you another concurrency option.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Template strings (t-strings)&lt;/strong&gt; in 3.14 make dynamic content — HTML, SQL — safer to build than f-strings.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;incremental garbage collector&lt;/strong&gt; shipped in 3.14.0 to reduce pause times in large, long-running applications — but it was &lt;strong&gt;reverted in 3.14.5 (June 2026)&lt;/strong&gt; back to the 3.13-style generational GC after memory-pressure regressions, with possible reintroduction in a later release. (Accurate at publication; corrected since.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zstandard (zstd) compression&lt;/strong&gt; is now in the standard library — faster and tighter than gzip for most workloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live-process debugging&lt;/strong&gt;: 3.14 lets you attach a debugger to a &lt;em&gt;running&lt;/em&gt; Python process — including, carefully, in production.&lt;/li&gt;
&lt;li&gt;Both releases &lt;strong&gt;removed long-deprecated modules&lt;/strong&gt; — the &quot;dead batteries&quot; cleanup — so legacy code needs a checkup before upgrading.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Python 3.13: quality of life, plus two experiments&lt;/h2&gt;
&lt;h3&gt;The new REPL and better errors&lt;/h3&gt;
&lt;p&gt;The interactive shell finally caught up with the decade: multi-line editing that works, color-coded output and tracebacks, &lt;code&gt;exit&lt;/code&gt; that just exits. Error messages continued their glow-up — misspell a keyword argument or a module attribute and Python now suggests what you probably meant. Small thing, huge cumulative time savings, especially for people learning the language.&lt;/p&gt;
&lt;h3&gt;Free-threading: the GIL becomes optional&lt;/h3&gt;
&lt;p&gt;The Global Interpreter Lock has always meant one thread executes Python bytecode at a time — multithreaded Python could wait in parallel but not &lt;em&gt;compute&lt;/em&gt; in parallel. Python 3.13 shipped a separate experimental build (&lt;code&gt;python3.13t&lt;/code&gt;) with the GIL removed.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The bridge analogy.&lt;/strong&gt; Picture a bridge that only ever allowed one vehicle to cross at a time, no matter how many lanes it had. Free-threading opens all the lanes. But some older vehicles — C extensions written assuming the GIL — aren&apos;t certified for the new bridge yet, so adoption needs care.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In 3.13 this was strictly experimental: single-threaded code ran noticeably slower on the free-threaded build, and many C extensions weren&apos;t compatible. &lt;strong&gt;In 3.14, free-threading was promoted to officially supported&lt;/strong&gt; (PEP 779) — still a separate build, but no longer a science project: the single-threaded penalty shrank substantially and the major scientific and web libraries have been landing support. If you have genuinely CPU-bound multithreaded workloads, 3.14&apos;s free-threaded build is now worth a real evaluation.&lt;/p&gt;
&lt;h3&gt;JIT compilation: the foundation is laid&lt;/h3&gt;
&lt;p&gt;3.13 added an experimental just-in-time compiler (off by default, enabled at build time). Gains are modest so far — this release was about landing the architecture, not the speed.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The barista analogy.&lt;/strong&gt; A new barista makes your regular order by reading the recipe every time. After a hundred repetitions, they&apos;ve internalized it and it&apos;s faster. That&apos;s the JIT: it watches which code paths run hot and compiles them to faster machine code as the program runs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Expect this to compound over the next several releases rather than deliver a big bang.&lt;/p&gt;
&lt;h3&gt;Housekeeping&lt;/h3&gt;
&lt;p&gt;3.13 completed the &quot;dead batteries&quot; removal (PEP 594) — nineteen crusty stdlib modules (&lt;code&gt;cgi&lt;/code&gt;, &lt;code&gt;telnetlib&lt;/code&gt;, &lt;code&gt;pipes&lt;/code&gt;, …) are gone. It also made frame locals behave predictably (PEP 667), which matters if you use debuggers or clever &lt;code&gt;locals()&lt;/code&gt; tricks.&lt;/p&gt;
&lt;h2&gt;Python 3.14: the features release&lt;/h2&gt;
&lt;h3&gt;Deferred annotations: type hints stop costing you at startup&lt;/h3&gt;
&lt;p&gt;Until now, Python evaluated every type annotation eagerly at import time — whether or not anything ever looked at them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The buffet analogy.&lt;/strong&gt; Imagine preparing a full buffet every morning regardless of whether anyone eats. Deferred annotations (PEP 649/749) cook the dish only when someone orders it: annotations are stored lazily and evaluated on access.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Practical wins: faster imports, no more &lt;code&gt;from __future__ import annotations&lt;/code&gt; dance, forward references that just work, and the end of quoting-your-own-class-name in method signatures.&lt;/p&gt;
&lt;h3&gt;Subinterpreters: concurrency without shared chaos&lt;/h3&gt;
&lt;p&gt;PEP 734 exposes multiple isolated interpreters inside a single process to pure Python code (&lt;code&gt;concurrent.interpreters&lt;/code&gt;), each with its own GIL.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The office analogy.&lt;/strong&gt; A large building where each team works in its own soundproof room — no interference, shared infrastructure. That&apos;s subinterpreters: isolation like multiprocessing, but cheaper, in one process.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Together with free-threading, Python now has a genuine menu of concurrency options: asyncio for I/O-bound, free-threading for shared-memory parallelism, subinterpreters for isolated parallelism, multiprocessing when you want OS-level separation.&lt;/p&gt;
&lt;h3&gt;Template strings: f-strings that don&apos;t blend everything together&lt;/h3&gt;
&lt;p&gt;PEP 750 adds t-strings: &lt;code&gt;t&quot;Hello {name}&quot;&lt;/code&gt; produces a &lt;em&gt;template object&lt;/em&gt; instead of a final string, so the consuming code can see — and safely escape — each interpolated value before assembly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The smoothie vs. LEGO analogy.&lt;/strong&gt; An f-string is a smoothie: once blended, you can&apos;t separate the ingredients, so if one of them was user input headed for SQL or HTML, it&apos;s already too late. A t-string is LEGO: every block stays visible and inspectable until &lt;em&gt;you&lt;/em&gt; decide how to snap them together.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the foundation for injection-safe HTML templating and SQL building in libraries — expect frameworks to adopt it broadly.&lt;/p&gt;
&lt;h3&gt;Debugging, memory, and compression&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Attach to a live process&lt;/strong&gt; (PEP 768): a zero-overhead-when-idle interface lets debuggers safely attach to a running Python program — &lt;code&gt;pdb&lt;/code&gt; can now connect to a live PID. For diagnosing a wedged production process, this is gold.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incremental garbage collection&lt;/strong&gt;: 3.14.0 introduced an incremental collector for shorter GC pauses and smoother latency on large heaps. &lt;strong&gt;Update:&lt;/strong&gt; this was &lt;strong&gt;reverted in 3.14.5 (June 2026)&lt;/strong&gt; — memory-pressure regressions led the core team to restore the 3.13-style generational collector, with the incremental approach possibly returning in a future release. This post was accurate when published in January 2026; on current 3.14 (3.14.5+) you&apos;re back on the generational GC. See the &lt;a href=&quot;https://discuss.python.org/t/reverting-the-incremental-gc-in-python-3-14-and-3-15/107014&quot;&gt;discussion on reverting the incremental GC&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;compression.zstd&lt;/code&gt;&lt;/strong&gt; (PEP 784): Zstandard in the stdlib — dramatically faster than gzip at comparable or better ratios, with &lt;code&gt;tarfile&lt;/code&gt;/&lt;code&gt;zipfile&lt;/code&gt;/&lt;code&gt;shutil&lt;/code&gt; integration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The REPL got syntax highlighting&lt;/strong&gt;, and error messages got another round of polish.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How I&apos;d adopt this&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Upgrade path&lt;/strong&gt;: if you&apos;re on 3.11/3.12, target 3.13 or 3.14 directly — the upgrade friction is mostly deprecated-module removals. Run your test suite with &lt;code&gt;-W error::DeprecationWarning&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Try the new REPL and error messages&lt;/strong&gt; — no action needed, they&apos;re the default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deferred annotations&lt;/strong&gt;: on 3.14, delete &lt;code&gt;from __future__ import annotations&lt;/code&gt; and un-quote forward references as you touch files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free-threading&lt;/strong&gt;: evaluate the 3.14 free-threaded build on a CPU-bound workload in a branch. Check your C-extension dependencies for compatibility first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;t-strings&lt;/strong&gt;: adopt where you build HTML or SQL by hand; watch your web framework&apos;s release notes for native support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;zstd&lt;/strong&gt;: swap in for gzip anywhere you compress logs, artifacts, or datasets.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Gotchas to check before upgrading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Code importing removed &quot;dead battery&quot; modules (&lt;code&gt;cgi&lt;/code&gt;, &lt;code&gt;telnetlib&lt;/code&gt;, &lt;code&gt;imghdr&lt;/code&gt;, …) breaks on 3.13 — most have drop-in PyPI replacements.&lt;/li&gt;
&lt;li&gt;Tools that poke at frame locals (custom debuggers, profilers) may need updates for the PEP 667 semantics.&lt;/li&gt;
&lt;li&gt;Custom GC tuning (&lt;code&gt;gc.set_threshold&lt;/code&gt; calls) deserves a re-benchmark after any upgrade. Note that the incremental collector added in 3.14.0 was reverted in 3.14.5, so current 3.14 is back on the generational collector — if you tuned for the brief incremental window, re-check against generational behavior.&lt;/li&gt;
&lt;li&gt;The free-threaded build requires compatible wheels — a dependency without a &lt;code&gt;cp314t&lt;/code&gt; wheel means compiling from source or waiting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both releases reward the same habit: upgrade early in a branch, run the tests loudly, and read the &lt;a href=&quot;https://docs.python.org/3/whatsnew/3.14.html&quot;&gt;&quot;What&apos;s New&quot; documents&lt;/a&gt; — they&apos;re genuinely well-written.&lt;/p&gt;
</content:encoded><category>python</category><category>python-3-14</category><category>free-threading</category><category>release-notes</category></item></channel></rss>