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.
Every feature designed to solve real infrastructure pain points.
Python-like syntax with real loops, conditionals, and functions. No more HCL limitations or string interpolation nightmares.
Every plugin runs in a WebAssembly sandbox with explicit capabilities. No more trusting random providers with full system access.
State stored as OCI artifacts or encrypted S3 objects. No corruption, full history, instant rollbacks. GitOps workflows built-in.
WIT interfaces ensure type safety across plugin boundaries. Catch errors at compile time, not in production.
Set breakpoints, step through code, inspect variables. Debug infrastructure like any other code.
Support for Hetzner, Cloudflare, Exoscale, Scaleway, DigitalOcean and more. Mix providers in a single configuration.
See how Skycrane makes infrastructure declaration intuitive and powerful.
# 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}")Deploy a production-ready web application on Hetzner Cloud with load balancing, persistent storage, and network isolation.
Skycrane automatically determines dependencies and creates resources in the correct order:
Network → Subnets → Volume
↓ ↓ ↓
└──→ Servers ←───┘
↓
Load Balancer# 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"]
)Seamlessly import and manage existing resources from any supported provider. No need to recreate - just import and continue building.
Import from all our supported cloud providers:
# 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
})Use each provider for what they do best. Mix and match services across providers with unified configuration.
Skycrane handles the complexity of multi-cloud:
Query existing infrastructure and perform imperative operations with ease.
Query existing resources and metadata without modifying state. Use queries to discover, filter, and reference existing infrastructure.
# 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}
)Execute one-time operations on resources that go beyond declarative state management. Perfect for maintenance, emergency operations, and lifecycle management.
# 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
)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
)Extend Skycrane with custom providers using our secure plugin architecture.
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 dependenciesmodule(
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},
),
],
),
)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,
})
}
}#[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>,
}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...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...Every release includes:
Automatic checks:
WASI sandboxing:
# 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 }Every architectural decision optimizes for security, performance, and developer experience.
Deterministic Python-like language for configuration. Sandboxed execution with controlled imports and no I/O operations.
Automatically builds DAG from resource dependencies. Parallel execution where possible, automatic rollback on failures.
WebAssembly runtime for secure plugin execution. Each plugin runs in isolation with explicit capability grants.
Immutable state artifacts pushed to OCI registries. Full history, cryptographic signatures, atomic operations.
State can be encrypted before pushing to S3 or OCI registries. Password-based encryption with secure key derivation.
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>
}Fine-grained permissions for plugin operations:
[capabilities]
hetzner:read = ["server:list", "network:list"]
hetzner:write = ["server:create", "server:delete"]
network = ["api.hetzner.cloud"]
filesystem = []Write plugins in any language that compiles to WASM: Rust, Go, AssemblyScript, C/C++, and more.
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 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. 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"
}
) 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:latestBenefits:
Every line of code, every architectural decision, every feature is designed with security first.
No embedded trust. No default keys. You decide who to trust.
Every plugin verified. Every change tracked. No surprises.
WebAssembly sandbox. Capability-based security. Defense in depth.
PLUGIN SECURITY NOTICE:
| System | Trust Model | Our Approach |
|---|---|---|
| npm/yarn | Central registry + optional signing | Decentralized, mandatory signing |
| Docker Hub | Notary (optional) | Mandatory signing, bundled keys |
| APT/YUM | Distro manages keys | User manages keys |
| App Store | Platform gatekeepers | No gatekeeper, user decides |
| Homebrew | Git repo, no signing | Git + mandatory signing |
Join the growing community of teams who've said goodbye to state file nightmares and hello to infrastructure that just works.
# 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