Infrastructure in a real programming language, and what it costs you

Loops, types and tests come free with a general-purpose language. What you give up is a plan a non-programmer can review, and a low floor for the people who have to maintain it at three in the morning.

The pitch is genuinely appealing. You already know TypeScript or Python, so why learn a domain-specific language with awkward loops, no real functions, and a type system you fight. Write infrastructure in a language with an IDE, a debugger, a package manager and a test framework.

That pitch is accurate. What it leaves out is that the properties making a programming language powerful are the same ones making infrastructure code hard to review, and infrastructure code is read under pressure by people who did not write it.

What is actually the same

Both tools build a dependency graph from your definitions, diff it against recorded state, and produce a plan of creates, updates and deletes before applying anything. Both store state that must be protected and locked. Both talk to cloud APIs through providers, and Pulumi can consume Terraform providers, so coverage is comparable.

The state discipline, the pipeline patterns, the drift problem and the review requirement are identical. If your state layout and pipeline are wrong, switching tools does not help, which is why the patterns in Terraform state and pipelines on AWS apply either way.

So this is not a choice between paradigms of infrastructure management. It is a choice of authoring language on top of the same model.

What you gain

Loops and conditionals that behave normally. Generating twenty similar resources from a data structure is a for loop. Filtering a list is a filter. Nobody has to remember which meta-argument reorders resources when an element is removed.

Real abstraction. Functions, classes and interfaces. A component that encapsulates a pattern is an object with a constructor, and composing components is composing objects. On a large estate with strong internal conventions this is genuinely more expressive than modules.

Types and an IDE. Autocomplete on provider arguments, compile-time errors for a misspelled property, refactoring that works. This is the benefit people notice first and it is real.

Testing with normal tools. Unit tests on your component logic with the test framework you already use, mocking the provider. Compared with the pyramid in testing infrastructure code, the unit layer is considerably easier.

Sharing code with the application. The same constants, the same validation, the same client libraries. For a team building a platform product rather than managing an estate, this matters.

What you lose

The plan stops being trivially reviewable. This is the serious one. A declarative configuration is close to a description of the result, so a reviewer can read a diff and reason about it. A program that computes resources requires the reviewer to execute it mentally. The plan output is still there and still the artefact you review, but the connection between the source change and the plan is less direct, and the review requirement is precisely the one described in reading a Terraform plan is not the same as seeing it.

The floor gets higher. Infrastructure code is maintained by whoever is on call. A declarative file has a low barrier: someone who does not write TypeScript can still find the instance size and change it. A program with inheritance, generics and a helper module does not have that property.

Infrastructure becomes software, with software's failure modes. Dependencies, transitive dependencies, version conflicts, a package with a build step. Cleverness is available, and it will be used. The worst infrastructure code we have seen was written in a general-purpose language by someone enjoying themselves.

The ecosystem is smaller. More modules, more examples, more Stack Overflow answers, more policy and cost tooling, and more available engineers exist for the declarative side. That asymmetry is a real cost when hiring or when handing work over.

Choose by who maintains it

The technical comparison rarely decides this. The team does.

Pick the declarative tool when the people maintaining infrastructure are a mixed group including engineers who do not primarily program, when you want reviews that a platform lead can do quickly, when you value ecosystem depth, or when auditors and security reviewers read your configuration. Most organisations are here.

Pick the programming language when the team is strongly software-engineering oriented, when you are building a platform that generates infrastructure programmatically for others, when your patterns genuinely need abstraction that modules express badly, or when the same team owns the application and the infrastructure and shares code between them.

Pick the cloud-native option when you are on one cloud and expect to stay. Bicep on Azure and CloudFormation on AWS have first-party support, appear on day one for new services, and need no state management. The trade is portability, and the honest comparison for one of them is in Bicep or Terraform for Azure.

If you choose the programming language, impose constraints

Everything that goes wrong here goes wrong through excess. A few rules keep it maintainable.

Keep it boring. Loops, functions and a small number of components. No metaprogramming, no deep inheritance, no runtime resource generation that a reader cannot follow.

Put the plan in the pull request and require it in review. The plan is the source of truth for what will happen, and the code is the source of truth for why.

Pin every dependency, including transitive ones, and treat the dependency tree as a supply chain. Code that runs in your pipeline with credentials to create infrastructure deserves the scrutiny described in OSV-Scanner and SBOMs.

Write the unit tests. It is the main technical advantage of the choice, and teams routinely do not take it, which leaves them with the costs and none of the benefit.

Keep configuration data out of code. Environment differences belong in configuration files, not in conditionals scattered through the program.

The things people forget

  • Migration between them is possible and not free. Both can import existing resources and there are conversion paths, but the work is in re-expressing structure, not in translating syntax.
  • State is still the crown jewel. The managed state service is a hosted dependency with its own availability and its own price; the self-managed backend is your responsibility.
  • Policy tooling differs. Check that your policy and cost checks support the tool before you commit, because losing the plan-level gate is a real regression.
  • A monorepo of infrastructure in a language invites cross-imports. Sharing a helper across two stacks that should be independent is easier than it should be.
  • Onboarding time is a real cost. Measure how long it takes a new engineer to make a safe change, in both.

What to do this week

Take one representative stack and have someone who did not write it read the source and predict what a plan would produce. Then run the plan. The gap between the prediction and the output is the reviewability cost, and it is the number that should decide this for you. We run that exercise during the platform review in a cloud engagement.

ConsultorIA

Want this done on your cloud?

A ten-day read-only assessment is free, and Skyline lets you see your estate on a map before you write to us.

Related articles

Modules people reuse instead of copying

The two failures are a module that wraps one resource and adds nothing, and a module that does everything and nobody dares change. A minimal interface, safe defaults and honest versioning are what separate them.