Skycrane

Infrastructure as Code
Reimagined

Declarative. Secure. Python-like syntax.

Built with Starlark, powered by WebAssembly, designed for modern cloud infrastructure. Say goodbye to HCL limitations and state file nightmares.

Why Skycrane?

Every feature designed to solve real infrastructure pain points.

Starlark Language

Python-like syntax with real loops, conditionals, and functions. No more HCL limitations or string interpolation nightmares.

  • • Real programming constructs
  • • Standard library support
  • • Type checking

WASI Security

Every plugin runs in a WebAssembly sandbox with explicit capabilities. No more trusting random providers with full system access.

  • • Capability-based permissions
  • • Resource isolation
  • • Supply chain protection

GitOps-Native State

State stored as OCI artifacts or encrypted S3 objects. No corruption, full history, instant rollbacks. GitOps workflows built-in.

  • • OCI registry & S3 backends
  • • Client-side encryption
  • • Version controlled state
  • • Atomic operations

Type Safety

WIT interfaces ensure type safety across plugin boundaries. Catch errors at compile time, not in production.

  • • Compile-time validation
  • • Auto-generated bindings
  • • IDE autocomplete

Real Debugging

Set breakpoints, step through code, inspect variables. Debug infrastructure like any other code.

  • • Interactive debugger
  • • Stack traces that help
  • • Variable inspection

Multi-Provider

Support for Hetzner, Cloudflare, Exoscale, Scaleway, DigitalOcean and more. Mix providers in a single configuration.

  • • Best-of-breed approach
  • • Unified interface
  • • Cross-provider deps

Real Code Examples

See how Skycrane makes infrastructure declaration intuitive and powerful.

web-app.star
# Create network infrastructure
network = hetzner.network(
    name = "app-network",
    ip_range = "10.0.0.0/16"
)

# Separate subnets for different tiers
web_subnet = hetzner.subnet(
    network_id = network.id,
    type = "cloud",
    ip_range = "10.0.1.0/24"
)

db_subnet = hetzner.subnet(
    network_id = network.id,
    type = "cloud", 
    ip_range = "10.0.2.0/24"
)

# Database with persistent storage
db_volume = hetzner.volume(
    name = "postgres-data",
    size = 100,  # GB
    location = "fsn1"
)

db_server = hetzner.server(
    name = "postgres-primary",
    server_type = "cx31",
    image = "ubuntu-22.04",
    location = "fsn1",
    networks = [db_subnet.id],
    volumes = [db_volume.id],
    user_data = file("scripts/install-postgres.sh")
)

# Web servers - Python loops!
web_servers = []
for i in range(1, 4):
    server = hetzner.server(
        name = f"web-{i}",
        server_type = "cx21",
        image = "ubuntu-22.04",
        location = "fsn1",
        networks = [web_subnet.id],
        user_data = template("scripts/deploy-app.sh", {
            "db_host": db_server.private_ip
        })
    )
    web_servers.append(server)

# Load balancer with health checks
lb = hetzner.load_balancer(
    name = "web-lb",
    type = "lb11",
    location = "fsn1",
    network = network.id,
    targets = [s.id for s in web_servers],
    health_check = {
        "protocol": "http",
        "port": 80,
        "path": "/health"
    }
)

# Output the load balancer IP
output("app_url", f"https://{lb.ipv4}")

Complete Web Application

Deploy a production-ready web application on Hetzner Cloud with load balancing, persistent storage, and network isolation.

Key Features:

  • ✓ Network isolation with separate subnets
  • ✓ Persistent volume for database
  • ✓ Multiple web servers created with a loop
  • ✓ Load balancer with health checks
  • ✓ Template-based configuration

Resource Graph:

Skycrane automatically determines dependencies and creates resources in the correct order:

Network → Subnets → Volume
    ↓        ↓        ↓
    └──→ Servers ←───┘
            ↓
      Load Balancer
import-existing.star
# Import existing Hetzner infrastructure
existing_servers = hetzner.import_servers(
    labels = {"environment": "production"}
)

# Import by specific IDs
db_server = hetzner.import_server(id = "12345678")
db_volume = hetzner.import_volume(id = "87654321")

# Import Cloudflare DNS zone
zone = cloudflare.import_zone(name = "example.com")

# Import all DNS records from zone
dns_records = cloudflare.import_dns_records(
    zone_id = zone.id
)

# Import Exoscale instances by tag
compute_pool = exoscale.import_instances(
    zone = "ch-gva-2",
    tags = ["web", "api"]
)

# Import DigitalOcean droplets and volumes
do_resources = digitalocean.import_all(
    resource_types = ["droplet", "volume", "load_balancer"],
    region = "nyc3"
)

# Build on imported resources
new_server = hetzner.server(
    name = "web-new",
    server_type = "cx21",
    image = "ubuntu-22.04",
    networks = [existing_servers[0].network_id],
    user_data = template("scripts/setup.sh", {
        "db_host": db_server.private_ip
    })
)

# Manage imported resources like native ones
for server in existing_servers:
    hetzner.firewall_rule(
        server_id = server.id,
        direction = "in",
        protocol = "tcp",
        port = "443",
        source_ips = ["0.0.0.0/0"]
    )

Import Existing Infrastructure

Seamlessly import and manage existing resources from any supported provider. No need to recreate - just import and continue building.

Import Methods:

  • ✓ Import by resource ID
  • ✓ Import by tags or labels
  • ✓ Import entire resource groups
  • ✓ Selective import with filters
  • ✓ Bulk import operations

Supported Providers:

Import from all our supported cloud providers:

  • Hetzner: Servers, volumes, networks, load balancers
  • Cloudflare: Zones, DNS records, workers, rules
  • Exoscale: Instances, storage, security groups
  • Scaleway: Instances, volumes, databases
  • DigitalOcean: Droplets, volumes, load balancers
multi-provider.star
# Compute on Hetzner (best price/performance)
app_servers = []
for i in range(3):
    server = hetzner.server(
        name = f"app-{i + 1}",
        server_type = "cx31",
        location = "fsn1"
    )
    app_servers.append(server)

# CDN and DDoS protection on Cloudflare
zone = cloudflare.zone(name = "myapp.com")

for idx, server in enumerate(app_servers):
    cloudflare.dns_record(
        zone_id = zone.id,
        name = f"app{idx + 1}",
        type = "A",
        value = server.ipv4,
        proxied = true
    )

# Object storage on Scaleway (S3-compatible)
bucket = scaleway.object_bucket(
    name = "myapp-assets",
    region = "fr-par",
    acl = "public-read"
)

# Database on Exoscale (managed PostgreSQL)
database = exoscale.database(
    name = "myapp-db",
    type = "postgresql",
    version = "15",
    plan = "business-4",
    zone = "ch-gva-2"
)

# Monitoring on DigitalOcean
monitor = digitalocean.droplet(
    name = "monitoring",
    size = "s-2vcpu-4gb",
    image = "ubuntu-22-04-x64",
    region = "nyc3",
    user_data = file("scripts/setup-monitoring.sh")
)

# Cross-provider configuration
for server in app_servers:
    server.set_env({
        "DATABASE_URL": database.connection_string,
        "S3_BUCKET": bucket.endpoint,
        "MONITOR_HOST": monitor.ipv4
    })

Best-of-Breed Multi-Cloud

Use each provider for what they do best. Mix and match services across providers with unified configuration.

Provider Strengths:

  • Hetzner: Unbeatable compute price/performance
  • Cloudflare: Global CDN and DDoS protection
  • Scaleway: Cost-effective object storage
  • Exoscale: Reliable managed databases
  • DigitalOcean: Simple, developer-friendly services

Cross-Provider Features:

Skycrane handles the complexity of multi-cloud:

  • • Automatic dependency resolution across providers
  • • Unified credential management
  • • Cross-provider resource references
  • • Consistent error handling

Powerful Queries & Actions

Query existing infrastructure and perform imperative operations with ease.

Read-Only Queries

Query existing resources and metadata without modifying state. Use queries to discover, filter, and reference existing infrastructure.

queries.star
# Query existing resources
existing_servers = query.servers(
    by_label = {'environment": "production"}
)

# Query single resource by name
db_server = query.server(by_name = "database-primary")

# Query available metadata
available_types = query.server_types()
datacenters = query.datacenters()
images = query.available_images(by_type = "system")

# Use queries to inform resource creation
for dc in datacenters:
    if dc.network_zones > 2:
        # Create redundant setup in this datacenter
        for zone in dc.network_zones[:2]:
            server(
                name = f"web-{dc.name}-{zone}",
                datacenter = dc.name,
                server_type = available_types[0].name,
                network_zone = zone
            )

# Reference existing infrastructure
existing_network = query.network(by_name = "production")
existing_lb = query.load_balancer(by_name = "api-lb")

# Create new resources that integrate with existing ones
api_server = server(
    name = "api-new",
    networks = [existing_network.id],
    labels = {'lb-target": existing_lb.name}
)

Query Features:

  • • Filter by ID, name, labels, or custom attributes
  • • Query provider metadata (types, regions, images)
  • • Results are strongly typed and validated
  • • Cached during planning for performance
  • • Never modifies infrastructure state

Imperative Actions

Execute one-time operations on resources that go beyond declarative state management. Perfect for maintenance, emergency operations, and lifecycle management.

actions.star
# Lifecycle operations
action.server.reboot(
    server = "web-1",
    type = "soft"  # or "hard"
)

# Create snapshots for backup
snapshot = action.server.create_snapshot(
    server = db_server,
    description = f"Pre-upgrade backup {datetime.now()}",
    labels = {'type": "backup", "retention": "30d"}
)

# Volume operations
action.volume.resize(
    volume = data_volume,
    size = 500  # GB
)

# Attach volume with specific device
action.server.attach_volume(
    server = db_server,
    volume = backup_volume,
    device = "/dev/sdb",
    automount = True
)

# Emergency operations
if monitoring.alert_triggered:
    # Get console access for debugging
    console = action.server.request_console(
        server = problematic_server
    )
    print(f"Console URL: {console.url}")
    print(f"Password: {console.password}")
    
    # Reset root password if locked out
    new_creds = action.server.reset_password(
        server = problematic_server
    )

# Load balancer management
for server in new_servers:
    action.load_balancer.add_target(
        load_balancer = api_lb,
        type = "server",
        server = server,
        use_private_ip = True
    )

Action Capabilities:

  • • Execute immediately when invoked
  • • Support for async operations with task tracking
  • • One-time operations (not idempotent)
  • • Emergency and maintenance procedures
  • • Create derived resources (snapshots, backups)

Combining Queries and Actions

Maintenance Automation:

# Find servers that need updates
servers_to_update = query.servers(
    by_label = {'needs_update": "true"}
)

# Perform rolling update
for server in servers_to_update:
    # Create snapshot before update
    snapshot = action.server.create_snapshot(
        server = server,
        description = "Pre-update backup"
    )
    
    # Remove from load balancer
    action.load_balancer.remove_target(
        load_balancer = lb,
        server = server
    )
    
    # Perform update
    action.server.reboot(server = server, type = "hard")
    
    # Wait and re-add to load balancer
    wait_for_healthy(server)
    action.load_balancer.add_target(
        load_balancer = lb,
        server = server
    )

Disaster Recovery:

# Query all production resources
prod_servers = query.servers(
    by_label = {'env": "production"}
)
prod_volumes = query.volumes(
    by_label = {'env": "production"}
)

# Create disaster recovery snapshots
dr_snapshots = []
for server in prod_servers:
    snapshot = action.server.create_snapshot(
        server = server,
        description = f"DR-{datetime.now()}",
        labels = {'type": "disaster-recovery"}
    )
    dr_snapshots.append(snapshot)

# Export snapshot list for DR procedures
export("dr_snapshots", dr_snapshots)
export("dr_timestamp", datetime.now())

# In another region, restore from snapshots
if disaster_recovery_triggered:
    for snapshot in dr_snapshots:
        server(
            name = snapshot.source_name + "-dr",
            image = snapshot.id,
            datacenter = dr_datacenter
        )

Build Your Own Plugins

Extend Skycrane with custom providers using our secure plugin architecture.

Plugin Architecture

Plugin Structure

my-plugin/
├── src/
│   ├── lib.rs         # Plugin implementation
│   └── wit/           # WIT interface definitions
├── spec/
│   ├── base.star      # Capabilities & metadata
│   ├── resources.star # Resource definitions
│   └── actions.star   # Actions & workflows
├── Cargo.toml         # Rust dependencies
└── wit.toml           # WIT dependencies

Capability Declaration

module(
    name = "my-provider",
    version = "v0.1.0",
    capabilities = capabilities(
        inherits = [
            INHERIT_ENV,      # API keys
            INHERIT_NETWORK,  # API calls
        ],
        mounts = [
            mount(
                host_path = "/var/lib/my-provider",
                guest_path = "/data",
                dir_perms = {"read": true, "write": true},
            ),
        ],
    ),
)

Development with SDK

Using Skyforge SDK

use skyforge_sdk::prelude::*;

#[skyforge_plugin]
impl Provider for MyProvider {
    async fn create_instance(
        &self, 
        config: InstanceConfig
    ) -> Result<Instance> {
        // Your implementation
        let instance = self.api_client
            .create_instance(&config)
            .await?;
            
        Ok(Instance {
            id: instance.id,
            ip: instance.public_ip,
            state: InstanceState::Running,
        })
    }
}

Using Macros

#[derive(SkycranResource)]
#[resource(provider = "my-provider")]
struct Server {
    #[resource(id)]
    name: String,
    
    #[resource(required)]
    size: String,
    
    #[resource(computed)]
    ip_address: String,
    
    #[resource(mutable)]
    tags: HashMap<String, String>,
}

Zero-Trust Plugin Security Model

No Default Trust

Skycrane ships with NO trusted keys. Every plugin must be explicitly trusted by YOU:

# First time installing any plugin
$ skycrane plugin install github:hetzner/skycrane-hetzner@v1.0.0

Downloading plugin bundle...
✓ plugin.wasm (2.3 MB)
✓ plugin.wasm.sig (455 bytes)
✓ public.key (3.2 KB)
✓ metadata.json (1.2 KB)

Verifying plugin...
Plugin signed by:
  Fingerprint: ABC123DEF456...
  Identity: Hetzner Cloud GmbH <plugins@hetzner.com>

❌ This key is NOT in your trust store.

To use this plugin, you must explicitly trust this key:
  skycrane trust add-key ABC123DEF456...

Your Trust Store

You control exactly which plugin authors you trust. No central authority:

# Add a trusted key
$ skycrane trust add-key ABC123DEF456... --name "Hetzner"
✓ Key added to trust store

# List your trusted keys
$ skycrane trust list
YOUR TRUSTED KEYS:
- ABC123DEF456... "Hetzner"
- DEF456789GHI... "My Company"

# Remove compromised key
$ skycrane trust remove-key ABC123DEF456...

Complete Security Architecture

Plugin Bundle

Every release includes:

  • • plugin.wasm (the code)
  • • plugin.wasm.sig (signature)
  • • public.key (signing key)
  • • metadata.json (checksums)
Verification Flow

Automatic checks:

  • • Signature validation
  • • Checksum verification
  • • Trust store lookup
  • • Capability review
Runtime Isolation

WASI sandboxing:

  • • No filesystem access
  • • No network by default
  • • Explicit capabilities
  • • Resource limits

Publishing Your Own Plugins

# Sign and publish your plugin
#!/bin/bash
VERSION=$1

# Build plugin
cargo component build --release

# Sign with your GPG key
gpg --detach-sign --armor plugin.wasm
gpg --export --armor YOUR_KEY > public.key
# GitHub Actions automated signing
name: Release Plugin
on:
  push:
    tags: ['v*']

jobs:
  release:
    steps:
      - name: Sign and publish
        env:
          GPG_KEY: $${ secrets.GPG_KEY }

Built for Scale

Every architectural decision optimizes for security, performance, and developer experience.

Core Architecture

Starlark Runtime

Deterministic Python-like language for configuration. Sandboxed execution with controlled imports and no I/O operations.

Resource Graph Engine

Automatically builds DAG from resource dependencies. Parallel execution where possible, automatic rollback on failures.

WASI Plugin Host

WebAssembly runtime for secure plugin execution. Each plugin runs in isolation with explicit capability grants.

OCI State Store

Immutable state artifacts pushed to OCI registries. Full history, cryptographic signatures, atomic operations.

Encrypted State

State can be encrypted before pushing to S3 or OCI registries. Password-based encryption with secure key derivation.

Plugin System

WIT Interfaces

Type-safe plugin interfaces using WebAssembly Interface Types:

interface provider {
  create-instance: func(config: instance-config) 
    -> result<instance, error>
  delete-instance: func(id: string) 
    -> result<unit, error>
  get-instance: func(id: string) 
    -> result<instance, error>
}

Capability Model

Fine-grained permissions for plugin operations:

[capabilities]
hetzner:read = ["server:list", "network:list"]
hetzner:write = ["server:create", "server:delete"]
network = ["api.hetzner.cloud"]
filesystem = []

Language Support

Write plugins in any language that compiles to WASM: Rust, Go, AssemblyScript, C/C++, and more.

GitOps-Ready State Management

OCI Registry Storage

Push state as immutable OCI artifacts to any container registry:

# Push state to registry
$ skycrane state push registry.io/myorg/state:v1.2.3

# Pull specific state version
$ skycrane state pull registry.io/myorg/state:v1.2.3

# List all state versions
$ skycrane state list registry.io/myorg/state
  • • Versioned with tags
  • • Content-addressable
  • • Works with any OCI registry
  • • GitOps workflow compatible

S3-Compatible Storage

Traditional state backend with modern features:

# Configure S3 backend
$ skycrane init --backend s3 \
  --bucket my-state-bucket \
  --region eu-central-1

# Enable encryption
$ skycrane state encrypt --enable
Enter encryption password: ****
State encryption enabled.
  • • Works with S3, MinIO, R2
  • • Automatic versioning
  • • State locking via DynamoDB
  • • Optional client-side encryption

State Encryption

Encrypt sensitive state data before storage:

# Configure encryption in .skycrane.star
config(
    backend = "oci",
    registry = "registry.io/myorg/state",
    encryption = {
        "enabled": true,
        "algorithm": "aes-256-gcm",
        "kdf": "argon2id"
    }
)
  • • AES-256-GCM encryption
  • • Argon2id key derivation
  • • Password prompts on pull
  • • Zero-knowledge architecture

GitOps Workflow Example

CI/CD Pipeline:

name: Deploy Infrastructure
on:
  push:
    branches: [main]

jobs:
  deploy:
    steps:
      - uses: actions/checkout@v3
      
      - name: Apply changes
        run: |
          skycrane apply --auto-approve
          
      - name: Push state to registry
        run: |
          VERSION="$${GITHUB_SHA::7}"
          skycrane state push \
            ghcr.io/$${GITHUB_REPOSITORY}/state:$${VERSION}
            
      - name: Tag as latest
        run: |
          skycrane state tag \
            ghcr.io/$${GITHUB_REPOSITORY}/state:latest

Benefits:

  • Version Control: Every state change is versioned and traceable
  • Rollback: Revert to any previous state version instantly
  • Audit Trail: Complete history of who changed what and when
  • Multi-Environment: Separate state per environment using tags
  • Disaster Recovery: State replicated across registry mirrors

Security is Non-Negotiable

Every line of code, every architectural decision, every feature is designed with security first.

Zero Trust Architecture

No embedded trust. No default keys. You decide who to trust.

  • ✓ User-controlled trust store
  • ✓ Explicit capability grants
  • ✓ No ambient authority
  • ✓ Decentralized trust model

Supply Chain Security

Every plugin verified. Every change tracked. No surprises.

  • ✓ Mandatory signatures
  • ✓ Self-contained bundles
  • ✓ Checksum verification
  • ✓ Git-based distribution

Runtime Isolation

WebAssembly sandbox. Capability-based security. Defense in depth.

  • ✓ WASI sandboxing
  • ✓ Resource limits
  • ✓ Network isolation
  • ✓ Filesystem boundaries

Security Deep Dive

What Our Security Prevents

  • Supply chain attacks: Can't inject code without private key
  • Impersonation: Can't pretend to be another publisher
  • Tampering: Can't modify plugins after signing
  • Accidental trust: Must explicitly trust each key
  • Privilege escalation: Plugins can't exceed granted capabilities

Legal & Compliance

PLUGIN SECURITY NOTICE:

  • • Skycrane does not embed or endorse any plugin signing keys
  • • Users are responsible for their own trust decisions
  • • CloudFlavor provides the verification mechanism only
  • • We make no warranties about third-party plugins
  • • Always verify key fingerprints through independent channels
  • • By trusting a key, you accept responsibility for plugins signed with it

Comparison to Other Systems

SystemTrust ModelOur Approach
npm/yarnCentral registry + optional signingDecentralized, mandatory signing
Docker HubNotary (optional)Mandatory signing, bundled keys
APT/YUMDistro manages keysUser manages keys
App StorePlatform gatekeepersNo gatekeeper, user decides
HomebrewGit repo, no signingGit + mandatory signing

Ready to Transform Your Infrastructure?

Join the growing community of teams who've said goodbye to state file nightmares and hello to infrastructure that just works.

Get Started in 60 Seconds

# Install Skycrane
$ curl -sSL https://skycrane.io/install | sh

# Initialize your project
$ skycrane init my-infrastructure

# Write your first resource
$ echo 'server = hetzner.server(name="web-1", type="cx21")' > main.star

# Deploy!
$ skycrane apply