Problem Context

Terraform and ARM templates are powerful but they're configuration languages โ€” not programming languages. When your infrastructure requirements involve loops over data fetched at runtime, complex conditional logic, or reusable abstractions that behave like classes, HCL and JSON start to feel limiting.

Pulumi lets you define cloud infrastructure using real programming languages โ€” and the Python SDK is one of the most popular choices. Your infrastructure code can import any Python library, use dataclasses, apply type hints, write unit tests with pytest, and integrate naturally with existing Python tooling your team already knows.

๐Ÿค” Sound familiar?
  • Your Terraform HCL has complex for_each and dynamic blocks that are hard to reason about
  • You want to unit-test your infrastructure logic before deploying
  • Your team already writes Python and doesn't want to learn another DSL
  • You need to call an API or read a database to generate resource configuration

Pulumi + Python means your infrastructure code is just Python โ€” with all the expressiveness that implies.

Installing Pulumi and Creating a Project

# Install Pulumi CLI
curl -fsSL https://get.pulumi.com | sh

# Create a new Azure Python project
pulumi new azure-python --name myapp --stack dev

# Project structure
myapp/
โ”œโ”€โ”€ __main__.py      # Infrastructure entry point
โ”œโ”€โ”€ Pulumi.yaml      # Project metadata
โ”œโ”€โ”€ Pulumi.dev.yaml  # Stack config (dev environment)
โ””โ”€โ”€ requirements.txt

Core Concepts: Resources, Config, Outputs

# __main__.py
import pulumi
import pulumi_azure_native as azure

# Config โ€” per-stack values
config = pulumi.Config()
environment = config.require('environment')
location = config.get('location') or 'westus2'

# Resource group
rg = azure.resources.ResourceGroup(
    f'rg-{environment}',
    resource_group_name=f'rg-myapp-{environment}',
    location=location,
    tags={
        'Environment': environment,
        'ManagedBy': 'pulumi',
    },
)

# Storage account
storage = azure.storage.StorageAccount(
    f'sa-{environment}',
    account_name=f'samyapp{environment}001',
    resource_group_name=rg.name,
    location=rg.location,
    sku=azure.storage.SkuArgs(name='Standard_LRS'),
    kind='StorageV2',
    allow_blob_public_access=False,
    minimum_tls_version='TLS1_2',
    tags=rg.tags,
)

# Export outputs
pulumi.export('resourceGroupName', rg.name)
pulumi.export('storageAccountName', storage.name)
pulumi.export('blobEndpoint', storage.primary_endpoints.blob)

Stack Configuration Per Environment

# Set config values per stack
pulumi stack select dev
pulumi config set environment dev
pulumi config set location westus2

pulumi stack select prod
pulumi config set environment prod
pulumi config set location eastus

# Secrets โ€” encrypted in Pulumi state
pulumi config set --secret dbPassword "super-secret"

# Access in code
db_password = config.require_secret('dbPassword')
# db_password is a pulumi.Output[str] โ€” never a plain string

Outputs and Input/Output Types

Pulumi uses Output[T]for values that are only known after a resource is created (like a generated resource name or an IP address). You can't use them as regular Python values โ€” instead, compose them using apply or helper functions:

import pulumi

# Wrong โ€” TypeError at runtime
connection_string = 'Server=' + storage.name + ';...'

# Correct โ€” using apply
connection_string = storage.name.apply(
    lambda name: f'DefaultEndpointsProtocol=https;AccountName={name};...'
)

# Or using pulumi.Output.concat()
connection_string = pulumi.Output.concat(
    'DefaultEndpointsProtocol=https;AccountName=',
    storage.name,
    ';AccountKey=',
    storage_key,
)

pulumi.export('connectionString', connection_string)

ComponentResource: Reusable Infrastructure Components

ComponentResourceis Pulumi's equivalent of a Terraform module โ€” a class that groups related resources:

from typing import Optional
import pulumi
import pulumi_azure_native as azure

class SecureStorageComponent(pulumi.ComponentResource):
    def __init__(
        self,
        name: str,
        resource_group_name: pulumi.Input[str],
        location: pulumi.Input[str],
        environment: str,
        opts: Optional[pulumi.ResourceOptions] = None,
    ):
        super().__init__('myapp:storage:SecureStorageComponent', name, None, opts)

        child_opts = pulumi.ResourceOptions(parent=self)

        self.account = azure.storage.StorageAccount(
            f'{name}-sa',
            account_name=f'sa{name}{environment}001',
            resource_group_name=resource_group_name,
            location=location,
            sku=azure.storage.SkuArgs(name='Standard_LRS'),
            kind='StorageV2',
            allow_blob_public_access=False,
            minimum_tls_version='TLS1_2',
            opts=child_opts,
        )

        self.register_outputs({
            'accountName': self.account.name,
            'blobEndpoint': self.account.primary_endpoints.blob,
        })

# Usage
storage = SecureStorageComponent(
    'app-storage',
    resource_group_name=rg.name,
    location=rg.location,
    environment='prod',
)
pulumi.export('storageEndpoint', storage.account.primary_endpoints.blob)

Unit Testing with pytest

# tests/test_storage.py
import pytest
import pulumi

class MyMocks(pulumi.runtime.Mocks):
    def new_resource(self, args):
        return [args.name + '_id', args.inputs]

    def call(self, args):
        return {}

pulumi.runtime.set_mocks(MyMocks())

import infra  # your __main__.py as a module

@pytest.mark.asyncio
async def test_storage_no_public_access():
    sa = infra.storage
    allow_public = await pulumi.Output.from_input(sa.allow_blob_public_access).future()
    assert allow_public is False, "Storage account must have public access disabled"

@pytest.mark.asyncio
async def test_storage_tls_version():
    sa = infra.storage
    tls = await pulumi.Output.from_input(sa.minimum_tls_version).future()
    assert tls == 'TLS1_2'

Deployment Commands

# Preview changes (like terraform plan)
pulumi preview

# Deploy
pulumi up

# Destroy (with confirmation prompt)
pulumi destroy

# Show current stack state
pulumi stack output

# Import existing resource into Pulumi state
pulumi import azure-native:storage:StorageAccount myStorage \
  /subscriptions/.../resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/st-prod-001

Production Checklist

  • Use a remote state backend โ€” Pulumi Cloud, Azure Blob, or S3 (never local filesystem for production)
  • Separate stacks per environment: pulumi stack select prod before any apply
  • Encrypt all secrets with config set --secret โ€” never store in Pulumi.yaml
  • Write unit tests for every ComponentResource; test properties that matter for security
  • Run pulumi preview in CI on every PR; gate merges on successful preview
  • Pin provider versions in requirements.txt; review changelogs before upgrades
  • Use ResourceOptions(protect=True) on stateful resources to prevent accidental deletion