Skip to content

Repository files navigation

machine

CLI utility to create and manage VMs

Supports DigitalOcean, Vultr, and Google Cloud Platform hosting platforms.

Creating a DigitalOcean droplet with DNS and cloud-init provisioning, checking its status, logging into it, then destroying it — all with machine

The animation above is a real, unedited run against a live DigitalOcean account; it is recorded by demo/record-demo.sh.

Prerequisites

This project uses uv for dependency management and builds.

Install uv:

curl -LsSf https://astral.sh/uv/install.sh | sh

Development Setup

uv sync

This creates a .venv virtual environment and installs all dependencies (including dev tools like ruff).

Run the CLI during development:

uv run machine --help

Run the linter:

uv run ruff check machine/

Build

Build a self-contained executable using shiv:

./sh/build-package.sh

This produces build/machine, a single-file Python zipapp.

Install

Install directly from the GitHub repository using uv:

uv tool install git+https://github.com/stirlingbridge/machine.git

Alternatively, download the machine binary from the releases page, make it executable, and place it on your PATH:

chmod +x machine
sudo mv machine /usr/local/bin/

Usage

Config File

Access token and other settings configured in the file ~/.machine/config.yml. The config file contains a provider section (digital-ocean, vultr, or gcp) and a machines section.

If only one provider section is present, it is auto-detected. If multiple provider sections exist, add a provider: key to select one explicitly:

provider: vultr

DigitalOcean Config

digital-ocean:
access-token: dop_v1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxssh-key: my-ssh-key-namedns-zone: example.commachine-size: s-4vcpu-8gbimage: ubuntu-22-04-x64region: nyc3project: Infrastructuremachines:
example:
new-user-name: alicescript-dir: /opt/setup-scriptsscript-url: https://raw.githubusercontent.com/example/setup-machine.shscript-path: /opt/setup-scripts/setup-machine.shscript-args: "-y"
KeyRequiredDescription
access-tokenYesDigitalOcean API access token
ssh-keyYesName of an SSH key in your DigitalOcean account to use for new machines, or a list of names (see Multiple SSH keys)
dns-zoneNoDNS zone for automatic DNS record creation/deletion
machine-sizeYesDefault machine size slug (e.g. s-4vcpu-8gb)
imageYesDefault image name (e.g. ubuntu-22-04-x64)
regionYesDefault region code (e.g. nyc3)
projectNoDigitalOcean project name to assign new machines to

Supported regions: NYC1, NYC3, AMS3, SFO2, SFO3, SGP1, LON1, FRA1, TOR1, BLR1, SYD1

Vultr Config

vultr:
api-key: ${VULTR_API_KEY}ssh-key: my-ssh-key-namedns-zone: example.commachine-size: vc2-1c-1gbimage: 2136region: ewrmachines:
example:
new-user-name: alicescript-dir: /opt/setup-scriptsscript-url: https://raw.githubusercontent.com/example/setup-machine.shscript-path: /opt/setup-scripts/setup-machine.shscript-args: "-y"
KeyRequiredDescription
api-keyYesVultr API key (from https://my.vultr.com/settings/#settingsapi)
ssh-keyYesName of an SSH key in your Vultr account to use for new machines, or a list of names (see Multiple SSH keys)
dns-zoneNoDNS zone for automatic DNS record creation/deletion
machine-sizeYesVultr plan slug (e.g. vc2-1c-1gb). Use machine list-plans or the Vultr API to list available plans
imageYesVultr OS ID (numeric, e.g. 2136 for Ubuntu 24.04). Use machine list-os or the Vultr API to list available OS IDs
regionYesVultr region code (e.g. ewr)

Supported regions: ewr, ord, dfw, sea, lax, atl, ams, lhr, fra, sjc, syd, nrt, cdg, icn, mia, sgp, sto, mex, mad, sao, del, hnl, yto, blr, jnb, bom, tlv

Note: Vultr does not have a "projects" concept, so the project config key and the projects command are not applicable when using the Vultr provider.

GCP Config

gcp:
project-id: my-gcp-projectcredentials-file: ~/.config/gcloud/service-account.jsonssh-key: alicedns-zone: example.commachine-size: e2-standard-2image: projects/debian-cloud/global/images/family/debian-12region: us-central1-a
KeyRequiredDescription
project-idYesGCP project ID where all resources (VMs, DNS, etc.) live
credentials-fileNoPath to a service account JSON key file. If omitted, Application Default Credentials are used (e.g. from gcloud auth application-default login)
ssh-keyYesUsername GCP associates with an SSH key in the project's ssh-keys metadata (see SSH key notes below), or a list of usernames (see Multiple SSH keys)
dns-zoneNoDNS name of a Cloud DNS managed zone (e.g. example.com)
machine-sizeYesGCE machine type (e.g. e2-standard-2)
imageYesImage self-link or family path (e.g. projects/debian-cloud/global/images/family/debian-12)
regionYesGCE zone (e.g. us-central1-a). Despite the key name, GCP requires a fully-qualified zone, not a region

Authentication:

machine supports two ways to authenticate with GCP:

  • Application Default Credentials (ADC) — omit credentials-file and machine falls back to ADC. The simplest way to set this up is to run gcloud auth application-default login once; it stores credentials under ~/.config/gcloud that machine picks up automatically. This is the easiest option for interactive use on a workstation that already has the gcloud CLI, and avoids creating and managing a long-lived key file.
  • Service account key file — set credentials-file to the path of a service account JSON key. This is appropriate for unattended environments or when you want a dedicated, tightly-scoped identity.

SSH keys:

GCP doesn't have a named SSH key registry like DigitalOcean or Vultr. Instead, public keys live in the project's ssh-keys metadata, and each one is associated with a username. The value of ssh-key in the config must match one of those usernames.

Two ways to add a key:

  1. Console (Compute Engine → Metadata → SSH Keys → Add SSH Key): paste a raw public key, e.g.

    ssh-rsa AAAAB3Nza... alice@laptop
    

    GCP derives the username from the comment portion at the end — everything before the first @. So alice@laptop and alice@example.com both produce the username alice. Set ssh-key: alice in the config.

  2. gcloud — write each line in the explicit <username>:<key> form to a file (no auto-derivation here), then upload:

    echo "alice:ssh-rsa AAAAB3Nza... alice@laptop" > keys.txt
    gcloud compute project-info add-metadata --metadata-from-file ssh-keys=keys.txt
    

The tool reads existing keys but does not create them.

Other notes:

  • The project config key and re-assignment to projects are not applicable when using GCP — every resource is owned by the project specified in project-id at creation time.
  • The region field is interpreted as a GCE zone. Validation only checks for plausibility; any zone string with a hyphen is accepted.

Machines Section

machines section:

Each entry under machines: defines a machine type that can be referenced with create --type:

KeyRequiredDescription
new-user-nameYesUsername for the non-root user created on the machine
script-urlNoURL to download an initialization script from
script-dirNoDirectory to store the initialization script
script-pathNoFull path for the initialization script
script-argsNoArguments passed to the initialization script (supports variable expansion). Either a single string, or a list where each item is passed to the script as exactly one shell argument

If script-url, script-dir, and script-path are all provided, the script is downloaded and executed as the new user during cloud-init.

When script-args is a list, each item reaches the script as one argument regardless of any spaces it contains. This composes well with runner scripts such as combine.sh, which accepts one "<script> [args...]" entry per script to run:

script-args:
- packages.sh build-essential
- podman.sh
- k3s-node.sh -y --letsencrypt-email user@example.com

The following variables are available for expansion in script-args (both forms):

  • $MACHINE_SCRIPT_URL — URL of the initialization script
  • $MACHINE_SCRIPT_DIR — directory path for the script
  • $MACHINE_FQDN — fully qualified domain name of the machine (if DNS is configured)

Multiple SSH keys

The ssh-key config value accepts either a single key name or a list of names. When a list is given, every key is installed on new machines (both on the provider account's default user and, when --initialize is used, on the new user created by cloud-init).

digital-ocean:
access-token: ${DO_API_TOKEN}ssh-key:
- alice-laptop
- bob-laptopmachine-size: s-4vcpu-8gbimage: ubuntu-22-04-x64region: nyc3

The single-name form is unchanged and remains fully supported:

ssh-key: my-ssh-key-name

Environment Variable Substitution

Config values support environment variable substitution using ${VAR} syntax, similar to Docker Compose files. This is useful for keeping secrets like API tokens out of the config file.

Supported forms:

  • ${VAR} — substitutes the value of VAR; errors if the variable is not set
  • ${VAR:-default} — substitutes the value of VAR, or default if the variable is not set

Examples:

# DigitalOceandigital-ocean:
access-token: ${DO_API_TOKEN}ssh-key: ${SSH_KEY_NAME:-my-ssh-key}dns-zone: example.commachine-size: ${MACHINE_SIZE:-s-4vcpu-8gb}image: ubuntu-22-04-x64region: ${DO_REGION:-nyc3}project: Infrastructure
# Vultrvultr:
api-key: ${VULTR_API_KEY}ssh-key: ${SSH_KEY_NAME:-my-ssh-key}dns-zone: example.commachine-size: ${MACHINE_SIZE:-vc2-1c-1gb}image: 2136region: ${VULTR_REGION:-ewr}

Substitution is applied to all string values throughout the config file, including the machines section.

Advanced Machine Setup

Examples of advanced machine setup scripts can be found in the machine-provisioning repository.

Session Management

Each invocation of machine uses a session ID (auto-generated and stored in ~/.machine/session-id.yml). Machines are tagged with their session ID on creation. By default, list, status, list-domain, and destroy only operate on machines from the current session. Use the --all flag to include machines from other sessions or machines not created by this tool.

The session ID can be overridden with the global --session-id option.

Automatic Tagging

Machines created by this tool are automatically tagged with:

  • machine:created — identifies the machine as created by this tool
  • machine:type:<type-name> — the machine type from the config
  • machine:session:<session-id> — the session that created the machine

Global Options

$ machine --help
Usage: machine [OPTIONS] COMMAND [ARGS]...
Options:
--debug Enable debug output
--quiet Suppress all non-essential output
--verbose Enable verbose output
--dry-run Run but do not do anything
--config-file <PATH> Specify the config file (default
~/.machine/config.yml)
--session-id <ID> Override the default session ID
-h, --help Show this message and exit.
Commands:
create Create a machine
destroy Destroy one or more machines
domains List dns domains
list List machines
list-domain List domain records
projects List projects
ssh-keys List ssh keys
status Machine status
types List configured machine types
version Display version

Commands

create

Create a new machine on the configured provider. By default, the machine is initialized with cloud-init (using the specified --type from config) and a DNS A record is created.

$ machine create --help
Usage: machine create [OPTIONS]
Create a machine
Options:
-n, --name <MACHINE-NAME> Name for new machine [required]
-t, --tag <TAG-TEXT> Tag to be applied to new machine
-m, --type <MACHINE-TYPE> Machine type from config (required if --initialize)
-r, --region <REGION-CODE> Region (overrides config default)
-s, --machine-size <MACHINE-SLUG>
Machine size (overrides config default)
-i, --image <IMAGE-NAME> Image (overrides config default)
--wait-for-ip / --no-wait-for-ip Wait for IP address assignment (default: off)
--progress / --no-progress Animate a progress indicator while waiting
for the IP address (default: off)
--update-dns / --no-update-dns Create DNS A record (default: on)
--initialize / --no-initialize Initialize with cloud-init (default: on)
-o, --output <FORMAT> Output format (json)
-h, --help Show this message and exit.

Output formats:

  • Default: human-readable status messages
  • --output json: JSON object with id, name, tags, region, ip, type

JSON output example:

{
"id": "12345678",
"name": "my-machine",
"tags": ["machine:created", "machine:type:example", "machine:session:abc12345"],
"region": "nyc3",
"ip": "203.0.113.10",
"type": "example"
}

When --update-dns is enabled (the default), the command waits for the instance's IP address and creates an A record in the configured dns-zone with a 5-minute TTL.

That wait takes tens of seconds and is silent by default. --progress animates a spinner with an elapsed-second count on stderr for the duration, so an interactive run does not look like it has hung. It is ignored when stderr is not a terminal, and --verbose (which reports the same wait in words) takes precedence over it, so scripted and piped output is unaffected either way.

When --initialize is enabled (the default), a cloud-config user-data payload is generated that creates a non-root user with sudo access, installs the configured SSH key(s), and optionally downloads and runs an initialization script.

If a project is configured (DigitalOcean only), the machine is automatically assigned to that project.

destroy

Destroy one or more machines by instance ID. By default, requires confirmation and deletes associated DNS records.

$ machine destroy --help
Usage: machine destroy [OPTIONS] [MACHINE-IDS]...
Destroy one or more machines
Options:
--confirm / --no-confirm Require confirmation (default: on)
--delete-dns / --no-delete-dns Delete associated DNS records (default: on)
--all Include machines not created by this tool
or by other sessions
-h, --help Show this message and exit.

Confirmation requires typing exactly YES (not "y", "yes", or "Yes"). Use --no-confirm to skip.

Safety checks prevent destroying machines that were not created by this tool or that belong to a different session, unless --all is specified.

list

List machines with optional filtering.

$ machine list --help
Usage: machine list [OPTIONS]
List machines
Options:
--id <MACHINE-ID> Filter by id
-n, --name <MACHINE-NAME> Filter by name
-t, --tag <TAG-TEXT> Filter by tag
-m, --type <MACHINE-TYPE> Filter by type
-r, --region <REGION> Filter by region
-o, --output <FORMAT> Output format (json)
-q, --quiet Only display machine IDs
--unique Return an error if more than one match
--all Include all machines from all sessions
-h, --help Show this message and exit.

Output formats:

  • Default: name (id, region, type): ip_address
  • --quiet: machine IDs only
  • --output json: JSON array of machine objects

JSON output example:

[
{
"id": "12345678",
"name": "my-machine",
"tags": ["machine:created", "machine:type:example", "machine:session:abc12345"],
"region": "nyc3",
"ip": "203.0.113.10",
"type": "example"
}
]

status

Check the status of machines, including querying a custom status endpoint.

$ machine status --help
Usage: machine status [OPTIONS]
Machine status
Options:
--id <MACHINE-ID> Filter by id
-n, --name <MACHINE-NAME> Filter by name
-t, --tag <TAG-TEXT> Filter by tag
-m, --type <MACHINE-TYPE> Filter by type
-r, --region <REGION> Filter by region
-o, --output <FORMAT> Output format (json)
--status-check <CHECK> Status check to perform (default: cloud-init-status)
-q, --quiet Only display machine IDs
--all Include all machines from all sessions
-h, --help Show this message and exit.

Output formats:

  • Default: human-readable status line
  • --output json: JSON array of status objects

JSON output example:

[
{
"name": "my-machine",
"id": "12345678",
"machine-status": "active",
"cloud-init-status": "done"
}
]

In addition to the provider-reported instance status, this command queries each machine at http://<ip>:4242/cgi-bin/<status-check> (default: cloud-init-status) for custom status information. If the endpoint is unreachable, the status is reported as UNKNOWN.

list-domain

List DNS records within a domain zone.

$ machine list-domain --help
Usage: machine list-domain [OPTIONS] [ZONE]
List domain records
Options:
-n, --name <RECORD-NAME> Filter by record name
-m, --type <RECORD-TYPE> Filter by record type (default: A and AAAA, use * for all)
-o, --output <FORMAT> Output format (json)
-q, --quiet Only display record names
--all Include all records from all sessions
-h, --help Show this message and exit.

If ZONE is omitted, uses the dns-zone from config. By default, only shows A and AAAA records associated with machines from the current session.

Output formats:

  • Default: name\ttype\tdata
  • --quiet: record names only
  • --output json: JSON array of record objects

JSON output example:

[
{
"id": "98765432",
"machine": {
"id": "12345678",
"name": "my-machine",
"tags": ["machine:created"],
"region": "nyc3",
"ip": "203.0.113.10",
"type": "example"
},
"name": "my-machine",
"fqdn": "my-machine.example.com",
"zone": "example.com",
"data": "203.0.113.10",
"ttl": 300,
"type": "A"
}
]

domains

List all DNS domains in your provider account.

Options:

  • -o, --output <FORMAT>: Output format (json)

JSON output example:

["example.com", "example.org"]

ssh-keys

List SSH keys in your provider account. Output format: id: name (fingerprint)

projects

List project names (DigitalOcean only).

Options:

  • -o, --output <FORMAT>: Output format (json)

JSON output example:

["Infrastructure", "Web Apps"]

types

List all machine types defined in the config file (from the machines section).

Options:

  • -o, --output <FORMAT>: Output format (json)

JSON output example:

["example", "webserver"]

About

CLI utility to create and manage VMs

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages