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.
- Your Terraform HCL has complex
for_eachanddynamicblocks 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.txtCore 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 stringOutputs 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-001Production Checklist
- Use a remote state backend โ Pulumi Cloud, Azure Blob, or S3 (never local filesystem for production)
- Separate stacks per environment:
pulumi stack select prodbefore any apply - Encrypt all secrets with
config set --secretโ never store inPulumi.yaml - Write unit tests for every
ComponentResource; test properties that matter for security - Run
pulumi previewin 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

