The argument for defining infrastructure in a general-purpose language is easy to make badly. 'You get loops and functions' is true and is not, by itself, a reason — HCL has both, in a restricted form, and the restrictions are partly the point. The stronger argument is about the things that remain genuinely awkward in a configuration language: types checked before apply, real unit tests, shared abstractions with proper interfaces, and computation that needs an actual library.
I have run production estates both ways. Here is where each one is clearly better, without pretending the choice is obvious.
What a real language buys
The largest win is the abstraction with a checked interface. A platform team can publish a component — a service with its database, queue, autoscaling policy, alerts and dashboards — as a typed class with sensible defaults. Consumers pass five parameters, get the organisation's standards by construction, and find out at type-check time that they passed a string where a subnet was required. The equivalent module in a configuration language works, but the contract is documented rather than enforced.
class Service(pulumi.ComponentResource):
"""Golden path: one service, its data, its scaling, its alerts."""
def __init__(self, name: str, spec: ServiceSpec, opts=None):
super().__init__('platform:Service', name, None, opts)
child = pulumi.ResourceOptions(parent=self)
db = aws.rds.Instance(f'{name}-db',
instance_class=spec.db_size.value,
# Standards are enforced in code, not in a review checklist.
storage_encrypted=True,
backup_retention_period=30,
deletion_protection=spec.tier is Tier.PRODUCTION,
opts=child)
for queue in spec.queues: # an ordinary loop over typed input
Worker(f'{name}-{queue.name}', queue, db.endpoint, opts=child)
Alarms(name, spec.slo, opts=child) # every service gets alerting, always
self.register_outputs({'endpoint': db.endpoint})
# Tested like code, before anything is created:
def test_production_databases_are_protected():
svc = Service('billing', ServiceSpec(tier=Tier.PRODUCTION, ...))
assert svc.db.deletion_protection is TrueTesting infrastructure logic without provisioning anything is the second real benefit. Policy assertions — production databases have deletion protection, no bucket is public, every service has an alarm — become unit tests that run in seconds on every pull request, rather than a policy engine you hope is configured correctly.
What it costs
Expressiveness is not free. A configuration language makes it hard to write infrastructure that is clever, and infrastructure should not be clever. I have reviewed Python stacks with conditional resource creation three branches deep, where nobody could answer what would exist after an apply without running it. HCL makes that awkward on purpose, and that constraint has protected more estates than it has frustrated.
- Keep stack code declarative in shape. If a reader cannot predict the resource graph, the abstraction is wrong.
- State management is identical in difficulty either way — remote backends, locking, and a plan reviewed before every apply.
- The ecosystem of examples, modules and hiring familiarity is larger for the declarative tools. That is a real cost, especially on a small team.
- Refactoring that changes resource names causes replacement in both. Use explicit aliases and read the plan; this is where outages come from.
- Whichever you pick, the plan output is the safety mechanism. A team that applies without reading plans has chosen the wrong risk regardless of language.
The goal is not infrastructure that is elegant to write. It is infrastructure whose diff you can read at 3 a.m. and believe.
My rule of thumb: for a small number of stacks with limited variation, a declarative tool is simpler and the constraints are a feature. For a platform team publishing golden paths to dozens of product teams, with policy that must be enforced rather than documented, a real type system and a test suite are worth the discipline they demand. The deciding question is not which language you prefer — it is whether you are configuring infrastructure or building a platform.