Engineering Practices · CI/CD
Why I Split Provisioning from Application Deployments
Infrastructure and application code change at very different speeds. Deploying them together makes pipelines slower, riskier and harder to debug.
There is no right or wrong in software engineering, only trade-offs. When you decide to do something, you are deciding not to do something else. Mixing infrastructure provisioning with application deployments is one of those decisions, and we typically see teams make it without noticing the cost.
The Lifecycles Are Completely Different
Imagine an application with its own database, a service bus, an App Service plan and an App Service. Once you create those and set the connection strings, how often do you change them?Very rarely.
Now, if you are deploying often (which I would encourage you to), you are running that infrastructure change alongside every single application deployment. Why are we checking if the foundations moved every time we ship a bug fix?
The best analogy I have is a building. The infra is the foundations, the walls, even the lighting. The application is the furniture. Furniture moves all the time. Foundations stay static.
In my projects, the provisioning repo (Bicep or Terraform) changes once every few weeks. The application code changes multiple times a day.
“But How Do I Handle Environments?”
This is the usual argument for putting everything in Bicep. In .NET, you don’t need to. You haveappsettings.Production.json, appsettings.Staging.json, appsettings.Test.json. The only thing the infra provides is one environment variable per environment, and the application selects the right config.
The advantage is significant: everything is visible in the application code. Got a production issue? Point the environment variable to the production config locally, and your application is configured exactly like production.
What about secrets and connection strings? Those rarely belong in config. They live in Key Vault (or KMS on AWS) and the config holds only a reference to them. If you need to debug locally, you need access to the secrets, and that’s it.
Mixing Them Convolutes Your Pipeline
A pipeline that runs Bicep, runs unit tests, builds the code, publishes packages and publishes Docker images becomes increasingly complex. In my experience, the provisioning part is 30 to 40% of that pipeline, in runtime and in YAML. Remove it and the pipeline is smaller, faster and less flaky.Think about how much pipeline time goes into validating and deploying Bicep scripts that have no changes at all. We are deploying a new version of something that didn’t change, every single time.
And there is a nastier risk. If you run Terraform or Bicep in every deployment, you also run the risk of deleting resources in every deployment. Running a script that can delete your database just to ship a new button is a risk worth revisiting.
What Broke?
You deploy to production and something fails. Is it the Bicep or the application code? Did the permissions get applied? Did a resource get recreated?If the deployment only contains application code, you typically know straight away. Managing complexity is one of the most important things we do in software development, and this is complexity you can simply remove.
Yes, you introduce the risk of infra and application diverging. That’s manageable, and in most cases much cheaper than the alternative.
Let the Application Create Its Own Queues
Another common pattern worth revisiting: using Bicep to create queues and topics.I would rather have the application create them at startup. The queue names, the topics, the number of partitions, the subscriptions: those are all application concerns. If Bicep owns them, you now have to coordinate two things every time you add a queue, and that coordination exists even if they live in the same repo. If the application owns them, there is nothing to coordinate. It just needs the right managed identity permission on the service bus.
Much simpler, much easier.
How I Do It
Two repos:- provision: the Terraform or Bicep. Deploys the resources. Changes every few weeks.
- application: the code. Deploys on top of the provisioned resources. Changes daily.
Run provision first, then deploy the application. Can you do both in the same repo, spin up a new environment and deploy in one go? Of course, it can be done. It just adds complexity to every deployment to optimise for something that rarely happens.
Wrapping It Up
“There is no right or wrong, only trade-offs.” Thomas Sowell
Keeping infra and application together is convenient on day one. The trade-off is slower, flakier, bigger pipelines, a riskier blast radius and harder debugging on every deployment after that.
How much of your pipeline is deploying things that rarely change?