* feat(e2e/harness): multinode cluster wait helpers + AWSClientForGateway (Stage A)
Stage A of mulga-siv-90. Compile-only; no tests wired yet.
multinode_cluster.go:
- Cluster.WaitNATSPeers polls /routez per node, dedupes by remote_id.
want=2 on healthy 3-node, =1 after a node-down event.
- Cluster.WaitPredastoreHealthy / WaitGatewayHealthy probe HTTPS per node
via insecureHTTPClient (multinode certs are self-signed across peers).
- Cluster.WaitDaemonReady runs DescribeInstanceTypes through every
node's gateway, not just env.ServiceIPs[0] — catches a daemon that's
only registered on one node.
multinode_aws_via.go:
- AWSClientForGateway clones *Env, swaps ServiceIPs to a single node's
Addr, returns a fresh AWSClient. Lets phases 6/7 issue describe/
stop/start across every node in turn (bash aws_via equivalent).
Init/Join helpers deferred to the pseudo-multinode bead (mulga-siv-91)
where they belong — real-multinode bootstrap is owned by tofu +
bootstrap-install.sh before the Go suite starts.
* feat(e2e/multinode): TestMultinodePreflight + TestMultinodeClusterHealth (Stage B)
Stage B of mulga-siv-90. First runnable Test* against a 3-node cluster.
multinode/main_test.go:
- Package-scoped singleton fixture, mirrors single/main_test.go.
- requireMultiNodeFixture skips when SPINIFEX_E2E unset or mode != multinode.
- Fixture = {Env, AWS, Harness, Cluster, Artifacts, TmpDir} — adds Cluster
to the single-node shape via harness.ClusterFromEnv (reads SPINIFEX_NODES
+ SPINIFEX_SSH_USER/KEY).
- TestMain drains harness.Fixture.Close + removes TmpDir at process exit.
multinode/helpers_test.go:
- needAZ / needInstanceTypeArch / needAMI shims (memoized via harness).
- readyNodeCount substring counter — matches bash `grep -c "Ready"`.
multinode/tests_test.go:
- Top-level Test* wrappers (TestMultinodePreflight, TestMultinodeClusterHealth).
multinode/preflight_test.go (phase 1):
- /dev/kvm writable check (no /dev/kvm = test environment is wrong, fail fast).
- peer_ssh hostname on every remote node. Skips node[0] to match bash —
the runner's loopback SSHd is not part of the contract.
multinode/cluster_health_test.go (phase 2):
- Cluster.WaitNATSPeers >= 2 per node.
- Cluster.WaitPredastoreHealthy / WaitGatewayHealthy across nodes.
- Cluster.WaitDaemonReady (DescribeInstanceTypes per gateway).
- spx get nodes shows N Ready, where N = cluster size.
- spx get vms returns (length only — content may be empty).
* feat(e2e/multinode): TestMultinodeInstanceDistribution + GuestSSH (Stage C)
Stage C of mulga-siv-90. Phase 3 (distribution) + phase 4 (guest SSH).
harness/multinode_distribution.go:
- InstanceHostingNode: SSH each peer, ps grep qemu-system | <id>.
Reuses NewPeerSSH; returns nil on no match (caller decides fatality).
- CountInstancesPerNode + AssertSpreadAcrossNodes: fan-out + minNodes
gate. Bash phase 3 warns-on-stacking; Go matches by accepting
minNodes=1 + caller-side log.
harness/multinode_guest_ssh.go:
- GuestSSHPort polls hosting node's qemu cmdline for hostfwd=tcp:...
reusing the existing hostfwdRE from ec2helpers.go (single regex, two
capture groups: host + port).
- GuestSSHEndpoint prefers PublicIpAddress, falls back to hosting-node
+ hostfwd. Mirrors the phase 4 conditional.
- GuestSSHReady polls `echo ready` via SSH with bash-equivalent -o flags.
multinode/helpers_test.go:
- needKeyPair (EnsureKeyPair shim).
- needInstanceTrio: pkg-level sync.Once launches 3 nano instances on
default VPC, registers terminate-on-process-exit. Phase 4+ reuse the
same trio without re-launch.
multinode/distribution_test.go (phase 3):
- launch trio + assert >= 1 node hosting (log warn on stacking).
- spx get vms contains every trio ID.
multinode/guest_ssh_test.go (phase 4):
- For each trio instance: resolve endpoint, poll SSH ready, assert id
reports ec2-user, log lsblk line count.
multinode/tests_test.go: TestMultinodeInstanceDistribution + TestMultinodeGuestSSH.
* feat(e2e/multinode): TestMultinode{Volume,CrossNodeGateway,CrossNodeOps} (Stage D)
Stage D of mulga-siv-90. Phases 5/6/7 — pure AWS API exercise (no
node-state churn).
volume_test.go (phase 5):
- Create 10GiB volume in discovered AZ, attach to trio[0] at /dev/sdf,
detach, delete. Inline lifecycle — DeleteVolume in-test would race a
registered Ensure* cleanup, surface as InvalidVolume.NotFound.
gateway_test.go (phase 6):
- Baseline DescribeInstances via the local gateway, then fan out
AWSClientForGateway per non-local node. Require.Equal across all —
cross-node fan-out regression surfaces immediately.
crossnode_ops_test.go (phase 7):
- Find trio[0]'s hosting node, stop via a different gateway, start via
a third (or same other on 2-node fallback). Returns to running so
later Test*s (phase 4 SSH, etc.) don't cascade-fail. otherTwoGateways
mirrors the bash fallback chain.
* feat(e2e/multinode): TestMultinodeNode{Failure,Recovery} (Stage E)
Stage E of mulga-siv-90. Phase 8 (failure) + phase 9 (recovery) +
diagnostic primitives.
harness/multinode_node_lifecycle.go:
- StopNode SSHes `sudo systemctl stop spinifex.target`. Non-fatal on
exit error (bash matches — cluster shutdown races the SSH conn).
- StartNode mirrors with sudo systemctl start.
- WaitNodeServiceReady polls a single node's HTTPS gateway. Distinct
from cluster-wide WaitGatewayHealthy so a recovering node can be
tracked without re-checking peers.
harness/multinode_diag.go:
- DumpAllNodeLogs fans out journalctl -u 'spinifex-*' per node into
artifactsDir/journal-<node>.log. Tolerates per-node SSH failure.
- SpxGetNodesAcrossCluster strips trailing whitespace off SpxGetNodes.
multinode/node_failure_test.go (phase 8):
- Stop node2, register t.Cleanup restart safety net.
- WaitNATSPeers want=1 (degraded), DescribeInstanceTypes via node1+node3,
DescribeInstances still returns the trio.
multinode/node_recovery_test.go (phase 9):
- StartNode node2 (idempotent — phase 9 in isolation still works).
- WaitNATSPeers want=2, WaitNodeServiceReady, spx get nodes 3 Ready,
DescribeInstanceTypes via the recovered gateway.
* feat(e2e/multinode): TestMultinodeVPCNetworking (Stage F)
Ports phase 10 from run-multinode-e2e.sh. Stands up a fresh non-default
VPC + subnet (10.200.0.0/16 + 10.200.1.0/24), launches 3 instances into
the subnet, then asserts:
- PrivateIpAddress is non-empty and matches 10.200.1.0/24
- All 3 IPs are unique
- SubnetId / VpcId on the instance match the freshly-created ids
- IPs persist across stop -> start cycles
Independent of the package-level trio (which uses the default VPC) —
this phase needs full control over subnet allocation so resources are
created and cleaned up inline via t.Cleanup (LIFO: instances ->
subnet -> VPC).
* feat(e2e/multinode): TestMultinodePlacementAndNAT (Stage G)
Ports phase 11 from run-multinode-e2e.sh — the largest single phase
(~340 bash lines covering spread placement + NAT Gateway egress). Eight
sub-steps preserved with matching harness.Step markers so the Go run
cross-references cleanly with the bash log:
Step 1: VPC + pub/priv subnets + IGW + main RT route + 2 SGs
(bastion: tcp/22 anywhere; private: tcp/22 from bastion-sg,
icmp from VPC)
Step 2: bastion launch, wait SSH, scp key
Step 3: spread placement group + 3 private instances via the group
Step 4: validate spread (>=2 unique hosting nodes; >=3 = full PASS)
Step 5: pre-NAT baseline — confirm NO internet from private VMs
Step 6: allocate EIP + create NAT GW + private RT + 0.0.0.0/0 -> NGW
Step 7: post-NAT — every private VM reaches 8.8.8.8 via NAT GW
Step 8: inline teardown of NAT GW + private RT + EIP
Each resource is owned by the test (no Ensure* memoization) so a failure
mid-flight tears the full VPC graph down via t.Cleanup in LIFO order,
matching the bash teardown sequence. NAT GW state polling is duplicated
locally (waitForNATGatewayStateFatal/Best); the equivalents in
single/natgw_test.go are unexported. Promote to harness/poll.go once a
third consumer lands.
* ci(e2e-go): add multi-node Go E2E job + cert/lb on 3-node cluster (Stage H)
Adds e2e_go_multinode as a parallel job alongside e2e_go_single on the
e2e-go workflow. The single-node job is untouched apart from extending
its SUITES filter to drop the multinode suite (which only runs on the
ci-multi cluster).
What the new job does:
- Provisions a 3-node tofu cluster via the same build-install-artifacts
+ bootstrap-install path as e2e_real_multi (bash).
- Builds multinode.test / cert.test / lb.test on the runner, ships
them to node1.
- Runs each suite with SPINIFEX_MODE=multinode and the cluster csv
in SPINIFEX_NODES / SPINIFEX_NODE_IPS. cert / lb branch internally
on env.Mode so the same binaries cover both single + multi.
- Pulls per-node journals (spinifex-journal-node1..N.log) for the
e2e-analyze action so a node2/3 failure isn't blind.
- Same JUnit + analyze + artifact-upload + status pipeline as the
single-node job.
Other moves:
- Default SUITES now includes e2e-multinode so push triggers fan
out across single + multi automatically. e2e_go_reboot stays
explicit-opt-in (unchanged).
- tests/e2e/Makefile: e2e-multinode now sets MODE=multinode so
`make e2e-multinode` doesn't fall back to single (which would
skip every TestMultinode* on the SPINIFEX_MODE check).
- tests/e2e/TEST_COVERAGE.md: cross-reference the Go port at the
head of the Real Multi-Node section. The bash driver stays in
place as executable spec until the Go run is soak-tested.
- e2e_go_status now needs e2e_go_multinode; multi=skipped counts
as success so a single-only dispatch still posts green.
* fix(e2e/multinode): NATS routez via peer_ssh + StartNode timeout + spx best-effort
CI run 26161792146 surfaced three regressions in the freshly-landed
multinode suite:
1. WaitNATSPeers dialled node.Addr:8222 directly. NATS monitor binds
127.0.0.1:8222 only (cmd/spinifex/cmd/templates/nats.conf:65), so
every routez poll returned "connection refused". Switched to
peer_ssh + curl http://127.0.0.1:8222/routez per node — matches
the bash run-multinode-e2e.sh phase 2 pattern.
2. StartNode / StopNode capped ssh at a 30s context. spinifex.target
has 6+ dependent units (predastore, NATS, awsgw, vpcd, daemon, ui)
and a clean systemctl start runs 60-90s; the 30s budget was
killing ssh mid-command ("signal: killed"). Bump StartNode to 5min
and StopNode to 3min.
3. SpxGetVMs is best-effort in bash (`spx get vms --timeout 5s
2>/dev/null` with no exit-code check) because the CLI's NATS dial
can race the cluster join shortly after bootstrap. The Go port
fatal'd on non-zero exit and broke TestMultinodeClusterHealth +
TestMultinodeInstanceDistribution downstream. Added
harness.SpxRunBestEffort and rewired both call sites.
* fix(e2e/multinode): spx CLI lenience + trio capacity retry + diag
Run 26163994815 cascaded from two issues that bash silently tolerated.
The Go port hard-failed where bash counts the failure and continues, so
3+ tests cascaded off transient first-failures.
- spx get nodes: cluster_health + node_recovery now downgrade <Ready
count to WARN with SpxRunBestEffort, matching bash phase 2
fail_test counter and phase 9 `|| echo ""`. Gateway+daemon health
checks above already prove end-to-end NATS routing.
- needInstanceTrio: retry RunInstances up to 6× on
InsufficientInstanceCapacity (10s backoff). Daemon resourceMgr can
briefly report 0 capacity right after node bootstrap before its
first inventory scan completes — AWS itself surfaces this
transiently. Stagger 2s between launches to match bash phase 3.
- CI workflow: new "Pre-flight diagnostics on node1" step dumps
groups/perms/spx output/systemctl status/journal tail to
preflight-diag.log so next run's artifacts surface what tf-user
sees before the test binary starts.
* fix(e2e): strip SPINIFEX_* from spx child env
Root cause of `spx get nodes` returning "nats: no servers available for
connection" in the multinode Go suite (mulga-siv-90 runs 26163994815,
26195522455): spx CLI calls viper.AutomaticEnv() with prefix "SPINIFEX"
inside config.LoadConfig. The CI workflow exports SPINIFEX_NODES (csv
of cluster WAN IPs) so harness.ClusterFromEnv can build the *Cluster.
viper picks SPINIFEX_NODES up as the `nodes` config key and silently
clobbers the loaded `[nodes.*]` map → nodeConfig.NATS.Host = "" →
nats.Connect("") → ErrNoServers.
Bash never set SPINIFEX_NODES so the bash port was unaffected. Direct
ssh `spx get nodes` from the runner also worked because the env was
clean. Only the test-binary-spawned spx hit the collision.
Fix: SpxRun / SpxRunBestEffort build a sanitized child env that
preserves the small SPINIFEX_* set spx's root command explicitly
BindEnvs (config path, AWS keys, host, base-dir, NATS dial overrides)
and drops everything else.
* fix(vpcd): make handleAddNAT idempotent (mulga-siv-124)
PR #232 (commit 5887927) added a second vpc.add-nat publish in
daemon/vm_adapters.go:onInstanceUpHook to cover instance recovery.
The publish fires on every instance up event including fresh launches,
racing the original publish from handlers/ec2/instance/service_impl.go.
Both publishes are queue-grouped to "vpcd-workers" so two cluster vpcd
instances handle them ~13s apart. handleAddNAT's existing
DeleteAllNATsByExternalIP -> AddNAT sequence is not idempotent under
concurrent writers: the second invocation deletes the rule the first
installed (after ovn-northd compiled SB flows and chassis installed
them), creating a flow-install gap during which the EIP is unreachable.
Symptom: TestMultinodePlacementAndNAT fails with ssh: Connection timed
out to the bastion EIP for 150s (mulga-siv-90 CI runs 26195522455,
26197248656). Bash port last green on e2e-baremetal run 26008057481
before #232 merged.
Fix: before delete+add, look up the existing rule via
FindNATByExternalIP. If it exists and its LogicalIP + spinifex:vpc_id
ExternalID (plus ExternalMAC + LogicalPort under distributed-NAT mode)
already match the target, no-op. Falls through to the existing
delete-then-add for the IP-reuse-across-VPCs case the original code
already handled.
Adds TestTopologyHandler_AddNAT_Idempotent covering the UUID-witness +
flow-barrier counter (two adds with identical state must not recreate
the row or re-arm the barrier).
* test(e2e/multinode): bump bastion SSH wait + ICMP probe
Run 26198221557 confirmed the vpcd dnat_and_snat race (mulga-siv-124)
was fixed — no "cleaned up stale NAT" lines for the bastion EIP, only
one writer + an idempotent skip. But TestMultinodePlacementAndNAT
still timed out at exactly 150s on SSH to the bastion's public IP,
suggesting a separate OVN flow-install lag on the freshly-created
VPC's external switch.
- Bump waitForBastionSSH budget 150s -> 5min. Default-VPC GuestSSH
reaches sshd in ~70s; a brand-new VPC needs longer for ovn-northd
to compile SB + every chassis to install the external ARP
responder + dnat_and_snat flow. 5min covers cloud-init + OVN warm.
- Add an ICMP probe per iteration. Pure diagnostic — ping failure
does not fail the step, but the next iteration's SSH error
carries ping_output so the log splits "datapath down" (ping fail)
from "sshd not up" (ping ok, ssh refused/timeout). Matches the
RCA agent's fix candidate 3.
* test(e2e/multinode): dump cluster datapath state on phase 11 failure
waitForBastionSSH currently fails opaquely when the EIP datapath is
broken — the log shows "ping=fail" but doesn't pin which node owns the
gw chassis or whether ARP / OVN NAT rules / chassis encap IPs are sane.
Register a t.Cleanup hook (after bastionPubIP is known) that fans out
arp / ip route / ovs-vsctl / ovn-sbctl / ovn-nbctl probes to every
cluster node and writes results to <Artifacts>/diag-<node>-<probe>.log
on failure only. Plus a local-side ping / traceroute / ip route block
so a future post-mortem can distinguish runner-network breakage from
cluster datapath breakage.
Best-effort: per-node SSH errors log but never re-fail the test.
* test(e2e/multinode): use IpPermissions form for SG authorize calls
Two AuthorizeSecurityGroupIngress calls in phase 11 used the legacy flat
form (top-level IpProtocol/FromPort/ToPort/CidrIp) instead of the
IpPermissions array. The Spinifex VPC handler at
spinifex/handlers/ec2/vpc/security_group.go:667 reads only
input.IpPermissions and silently drops the flat fields — newRules=0,
no error returned.
Run 26200626265 journal:
groupId=sg-922de57bc74b85c9c (natgw-bastion) newRules=0
groupId=sg-758b05b4a5e7161fd (natgw-private) newRules=1
Bastion SG had zero ingress rules; TCP SYN to bastion :22 was dropped
by ACL, causing waitForBastionSSH to time out after 5min.
Bash version passes because AWS CLI v2 normalizes --protocol/--port/--cidr
shorthand into IpPermissions.N.* on the wire.
Fix: re-shape both calls (bastion tcp/22, private icmp) to use
IpPermissions, matching the third authorize call in the same function
(line 163-175) that already works.
Server-side fix to honor flat form follows in a separate bead.
What is Spinifex? · Platform · Components · Architecture · Installation · Docs
Spinifex: An Open Source AWS-Compatible Stack for Bare-Metal, Edge, and On-Prem Deployments
Spinifex developed by Mulga Defense Corporation is an open source infrastructure platform that brings the core services of AWS—like EC2, VPC, EBS, and S3—to environments where running in the cloud isn't an option. Whether you're deploying to edge sites, private data-centers, or need to operate in low-connectivity or highly contested environments, Spinifex gives you AWS-style workflows on your own hardware.
What is Spinifex?
Spinifex replicates essential AWS primitives—virtual machines, block volumes, and object storage—using lightweight, self-contained components that are easy to deploy and integrate.
It's designed for developers and operators who need:
- Cloud-like infrastructure without the cloud
- Minimal dependencies, full control
- Drop-in compatibility with tools like the AWS CLI, SDKs, and Terraform
- A secure cloud environment you control and own the entire hardware, network and software stack
You can run Spinifex on a few servers in a rack, a field site, or anywhere centralized cloud services aren't feasible.
The Platform
From commodity hardware up to unmodified AWS tooling, every layer is replaceable and yours to own.
Core Components
Spinifex (Compute Service – EC2 Alternative)
Spinifex is a minimal VM orchestration layer built on top of QEMU, exposing APIs similar to EC2. It manages lifecycle operations like start, stop, and terminate, using QEMU's QMP interface. Designed to be straightforward and scriptable, Spinifex lets you launch VMs using the AWS CLI, SDKs, or Terraform—without needing Kubernetes or heavyweight orchestrators. Keep in mind, you can also setup a Kubernetes environment using Spinifex with underlying instances.
- EC2-like VM management on bare metal
- Launches with cloud-init metadata support
- Works with standard AWS tooling
Viperblock (Block Storage – EBS Alternative)
Viperblock is a high-performance, WAL-backed block storage service that replicates volumes across multiple nodes. It's built for reliability and speed, with support for snapshots, recovery, and direct connection to QEMU instances using NBD or virtio-blk.
- Fast, durable virtual disks
- Replication for resilience
- Exposed over NBD or embedded in VMs
- Supports high performance WAL logs using local NVMe drives to reduce IO traffic to S3.
- In memory read/write block cache for blazing performance.
Predastore (Object Storage – S3-Compatible)
Predastore is a fully S3-compatible object storage system. It supports the AWS S3 API, including Signature V4 authentication, multipart uploads, and Terraform provisioning. Data is chunked and distributed across nodes using Reed-Solomon erasure coding, making it fault-tolerant and ideal for large-scale or low-bandwidth scenarios.
- S3-compatible API and auth
- Multipart uploads, streaming reads/writes
- Data redundancy with Reed-Solomon encoding
Architecture at a Glance
Every AWS API call is authenticated at the gateway, published to a NATS subject, and answered by whichever daemon claims it. Daemons are stateless — scale horizontally by starting more. No etcd, no Kubernetes, no external control plane. Deep dive: docs/DESIGN.md.
Key Features
AWS-Compatible Interfaces – Provision infrastructure with awscli, Terraform, or SDKs you already use.
- Designed for Control – Run on your own terms, whether that's in a datacenter, remote site, or sensitive environment.
- Minimal Dependencies – Each service is standalone and avoids complex orchestration layers.
- Works Offline – No reliance on centralized cloud services or external networks.
- Open Source – Licensed under AGPL 3.0. Fork it, extend it, or deploy it as-is.
Installation
Installation requires an Ubuntu / Debian system. See the detailed documentation at docs.mulgadc.com for maintaining and installing Spinifex.
Bare Metal ISO
The recommended installation is a bootable x86 installer for bare-metal hardware.
curl -fLO https://iso.mulgadc.com/spinifex.iso
Follow the USB install guide to write the ISO to USB and install on your hardware. The install guide walks through the full process.
Single Node Install
The installation is straightforward to set up and running on a single node for testing purposes. Debian 13 is currently supported, additional Linux distributions are on the immediate roadmap.
Prerequisite: Linux bridge for networking.
Spinifex requires a Linux bridge configured on the host for VM networking. See the single-node install guide prerequisites for setup details.
curl -fsSL https://install.mulgadc.com | bash
sudo /usr/local/share/spinifex/setup-ovn.sh --management
sudo spx admin init --node node1 --nodes 1
sudo systemctl start spinifex.target
export AWS_PROFILE=spinifex
aws ec2 describe-instance-types
Development Setup
For a complete development environment see the Source Install documentation
Component Repositories
Spinifex coordinates these independent components:
- Predastore - S3-compatible object storage
- Viperblock - EBS-compatible block storage
Each component can be developed independently. See component-specific documentation for focused development guides.
Spinifex UI
Spinifex ships with a built-in web console — an optional alternative to the AWS CLI, SDKs, and Terraform. If you're familiar with the AWS Management Console, the Spinifex UI fills the same role: a browser-based view of your instances, volumes, buckets, VPCs, and IAM resources, without leaving your own network.
The console is served by each node on port 3000 over TLS, and becomes available as soon as spinifex.target is up:
open https://YOUR_NODE_IP:3000
- Same API, different surface. Every action in the UI is the same AWS SigV4 call the CLI makes — so RBAC, audit trails, and IAM policies apply uniformly.
- Single sign-on against your AWS credentials. Log in with the access keys from
~/.aws/credentialson the node where Spinifex is installed — no separate user database. - Self-hosted, works offline. The UI is embedded in the Spinifex binary and served from the node itself. No external CDN, no analytics calls, no cloud dependency.
For the full walkthrough — first-time TLS certificate trust, login, and feature tour — see Launching the Web UI in the cluster setup guide.
Development Philosophy
Built by Engineers, For Engineers
Spinifex is developed by experienced infrastructure engineers with deep AWS expertise, including former AWS team members who understand the intricacies of building production-grade cloud services. Our team brings decades of combined experience from AWS, enterprise infrastructure, and edge computing environments.
Real-World Experience:
- Production AWS service development and operations
- Large-scale infrastructure deployment and management
- Edge computing and resource-constrained environments
- Enterprise security and compliance requirements
AI-Assisted Development
While Spinifex is architected and implemented by experienced engineers, we leverage Claude Code (Anthropic's AI coding assistant) to accelerate certain development tasks. This approach combines human expertise with AI efficiency:
How We Use Claude Code:
- Code Generation: Boilerplate AWS API structures and handlers
- Documentation: Comprehensive development guides and API documentation
- Testing: Test case generation and validation scenarios
- Refactoring: Large-scale code restructuring and optimization
What Remains Human-Driven:
- Architecture Decisions: Core system design and scalability choices
- Security Implementation: Authentication, encryption, and threat modeling
- Performance Optimization: Real-world performance tuning and benchmarking
- Production Operations: Deployment strategies and operational procedures
This hybrid approach ensures Spinifex benefits from both proven engineering expertise and modern development acceleration, while maintaining the quality and reliability standards required for production infrastructure.
License
Spinifex is open source under the GNU Affero General Public License v3.0. You're free to use, modify, and deploy it anywhere you need reliable infrastructure without depending on centralized cloud platforms.
