{"id":32165609,"url":"https://github.com/beamx/control-node","last_synced_at":"2026-02-18T09:40:42.327Z","repository":{"id":47757400,"uuid":"295397695","full_name":"beamX/control-node","owner":"beamX","description":"🚀 Continuous Delivery and Orchestration as code for Elixir","archived":false,"fork":false,"pushed_at":"2025-06-14T19:56:47.000Z","size":11151,"stargazers_count":23,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-14T20:45:20.144Z","etag":null,"topics":["continuous-delivery","elixir","orchestration","release-automation","release-management"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beamX.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-09-14T11:41:33.000Z","updated_at":"2024-10-11T15:12:10.000Z","dependencies_parsed_at":"2024-12-20T00:18:42.286Z","dependency_job_id":"d461bf16-0937-402b-9750-7a85d8497dba","html_url":"https://github.com/beamX/control-node","commit_stats":{"total_commits":51,"total_committers":3,"mean_commits":17.0,"dds":0.07843137254901966,"last_synced_commit":"5f054ca1318abc1184c2c881f1e76f6486878dfe"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/beamX/control-node","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beamX%2Fcontrol-node","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beamX%2Fcontrol-node/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beamX%2Fcontrol-node/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beamX%2Fcontrol-node/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beamX","download_url":"https://codeload.github.com/beamX/control-node/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beamX%2Fcontrol-node/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280281402,"owners_count":26303709,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-21T02:00:06.614Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["continuous-delivery","elixir","orchestration","release-automation","release-management"],"created_at":"2025-10-21T15:01:10.095Z","updated_at":"2025-10-21T15:02:00.801Z","avatar_url":"https://github.com/beamX.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Control Node\n\n[![github.com](https://github.com/beamX/control-node/workflows/ci-test/badge.svg)](https://github.com/beamX/control-node/actions)\n[![hex.pm](https://img.shields.io/hexpm/v/control_node.svg)](https://hex.pm/packages/control_node)\n[![hexdocs.pm](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/control_node/)\n\n🚀 **Continuous Delivery and Orchestration as code for Elixir**\n\n## Installation\n\n```elixir\ndef deps do\n  [\n    {:control_node, \"~\u003e 0.7.0\"}\n  ]\nend\n```\n\n## Introduction\n\n`control_node` is a Elixir library that enables building custom continous\ndelivery and orchestration service. It offers APIs to store release tars and\ndeploy them to remote hosts (via **SSH**), monitor and manage deployed\nservice nodes.\n\n## Pre-requisites\n\nIn order to build with `control_node` you must ensure the following,\n\n- **Control node should have SSH access to all host machines where releases will be deployed**\n- **Deployed Elixir services should register with EPMD** (this happens by default when an Elixir\n  release is started if you don't change the config)\n\n## Features\n\n- [x] Support multiple namespaces for a release\n- [x] Rollout releases to hosts via SSH\n- [x] Native node monitoring and restart on failover\n- [x] Dynamically scale up/down your release instances\n- [x] Native service monitoring/health check\n- [x] Blue-Green deployment\n- [x] Support failover via [heart](http://erlang.org/doc/man/heart.html)\n- [x] Rollback releases\n- [x] Support namespace environment variable configuration\n- [ ] Support package registries other than local file system\n\n## Quick example\n\nControl node ships with an example `service_app` under `example/` folder which\ncan be used to create an example service deployment. Below are the details,\n\n\nClone the repo and start a remote docker SSH server where the release will be\ndeployed,\n\n```\n$ git clone https://github.com/beamX/control-node\n$ cd control-code/\n$ docker-compose up -d  # start a SSH server\n```\n\nStart `iex` with distribution turned on\n\n```elixir\n$ iex -S mix\nErlang/OTP 23 [erts-11.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]\n\nInteractive Elixir (1.10.4) - press Ctrl+C to exit (type h() ENTER for help)\niex(1)\u003e :net_kernel.start([:control_node_test, :shortnames])\niex(control_node_test@hostname)2\u003e \n```\n\n### Define the release\n\n```elixir\ndefmodule ServiceApp do\n  use ControlNode.Release,\n    spec: %ControlNode.Release.Spec{name: :service_app, base_path: \"/app/service_app\"}\nend\n```\n- `ServiceApp` module exposes APIs to deploy the release\n\n\n### Define a host to deploy to\n\n```elixir\nhost_spec = %ControlNode.Host.SSH{\n  host: \"localhost\",\n  port: 2222,\n  user: \"linuxserver.io\",\n  private_key_dir: Path.join([File.cwd!(), \"test/fixture\", \"host-vm/.ssh\"])\n}\n```\n\n- `host_spec` host the configuration of a single host the release can be deployed to\n\n\n### Declare a namespace \n\n- This defines the environment for a given release\n\n```elixir\nnamespace_spec = %ControlNode.Namespace.Spec{\n  tag: :testing,\n  hosts: [host_spec],\n  registry_spec: %ControlNode.Registry.Local{path: Path.join(File.cwd!(), \"example\")},\n  deployment_type: :incremental_replace,\n  release_cookie: :\"YFWZXAOJGTABHNGIT6KVAC2X6TEHA6WCIRDKSLFD6JZWRC4YHMMA====\"\n}\n```\n\n- `registry` defines where to download the release tarball from\n  - `%ControlNode.Registry.Local{}` defines that release tarball will be fetched from local filesystem\n- `hosts` defines a list of servers where the release will be deployed\n\n\n### Deploy the release\n\n```elixir\n{:ok, namespace_manager} = ControlNode.Namespace.start_link(namespace_spec, ServiceApp)\nControlNode.Namespace.deploy(namespace_manager, \"0.1.0\")\nNode.list()\n```\n\n- The above deploys the release to `namespace_spec` environment i.e. the release\n  we be started on all the `hosts` specified in the `namespace_spec`. \n  - NOTE that once the deployment is finished `control_node_test@hostname`\n    automatically connects to release nodes,\n\n\n### Connect and observe with observer\n\nOnce `Node.list()` shows that the control node is connected to the release nodes\nthen `observer` can be used to observe and inspect the remote nodes,\n\n```elixir\nl(:observer)\n:observer.start()\n```\n\n## Real world example\n\nhttps://github.com/kansi/cnops (a bit outdated)\n\n\n## Can control node be used to deploy non Elixir/Erlang project?\n\nYes! The general idea would be to compile target project into a command and run\nand monitor that command from an elixir service. This maybe more work but you\nhave the option of avoiding multiple deploy tools\n\nhttps://github.com/kansi/cnops deploys a Golang service `hello_go`\n\nNOTE: The above is old but still valid inspiration\n\n\n## Under the hood\n\n\u003cimg src=\"./assets/how_it_works.png\" alt=\"How it works\" width=\"700\"/\u003e\n\n- Upon starting, `control_node` will try to connect to EMPD process for each\n  specified host and gather info regarding running services on each host\n- In case a service managed by control is already running on a given node\n  control node will retrieve the current running version and start monitoring\n  the release\n- In case no service is found running on a given host, `control_node` will\n  establish a connection to the host and wait for a deployment command to be\n  issued\n- If any of the monitored service nodes goes down control node will attempt\n  (max. 5) to restart the node\n\n### SSH server config to enable tunneling\n\nIn order to ensure that Control Node can connect to release node the SSH servers\nrunning the release should allow tunneling,\n\n```\n...\nAllowTcpForwarding yes\n...\n```\n\n## SSH key rotation\n\nA general good security practice is to routinely rotate your SSH keys. Control\nnode expose APIs via `ControlNode.Host.SSH` module which can be leveraged to\nperform this rotation. Below is an example,\n\n``` elixir\nhost_spec = %ControlNode.Host.SSH{\n  host: \"localhost\",\n  port: 2222,\n  user: \"linuxserver.io\",\n  private_key_dir: \"/path/to/ssh_dir\"\n}\n\nauthorized_keys = \"\"\"\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDg+KMD7QAU+qtH3duwTHmBaJE/WUdiOwC87cqP5cL21 control-node@email.com\n\"\"\"\n\nhost_state = ControlNode.Host.SSH.connect(host_spec)\nControlNode.Host.SSH.exec(host_state, \"echo '#{authorized_key}' \u003e /user/.ssh/authorized_keys\")\n```\n\n## Limitations\n\n- **SSH client only supports `ed25519` keys**. Other keys types are supported\n  only via SSH agent\n- Only short names for nodes are allowed ie. `sevice_app@hostname` is support\n  and **not** `sevice_app@host1.server.com`\n- Nodes of a given release (deployed to different) should have different\n  hostname for eg. if node 1 has node name `service_app@host1` then another node\n  of `service_app` should have a different node name.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeamx%2Fcontrol-node","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeamx%2Fcontrol-node","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeamx%2Fcontrol-node/lists"}