Docs / Automation & IaC / Manage Servers with Pulumi and Python

Manage Servers with Pulumi and Python

By Admin · Mar 15, 2026 · Updated Apr 24, 2026 · 169 views · 5 min read

Pulumi lets you define infrastructure using real programming languages instead of domain-specific languages like HCL. With Python, you get loops, conditionals, functions, classes, and the entire Python ecosystem for managing your cloud infrastructure. This guide covers using Pulumi with Python for server management on your VPS.

Why Pulumi over Terraform?

  • Real programming languages: Python, TypeScript, Go, C#, Java — not a DSL
  • Full language features: Loops, conditionals, functions, classes, testing frameworks
  • Package ecosystem: Use any Python library alongside infrastructure code
  • IDE support: Full autocomplete, type checking, refactoring tools
  • State management: Built-in state backend (Pulumi Cloud) or self-hosted (S3, local)

Installation

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

# Verify installation
pulumi version

# Install Python provider packages
pip install pulumi pulumi-command pulumi-hcloud pulumi-cloudflare

# Create a new project
mkdir my-infra && cd my-infra
pulumi new python

# Or create with a specific template
pulumi new python --name my-infra --description "Server infrastructure"

Define Infrastructure in Python

# __main__.py
import pulumi
import pulumi_hcloud as hcloud
import pulumi_cloudflare as cloudflare

# Configuration
config = pulumi.Config()
environment = config.require("environment")
ssh_key_id = config.require_int("ssh_key_id")

# Define server configurations
server_configs = {
    "web-1": {"type": "cx32", "location": "nbg1", "role": "webserver"},
    "web-2": {"type": "cx32", "location": "fsn1", "role": "webserver"},
    "db-1":  {"type": "cx42", "location": "nbg1", "role": "database"},
}

# Create servers using a loop
servers = {}
for name, cfg in server_configs.items():
    full_name = f"{name}-{environment}"
    server = hcloud.Server(
        full_name,
        name=full_name,
        server_type=cfg["type"],
        image="ubuntu-24.04",
        location=cfg["location"],
        ssh_keys=[str(ssh_key_id)],
        backups=True,
        labels={
            "environment": environment,
            "role": cfg["role"],
            "managed_by": "pulumi",
        },
        user_data=f"""#!/bin/bash
apt-get update
apt-get install -y python3 curl
hostnamectl set-hostname {full_name}
""",
    )
    servers[name] = server

# Create firewall for web servers
web_firewall = hcloud.Firewall(
    f"web-fw-{environment}",
    name=f"web-firewall-{environment}",
    rules=[
        hcloud.FirewallRuleArgs(
            direction="in",
            protocol="tcp",
            port="80",
            source_ips=["0.0.0.0/0", "::/0"],
        ),
        hcloud.FirewallRuleArgs(
            direction="in",
            protocol="tcp",
            port="443",
            source_ips=["0.0.0.0/0", "::/0"],
        ),
        hcloud.FirewallRuleArgs(
            direction="in",
            protocol="tcp",
            port="22",
            source_ips=["203.0.113.0/24"],  # Office IP
        ),
    ],
)

# Attach firewall to web servers
for name, server in servers.items():
    if server_configs[name]["role"] == "webserver":
        hcloud.FirewallAttachment(
            f"fw-attach-{name}",
            firewall_id=web_firewall.id.apply(lambda id: int(id)),
            server_ids=[server.id.apply(lambda id: int(id))],
        )

# Create DNS records
for name, server in servers.items():
    if server_configs[name]["role"] == "webserver":
        cloudflare.Record(
            f"dns-{name}",
            zone_id=config.require("cloudflare_zone_id"),
            name=name,
            type="A",
            content=server.ipv4_address,
            proxied=True,
        )

# Export outputs
for name, server in servers.items():
    pulumi.export(f"{name}_ip", server.ipv4_address)
    pulumi.export(f"{name}_id", server.id)

Using Component Resources (Classes)

# components/web_stack.py
import pulumi
from pulumi import ComponentResource, ResourceOptions
import pulumi_hcloud as hcloud

class WebStack(ComponentResource):
    """A reusable web stack component with server, firewall, and DNS."""

    def __init__(self, name: str, args: dict, opts=None):
        super().__init__("custom:infra:WebStack", name, None, opts)

        child_opts = ResourceOptions(parent=self)

        # Create server
        self.server = hcloud.Server(
            f"{name}-server",
            name=f"{name}-server",
            server_type=args.get("plan", "cx22"),
            image=args.get("image", "ubuntu-24.04"),
            location=args.get("location", "nbg1"),
            ssh_keys=args.get("ssh_keys", []),
            backups=args.get("backups", True),
            labels={"stack": name, "managed_by": "pulumi"},
            opts=child_opts,
        )

        # Create firewall
        self.firewall = hcloud.Firewall(
            f"{name}-fw",
            name=f"{name}-firewall",
            rules=[
                hcloud.FirewallRuleArgs(
                    direction="in", protocol="tcp", port="22",
                    source_ips=args.get("ssh_allow", ["0.0.0.0/0"]),
                ),
                hcloud.FirewallRuleArgs(
                    direction="in", protocol="tcp", port="80",
                    source_ips=["0.0.0.0/0", "::/0"],
                ),
                hcloud.FirewallRuleArgs(
                    direction="in", protocol="tcp", port="443",
                    source_ips=["0.0.0.0/0", "::/0"],
                ),
            ],
            opts=child_opts,
        )

        # Register outputs
        self.ip_address = self.server.ipv4_address
        self.server_id = self.server.id
        self.register_outputs({
            "ip_address": self.ip_address,
            "server_id": self.server_id,
        })

# Usage in __main__.py:
from components.web_stack import WebStack

prod_web = WebStack("prod-web", {
    "plan": "cx32",
    "location": "nbg1",
    "ssh_keys": ["12345"],
    "ssh_allow": ["203.0.113.0/24"],
})

pulumi.export("prod_web_ip", prod_web.ip_address)

Stack Management

# Create stacks for different environments
pulumi stack init dev
pulumi stack init staging
pulumi stack init production

# Set configuration per stack
pulumi config set environment production --stack production
pulumi config set ssh_key_id 12345 --stack production
pulumi config set --secret db_password "SecurePass!" --stack production

# Deploy
pulumi up --stack production

# Preview changes (dry run)
pulumi preview --stack production

# Destroy infrastructure
pulumi destroy --stack staging

Self-Hosted State Backend

# Use S3-compatible storage instead of Pulumi Cloud
pulumi login s3://my-pulumi-state-bucket

# Or use a local backend
pulumi login --local

# With MinIO (self-hosted S3)
export AWS_ACCESS_KEY_ID=minioadmin
export AWS_SECRET_ACCESS_KEY=minioadmin
pulumi login "s3://pulumi-state?endpoint=https://minio.yourdomain.com"

Best Practices

  • Use Component Resources (classes) to create reusable infrastructure building blocks
  • Leverage Python's type system with dataclasses or Pydantic for configuration validation
  • Use stacks to manage multiple environments (dev, staging, production)
  • Store secrets with pulumi config set --secret — they're encrypted in the state
  • Write tests using Pulumi's testing framework or pytest
  • Pin provider versions in requirements.txt for reproducibility

Was this article helpful?