Is Pulumi a worthy contender in the IaC race? Part 3: Best practices and the verdictIsPulumiaworthycontenderintheIaCrace?Part3:BestpracticesandtheverdictIsPulumiaworthycontenderintheIaCrace?Part3:Bestpracticesandtheverdict
Jeevanandham Poongavanam
21 Oct 2025
Share
The best practices and hard truths that shaped our final verdict on Pulumi.
This is Part 3 of a three-part series on our production experience with Pulumi. Read Part 1 for the landscape analysis and Part 2 for our journey from chaos to clarity.
Hard-won best practices
Through our journey from chaos to clarity, we've distilled our experience into four critical practices. These aren't just theoretical recommendations — they're battle-tested patterns that transformed our Pulumi deployments from unpredictable adventures into reliable, maintainable infrastructure. If you take nothing else from our experience, implement these four practices from day one.
1. Use ComponentResource for abstraction
Because we're writing real code, we can use real abstraction patterns. ComponentResources are Pulumi's secret weapon for managing complexity, allowing you to create reusable, testable infrastructure components just like you would with application code.
classStorageComponent(pulumi.ComponentResource):def__init__(self, name:str, args: StorageComponentArgs, opts=None):super().__init__("custom:storage:StorageComponent", name,{}, opts)# All child resources use parent=self child_opts = pulumi.ResourceOptions(parent=self)# Create storage account with standard configuration self.storage_account = storage.StorageAccount(f"{args.environment}storage", resource_group_name=args.resource_group.name, sku=storage.SkuArgs(name=storage.SkuName.STANDARD_LRS),# Security defaults encoded in component enable_https_traffic_only=True, allow_blob_public_access=False, minimum_tls_version="TLS1_2", opts=child_opts
)# Always create diagnostic settings self.diagnostics = insights.DiagnosticSetting(f"{name}-diagnostics", resource_uri=self.storage_account.id, workspace_id=args.log_analytics_workspace.id, opts=child_opts
)# Generate connection string using proper Output handling self.connection_string = self._get_connection_string()# Register outputs for external access self.register_outputs({"storage_account_name": self.storage_account.name,"connection_string": self.connection_string
})
python
Key principles:
Encode security and compliance requirements in components
Always set parent=self for child resources
Register outputs for properties that other components might need
Include monitoring and diagnostics by default
2. Leverage Pulumi configuration and secrets
Real code needs real configuration management. Just as you wouldn't hardcode database credentials in your application, your infrastructure code needs proper configuration and secret handling. Here's our approach:
Use namespace'd configuration: pulumi.Config("namespace")
Always use require_secret for sensitive values
Implement feature flags for optional components
Keep environment-specific values in Pulumi configuration, not code
3. Write deterministic, idempotent code
The biggest mindset shift is understanding that Pulumi programs must be deterministic:
# WRONG: Non-deterministicimport datetime
resource_name =f"backup-{datetime.now().strftime('%Y%m%d')}"# WRONG: External API callsimport requests
latest_version = requests.get("https://api.example.com/latest").json()["version"]# RIGHT: Deterministic namingresource_name =f"backup-{environment}-{region}"# RIGHT: Version in configconfig = pulumi.Config()app_version = config.require("app_version")
python
For dynamic values, use Pulumi's built-in providers:
# Generate random suffix that persists across runsrandom_suffix = random.RandomString("suffix", length=5, special=False)storage_account = storage.StorageAccount("storage", account_name=pulumi.Output.concat("stg", environment, random_suffix.result))
python
4. Organise and modularise your project
Just like any real codebase, organisation is critical. Our project structure principles mirror those of well-architected software projects because that's exactly what Pulumi projects are.
Our project organisation principles:
Separate components by domain, not by resource type
Use explicit argument classes for component inputs:
classUtilities:@staticmethoddefget_public_ip():"""Get runner's public IP for firewall rules"""return requests.get("https://api.ipify.org").text
@staticmethoddefget_keyvault_secret(vault_name, secret_name):"""Retrieve secret value using managed identity""" credential = DefaultAzureCredential() client = SecretClient( vault_url=f"https://{vault_name}.vault.azure.net", credential=credential
)return client.get_secret(secret_name).value
python
Stack references for cross-stack dependencies:
# In networking stackpulumi.export("vnet_id", networking.virtual_network.id)# In application stacknetwork_stack = pulumi.StackReference("organization/networking/prod")vnet_id = network_stack.get_output("vnet_id")
python
The verdict: is it a worthy contender?
After one year in the trenches, here's my honest assessment.
3 places where Pulumi shines
Complex, multi-service architectures: When you're orchestrating dozens of services with intricate dependencies, Pulumi's programming model excels. Our OpenFGA authorisation service deployment involves Container Apps, PostgreSQL with replication, DNS zones, managed identities, and complex initialisation logic—all orchestrated seamlessly.
Teams with strong programming skills: If your team already knows Python, TypeScript, or Go, the productivity gains are immediate. Our developers contribute infrastructure improvements alongside application features.
Rapid Azure feature adoption: Native providers mean same-day access to new Azure features as opposed to waiting for the provider updates in case of Terraform. When Azure released new features, we were able to use them on the same day (native provider updates automated).
5 examples where you should consider alternatives to Pulumi
Simple infrastructure needs: If you're deploying a basic web app with a database, Terraform's maturity and extensive examples might serve you better.
Teams without programming experience: HCL's constraints can actually be helpful for teams less familiar with programming concepts, but come with a cost of learning a DSL language. Pulumi's flexibility requires discipline.
Existing Terraform investment: If you have substantial Terraform modules and expertise, the migration cost might not be justified. But Pulumi enables you to write a bridge package for Terraform modules.
Compliance-heavy environments: While Pulumi supports policy-as-code, Terraform's ecosystem has more pre-built compliance modules and patterns.
Security and separation of duties concerns: Pulumi's "real code" philosophy requires exceptional discipline around secrets management. We've seen developers accidentally commit passwords directly in code (password = "SuperSecret123!") because they treated infrastructure code like application code — while Pulumi has excellent secret management through config.require_secret(), it's dangerously easy to bypass.
Without strong guardrails (code reviews, linting rules, CrossGuard policies), teams can inadvertently expose credentials, violate separation of duties, or scatter infrastructure definitions across repositories, making compliance audits nightmarish.
The verdict
For our team at Nearform, Pulumi has become one of our go-to choices for new projects. The initial learning curve was steep, and we made mistakes that cost us time and sleep. But the investment has paid dividends:
Infrastructure deployments have become smoother since we started following the best practices
Developers own and improve infrastructure rather than throwing requests over the wall
Complex architectures that would be unwieldy in HCL are manageable and testable
We can leverage the broad open source ecosystems of languages we’re experts in (Python, Javascript/Typescript, etc.) for infrastructure tasks
The key insight? Pulumi isn't just "infrastructure as code" — it's infrastructure as real code, with all the power and responsibility that entails.
Looking forward
The IaC landscape continues to evolve rapidly. Pulumi's recent performance improvements, especially the Azure Native v3 provider's optimisation, have addressed many performance concerns. The enhanced refresh experiences and growing provider ecosystem suggest a bright future.
The addition of Pulumi ESC has revolutionised how we handle configuration across environments, while Pulumi Insights gives us visibility we never had with traditional IaC tools. The component package registry is growing daily, creating a rich ecosystem of reusable infrastructure patterns.
For teams evaluating IaC tools in 2025, my advice has evolved: If you have strong programming skills and need enterprise governance, Pulumi isn't just a contender — it's often the superior choice. The combination of flexibility, governance and ecosystem makes it particularly compelling for organisations that need to move fast while maintaining compliance.
Yes, you'll make mistakes. Yes, there's a learning curve. But with CrossGuard policies preventing those mistakes from reaching production and the wealth of component packages available, the path is much smoother than our journey a year ago.
Start small, invest in understanding Outputs and ComponentResources early, leverage the package ecosystem and implement policies from day one. The infrastructure-as-real-code approach isn't just a different way to declare resources — it's a fundamental shift in how we think about and manage cloud infrastructure.
One year ago, we wondered if choosing Pulumi was a mistake. Today, with the full ecosystem at our disposal and governance capabilities that exceed anything else we've used, we can't imagine building complex infrastructure any other way.