What's New in Pulumi 3.261.0 (released 2026-09-02): a CLI-and-engine-feature drop with no breaking changes. Two feature changes plus three bug fixes that touch the state-recovery path, the cloud OIDC trust setup path, and the PCL compiler's type-binding surface. State migration callbacks on resource registrations (#24328) — until 3.261.0, a resource that needed a state-migration callback (a custom transformation on the persisted state to recover from a schema change, a provider rename, or a resource type move) had to register that callback through the engine's lower-level migration API, which meant a one-off code change for every state-migration event. 3.261.0 exposes the callback as part of the resource registration surface, so a Pulumi provider can declare the migration callback at the same place it declares the resource type. For an ML platform team running a custom provider for a niche accelerator or an in-house stateful resource (the pattern the Kubernetes GPU operators guide covers for cross-vendor GPU scheduling), this is the release where the state-migration callback moves from "engine patch required" to "provider config option." pulumi env setup {aws,azure,gcp}: configure cloud OIDC trust and create the ESC environments that use it (#23911) — A workload-identity CLI command. Until 3.261.0, setting up the OIDC trust between Pulumi Cloud (or Pulumi ESC) and a hyperscaler — the trust that lets a CI run from Pulumi fetch short-lived cloud credentials without storing a static secret — required three or four manual steps: register the Pulumi Cloud SAML/OIDC app in the cloud IAM console, copy the issuer URL and audience into the ESC environment, attach the resulting trust to the workload identity, and verify the trust by issuing a test token. 3.261.0 collapses that into one command: pulumi env setup aws (or azure, gcp) walks the OIDC trust registration end-to-end and creates the ESC environment that consumes it. For an ML platform team that wires CI-run pulumi up against a Bedrock + S3 + SageMaker training pipeline (the multi-cloud IaC pattern this article's Hybrid Approach section covers), this is the release where the cloud-side OIDC setup stops being a half-day incident the first time someone tries to onboard a new repo. Three bug fixes round out the drop: report a clear error instead of panicking when the stack's state contains a custom resource with no ID (#24438) — a long-standing panic-class fix on a state file where a custom-resource registration was partially written. Until 3.261.0, this condition surfaced as a stack-level panic during pulumi up, taking down the IaC run with a stack trace instead of an actionable error. 3.261.0 returns a structured error pointing at the affected URN. show the agent account's claim URL instead of an inaccessible View Live link when running on ephemeral agent credentials (#24476) — a CLI UX fix. Until 3.261.0, a CLI run authenticated through an ephemeral Pulumi agent credential showed a "View Live" link that pointed at a Pulumi Cloud console page the agent credentials could not reach; 3.261.0 surfaces the claim URL the agent can actually open. fix a crash when binding a schema property with an integer constant value (#24480) — a PCL compiler correctness fix. Until 3.261.0, a PCL schema-property binding that used an integer constant (e.g. replicas = 3 on a Kubernetes Deployment spec) could crash the compiler on a particular shape of provider schema; 3.261.0 handles the binding correctly.

No migration is required to upgrade from 3.260.0 to 3.261.0. The state file format, the ESC environment contract, and the Pulumi CLI command surface are unchanged; the state-migration callback and the pulumi env setup commands are additive. The 3.261.0 release is the right time to revisit the cloud OIDC trust configuration on any ESC environment that currently uses static credentials — the new pulumi env setup command can replace the manual setup on the next legitimate environment refresh. The supply-chain security for DevOps 2026 guide is the broader read on the workload-identity posture the cloud OIDC trust closes the static-credential loop on; the Kubernetes GPU scheduling guide covers the cluster-side provisioning pattern that the pulumi env setup workflow feeds.

What's New in Pulumi 3.260.0 (released 2026-08-28): a CLI-and-SDK-feature drop with no breaking changes. Three features and two bug fixes close real silent-failure classes on the state-secrets-stack side of the IaC layer. pulumi deployment settings get renders every source kind and shows env var values, with --output=json shape of environmentVariables changing from a list of names to a list of objects (#24284) — until 3.260.0, the deployment settings read only displayed raw env-var names and left the values opaque to anyone running audit or compliance checks against the stack config. 3.260.0 makes every source kind render its env-var content and shapes the JSON output as objects rather than bare names. For an ML platform team that provisions secrets into a training-orchestrator ECS task or a vector-DB container, this is the release where pulumi deployment settings get -o json stops being the command that requires a second tool to read its output. pulumi state promote (#24453) — turn a stateful snippet into Pulumi program code, so an engineer recovering from a stuck apply can promote the surviving state into the IaC source rather than hand-porting the resource graph. For a platform team that treats stateful snippets as the breadcrumb trail of emergency changes, this is the line where the trail stops being write-only. Go 1.27 SDK support (#24401) — the Go SDK compiles against the 1.27 toolchain, which matters for teams that pin their IaC codebase to a Go version newer than the 3.259.0 SDK supported. Two bug fixes round out the drop: replace resources whose deletedWith target is being replaced instead of leaving them orphaned in state (#23818) — previously, a resource that named a deletedWith target would be left dangling in state when the target was replaced; 3.260.0 reparents the resource along with the replacement so the state graph stays coherent. Python SDK serializes typed object stack outputs with their wire-format camelCase property names, matching the other language SDKs (#24246) — until 3.260.0, a Python stack output that was a typed object emitted snake_case keys while TypeScript and Go SDKs emitted camelCase, which silently broke downstream consumers that read the output across language SDKs. The 3.259.0 production pin holds; 3.260.0 is the same line with two long-running silent-failure classes closed. For teams that already use the patterns in the Kubernetes GPU scheduling guide and the Kubernetes GPU operators guide, the state-promote command is the most directly useful — it converts the breadcrumb trail of an emergency GPU-cluster patch into actual IaC source that survives the next stack refresh. The Pulumi 3.260.0 release page carries the full changelog.
What's New in Pulumi 3.259.0 (released 2026-08-19): a CLI-and-state-feature drop with no breaking changes. Two features and a long tail of schema/codegen/CLI correctness fixes close real classes of silent failure on AI infrastructure stacks. PULUMI_DEFAULT_ORGANIZATION env var + interactive pulumi new one-shot confirmation (#24384 + #24223) — until 3.259.0, an engineer installing Pulumi against a Pulumi Cloud account that belonged to multiple organizations had to disambiguate the org via --org on every command, and a fresh pulumi new walked through three prompts (project, stack, config defaults) on every invocation. 3.259.0 ships PULUMI_DEFAULT_ORGANIZATION as the env-var-shaped disambiguator and folds the three pulumi new prompts into a single confirmation line that lists the resulting defaults. For ML platform teams onboarding new engineers to a multi-org account (the pattern an AI platform team that runs GPU pools across org-research and org-prod lives with daily), this is the line where the onboarding runbook stops requiring the org flag to be remembered. ESC OpenAPI schema: esc.Value and esc.Range emit value and environment keys on serialize (#24346) — a long-running schema-correctness fix. Until 3.259.0, the Go SDK serialization of esc.Value and esc.Range omitted keys the ESC OpenAPI contract marks required, which on a strict OpenAPI consumer (a Pulumi ESC SaaS backend, a third-party secrets consumer) failed validation silently with a 422 the Go caller never saw. 3.259.0 fixes the contract drift. For teams using ESC to inject secrets into a training-orchestrator ECS task or a vector-DB container, this is the release where the secrets layer stops being the silent-failure leg of the IaC stack. CLI: parallel plugin installs (pulumi plugin install --parallel) + progress bar fix (#24369 + #24372) — a CI-time accelerator. Until 3.259.0, pulumi plugin install ran the provider plugin downloads serially, which on a 12-provider stack added 90-180 seconds of CI wall-clock on a fresh CI lane. 3.259.0 ships a --parallel flag and a progress bar that doesn't stall on parallel installs. For ML platform CI lanes that re-install providers on every run (the same ones the Kubernetes GPU scheduling guide covers for nightly GPU-pool rebuilds), this is the release where a 6-minute CI lane drops to a 90-second one. State backend error visibility: name which backend pulumi login failed against + DIY backend URL reporting (#24322 + #24335) — two state-backend error-correctness fixes. Until 3.259.0, a pulumi login failure against a misconfigured backend returned a generic error with no name of the backend, and a DIY backend URL that failed to open returned a bare error without the resolved URL. Both fixes name the backend and the resolved URL, which on an incident runbook is the difference between a 5-minute triage and a 30-minute one. Pairs with the agentic incident harness guide when the IaC runbook is part of the agent's recovery surface. Bug fixes worth flagging: extension-package refs no longer returned for non-parameterised packages against the same base (#24347), pulumi do approval replies and mode changes no longer silently dropped on full outbound event queue (#24370 — a real agent-loop hazard), Go codegen non-determinism on null/typed property mix fixed (#24366), pulumi do keep snippets when delete fails so delete can be retried (#24271), pulumi package add reports every schema validation error (#24367), Python casing of local component outputs correct (#24379), policy group ls respects default org (#24385). Improvements: SDK Python generation types constant properties as Literal[value] (#24358) — a type-safety improvement that catches API contract drift at typecheck time; Node.js Automation API output buffer limit removed (#24333) — closes a silent-truncation class on long-running destroy operations. No migration required. The 3.258.0 production pin holds; 3.259.0 is a drop-in minor bump with material CI-time and error-message wins. The Kubernetes GPU Operators guide is the adjacent deployment reference for when an IaC change reaches the cluster, and the AWS FinOps agent guide covers the cost attribution that should inform a multi-cloud module decision.
Terraform 1.17.0-alpha: what changed and should you run it? (published July 29, 2026): this is a prerelease, not a stable Terraform 1.17.0 release. The authoritative tag is v1.17.0-alpha20260729; the latest stable line remains 1.15.x as of this refresh. The alpha adds provider-version details to terraform init log messages and fixes pow and log from panicking when their result is NaN. It also carries alpha-only experiments: deferred actions behind -allow-deferral, test cleanup, persistent test backends and skip_cleanup, and policy evaluation through terraform query -policies. HashiCorp explicitly says these experiments are not available in stable releases.

For an AI platform's production IaC, keep the stable pin. Use this alpha only in an isolated CI matrix if you need to test deferred actions or the new diagnostics; do not promote it as the baseline for a GPU cluster. The release notes contain no upgrade notes or breaking-change section, but prerelease status is itself the operational warning. The Kubernetes GPU Operators guide is the adjacent deployment reference to exercise when an IaC change reaches the cluster, and the AWS FinOps agent guide covers the cost attribution that should inform a multi-cloud module decision.

What's New in Pulumi 3.257.0 (released 2026-08-13): a CLI-and-state-feature drop with no breaking changes. The release concentrates on the pulumi do and pulumi state surfaces — the two commands an ML platform team reaches for first during an incident or a state-recovery task — plus a long tail of Go and Python SDK correctness fixes that close real silent-failure classes on a multi-cloud stack. Five feature changes matter for AI infrastructure teams who keep their IaC close to the GPU cluster. pulumi state get shows individual resources (#24191) — until 3.257.0, the state CLI could list or export an entire stack but could not point at one resource by name and show its full state. A platform engineer triaging a stuck training-cluster apply now has a one-shot path: pulumi state get against the affected resource URN, no JSON parse of a full export. Auto-assigned identifiers for existing stack resources in pulumi do input expressions, plus pulumi do show-resources (#24184) — pulumi do input expressions that referenced a stack resource previously had to be rewritten by hand with the right identifier; 3.257.0 auto-assigns the identifier and ships pulumi do show-resources for an explicit listing. For teams that orchestrate ad-hoc GPU pool patches through pulumi do (the pattern the Kubernetes GPU scheduling guide covers), this is the line where the patch command stops requiring the engineer to remember the resource URN. pulumi do <pkg> <resource> patch <name> works in stateful mode (#24295) — the patch expression now overlays the supplied inputs onto an existing stateful snippet rather than rejecting the operation. pulumi do falls back to an auto-created project and stack under PULUMI_HOME (#24231) — a critical ergonomics fix for incident response. Until 3.257.0, an on-call engineer running pulumi do outside of a Pulumi project directory hit a hard error and had to manually create a project before they could run the recovery command. 3.257.0 auto-creates a project and stack under PULUMI_HOME on the fly. For teams that document their GPU-cluster incident runbook around pulumi do, this is the release where the runbook stops failing on the very first command in a cold shell. Offer to create a first project after logging in to an account with no stacks (#24133) — a first-run ergonomics fix. A new Pulumi Cloud account that logs in for the first time now sees a one-shot "create your first project" prompt rather than an empty dashboard. For ML platform teams onboarding new engineers, this is the line where the first pulumi up takes one prompt instead of three doc lookups. Three smaller changes round out the drop: plugin last-used time tracking across platforms (#24251) — pulumi plugin ls now reports an accurate last-used time on macOS, Linux, and Windows rather than the Linux-only behavior; secrets visible in diff display when --show-secrets is passed (#24253) — explicit opt-in for secrets in diffs, which on a GPU-cluster security review is the lever an auditor needs to confirm a stack's secret boundary; and pulumi api repeated header values (#24180) — -H/--header flags for the same header name now all reach the wire instead of each one silently overwriting the last (a real bug for auth flows that need multiple Authorization-style headers on a single call). Bug fixes worth flagging: Go SDK panic-in-hooks no longer crashes the entire process (#24218) — a Go runtime panic inside a resource hook previously took the whole Pulumi process down; 3.257.0 recovers and surfaces the panic as an error. Python SDK reports an error instead of hanging on a self-parenting resource (#24230) — a long-standing deadlock on circular resource dependencies. Go codegen now emits for expressions as Go loops (#24228) — the previous output was a runtime iterator that was measurably slower than a native loop on large GPU-pool loops. For teams already running multi-cloud GPU training pipelines, the pulumi state get command and the PULUMI_HOME fallback for pulumi do are the two most directly useful changes — the first replaces a JSON-parse step with a one-shot CLI, the second closes the cold-shell failure mode on the incident runbook. The 3.256.0 production pin holds; 3.257.0 is a drop-in minor bump for the CLI surfaces and the SDK fixes. The AWS FinOps agent guide covers the cost attribution that should inform a multi-cloud module decision, and the Kubernetes GPU Operators guide is the adjacent deployment reference for when an IaC change reaches the cluster.
What's New in Pulumi 3.255.0 (released 2026-07-28): a CLI-and-engineering-focused feature drop with no breaking changes. Three changes matter for AI infrastructure teams who keep their IaC close to the GPU cluster. --extension flag on package commands — extension-parameterized packages can be reinstalled from Pulumi.yaml on pulumi install, so an ML platform team that ships a custom provider for a niche accelerator (e.g. an in-house TPU-like scheduler) no longer needs a separate reinstall step after a stack refresh. Project files surfaced as environment variables on ESC open (#23993) — when you open an ESC environment, Pulumi writes the project files to disk and exposes their paths as environment variables across all output formats. That makes downstream tools (training orchestrators, vector DB clients, FinOps agents) reach their config without a parallel secret loader. Invoke provider resolution now matches resource registration (#24016) — the engine resolves an invoke's provider from the providers option of the parent named on the request, the same resolution path that resource registrations use. The provider-mismatch footgun that affected multi-cloud stacks (an invoke accidentally hitting the wrong region) is now harder to trip. Two smaller CLI additions round out the drop: --resources on do input expressions (#24037) to reference existing state resources, and a nodejs programgen fix (#24017) that parents an invoke inside a component to that component so it resolves the component's providers. For teams already running multi-cloud GPU training pipelines with the Kubernetes GPU scheduling guide, the ESC env-var change is the most directly useful — the orchestrator no longer has to be told where its config lives. The 3.254.0 production pin holds; 3.255.0 is a drop-in upgrade.
What's New in Pulumi 3.254.0 (released 2026-07-23): a CLI-focused feature drop with three changes that matter for AI infrastructure teams. pulumi logs share (#22546) — share a running service's logs with Pulumi support or a teammate via a one-shot shareable token, useful when an ML platform engineer is on call and needs to pull a live log stream from a flaky training orchestrator without handing over cloud credentials. pulumi stack migrate (#22902) — migrate a stack from one backend to another (e.g. moving a self-hosted Pulumi Service backend to Pulumi Cloud, or moving between S3 buckets) with automatic re-encryption of config secrets and stack state under the target secrets provider; previously this was a manual export/import cycle that required you to decrypt and re-encrypt every secret. --override-env (#23562) on up, preview, destroy, and refresh — substitute a different environment configuration for a single run without editing the stack config, useful for running a dry-run of a GPU cluster change against a staging environment while the real stack stays on prod. The 3.254.0 line also adds the pulumi neo acp agent-client-protocol integration (#23886), the pulumi neo resume command (#23835) with chat history, and Ctrl+A / Ctrl+E line-navigation preservation in the Neo REPL (#23932) — Pulumi's AI assistant is graduating from a CLI toy into a real infrastructure workflow tool. None of these are breaking changes; the 3.253.0 production pin holds. For teams that already use the patterns in the Kubernetes GPU scheduling guide, the --override-env flag is the most directly useful — it lets you preview a cluster re-size against a staging config without touching the prod stack.

If you are standing up a GPU training cluster, deploying a model serving endpoint, or wiring up a vector database on Kubernetes, you need to provision infrastructure reproducibly. Spinning up a cluster manually through a cloud console works once. It does not work when you need to tear it down and rebuild it at 3 AM during an incident, or when your training pipeline needs to provision a fresh compute environment for every experiment run.

Infrastructure as Code (IaC) solves this. And for AI and ML teams, two tools sit at the top of the evaluation list: Terraform (HashiCorp) and Pulumi (Pulumi Corp). This is not a generic comparison — it is a practical guide for AI infrastructure decisions: GPU clusters, Kubernetes, multi-cloud routing, and the specific demands that ML workloads place on your provisioning layer.

The Fundamental Difference: Declarative vs Programmatic

Terraform and Pulumi approach infrastructure as code in fundamentally different ways, and understanding this difference is the key to making the right choice for your team.

Terraform uses HashiCorp Configuration Language (HCL), a declarative domain-specific language. You describe the desired state of your infrastructure in .tf files, and Terraform figures out how to reach that state. You write configuration; Terraform plans and applies. The state file (typically stored in Terraform Cloud or locally) tracks what exists, and Terraform reconciles your configuration against that state on every apply.

Pulumi uses general-purpose programming languages — Python, TypeScript, Go, C#, or Java. You write real code that creates and configures infrastructure. Pulumi still has a plan and apply workflow, but because you are writing code, you can use loops, conditionals, functions, and classes to model complex infrastructure patterns. A Python Pulumi program can call a function that generates a GPU cluster configuration, and that function can embed business logic that would be impossible to express in HCL.

For AI infrastructure specifically, this difference matters in several concrete ways.

Dynamic GPU Cluster Provisioning

AI workloads are not static. A training run needs dozens of A100 GPUs for four hours and then the cluster sits idle. An inference service has baseline capacity that spikes at 10 AM when batch processing kicks in. Traditional infrastructure can be provisioned once; AI infrastructure needs to breathe.

Terraform handles dynamic provisioning through data sources, dynamic blocks, and for_each expressions — and it works. You can provision a fleet of GPU instances with Terraform. But the HCL syntax for dynamic clusters is verbose, and the plan/apply cycle must run to completion before Terraform knows what changed. For rapidly scaling GPU clusters where the provisioning logic depends on runtime data (spot instance availability, current job queue depth), Pulumi's native Python loops and runtime API calls fit more naturally.

In Pulumi, you can write a function that queries the current spot instance availability across AWS and GCP, compares pricing, and provisions the optimal cluster — all in a single Python script with standard control flow. In Terraform, this requires external data sources, complex module composition, and often a separate orchestration layer.

Advertisement
Advertisement

Kubernetes and ML Tooling Integration

Most AI infrastructure runs on Kubernetes, and both Terraform and Pulumi have strong Kubernetes support. But the experience differs.

Terraform's kubectl provider and helm provider are mature and battle-tested. You can define a complete Kubernetes cluster, install the Karpenter autoscaler, deploy a KubeRay operator, and configure a model serving layer — all in Terraform HCL. The Terraform Registry has providers for every major cloud and many specialized tools (NVIDIA GPU operators, Prometheus, Grafana). For teams that want to define their entire infrastructure in one place, Terraform's provider ecosystem is unmatched.

Once the cluster is up, the visibility question is what you actually get out of the network telemetry — and that is where the eBPF for AI networking reference is the next read after the IaC layer. eBPF gives you per-flow visibility on the GPU-to-GPU and GPU-to-storage paths the IaC provisions, which the kubectl provider's resource model does not surface on its own.

Pulumi's Kubernetes provider is equally capable, but goes further — you can use the Pulumi Kubernetes SDK to create Kubernetes resources directly from Python or TypeScript, and you can embed raw YAML generation inside Python functions. This is particularly useful for ML tooling like KubeFlow pipelines, Ray clusters, and Seldon model servers where the resource definitions are complex and often generated programmatically. Pulumi's ConfigFile and Chart abstractions let you mix raw YAML with Pulumi-managed resources, giving you flexibility without abandoning the Pulumi programming model.

Both tools handle Custom Resource Definitions (CRDs) well, but Pulumi's ability to iterate over CRDs in Python makes it easier to manage large collections of similar resources — for example, dozens of inference endpoints across different model versions.

Multi-Cloud GPU Routing

AI infrastructure increasingly spans multiple cloud providers. Lambda Labs and RunPod offer GPU compute that AWS and GCP cannot match on price for certain workloads. CoreWeave has become the de facto choice for some vLLM deployments. Team members may develop on a local Ray cluster and deploy to a cloud provider for production.

Terraform addresses multi-cloud through its provider ecosystem. Each cloud provider has an official Terraform provider, and you can write Terraform configuration that targets AWS for some resources and GCP for others. The challenge is that each provider is a separate plugin, state management spans providers awkwardly, and expressing cross-cloud logic (like "route training jobs to the cheapest available GPU right now") requires external scripts or a higher-level orchestration tool.

Pulumi's multi-cloud SDK abstracts provider differences in code. The same Python function can target AWS EC2 or GCP Compute Engine, and the function can make runtime decisions based on pricing APIs, availability, or custom constraints. For teams building ML platforms that need to route workloads dynamically across GPU providers, Pulumi's programmatic model is a significant advantage.

State Management for AI Workloads

Both Terraform and Pulumi track infrastructure state, but the operational characteristics matter for AI teams.

Terraform stores state in a flat file (by default locally, or in Terraform Cloud for team environments). As infrastructure grows — hundreds of GPU instances, multiple Kubernetes clusters, complex networking — the state file grows and the plan step slows. Terraform's state locking prevents concurrent applies from corrupting state, which is critical when your training pipeline provisioner and your incident response script might both try to modify infrastructure simultaneously.

Pulumi stores state in the Pulumi Cloud Service (a SaaS offering with a generous free tier) or in a self-hosted backend. The Pulumi Service handles state locking, history, and team collaboration automatically. Pulumi also supports checkpointing — every successful apply creates a checkpoint you can roll back to. For AI infrastructure teams that move fast and sometimes need to undo a bad provisioning run, this is valuable.

One concrete advantage of Pulumi's state management: because Pulumi programs are real code, you can write unit tests for your infrastructure using standard Python testing tools (pytest). You can test that your GPU cluster configuration creates the right number of instances, that your security group rules are correct, and that your cost tagging strategy is applied consistently — before you run pulumi up. For teams that also want a signed audit trail of every stack change (who changed what module, when, and which CVE scan ran on the dependency tree), the policy-as-code and SLSA-style provenance story is covered in the supply-chain security for DevOps 2026 guide, which pairs cleanly with Pulumi's team-dashboard and policy-as-code surfaces on the IaC side.

Real-World Example: Provisioning a GPU Spot Cluster

Consider the practical task of provisioning a GPU training cluster using AWS EC2 Spot Instances. Both Terraform and Pulumi can do this, but the implementation differs.

In Terraform, you would use a combination of aws_ec2_spot_fleet or aws_launch_template with spot options, an autoscaling group, and conditional block device mappings. The configuration involves multiple resources with complex dependencies. Handling spot interruptions (when AWS reclaims your instances) requires external automation — a Lambda function triggered by CloudWatch events, for example.

In Pulumi (Python), the same provisioning logic is a Python function that creates the autoscaling group, configures the launch template, and attaches a CloudWatch event rule that triggers a handler when a spot interruption notice arrives. That handler — also in Python — can decide whether to wait for replacement capacity, scale down gracefully, or notify the training job scheduler. Because it is all Python, the spot interruption handler can call your job queue API, update a tracking database, and send a Slack notification — all in the same codebase.

The Terraform equivalent requires Terraform configuration plus external Lambda code in a different language. The Pulumi version keeps everything in one language, one codebase, one review process.

Comparison Table: Terraform vs Pulumi for AI Infrastructure

Which is better for AI infrastructure: Terraform or Pulumi?

Dimension Terraform Pulumi
Language HCL (declarative DSL) Python, TypeScript, Go, C#, Java
Learning curve Steeper for engineers without HCL experience; HCL is unique to Terraform Easier for teams that already write Python or TypeScript
AI/ML workload flexibility Good for static configs; dynamic provisioning requires external scripts Native loops, conditionals, and runtime API calls handle dynamic workloads naturally
Kubernetes support Mature kubectl and Helm providers; extensive CRD support Strong K8s SDK; can embed YAML generation in Python; ConfigFile for mixing approaches
Multi-cloud GPU routing Provider ecosystem (AWS, GCP, Azure mature); cross-cloud logic needs orchestration layer Native multi-cloud SDK; runtime decisions across providers in one language
State management Local or Terraform Cloud; state locking; large state slows plan Pulumi Service (SaaS) or self-hosted; automatic state locking and checkpointing
Cost Open-source core; Terraform Cloud free tier (500 actions per month); Team at 20 dollars per user per month Free tier (unlimited resources); Team at 0 dollars per user per month; Enterprise at 15 dollars per user per month
Testing Terraform validate, plan; limited unit testing Native pytest, Go, TypeScript testing; test infrastructure before apply
Module ecosystem Terraform Registry — thousands of providers and modules Pulumi Package Registry — growing, but smaller than Terraform Registry
AI GPU provider support AWS, GCP, Azure providers mature; RunPod and Lambda Labs providers exist AWS, GCP, Azure providers; Lambda Labs and custom providers via Pulumi SDK
Vendor lock-in Low (open-source core, Terraform Cloud is optional) Low (open-source core, Pulumi Service is optional)
Team collaboration Terraform Cloud or Terraform Cloud for Teams; VCS integration Pulumi Service with team dashboard, audit logs, policy as code

When to Choose Terraform

Terraform is the right choice when your team already knows Terraform and has established modules and patterns. IaC is only valuable if your team actually uses it. If your infrastructure is relatively static — managing production Kubernetes clusters, VPC networks, and storage that do not change frequently — Terraform's declarative model is clean and auditable. You write the config, review the plan, apply, and move on.

You need the Terraform Registry ecosystem when you are working with specialized infrastructure: NVIDIA GPU operators, cloud-specific ML services, monitoring integrations. Terraform's provider ecosystem is deeper. If a managed service you need is not in Pulumi's registry, it is almost certainly in Terraform's.

Compliance requires immutable, auditable infrastructure when every infrastructure change must be reviewed and approved (SOC2, HIPAA, GDPR-adjacent). Terraform's plan/apply workflow provides a clean audit trail. And if you are running standard multi-cloud patterns — using both AWS and GCP with standard resource types — Terraform's provider model covers it without needing the programmatic flexibility of Pulumi.

When to Choose Pulumi

Pulumi is the right choice when your team writes Python or TypeScript for ML pipelines. If your ML engineers are already writing PyTorch training scripts, data processing pipelines, or model serving code in Python, Pulumi integrates naturally. Your infrastructure team can write IaC in the same language as your ML team and share libraries.

AI infrastructure that is dynamic and complex — GPU clusters that scale based on job queue depth, training workloads routed across spot instance availability, multi-cloud inference endpoints with dynamic routing — all benefit from Pulumi's programmatic model over Terraform's declarative approach.

Pulumi programs are Python (or TypeScript, Go, etc.), which means you can write unit tests using pytest, assert on resource properties, and validate complex configurations before running pulumi up. For AI infrastructure where a misconfigured security group can expose a model endpoint, testing is not a luxury — it is a necessity.

Pulumi can be called programmatically from Python, which means your training pipeline can provision its own compute environment, wait for resources to be ready, run the training job, and tear down the cluster — all without a separate IaC workflow. Terraform requires an external trigger (a CI/CD pipeline step, a Terraform Cloud run). And if you are building a self-service ML platform where ML engineers provision their own training environments through a portal, Pulumi's programmatic model lets you build typed, documented APIs for infrastructure provisioning that feel like normal Python libraries.

Recommended Tool Pulumi

Pulumi's open-source IaC lets your ML team use real programming languages for infrastructure. Free for individuals and small teams.

The Hybrid Approach: Use Both

The most common pattern for mature AI platforms is not "Terraform OR Pulumi" — it is "Terraform for base infrastructure, Pulumi for workload-specific provisioning." This is practical, not ideological.

Terraform for the base layer: VPC networking, IAM roles and policies, core Kubernetes control plane, centralized logging and monitoring infrastructure. These are static, well-defined, and benefit from Terraform's auditable plan/apply workflow and massive module ecosystem. You define this once, review it carefully, and apply it when you stand up a new environment.

Pulumi for dynamic workloads: Training clusters, model serving endpoints, feature stores, experiment tracking infrastructure, and any provisioning that needs to respond to runtime conditions (job queue depth, spot pricing, model version). These change frequently, often need custom logic, and are natural candidates for Pulumi's programmatic model. Your ML platform team can own the Pulumi programs for workload provisioning without needing to understand the entire infrastructure stack.

Pulumi can manage Terraform state through the @pulumi/terraform provider, and Terraform can import Pulumi stacks — so the boundary between the two is not a wall but a permeable membrane. Many teams start with Terraform, realize they need more flexibility for AI workloads, and add Pulumi incrementally without migrating existing infrastructure.

Decision Framework

Is your AI infrastructure mostly static? (Production Kubernetes, VPC, managed services that do not change frequently) — Choose Terraform. Lower learning curve within your existing team, massive module ecosystem, clean audit trail.

Does your team already write Python or TypeScript for ML work? — Lean toward Pulumi. Your team can own infrastructure without learning HCL, and your ML pipelines can call provisioning logic directly.

Do you need to route workloads dynamically across multiple GPU providers? — Pulumi. Native multi-cloud SDK with runtime decision-making in code.

Are you building a self-service ML platform? — Pulumi. Type-safe, documented infrastructure APIs that feel like libraries.

Do you need to test infrastructure before applying? — Pulumi. pytest for infrastructure. No contest.

Do you need providers for niche managed services (specific GPU operators, niche ML platforms)? — Terraform. Registry ecosystem is deeper and more mature.

Conclusion

Neither Terraform nor Pulumi is the universal right answer for AI infrastructure. The decision is contextual — it depends on your team's existing skills, the dynamic nature of your workloads, and whether you need to embed infrastructure provisioning inside ML pipelines.

Terraform wins when your infrastructure is relatively static, your team has existing Terraform expertise, and you need the depth of the Terraform Registry provider ecosystem. It is the industry standard for a reason, and for base infrastructure layers (VPC, IAM, core Kubernetes), it remains the cleanest choice.

Pulumi wins when your AI workloads are dynamic and complex, your team writes Python or TypeScript, and you want infrastructure that can be tested, embedded in ML pipelines, and extended with real programming logic. The ability to write infrastructure in the same language as your training pipelines removes an entire category of friction.

The most sophisticated AI platforms use both — Terraform for the static base, Pulumi for the dynamic workload layer. The two tools are not competitors; they solve different problems well. If you are building AI infrastructure from scratch, start with Terraform for the foundation and add Pulumi where the dynamic nature of AI workloads demands it.

The IaC layer is foundational to everything that comes after it. Investing time in making the right choice now — based on your team's actual situation, not ideological preferences — will pay dividends every time you provision a new training cluster or deploy a new model version.

Once the IaC layer is in place, the unit-economics question is what the choice actually costs you. The GPU cluster you provision through either tool will dominate the per-token cost of any model serving workload, and the easiest way to model that cost before you commit to a cluster shape is to run the actual token-cost arithmetic against your traffic. The free LLM API Cost Calculator lets you model cost-per-token across providers and quantization choices, which is the input you need when the Terraform module or Pulumi program is deciding how many GPUs to request.

The observability side of the cluster is a separate decision: which tool watches the workloads the IaC provisions. If the team is starting from a blank observability slate rather than an existing Datadog bill, the Datadog alternatives 2026 reference covers the open-source-first stack that pairs naturally with a Terraform-or-Pulumi-managed cluster, and it is worth reading alongside the IaC decision so the observability surface is provisioned as IaC-managed infrastructure rather than ad-hoc.