Is Pulumi a worthy contender in the IaC race? Part 3: Best practices and the verdict

avatar
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.

class StorageComponent(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:

# Config structure per environment
config = pulumi.Config()
env = config.require("environment")  # 'dev', 'staging', 'prod'

# Environment-specific configurations
db_config = pulumi.Config("database")
db_password = db_config.require_secret("admin_password")

# Feature flags for gradual rollouts
features = pulumi.Config("features")
enable_redis = features.get_bool("cache_enabled") or False

# Using config in resources
if enable_redis:
    redis_cache = cache.Redis(
        "redis",
        resource_group_name=resource_group.name,
        sku=cache.SkuArgs(
            name="Standard" if env == "prod" else "Basic",
            family="C",
            capacity=2 if env == "prod" else 0
        )
    )
python

Best practices:

  • 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-deterministic
import datetime
resource_name = f"backup-{datetime.now().strftime('%Y%m%d')}"

# WRONG: External API calls
import requests
latest_version = requests.get("https://api.example.com/latest").json()["version"]

# RIGHT: Deterministic naming
resource_name = f"backup-{environment}-{region}"

# RIGHT: Version in config
config = pulumi.Config()
app_version = config.require("app_version")
python

For dynamic values, use Pulumi's built-in providers:

# Generate random suffix that persists across runs
random_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:

  1. Separate components by domain, not by resource type
  2. Use explicit argument classes for component inputs:
class NetworkingComponentArgs:
    def __init__(
        self,
        environment: str,
        resource_group: resources.ResourceGroup,
        address_space: str,
        tags: dict = None
    ):
        self.environment = environment
        self.resource_group = resource_group
        self.address_space = address_space
        self.tags = tags or {}
python
  1. Create utility functions for common patterns:
class Utilities:
    @staticmethod
    def get_public_ip():
        """Get runner's public IP for firewall rules"""
        return requests.get("https://api.ipify.org").text

    @staticmethod
    def get_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
  1. Stack references for cross-stack dependencies:
# In networking stack
pulumi.export("vnet_id", networking.virtual_network.id)

# In application stack
network_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.

But wait - there's more.

Nearform publishes real-world learnings on data & AI, engineering, and digital strategy - with more merged in weekly.

Insights

Perspectives on AI in engineering, product development, and strategy, for enterprise executives.

Community

Deep dives and tutorials by engineers, for engineers.


You may also like

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.