Introduction to Pulumi: Python IaC Framework
Pulumi is an open-source Infrastructure as Code framework that lets you define cloud infrastructure using general-purpose programming languages—including Python—rather than domain-specific languages like Terraform's HCL. You write Python code that declares what cloud resources you want, and Pulumi handles the rest: creating a dependency graph, computing the diff between desired and actual state, and applying changes in the right order. Teams report 50% faster infrastructure iteration and 30% fewer human errors compared to manual cloud console navigation.
Why Pulumi Over Terraform?
Terraform uses HCL (HashiCorp Configuration Language), a custom DSL. You learn HCL syntax, loops, conditionals, and functions all over again. Pulumi uses your existing programming language—Python, in this case—so you skip the syntax learning curve and focus on infrastructure concepts.
Pulumi advantages for Python developers:
- Write infrastructure in Python; reuse functions, classes, and packages.
- Full programming language: conditions, loops, imports, custom functions (Terraform's HCL is limited).
- Automatic state management (no separate state files to manage; default storage is Pulumi Cloud, or use S3).
- Built-in secrets management; secrets are encrypted in the state file automatically.
- Type-safe: Python type hints catch errors before deployment.
Trade-offs:
- Smaller ecosystem than Terraform (fewer community providers, but major clouds are covered).
- Requires understanding cloud concepts; less beginner-friendly than point-and-click consoles.
- Pulumi Cloud has a free tier but charges for advanced features; self-hosting is available.
Pulumi Concepts: Projects, Stacks, and Resources
Three terms are core to Pulumi:
Project: A folder containing your infrastructure code. Projects have a Pulumi.yaml file declaring the project name, runtime (Python), and settings. One project often manages multiple environments (dev, staging, production) via different stacks.
Stack: A named, independent deployment of your project. The same code deployed with different variables. For example, Pulumi.dev.yaml and Pulumi.prod.yaml define dev and production stacks; the code is identical, but stack values differ (e.g., instance counts, region).
Resource: A cloud object managed by Pulumi: an EC2 instance, S3 bucket, VPC, etc. You declare resources in code; Pulumi creates, updates, or deletes them as needed.
Example:
my-infra/ (project)
├── __main__.py
├── Pulumi.yaml
├── Pulumi.dev.yaml
├── Pulumi.prod.yaml
└── requirements.txt
Stacks: dev (small instances), prod (large instances)
Resources: VPC, security groups, EC2 instances, RDS database, S3 bucket
Install Pulumi and Initialize a Project
Install Pulumi CLI from the official website, then initialize a new project:
# Install Pulumi CLI (macOS/Linux/Windows)
curl -fsSL https://get.pulumi.com | sh
# Verify installation
pulumi version
# Create a new project
pulumi new aws-python
# This creates a folder with Python files and Pulumi.yaml
# Follow the prompts: project name, stack name (dev), AWS region
The generated project structure:
my-first-stack/
├── __main__.py # Your infrastructure code
├── Pulumi.yaml # Project metadata
├── Pulumi.dev.yaml # Stack values for 'dev' stack
└── requirements.txt # Python dependencies (pulumi, pulumi-aws)
Your First Pulumi Program
The generated __main__.py is a minimal stack that creates an S3 bucket:
import pulumi
import pulumi_aws as aws
# Create an AWS resource (S3 bucket)
bucket = aws.s3.Bucket('my-bucket',
acl='private',
tags={'Environment': 'dev'}
)
# Export the bucket URL so it's visible after deployment
pulumi.export('bucket_url', bucket.arn)
This declares one resource: an S3 bucket with private ACL and tags. The pulumi.export line outputs values after the stack is deployed (useful for scripts that consume the outputs).
Deploy this stack:
# Authenticate with Pulumi Cloud (free account)
pulumi login
# Preview the changes (shows what Pulumi will do)
pulumi up
# At the prompt, select 'yes' to apply
# Pulumi creates the bucket, then prints:
# Outputs:
# bucket_url: arn:aws:s3:::my-bucket-a1b2c3d4
Adding More Resources and Outputs
A realistic stack declares multiple related resources. Here's a VPC with subnets and a security group:
import pulumi
import pulumi_aws as aws
# Create VPC
vpc = aws.ec2.Vpc('app-vpc',
cidr_block='10.0.0.0/16',
tags={'Name': 'app-vpc'}
)
# Create public subnet
public_subnet = aws.ec2.Subnet('public-subnet',
vpc_id=vpc.id,
cidr_block='10.0.1.0/24',
availability_zone='us-east-1a',
tags={'Name': 'public-subnet'}
)
# Create security group (firewall)
sg = aws.ec2.SecurityGroup('app-sg',
vpc_id=vpc.id,
description='Allow HTTP and HTTPS',
ingress=[
aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=80,
to_port=80,
cidr_blocks=['0.0.0.0/0']
),
aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=443,
to_port=443,
cidr_blocks=['0.0.0.0/0']
)
],
tags={'Name': 'app-sg'}
)
# Export outputs
pulumi.export('vpc_id', vpc.id)
pulumi.export('subnet_id', public_subnet.id)
pulumi.export('security_group_id', sg.id)
Notice the dependency: public_subnet references vpc.id, so Pulumi knows to create the VPC before the subnet. You don't explicitly order resources; Pulumi infers the graph.
Deploy and see the outputs:
pulumi up
# Outputs:
# security_group_id: sg-0a1b2c3d4e5f6g7h8
# subnet_id: subnet-0a1b2c3d4e5f6g7h8
# vpc_id: vpc-0a1b2c3d4e5f6g7h8
Managing Multiple Stacks
Create a second stack for production:
pulumi stack init prod
# Select AWS region: us-west-2 (different from dev)
pulumi stack select dev
pulumi up # Deploy dev stack
pulumi stack select prod
pulumi up # Deploy prod stack
Each stack has its own state file and Pulumi.yaml values. To customize resources per stack, use pulumi.get_config():
import pulumi
config = pulumi.Config()
# Read stack-specific values from Pulumi.dev.yaml or Pulumi.prod.yaml
instance_count = config.get_int('instance_count') or 1
instance_type = config.get('instance_type') or 't3.micro'
environment = pulumi.get_stack() # 'dev' or 'prod'
print(f"Deploying {instance_count} x {instance_type} instances to {environment}")
Set stack values:
pulumi config set instance_count 3 --stack dev
pulumi config set instance_count 10 --stack prod
pulumi config set instance_type t3.large --stack prod
Now the same code deploys 1 small instance to dev and 10 large instances to prod.
Key Takeaways
- Pulumi is an IaC framework that lets you write infrastructure as code in Python (not a DSL like HCL).
- Projects contain code; stacks are named deployments (dev, staging, prod) of the same code with different values.
- Resources are cloud objects declared in Python; Pulumi manages their creation, updates, and deletion.
- Pulumi handles dependency inference, state management, and diffing automatically.
- Export outputs to make resource IDs available to scripts or other infrastructure.
Frequently Asked Questions
Do I need a Pulumi Cloud account to use Pulumi?
No. The default backend is Pulumi Cloud (free for personal use), but you can self-host state in S3 or another backend. For teams, Pulumi Cloud adds features like role-based access control.
Can I use Pulumi with multiple AWS accounts?
Yes. Create a stack per account, or assume IAM roles in other accounts from your Pulumi code. Pulumi's Python API lets you assume roles and create clients for other accounts.
How does Pulumi compare to CloudFormation (AWS's native IaC)?
CloudFormation uses JSON/YAML templates and is AWS-only. Pulumi uses Python and works across AWS, Azure, GCP, Kubernetes, etc. Pulumi is more developer-friendly; CloudFormation is more AWS-integrated.
What if I accidentally run pulumi destroy?
Pulumi deletes all resources declared in the stack. State is preserved, so you can redeploy by running pulumi up again. For safety, use stack policies or AWS backup to protect critical resources.
Can I import existing AWS resources into Pulumi?
Yes. Use pulumi import to adopt existing resources into your stack. Example: pulumi import aws:s3/bucket:Bucket my-bucket my-existing-s3-bucket.