Files
beanflows/infra/__main__.py
Deeman f207fb441d Add supervisor deployment with continuous pipeline orchestration
Implements automated supervisor instance deployment that runs scheduled
pipelines using a TigerBeetle-inspired continuous orchestration pattern.

Infrastructure changes:
- Update Pulumi to use existing R2 buckets (beanflows-artifacts, beanflows-data-prod)
- Rename scheduler → supervisor, optimize to CCX11 (€4/mo)
- Remove always-on worker (workers are now ephemeral only)
- Add artifacts bucket resource for CLI/pipeline packages

Supervisor architecture:
- supervisor.sh: Continuous loop checking schedules every 15 minutes
- Self-updating: Checks for new CLI versions hourly
- Fixed schedules: Extract at 2 AM UTC, Transform at 3 AM UTC
- systemd service for automatic restart on failure
- Logs to systemd journal for observability

CI/CD changes:
- deploy:infra now runs on every master push (not just on changes)
- New deploy:supervisor job:
  * Deploys supervisor.sh and systemd service
  * Installs latest materia CLI from R2
  * Configures environment with Pulumi ESC secrets
  * Restarts supervisor service

Future enhancements documented:
- SQLMesh-aware scheduling (check models before running)
- Model tags for worker sizing (heavy/distributed hints)
- Multi-pipeline support, distributed execution
- Cost optimization with multi-cloud spot pricing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 22:23:55 +02:00

140 lines
4.3 KiB
Python

"""
BeanFlows.coffee Infrastructure
Cloudflare R2 + Iceberg + Hetzner compute stack
"""
import pulumi
import pulumi_cloudflare as cloudflare
import pulumi_hcloud as hcloud
# Load configuration
config = pulumi.Config()
cloudflare_account_id = config.require("cloudflare_account_id")
hetzner_location = config.get("hetzner_location") or "nbg1" # Nuremberg datacenter
# ============================================================
# Cloudflare R2 Storage + Data Catalog (Iceberg)
# ============================================================
# R2 bucket for artifacts (CLI + extract/transform packages)
# Note: Import existing bucket with:
# pulumi import cloudflare:index/r2Bucket:R2Bucket materia-artifacts <account_id>/beanflows-artifacts
artifacts_bucket = cloudflare.R2Bucket(
"materia-artifacts",
account_id=cloudflare_account_id,
name="beanflows-artifacts",
location="weur", # Western Europe
)
# R2 bucket for lakehouse (Iceberg tables)
# Note: Import existing bucket with:
# pulumi import cloudflare:index/r2Bucket:R2Bucket materia-lakehouse <account_id>/beanflows-data-prod
lakehouse_bucket = cloudflare.R2Bucket(
"materia-lakehouse",
account_id=cloudflare_account_id,
name="beanflows-data-prod",
location="weur",
)
# ============================================================
# Hetzner Cloud Infrastructure
# ============================================================
# SSH key for server access
ssh_key = hcloud.SshKey(
"materia-ssh-key",
name="materia-deployment-key",
public_key=config.require_secret("ssh_public_key"),
)
# Small CCX instance for supervisor (runs materia CLI to orchestrate pipelines)
# This is an always-on instance that creates/destroys ephemeral workers on-demand
supervisor_server = hcloud.Server(
"materia-supervisor",
name="materia-supervisor",
server_type="ccx11", # 2 vCPU, 4GB RAM, ~€4/mo (cheapest option)
image="ubuntu-24.04",
location=hetzner_location,
ssh_keys=[ssh_key.id],
labels={
"role": "supervisor",
"project": "materia",
},
user_data="""#!/bin/bash
set -e
# Basic server setup
apt-get update
apt-get install -y python3.13 python3-pip curl unzip
# Install Pulumi ESC CLI
curl -fsSL https://get.pulumi.com/esc/install.sh | sh
export PATH="$HOME/.pulumi/bin:$PATH"
echo 'export PATH="$HOME/.pulumi/bin:$PATH"' >> /root/.bashrc
# Create deployment directory
mkdir -p /opt/materia
# Configure environment
echo 'Setup complete. Materia CLI will be deployed via CI/CD.' > /opt/materia/README.txt
""",
)
# Note: Workers are created on-demand by the materia CLI
# No always-on worker instances in this architecture
# Firewall for servers (restrict to SSH + outbound only)
firewall = hcloud.Firewall(
"materia-firewall",
name="materia-firewall",
rules=[
# Allow SSH from anywhere (consider restricting to your IP)
hcloud.FirewallRuleArgs(
direction="in",
protocol="tcp",
port="22",
source_ips=["0.0.0.0/0", "::/0"],
),
# Allow all outbound traffic
hcloud.FirewallRuleArgs(
direction="out",
protocol="tcp",
port="any",
destination_ips=["0.0.0.0/0", "::/0"],
),
hcloud.FirewallRuleArgs(
direction="out",
protocol="udp",
port="any",
destination_ips=["0.0.0.0/0", "::/0"],
),
],
)
# Apply firewall to supervisor
supervisor_firewall = hcloud.FirewallAttachment(
"supervisor-firewall",
firewall_id=firewall.id,
server_ids=[supervisor_server.id],
)
# ============================================================
# Outputs
# ============================================================
pulumi.export("artifacts_bucket_name", artifacts_bucket.name)
pulumi.export("lakehouse_bucket_name", lakehouse_bucket.name)
pulumi.export("supervisor_ip", supervisor_server.ipv4_address)
# Export connection info for DuckDB
pulumi.export(
"duckdb_r2_config",
pulumi.Output.all(cloudflare_account_id, lakehouse_bucket.name).apply(
lambda args: {
"account_id": args[0],
"bucket": args[1],
"catalog_uri": f"https://catalog.cloudflarestorage.com/{args[0]}/r2-data-catalog",
}
),
)