Terraform vs Pulumi: choosing IaC for security and environment separation
Terraform vs Pulumi: how to choose IaC to meet security requirements, separate dev/test/prod, and set up access control and infrastructure change audit.

Why the IaC choice comes down to security and change control
Teams usually adopt Infrastructure as Code not out of love for “pretty code,” but because infrastructure changes tend to turn into chaos over time. Someone adjusted a setting manually on a server, another person forgot to document a network rule, and the system keeps running “somehow” until the first outage or audit.
At that point it becomes clear: the problem isn’t syntax, it’s governance. How do you make changes safely and prove they were made according to policy? So comparisons of Terraform vs Pulumi almost always hinge on security requirements, auditability and approval processes, not on which language or format is more pleasant to write.
In practice, “change control” means simple things: it must be clear who proposed a change and when; what will change before you apply it; that the change went through review and approval; and that you can restore a previous state and explain why (ticket, incident, request).
Imagine a common scenario: a test environment needs access opened for a service. An engineer makes a manual change, and a week later the same change ends up in prod by accident. No trace left except “that’s how it happened.” IaC makes changes repeatable and reviewable, but only if the tool and process enforce your access and audit rules.
Most teams end up choosing between Terraform (or its fork OpenTofu for IaC) and Pulumi. What matters next is not “which is easier to write” but “how this will fit your environments, roles, review flow, state storage and reporting requirements.”
Security requirements to agree on before choosing a tool
The Terraform vs Pulumi debate often starts too early. First agree on security requirements for the change process and the tool choice becomes mostly technical, not political.
A minimal set usually includes:
- governance (who changes what is clear);
- auditability (you can reconstruct the decision chain);
- repeatability (same input produces the same result);
- predictable rollback.
If you don’t lock these down first, IaC quickly becomes a pile of scripts rather than a controlled process.
Where control points are needed
Decide in advance where a change must “stop” and wait for verification. A pragmatic basic control loop looks like this:
- Plan: is there a clear list of what will change before applying?
- Review: who must check the change and by what criteria?
- Apply: who is allowed to run
applyand from which environment? - Rollback: what counts as a rollback and who triggers it?
This loop matters for cloud and on-premise infrastructure (for example, in a data center with servers and workstations) because the risk is the same: uncontrolled changes.
Why changes made “as admin” are dangerous
Manual edits without history and approvals remove three things at once: explainability (why), reproducibility (how to repeat) and accountability (who did it). In audited organizations the question is simple: “Show us on what basis you changed the network rules or server parameters.” If there is no answer, the issue is no longer just technical.
To avoid arguments when rolling out IaC, define the rules up front: how changes are documented, who approves them, which emergency exceptions exist, and what constitutes a policy violation. After that, any IaC tool is judged by how well it supports those rules, not whether it “replaces” them.
Terraform and OpenTofu in plain terms: what you get
Terraform (and its close cousin OpenTofu for IaC) lets you describe infrastructure as a set of rules: what should exist and with which parameters. You declare the desired state and the tool calculates what actions are needed to make reality match that description.
A typical workflow has two steps. First you run plan: the tool shows what will change, what will be created or destroyed. Then apply: the changes are executed. For security this is useful because plan can be a mandatory review step and apply can be allowed only from a controlled place (for example, CI/CD).
OpenTofu appeared as a fork of Terraform and is often considered alongside it because the approach and configuration language are very similar. For teams this usually means an easier migration: you can keep familiar practices, repository structure and most modules. When choosing, teams look at maintenance model, update cadence and how auditing and dependency controls are handled.
A particular value of Terraform/OpenTofu is modules and registries. A module is a reusable template (for example, “standard server,” “VPC/network,” “set of IAM roles”). When a team standardizes modules, manual effort drops and so do mistakes: instead of hundreds of lines every time, you use a tested assembly.
Risks usually lie not in the tool itself but around it. The main problem sources are providers (they perform the actual changes), drifting versions (tool, providers, modules) and team habits (skipping plan, missing reviews, overly broad apply permissions).
So the Terraform vs Pulumi argument often boils down to: can you build discipline around versions, modules and the plan/review/apply process? If yes, Terraform/OpenTofu provide a clear foundation that is easy to align with security and audit requirements.
Pulumi in plain terms: what’s different
Pulumi is often described as “infrastructure as ordinary code.” That means you declare cloud resources, networks, clusters and access not in a separate DSL but in a familiar programming language (for example, TypeScript, Python, Go, C#). This changes not only syntax but habits: IaC lives alongside application code, in the same repos and with the same practices.
Pulumi leans more on development practices. This gives more flexibility where engineering culture is already mature.
What “ordinary code” gives the team
You can use typing, modularity and tests. That helps catch errors earlier and maintain a unified style. In practice the value is often threefold: fewer accidental parameter mistakes (thanks to types and autocompletion), the ability to assert important rules with tests (for example, “public access is forbidden”), and reuse of shared libraries of standards (networks, roles, logging) without copying.
Risks for security and change control
The flip side of freedom is that it’s easier to introduce subtle mistakes. In IaC written in a general-purpose language you can quietly add unsafe logic, bypass agreed templates or hide critical changes inside helper functions.
Therefore, for security the important thing isn’t the tool but the rules around it: mandatory reviews, prohibition on direct applies from laptops, policy checks in CI/CD, and pinned dependency versions.
Pulumi typically fits well where the team already follows development rules and all changes go through pipelines. For example, when an organization builds infrastructure for multiple environments in parallel and needs a single set of validated libraries for servers, workstations and system components, this approach often proves more convenient.
Separating dev, test and prod: practical options
You separate dev, test and prod not for the sake of “neatness” but to manage risk. Dev changes are frequent and sometimes rough. In prod the cost of a mistake is higher: service downtime, data leaks, or regulatory breaches. So environments must differ in permissions, change tempo and control level.
The most reliable approach is isolation at the cloud account, subscription or project level (depending on the platform). That way a wrong variable or command cannot reach production. When such isolation is absent, you compensate with network and IAM restrictions, but that’s significantly weaker.
Practical schemes that work both for Terraform/OpenTofu and Pulumi:
- separate accounts/subscriptions for dev, test and prod, each with its own CI pipeline;
- separate projects or resource groups within a single account, but with strict policies and bans on creating resources outside assigned zones;
- separate state per environment (and per major component) so dev cannot affect prod through a shared state.
Hygiene rules help prevent dev/prod mixing: identical directory structure, name prefixes (for example, prd-, tst-), environment and owner tags, and bans on manual prod changes. If your organization is audited (for example, government or finance), put these rules in a standard and verify them automatically.
Access is easiest to manage with the principle “fewer rights in prod.” Developers can apply in dev, test applies happen by request, and prod applies only via review and a dedicated role. A minimal role set could be: dev applier with no prod access; test operator who applies by agreement; prod operator who applies only from CI after approval; and a read-only observer for security, auditors and owners.
With this model the dev/test/prod separation becomes clear responsibility boundaries, not an informal agreement.
Controlling change rights: roles, reviews and policies
The most sensitive IaC question isn’t who can write config, but who can apply changes to real infrastructure. Code can be discussed and fixed. A single careless apply can open network access, delete a disk or create expensive resources.
Often four roles are enough (even for a small team): the change author (prepares code and justification), the reviewer (checks security and standards), the apply operator (runs plan and apply in the correct environment) and the auditor (reviews the logs: who changed what and why).
Main principle: separate apply rights from development rights. An author can create branches and pull requests but should not have the keys/tokens to perform applies in test or especially prod. This reduces risk and simplifies investigations.
Process via pull request
A solid base process is built around pull requests and mandatory checks. It’s important the checks are enforced technically, not just verbally:
- mandatory review (at least 1–2 people) and a ban on direct pushes to main;
- automatic plan/preview in CI with the result saved as an artifact;
- a separate confirmation step for prod apply (manual approval window);
- logs showing who approved, who applied, and which plan was executed.
Policies and restrictions
Policies prevent dangerous settings before apply. Typical rules: ban public IPs by default, forbid open security group rules (0.0.0.0/0 on admin ports), restrict regions and resource types, require disk encryption and mandatory tags for inventory.
In organizations with strict requirements (for example, government or finance in Kazakhstan) it’s convenient to codify policies. Then it doesn’t matter if you use Terraform/OpenTofu or Pulumi: rules remain the same and are checked on every PR.
State and secrets: where risks usually hide
State is the file or storage where the IaC tool keeps the “truth” about what’s already created: resource IDs, relationships, parameters and often fragments of configuration. If an attacker gains access to state, they see how the infrastructure is organized and sometimes gain access. So IaC security often starts with protecting state rather than code.
Where to store state and what matters
Rule one: state should be remote, with access control and logging. A local file on a developer’s laptop is a common cause of leaks and loss of control.
Check basics: who can read state and who can modify it; is it encrypted; is there locking during apply (so two people don’t apply simultaneously); how are backups made and how fast can you recover.
Example: a team sets up dev/test/prod for a cluster on data center servers. If prod state is accessible to the same people as dev, a permissions mistake gives a path to make changes in production without proper approval.
Secrets: not in code and preferably not in state
Secrets (passwords, tokens, keys) must not be stored in the repo. More importantly, secrets often end up in state via resource parameters. In Terraform/OpenTofu and Pulumi this can happen even if you didn’t intend it.
Measures that truly reduce risk:
- separate credentials and keys for dev/test/prod;
- least privilege for IaC (only the permissions needed to create and change resources);
- encryption of state at rest and in transit, plus restricted read access;
- regular rotation of keys and tokens, especially for prod;
- CI checks to prevent secrets in variables and config files.
If the tool supports engine-level “secrets” (for example, encrypted values), that helps but doesn’t replace the main rule: design so secrets don’t end up in state unless necessary.
Step-by-step: how to choose a tool without breaking the process
The choice of IaC usually fails not because Terraform vs Pulumi is a bad comparison, but because the change process wasn’t defined. Agree the rules first, then compare tools for convenience.
Start with a short 1–2 page document. Capture security and operational requirements: where apply can be run, what logs and artifacts must be kept and for how long, who can approve changes, and what is forbidden (for example, public IPs or open ports without an exception).
Next define environment and ownership models. A common mistake is letting dev, test and prod “exist as they happen,” then being unable to prove who changed the network or access rules. Decide ahead: separate accounts or projects, separate state, separate keys and budgets, and who owns each layer (network, compute, databases, monitoring).
Practical order of actions:
- gather security and operational requirements into one short document and agree them;
- describe environments and ownership boundaries;
- fix the change process: plan, review, conditions for apply, maintenance window, rollback;
- run a pilot on one service and verify audit, rights, repeatability and incident response;
- approve standards: repo structure, modules, naming, provider versions and rules.
Do the pilot on something real but not critical: e.g., a standard internal portal service with networking, a few VMs and secrets. If you work with government contracts or strict audits, check you can show auditors: who made the change, who approved it, what the plan was and what exactly was applied.
A final check is simple: a new team member should understand the structure, safely make a change through review, and not be able to accidentally touch prod without explicit permission.
Example scenario: an organization with three environments and mandatory audit
Imagine an organization with dev, test and prod. Internal rules require local control of changes and an audit trail showing who initiated an edit, who approved it, what changed and when it reached production. There is also a requirement that developers don’t get direct cloud or virtualization access.
You can build a process that minimizes manual prod actions and maximizes the audit trail. Here syntax (Terraform vs Pulumi) matters less than how you enforce change and apply rules.
What the change flow looks like
A common flow where changes are applied by automation:
- a change request describing purpose, risk and rollback plan;
- a pull request in the IaC repo with mandatory reviews from operations and security;
- CI auto-checks: formatting, linters, plan/preview and a saved report;
- apply only from CI after manual approval and only for the target environment;
- saving the apply report and linking it to the request for audit.
Key point: people must not get into the habit of “quickly fixing something by hand and then moving it to code.” In IaC this almost always leads to divergence between declared and actual state.
Common mistakes when rolling out IaC
The most common mistake is starting to write Terraform or Pulumi code before agreeing the rules: who can change infrastructure, how reviews work, what constitutes an apply, where state is stored and who can see it. Later you retrofit rules and the team gets tired of workarounds and exceptions.
Second pain point is environment mixing. When dev, test and prod share state or credentials, a single slip becomes an incident. This is especially dangerous where audit is required: it’s hard to prove prod changes were controlled.
Secrets leaks are common: passwords, tokens and keys end up in repos, CI variables or tfvars files. Even in a private repo this is a risk: secrets get copied, land in logs and remain in history.
Another class of errors is drifting behavior due to versions. If you don’t pin provider, module and dependency versions, the same code can produce different plans on different machines or times. In Terraform vs Pulumi debates this is often forgotten: the problem is discipline, not the tool.
Usually five things break a rollout fastest: direct prod access without mandatory review and clear roles; one state for multiple environments or teams; secrets in the repo, task tracker or CI logs; unpinned provider and module versions; lack of a tested rollback and state recovery path.
To lower risk, set minimal process rails and test them in a small project: separate environments into different state and credentials; require review and forbid manual prod edits; store secrets in a dedicated vault, not in code; pin versions and add plan checks to CI; practice restoring state and rolling back after a bad apply.
Readiness checklist and next steps
Before the first production change ensure the process is safe in practice, not just on paper. Even after you pick Terraform vs Pulumi, most risks arise from environment settings, permissions and secret storage.
Readiness checklist before the first live change:
- dev, test and prod are separated (accounts, subscriptions, projects or at least separate state and networks);
- direct apply to prod is restricted: only via CI/CD, by role, and after mandatory review;
- auditing is enabled: action logs, history of plans and applies, with a clear trail of who changed what and why;
- secrets are protected: no passwords in repos or logs, rotation and access control in place;
- a rollback plan exists: how to quickly restore the previous configuration and who does it.
Also verify operations are ready. Who is on call if a service falls after a change at 02:00? Where is the runbook with simple diagnostics and recovery steps? How will you determine whether the cause was an IaC change or an external factor? Lock these answers in before launch.
In 1–2 months success is usually visible by simple signals: fewer manual console edits and more changes through code; infrastructure releases become predictable in time and result; every change can be explained and reproduced from history; and configuration-related incidents decline.
Next steps: choose a small pilot (for example, a standard service and its network), bring security, infrastructure and development together to agree requirements, then define roles, policies and environment boundaries.
If you need help getting started, GSE.kz can join as a systems integrator: design environments and access, build infrastructure (including servers and data center solutions) and help establish operational support with clear procedures.
FAQ
Where should I start when choosing between Terraform/OpenTofu and Pulumi if security is important?
Start by documenting security requirements for the change process: who can propose changes, who must review them, where applies may be run, which artifacts must be kept for audit, and how rollback works. Once this process is clear, choosing between Terraform/OpenTofu and Pulumi usually comes down to which tool fits your CI/CD, roles and state storage best.
Why make plan/preview a required step before apply?
A plan is the mandatory “preview of changes” before applying: it shows what will be created, changed or removed so you can send it for review and approval. Allowing apply without a plan removes predictability and makes audits difficult, because it becomes hard to prove what was intended and what actually changed.
Why are manual changes in the console considered a security risk?
Manual edits leave little verifiable trace: it’s unclear who changed what and why, and it’s hard to reproduce the state later. A dev experiment can easily “leak” into prod, and during an audit you won’t be able to show the approval chain and justification for the change.
How should dev/test/prod be separated so dev can’t break prod?
The most reliable option is physical isolation using separate accounts, subscriptions or projects so a mistaken command cannot reach production. If that’s impossible, compensate with separate state, separate credentials and strict IAM/network boundaries—but those are weaker and harder to maintain.
What minimum roles are needed to control IaC changes?
A minimal model is: the author prepares the change and its justification, the reviewer checks security and standards, an operator applies changes from a controlled environment, and an auditor can access logs and artifacts. The key principle is to separate the right to write code from the right to apply changes in test and especially prod.
How to organize a pull request flow so it’s audit-ready?
Make direct pushes to the main branch forbidden, require mandatory review, run automated plan/preview in CI and save the result. For production require a separate approval step for apply and record who approved and who applied, so auditors can reconstruct the full decision chain.
Why is state one of the main IaC risk points and how to protect it?
State often contains resource IDs, relationships and sometimes sensitive configuration details, so compromising it reveals your infrastructure and may expose access. Store state remotely with encryption, strict read/write permissions, logging and locking during applies, and keep separate state for dev/test/prod to avoid accidental cross-impact.
How to avoid leaking secrets with Terraform/OpenTofu or Pulumi?
Don’t store secrets in the repo. Remember secrets can also end up in state or CI logs. Use separate credentials per environment, give IaC roles only minimal needed permissions, rotate keys regularly and add pipeline checks to prevent secrets in variables and config files.
Which security policies should be checked automatically during IaC changes?
Policies catch dangerous configurations before apply: for example, ban default public IPs, forbid open security group rules, require encryption and mandatory tags. Make these checks automatic in CI/CD and apply them consistently across teams and environments so the IaC tool becomes an implementation detail and rules stay enforced.
When is Pulumi a better fit and when is Terraform/OpenTofu more logical?
Choose Pulumi when your organization already follows software development practices, uses mainstream programming languages, tests and shared libraries. Choose Terraform/OpenTofu when you prefer predictable declarative workflows, a mature module ecosystem and a familiar plan/apply model with straightforward review. In the end, discipline around versions, permissions and pipelines matters more than syntax.