If infrastructure is written in a general-purpose programming language such as Python or TypeScript, does that make the approach imperative rather than declarative?
answer
- authoring surface versus execution model
- the loops run before anything deploys
- program emits a description, engine diffs it
- direct SDK calls are the truly imperative case
- review the synthesised output, not just the source
basics
~20 sNo. The authoring language and the deployment model are separate axes. A program with loops and conditionals can still produce a complete description of desired state, which an engine then compares against reality before changing anything.
solid answer
~50 sThe two things get conflated all the time, but they are independent. Writing infrastructure in a general-purpose language means an ordinary program executes on your machine or in CI — with loops, functions, tests and type checking — and its *output* is a full description of the resources that should exist. That description is then handed to an engine that compares it against what is there and applies a diff. The deployment is as declarative as any markup-authored equivalent, because the model of desired state is complete and removing a resource from the program removes it from the estate. What would genuinely be imperative is a program that calls create and delete APIs directly: no model, no diff, no preview, and no notion of what should stop existing. So the test is not "is there a for loop?" but "does the tool end up holding a complete statement of intent that it reconciles against reality?"
go deeper
Know that the language you write in and the way the change is applied are different things. A program can still describe the end state rather than perform the steps.
Explain the sequence: the program runs first and emits a complete resource description, then an engine diffs that against reality and applies it. Contrast it with a script that calls create APIs directly and has nothing to diff.
Raise what the expressiveness costs: the source no longer reads as the result, synthesis can be non-deterministic if it touches the clock or the network, and review has to move to the synthesised output or the preview.
Own the choice for the organisation. Weigh abstraction and testability against who can safely read and change production infrastructure, and set the review controls — pinned dependencies, deterministic synthesis, diff review — that make the more expressive surface safe at scale.
## Two axes, not one People collapse two independent questions into one: 1. **What do I write in?** Markup, a domain-specific language, or a general-purpose programming language. 2. **How does the change get made?** By handing a complete desired-state description to an engine that diffs and applies it — or by making API calls directly. The first is the *authoring surface*. The second is the *execution model*, and only the second decides whether the approach is declarative. ## What a general-purpose-language IaC tool actually does The program runs first. It is a normal program: it can loop over a list of environments, call a function to build a standard service, branch on a flag, be unit tested, be type checked by a compiler, be refactored by an IDE. What it *produces*, though, is not a stream of API calls. It builds up an in-memory graph of resource declarations, and a synthesis step turns that graph into a complete description of desired state — in the AWS CDK's case, a CloudFormation template that the CloudFormation service then deploys; in Pulumi's case, a resource graph its engine diffs against the recorded state of the stack. So the sequence is: **imperative program → declarative description → diff → apply.** The loops all ran before anything was deployed. By the time infrastructure changes, the tool is holding a full statement of what should exist, and every declarative property follows from that: a preview of what will change, deletion of resources you stopped declaring, and no duplication on re-run. ## What genuinely imperative looks like A script that calls a cloud SDK to create a bucket, then create a queue, then attach a policy. Same language, completely different model: - There is no description of the whole, only a sequence of effects. - Nothing can be previewed, because the only way to find out what the script does is to run it. - Removing lines removes nothing; the resources they created still exist. - Re-running duplicates or errors, unless you hand-wrote the checks. The language is identical in both cases. What differs is whether a model of desired state is built before anything is touched. ## The test to state in an interview **Does the tool end up with a complete description of what should exist, and does it compare that against reality before it changes anything?** If yes, it is declarative regardless of syntax. If no, it is a script regardless of how nicely it is structured. A sharper practical variant: **delete a chunk of code and re-run.** If the corresponding infrastructure disappears, the tool holds a desired-state model. ## What the general-purpose language actually buys and costs **Buys:** real abstraction (functions, classes, packages), reuse through the language's own package manager, static types catching mistakes before deploy, unit testing the code that builds the graph, and an escape from the ceiling that DSLs hit once configuration gets genuinely complex. **Costs:** the source is no longer directly readable as the result. With markup, what you see is roughly what you get; with a program, you have to run the synthesis to know what it produces. Arbitrary code at synthesis time can also be non-deterministic — reading the clock, hitting the network, depending on a local file — so the same commit can produce different infrastructure on different runs. And review shifts: reviewing the source tells you about the logic, not the outcome, so teams that take this seriously review the *diff of the synthesised output* or the preview, not just the pull request. There is a people dimension too. Markup-authored infrastructure is legible to operators who are not developers in that language; a codebase of classes and inheritance is not. That is a real tradeoff about who can safely change production, not just a matter of taste. ## The related confusion worth pre-empting The same conflation drives the claim that a markup or DSL-based tool becomes imperative once it grows loops, conditionals or functions. It does not. Expressiveness in the authoring surface says nothing about the execution model. Both families converge on the same architecture from opposite directions: DSLs gaining programming features, and programming languages gaining a declarative deployment engine. Neither movement changes what happens at apply time.
- So what property actually makes a tool declarative?That it holds a complete description of what should exist and compares it against reality before acting. Two behaviours follow and are worth naming: it can tell you in advance what will change, and removing something from the description removes it from the estate. A tool that only makes forward API calls can do neither, whatever its input format looks like.
- What risk does letting arbitrary code run at synthesis time introduce?Non-determinism and unreviewability. Code that reads the clock, calls the network, or depends on a local file can make the same commit produce different infrastructure on different runs. And reviewing the source tells you about the logic, not the outcome — which is why teams review the synthesised output or the preview diff rather than only the pull request.
- Does adding loops and conditionals to a markup or DSL-based tool make it imperative?No — that is the same conflation from the other direction. Expressiveness in the authoring surface is independent of the execution model. Both families are converging on the same architecture: DSLs gaining programming features, programming languages gaining a declarative deployment engine. Apply time works identically in either case.
saying these in an interview costs you the question
- Writing infrastructure in Python makes the deployment imperative
- A general-purpose language removes the need for a preview step
- Any code that calls a cloud SDK counts as infrastructure as code
- Declarative means the authoring language has no loops or functions
- Reviewing the source is enough; the synthesised output does not matter