← All posts
Infrastructure

Infrastructure as Code May Say Everything Is Fine. Sometimes It's Wrong.

Claude's infrastructure template passed the dry run clean, then the real deploy failed five times over. A green preview proves the template is consistent, not that it will deploy, and knowing that gap is the review I had to bring.

The promise of infrastructure as code is that you describe what you want, run a preview to check it, and deploy with confidence. Being rigidly defined and deeply documented, IaC in Azure with Bicep seems like the exact place Claude would excel. So I had it write a template for some new infrastructure and the build and what-if came back clean. The first real deploy, however, reminded me where the promise has a hole and the types of blindspots Claude often has.

The dry run passes, the deploy fails, one trap at a time

The first from-scratch deploy failed, we fixed it, and it failed again on something new. That repeated several times, and the structure of the pain is worth naming: each failure aborts the deployment before the later resources are even attempted, so you discover the problems one per retry rather than all at once. It feels like the system is hazing you.

The first failure was permissions. The deployment identity had Contributor rights, which sounds like plenty, but Contributor explicitly cannot create role assignments. The whole keyless design depends on the template granting the app’s identity permission to read secrets and storage, which is itself a role assignment, so the deploy needed a higher role to hand out roles. The dry run had no way to know that, because it does not evaluate your identity’s permissions.

The second failure was that the chosen database, a managed Postgres flexible server, was offer-restricted in the region I picked. Other resource types deployed there fine. This specific offer was blocked for my subscription in that region, and it is the kind of constraint that exists only on the live provider, so the template compiled, the dry run passed, and only the actual provisioning call failed.

Then, when I changed regions to dodge that, a third trap sprang. A subscription-level deployment is stored as a named record, the default name is shared, and its location is immutable once set. Re-running with a new region reused the existing name and the platform rejected the location change. The fix was to give every deployment a unique name per environment and per run, which also stops two environments from clobbering each other’s deployment history.

The fourth was almost funny by then. The template asked for a staging slot on a Basic-tier app service, and Basic does not support slots. The dry run did not flag it. The deploy did.

The traps that only appear on the second deploy

It got more interesting once the environment existed, because a whole new class of failure only shows up on a re-deploy. A redeploy to flip one setting made the database module fail, complaining that the server was not in an accessible state, while a status check insisted the server was perfectly ready. The cause was that the template declared two administrators on the database with no ordering between them, and the platform deploys independent resources in parallel. Each administrator write briefly makes the server inaccessible, so when both fire at once, the second one collides with the window the first one opened. The reason it had worked the first time is that on a from-scratch run the server takes minutes to create, which naturally spaced the two writes apart. On a re-deploy the server already existed, so the writes launched together and reliably collided. The fix was one line forcing the second write to wait for the first.

Separately, bringing a TLS certificate I had created by hand in the portal under code management failed with a duplicate conflict. The platform enforces one managed certificate per hostname per plan, and the code declared a new certificate resource with a different name but the same hostname, so the platform saw a duplicate rather than an adoption. The dry run cheerfully showed it as a clean creation and missed the conflict entirely. The fix was to delete the hand-made certificate first, or to name the code resource to exactly match the existing one so the platform updates in place instead of duplicating.

The two habits that actually help

Out of all of this, three habits matter more than memorizing any single trap.

The first and most important is to remember that Claude has blindspots and needs review and guidance. Build your attention into your AI process or the inevitable error logs will demand it later.

Second, never trust the dry run as proof that a deploy will succeed. It proves your template is internally consistent and diffs it against what is already deployed. What it cannot see is your permissions, your region’s offer restrictions, immutable properties, parallel-write timing races, or platform rules like one managed certificate per hostname. Treat a clean preview as “the syntax is fine and the diff looks right,” not “this will work.”

Finally, always read past the error you are handed. Every one of these surfaced first as a generic, useless top-level “deployment failed.” The actionable message was one or two levels down, in the nested operation details, and it would be easy to lose hours staring at the wrapper. The real error is almost never in the headline. It is in the operation that the headline is summarizing, and learning to drill straight to it is the most useful infrastructure debugging skill I bring to a deploy the assistant swears is ready.