Skip to content

elpddev/fusion

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

93 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fusion

Fusion

Remote task runner using Erlang distribution over SSH. Zero dependencies.

Fusion connects to remote servers via SSH, sets up port tunnels for Erlang distribution, bootstraps a remote BEAM node, and lets you run Elixir code on it. Think Ansible/Chef but for Elixir - push modules and execute functions on remote machines without pre-installing your application.

Hex.pm HexDocs

Articles

Requirements

  • Elixir ~> 1.18 / OTP 28+
  • Remote server with Elixir/Erlang installed
  • SSH access (key-based or password)

Installation

Add fusion to your list of dependencies in mix.exs:

def deps do
  [
    {:fusion, "~> 0.3.0"}
  ]
end

Usage

Your local BEAM must be started as a distributed node:

iex --sname myapp@localhost -S mix

Then connect and run code remotely:

# Define the target
target = %Fusion.Target{
  host: "10.0.1.5",
  port: 22,
  username: "deploy",
  auth: {:key, "~/.ssh/id_ed25519"}
}

# Connect (sets up tunnels, bootstraps remote BEAM, joins cluster)
{:ok, manager} = Fusion.NodeManager.start_link(target)
{:ok, remote_node} = Fusion.NodeManager.connect(manager)

Run functions on the remote:

# Get remote system info
{:ok, version} = Fusion.run(remote_node, System, :version, [])
{:ok, {hostname, 0}} = Fusion.run(remote_node, System, :cmd, ["hostname", []])

Run anonymous functions directly:

{:ok, info} = Fusion.run_fun(remote_node, fn ->
  %{
    node: Node.self(),
    otp: System.otp_release(),
    os: :os.type()
  }
end)

Push and run your own modules — dependencies are resolved automatically:

defmodule RemoteHealth do
  def check do
    %{
      hostname: hostname(),
      elixir_version: System.version(),
      memory_mb: memory_mb()
    }
  end

  defp hostname do
    {name, _} = System.cmd("hostname", [])
    String.trim(name)
  end

  defp memory_mb do
    {meminfo, _} = System.cmd("cat", ["/proc/meminfo"])

    meminfo
    |> String.split("\n")
    |> Enum.find(&String.starts_with?(&1, "MemTotal"))
    |> String.split(~r/\s+/)
    |> Enum.at(1)
    |> String.to_integer()
    |> div(1024)
  end
end

{:ok, health} = Fusion.run(remote_node, RemoteHealth, :check, [])
# => %{hostname: "web-01", elixir_version: "1.18.4", memory_mb: 7982}

Disconnect when done:

Fusion.NodeManager.disconnect(manager)

SSH Backend

Fusion uses Erlang's built-in SSH module by default. No system ssh binary required.

To use the legacy system SSH backend instead:

target = %Fusion.Target{
  host: "10.0.1.5",
  username: "deploy",
  auth: {:key, "~/.ssh/id_ed25519"},
  ssh_backend: Fusion.SshBackend.System  # uses system ssh/sshpass
}

Automatic Dependency Resolution

When you run RemoteHealth remotely, Fusion reads the BEAM bytecode, walks the dependency tree, and pushes everything the module needs. You don't need to manually track the dependency chain.

# You can also push modules explicitly
Fusion.TaskRunner.push_module(remote_node, MyApp.Worker)
Fusion.TaskRunner.push_modules(remote_node, [MyApp.Config, MyApp.Utils])

Standard library modules (Kernel, Enum, String, etc.) are already on the remote and don't need pushing.

How It Works

1. SSH Tunnel Setup

Fusion creates 3 SSH tunnels between local and remote:

Local Machine                         Remote Server
─────────────                         ─────────────
                 ┌─── Reverse ────┐
Local node port ◄┘   tunnel #1    └── Remote can reach local node

                 ┌─── Forward ────┐
localhost:port ──┘   tunnel #2    └►  Remote node's dist port

                 ┌─── Reverse ────┐
Local EPMD     ◄─┘   tunnel #3    └── Remote registers with local EPMD
(port 4369)

2. Remote BEAM Bootstrap

Starts Elixir on the remote via SSH with carefully configured flags:

  • ERL_EPMD_PORT=<tunneled> - routes EPMD registration through tunnel #3 back to local EPMD
  • --sname worker@localhost - uses @localhost because all traffic goes through localhost-bound tunnels
  • --cookie <local_cookie> - matches the local cluster's cookie
  • --erl "-kernel inet_dist_listen_min/max <port>" - pins distribution port to match tunnel #2

3. Transparent Connection

Since the remote registered with the local EPMD, Node.connect/1 works as if the remote node were local. All distribution traffic is routed through the SSH tunnels.

4. Code Pushing

Module bytecode is transferred via Erlang distribution:

  1. Read .beam binary locally with :code.get_object_code/1
  2. Parse BEAM atoms table to find non-stdlib dependencies
  3. Push each dependency recursively (bottom-up)
  4. Load on remote with :code.load_binary/3
  5. Execute via :erpc.call/4

Testing

# Unit tests (no external dependencies)
mix test

# Docker integration tests (requires Docker)
cd test/docker && ./run.sh start
elixir --sname fusion_test@localhost -S mix test --include external

# Stop the test container
cd test/docker && ./run.sh stop

Test Tiers

  • Tier 1 (Unit) - Doctests and pure logic tests. No network, no SSH.
  • Tier 2 (Integration) - Tests against localhost SSH. Skips gracefully if not configured.
  • Tier 3 (External) - End-to-end tests against a Docker container with SSH + Elixir. Requires ./run.sh start.

Architecture

Fusion (public API)
├── TaskRunner        - Remote code execution + module pushing + dependency resolution
├── NodeManager       - GenServer: tunnel setup, BEAM bootstrap, connection lifecycle
├── Target            - SSH connection configuration struct (includes ssh_backend selection)
├── SshBackend        - Behaviour for pluggable SSH implementations
│   ├── Erlang        - Default: uses OTP's built-in :ssh module
│   └── System        - Legacy: shells out to system ssh/sshpass binaries
├── SshKeyProvider    - Custom ssh_client_key_api for specific key file paths
├── TunnelSupervisor  - DynamicSupervisor for tunnel processes
├── Net               - Port generation, EPMD utilities
├── Connector         - SSH connection GenServer
├── SshPortTunnel     - SSH port tunnel process wrapper
├── PortRelay         - Port relay process wrapper
├── UdpTunnel         - UDP tunnel process wrapper
└── Utilities
    ├── Ssh           - SSH command string generation
    ├── Exec          - OS process execution (Port/System.cmd)
    ├── Erl           - Erlang CLI command builder
    └── Bash/Socat/Netcat/Netstat/Telnet - CLI tool wrappers

License

MIT

About

Remote server connection and control library

Resources

License

Stars

Watchers

Forks

Packages