BlogTutorialsInfrastructure as Code with Pulumi and Python: DanubeData Complete Guide

Infrastructure as Code with Pulumi and Python: DanubeData Complete Guide

Adrian Silaghi
Adrian Silaghi
January 30, 2026
13 min read
23 views
#pulumi #python #infrastructure-as-code #iac #devops #cloud #automation

Python developers love Pulumi because it lets you define infrastructure using the same language you build applications with. No YAML, no HCL—just Python. This guide shows you how to manage your DanubeData infrastructure with clean, Pythonic code.

Why Pulumi with Python?

Python is a natural fit for infrastructure as code:

  • Use familiar Python constructs: loops, conditionals, classes, dataclasses
  • Leverage the entire Python ecosystem (requests, boto3, etc.)
  • Type hints with mypy for safer infrastructure code
  • Virtual environments for dependency isolation
  • Integration with Jupyter notebooks for exploration

Getting Started

Install Pulumi CLI

# macOS
brew install pulumi

# Linux
curl -fsSL https://get.pulumi.com | sh

# Windows
choco install pulumi

Create a New Project

# Create a new directory
mkdir my-infrastructure && cd my-infrastructure

# Create and activate virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venvScriptsactivate

# Initialize a new Python project
pulumi new python

# Install the DanubeData provider
pip install pulumi_danubedata

Configure Authentication

# Set your DanubeData API token
pulumi config set danubedata:apiToken your-api-token --secret

Project Structure

A well-organized Pulumi Python project:

my-infrastructure/
├── Pulumi.yaml              # Project configuration
├── Pulumi.dev.yaml          # Development stack config
├── Pulumi.prod.yaml         # Production stack config
├── __main__.py              # Main entry point
├── requirements.txt         # Python dependencies
├── venv/                    # Virtual environment
└── infra/
    ├── __init__.py
    ├── vps.py               # VPS resources
    ├── database.py          # Database resources
    ├── cache.py             # Cache resources
    └── storage.py           # Storage resources

Creating Your First VPS

# __main__.py
import pulumi
import pulumi_danubedata as danubedata

# Create an SSH key for authentication
ssh_key = danubedata.SshKey("deploy-key",
    name="deploy-key",
    public_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample... deploy@example.com"
)

# Create a VPS instance
web_server = danubedata.Vps("web-server",
    name="web-server",
    image="ubuntu-24.04",
    datacenter="fsn1",
    resource_profile="small_shared",  # 2 vCPU, 4GB RAM
    auth_method="ssh_key",
    ssh_key_id=ssh_key.id,
    enable_ipv6=True,
    enable_backups=True
)

# Export the VPS public IP
pulumi.export("web_server_ip", web_server.public_ip)
pulumi.export("web_server_ipv6", web_server.ipv6_address)

Complete Web Application Stack

Here's a complete example using Python best practices:

# __main__.py
import pulumi
from pulumi import Config, Output, export, get_stack
import pulumi_danubedata as danubedata
from dataclasses import dataclass
from typing import Optional

# Configuration
config = Config()
environment = get_stack()
app_name = config.require("app_name")

@dataclass
class ResourceProfiles:
    """Resource profiles for different environments."""
    vps: str
    database: str
    cache: str

    @classmethod
    def for_environment(cls, env: str) -> "ResourceProfiles":
        profiles = {
            "dev": cls("nano_shared", "small", "micro"),
            "staging": cls("small_shared", "small", "small"),
            "prod": cls("medium_dedicated", "medium", "medium"),
        }
        return profiles.get(env, profiles["dev"])

profiles = ResourceProfiles.for_environment(environment)

# =====================
# SSH Key
# =====================
ssh_key = danubedata.SshKey("deploy-key",
    name=f"{app_name}-{environment}-deploy",
    public_key=config.require_secret("ssh_public_key")
)

# =====================
# PostgreSQL Database
# =====================
database = danubedata.Database("app-db",
    name=f"{app_name}-{environment}-db",
    engine="postgresql",
    engine_version="16",
    resource_profile=profiles.database,
    database_name=app_name.replace("-", "_"),
    datacenter="fsn1",
    enable_ssl=True
)

# =====================
# Redis Cache
# =====================
cache = danubedata.Cache("app-cache",
    name=f"{app_name}-{environment}-cache",
    cache_provider="redis",
    resource_profile=profiles.cache,
    datacenter="fsn1"
)

# =====================
# Storage Bucket
# =====================
bucket = danubedata.StorageBucket("app-storage",
    name=f"{app_name}-{environment}-assets",
    region="fsn1",
    versioning_enabled=True
)

storage_keys = danubedata.StorageAccessKey("app-storage-key",
    name=f"{app_name}-{environment}-key"
)

# =====================
# Application VPS
# =====================
app_server = danubedata.Vps("app-server",
    name=f"{app_name}-{environment}-app",
    image="ubuntu-24.04",
    datacenter="fsn1",
    resource_profile=profiles.vps,
    auth_method="ssh_key",
    ssh_key_id=ssh_key.id,
    enable_ipv6=True,
    enable_backups=environment == "prod"
)

# =====================
# Firewall Rules
# =====================
admin_ip = config.require("admin_ip")

web_firewall = danubedata.Firewall("web-firewall",
    name=f"{app_name}-{environment}-web-fw",
    rules=[
        {
            "direction": "inbound",
            "protocol": "tcp",
            "ports": "80",
            "source_ips": ["0.0.0.0/0", "::/0"],
            "description": "Allow HTTP",
        },
        {
            "direction": "inbound",
            "protocol": "tcp",
            "ports": "443",
            "source_ips": ["0.0.0.0/0", "::/0"],
            "description": "Allow HTTPS",
        },
        {
            "direction": "inbound",
            "protocol": "tcp",
            "ports": "22",
            "source_ips": [admin_ip],
            "description": "SSH from admin only",
        },
    ]
)

# Database firewall - allow only from app server
db_firewall = danubedata.Firewall("db-firewall",
    name=f"{app_name}-{environment}-db-fw",
    rules=[
        {
            "direction": "inbound",
            "protocol": "tcp",
            "ports": "5432",
            "source_ips": [app_server.public_ip.apply(lambda ip: f"{ip}/32")],
            "description": "PostgreSQL from app server",
        },
    ]
)

# =====================
# Exports
# =====================
export("app_server_ip", app_server.public_ip)
export("app_server_ipv6", app_server.ipv6_address)
export("database_endpoint", database.endpoint)
export("database_name", database.database_name)
export("cache_endpoint", cache.endpoint)
export("storage_endpoint", bucket.endpoint_url)
export("storage_access_key", storage_keys.access_key)
export("storage_secret_key", storage_keys.secret_key)

Using Component Resources

Create reusable Python classes for complex infrastructure:

# infra/webapp.py
import pulumi
from pulumi import ComponentResource, ResourceOptions, Output
import pulumi_danubedata as danubedata
from dataclasses import dataclass
from typing import Optional


@dataclass
class WebAppArgs:
    """Arguments for creating a WebApp component."""
    name: str
    environment: str
    ssh_public_key: pulumi.Input[str]
    vps_profile: str = "small_shared"
    db_profile: str = "small"
    cache_profile: str = "micro"
    enable_backups: bool = False


class WebApp(ComponentResource):
    """A complete web application infrastructure component."""

    def __init__(self, name: str, args: WebAppArgs,
                 opts: Optional[ResourceOptions] = None):
        super().__init__("custom:resource:WebApp", name, {}, opts)

        child_opts = ResourceOptions(parent=self)

        # SSH Key
        self.ssh_key = danubedata.SshKey(f"{name}-ssh",
            name=f"{args.name}-{args.environment}",
            public_key=args.ssh_public_key,
            opts=child_opts
        )

        # Database
        self.database = danubedata.Database(f"{name}-db",
            name=f"{args.name}-{args.environment}-db",
            engine="postgresql",
            engine_version="16",
            resource_profile=args.db_profile,
            database_name=args.name.replace("-", "_"),
            datacenter="fsn1",
            opts=child_opts
        )

        # Cache
        self.cache = danubedata.Cache(f"{name}-cache",
            name=f"{args.name}-{args.environment}-cache",
            cache_provider="redis",
            resource_profile=args.cache_profile,
            datacenter="fsn1",
            opts=child_opts
        )

        # Storage
        self.bucket = danubedata.StorageBucket(f"{name}-storage",
            name=f"{args.name}-{args.environment}-assets",
            region="fsn1",
            opts=child_opts
        )

        # VPS
        self.vps = danubedata.Vps(f"{name}-vps",
            name=f"{args.name}-{args.environment}",
            image="ubuntu-24.04",
            datacenter="fsn1",
            resource_profile=args.vps_profile,
            auth_method="ssh_key",
            ssh_key_id=self.ssh_key.id,
            enable_backups=args.enable_backups,
            opts=child_opts
        )

        self.register_outputs({
            "vps_ip": self.vps.public_ip,
            "db_endpoint": self.database.endpoint,
            "cache_endpoint": self.cache.endpoint,
        })
# __main__.py
from infra.webapp import WebApp, WebAppArgs

app = WebApp("myapp", WebAppArgs(
    name="myapp",
    environment="prod",
    ssh_public_key="ssh-ed25519 AAAA...",
    vps_profile="medium_shared",
    db_profile="medium",
    enable_backups=True
))

pulumi.export("app_ip", app.vps.public_ip)
pulumi.export("db_endpoint", app.database.endpoint)

Creating Multiple Resources with Loops

# __main__.py
import pulumi
import pulumi_danubedata as danubedata

# Define your servers
servers = [
    {"name": "web-1", "profile": "small_shared"},
    {"name": "web-2", "profile": "small_shared"},
    {"name": "api-1", "profile": "medium_shared"},
    {"name": "worker-1", "profile": "medium_dedicated"},
]

# Create SSH key once
ssh_key = danubedata.SshKey("shared-key",
    name="shared-deploy-key",
    public_key="ssh-ed25519 AAAA..."
)

# Create all servers
vps_instances = {}
for server in servers:
    vps = danubedata.Vps(server["name"],
        name=server["name"],
        image="ubuntu-24.04",
        datacenter="fsn1",
        resource_profile=server["profile"],
        auth_method="ssh_key",
        ssh_key_id=ssh_key.id
    )
    vps_instances[server["name"]] = vps

# Export all IPs
for name, vps in vps_instances.items():
    pulumi.export(f"{name}_ip", vps.public_ip)

Serverless Containers

# serverless.py
import pulumi
import pulumi_danubedata as danubedata

# Deploy from Docker image
api_service = danubedata.Serverless("api",
    name="api-service",
    deployment_type="image",
    image_url="ghcr.io/myorg/api:latest",
    port=8080,
    min_instances=0,  # Scale to zero
    max_instances=10,
    memory=512,
    cpu=0.5,
    env_vars={
        "NODE_ENV": "production",
        "DATABASE_URL": database.endpoint,
        "REDIS_URL": cache.endpoint,
    }
)

# Deploy from Git repository
web_app = danubedata.Serverless("web",
    name="web-app",
    deployment_type="git",
    git_url="https://github.com/myorg/web-app.git",
    git_branch="main",
    port=3000,
    min_instances=1,
    max_instances=5,
    build_command="npm run build",
    start_command="npm start"
)

pulumi.export("api_url", api_service.url)
pulumi.export("web_url", web_app.url)

Using Data Sources

# data_sources.py
import pulumi
import pulumi_danubedata as danubedata

# Get available VPS images
images = danubedata.get_vps_images()
for image in images.images:
    print(f"Image: {image.name} - {image.slug}")

# Get existing SSH keys
existing_keys = danubedata.get_ssh_keys()

# Get existing VPS instances
existing_vps = danubedata.get_vpss()
for vps in existing_vps.instances:
    pulumi.log.info(f"VPS: {vps.name} - {vps.public_ip}")

# Get all databases
databases = danubedata.get_databases()

Type Hints with Mypy

# infra/types.py
from typing import TypedDict, List, Optional, Literal
import pulumi

class FirewallRule(TypedDict):
    direction: Literal["inbound", "outbound"]
    protocol: Literal["tcp", "udp", "icmp"]
    ports: str
    source_ips: List[str]
    description: Optional[str]

class ServerConfig(TypedDict):
    name: str
    profile: str
    image: str
    enable_backups: bool

def create_firewall_rules(rules: List[FirewallRule]) -> List[dict]:
    """Create validated firewall rules."""
    return [dict(rule) for rule in rules]

Dynamic Providers

Create custom resources using Python:

# custom_resources.py
from typing import Any, Optional
import pulumi
from pulumi.dynamic import Resource, ResourceProvider, CreateResult, UpdateResult
import requests

class DnsRecordProvider(ResourceProvider):
    """Custom provider for external DNS records."""

    def create(self, props: dict) -> CreateResult:
        # Call external API
        response = requests.post(
            f"https://api.dns-provider.com/records",
            json={
                "name": props["name"],
                "type": props["type"],
                "value": props["value"],
                "ttl": props.get("ttl", 300),
            },
            headers={"Authorization": f"Bearer {props['api_key']}"}
        )
        record_id = response.json()["id"]
        return CreateResult(id_=record_id, outs=props)

    def delete(self, id: str, props: dict) -> None:
        requests.delete(
            f"https://api.dns-provider.com/records/{id}",
            headers={"Authorization": f"Bearer {props['api_key']}"}
        )

class DnsRecord(Resource):
    def __init__(self, name: str, props: dict,
                 opts: Optional[pulumi.ResourceOptions] = None):
        super().__init__(DnsRecordProvider(), name, props, opts)

CI/CD with GitHub Actions

# .github/workflows/pulumi.yml
name: Pulumi Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
  DANUBEDATA_API_TOKEN: ${{ secrets.DANUBEDATA_API_TOKEN }}

jobs:
  preview:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - uses: pulumi/actions@v5
        with:
          command: preview
          stack-name: dev

  deploy:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - uses: pulumi/actions@v5
        with:
          command: up
          stack-name: prod

Testing Infrastructure Code

# test_infrastructure.py
import unittest
import pulumi

class InfrastructureTest(unittest.TestCase):
    @pulumi.runtime.test
    def test_vps_has_correct_profile(self):
        from __main__ import app_server

        def check_profile(profile):
            self.assertEqual(profile, "small_shared")

        return app_server.resource_profile.apply(check_profile)

    @pulumi.runtime.test
    def test_database_has_ssl_enabled(self):
        from __main__ import database

        def check_ssl(ssl_enabled):
            self.assertTrue(ssl_enabled)

        return database.enable_ssl.apply(check_ssl)

if __name__ == "__main__":
    unittest.main()

Useful Commands

# Preview changes
pulumi preview

# Apply changes
pulumi up

# View outputs
pulumi stack output

# Export to JSON
pulumi stack output --json

# Destroy resources
pulumi destroy

# Import existing resource
pulumi import danubedata:index:Vps my-vps existing-vps-id

Requirements File

# requirements.txt
pulumi>=3.0.0
pulumi_danubedata>=1.0.0

Cost Estimation

Resource Profile Monthly Cost
VPS (small_shared) 2 vCPU, 4GB RAM 8.99
PostgreSQL (small) 1 vCPU, 2GB RAM 19.99
Redis (micro) 256MB RAM 4.99
Storage Bucket 1TB included 3.99
Total 37.96/month

Get Started

Ready to manage your infrastructure with Python?

Create your free DanubeData account

Then install the Pulumi provider:

pip install pulumi_danubedata

Full documentation and examples are available in our GitHub repository.

Share this article

Ready to Get Started?

Deploy your infrastructure in minutes with DanubeData's managed services.