Dark navy and teal Ruby Fleet DevOps cover showing a red Ruby gem linked to a safe server fleet

Ruby for DevOps — Part 6 of 6

A fleet task looks deceptively simple: select the right hosts, run one command, and summarize the outcome. The operational risk hides in the details. A loose selector can hit the wrong environment; an SSH prompt can freeze CI; unbounded concurrency can overload a bastion; and an arbitrary shell string can turn data into execution.

Ruby is a useful option for this kind of focused fleet utility. It does not replace Python, Ansible, a cloud control plane, or a fleet-management product. It gives a team another small, testable implementation choice when the team already owns Ruby services, Chef code, Vagrantfiles, or internal Ruby tools. The important contract is language-neutral: validated inventory, explicit targeting, JSON output, readable logs, and meaningful exit status.1 2

Safety boundary: This tutorial only runs a fixed, read-only uptime command, defaults to a dry run, requires --execute before any network connection, keeps normal SSH host-key verification enabled, and never stores passwords, tokens, private keys, or host-key overrides in the inventory.

Fork or follow the code

The complete project is in the public DevOps Coach repository. Open the exact Ruby Fleet DevOps code directory, the fleet inventory loader, and the fleet checker.

Fork and clone the complete tutorial project with GitHub CLI:

gh repo fork jjam3774/devop-coach --clone
cd devop-coach/tutorials/ruby-for-devops/code
bundle install

Or clone the upstream repository if you only want to follow the examples:

git clone https://github.com/jjam3774/devop-coach.git
cd devop-coach/tutorials/ruby-for-devops/code
bundle install

Use Watch on GitHub to follow updates. Use Fork when you want to replace the sample names with your own inventory, extend the checker, or activate the copy-ready CI workflow in your fork.

What you will build

The new fleet command family has one planning command and one read-only check command.

Command Purpose Default behavior
fleet plan Validate YAML and show which hosts a selector matches. No network connection
fleet check Construct and report one SSH uptime check per selected host. Dry run; no SSH connection
fleet check --execute Run bounded concurrent, read-only SSH checks. Explicit operator opt-in

The workflow separates selection from execution. That lets an operator inspect the exact host list before adding --execute. It also creates a simple interface that Bash, Python, CI systems, or a monitoring platform can invoke through JSON and exit codes.

Define a declarative fleet inventory

The sample config/fleet.yml describes hosts as data rather than embedding them in a script:

hosts:
  - name: web-01
    address: web-01.example.internal
    environment: production
    roles: [web, api]
    ssh_user: deploy
    port: 22
    required: true

  - name: worker-01
    address: worker-01.example.internal
    environment: production
    roles: [worker]
    ssh_user: deploy
    port: 22
    required: false

The loader uses YAML.safe_load with no permitted classes, symbols, or aliases. It also requires a non-empty host name, a simple hostname/IP address, an environment, one or more roles, a valid port, and a real boolean for required. Ruby documents YAML as its Psych-backed YAML interface; safe loading is the right starting point for configuration that can be edited outside the program code.1 3

The inventory deliberately contains no credentials. Configure SSH authentication through your platform’s normal mechanisms: an agent, a hardware-backed key, a short-lived certificate, a protected CI secret, or an approved identity provider. Do not add passwords, private keys, StrictHostKeyChecking=no, or a known_hosts bypass to this YAML file.

Plan before you connect

Fleet selection supports a narrow, auditable selector grammar:

role=VALUE,environment=VALUE

Start with the plan. It validates the file and prints the exact hosts that match:

bin/devops-toolkit fleet plan \
  --file config/fleet.yml \
  --selector role=web,environment=production

Expected shape:

Fleet plan: role=web,environment=production (2 hosts)
  web-01 web-01.example.internal [production; web, api]
  web-02 web-02.example.internal [production; web, api]

For CI, request JSON instead:

bin/devops-toolkit fleet plan \
  --selector environment=production \
  --json

A selector with an unsupported key, a malformed clause, a repeated key, or no matches fails clearly. In particular, a no-match result returns exit status 66; an invalid inventory returns 65; and malformed options return 64. Treating an empty deployment target as an error is safer than silently reporting success.

Default to a dry run

Before connecting, ask the tool to create the plan for each SSH command:

bin/devops-toolkit fleet check \
  --selector role=web,environment=production \
  --workers 4 \
  --timeout 5 \
  --json

The command is intentionally a dry run unless --execute appears. The JSON report includes "dry_run": true, each selected host, and the display form of the command that would run. This gives an operator a review point and gives a pipeline a non-destructive way to validate inventory and selection logic.

The underlying transport constructs an argument array rather than one interpolated shell string:

[
  "ssh",
  "-o", "BatchMode=yes",
  "-o", "StrictHostKeyChecking=yes",
  "-o", "ConnectTimeout=5",
  "-p", "22",
  "[email protected]",
  "uptime"
]

OpenSSH documents BatchMode yes as a way to disable password prompts and host-key confirmation for scripts and batch jobs. ConnectTimeout bounds TCP connection and the initial SSH protocol handshake. The example also explicitly retains StrictHostKeyChecking=yes; an automation convenience should not silently weaken host identity verification.4

Ruby passes that array to the project’s CommandRunner, which uses Open3.popen3 without an implicit shell. A hostname, user, port, or flag therefore remains an argument rather than becoming shell syntax.1

Execute a read-only check only after review

Once the inventory, selector, host keys, and dry-run output are correct, add the explicit opt-in:

bin/devops-toolkit fleet check \
  --selector role=web,environment=production \
  --workers 4 \
  --timeout 5 \
  --execute \
  --json

The fixed remote command is uptime. It does not write files, restart services, modify packages, or deploy an artifact. The tool returns 0 when every required host succeeds and 2 when one or more required hosts fail. A failed optional host still appears in the result but does not fail the overall fleet check.

Result Meaning Typical pipeline response
0 All required checks succeeded, or a dry-run plan was valid. Continue or archive the JSON report.
2 At least one required host failed its SSH uptime check. Alert, stop a risky rollout, or invoke an approved fallback.
64 CLI usage, selector format, timeout, or worker count was invalid. Correct the job configuration.
65 Fleet inventory could not be safely loaded or validated. Fix the version-controlled YAML.
66 The selector matched no hosts. Treat the target definition as a configuration problem.

Bound concurrency and preserve result order

A large fleet does not mean “one thread per host.” The checker adds each selected host to a Queue, starts no more than the requested worker count, and writes results back to their original inventory index:

queue = Queue.new
hosts.each_with_index { |host, index| queue << [index, host] }
results = Array.new(hosts.length)

threads = Array.new(worker_count) do
  Thread.new do
    loop do
      index, host = queue.pop(true)
      results[index] = check(host, timeout: timeout, execute: execute)
    rescue ThreadError
      break
    end
  end
end
threads.each(&:join)

The bounded pool limits local connection pressure and avoids making every selected host connect simultaneously. Storing the result at the original index keeps reports stable even when the faster host finishes first. Stable output makes diffs, alerts, and tests easier to read.

Choose a low worker count first. A bastion, VPN, cloud API, or SSH server may impose connection limits. Increase concurrency only after observing your own network capacity and service behavior.

Test the fleet behavior without a fleet

The test suite injects a fake transport, so it never contacts a real server. It verifies safe YAML handling, selector rules, default dry-run behavior, explicit execution opt-in, required versus optional failures, deterministic output order, and the exact OpenSSH argument vector.

bundle exec ruby -Itest test/fleet_inventory_test.rb
bundle exec ruby -Itest test/fleet_checker_test.rb
bundle exec ruby -Itest test/cli_test.rb
bundle exec rake

The last command runs syntax checks, every test, and the existing non-destructive smoke examples. This same contract is what the project’s copy-ready Ruby CI template uses after you activate it in a fork.

Where Ruby gives a fleet team options

Ruby’s benefit here is not a special protocol that Python lacks. Python can build the same workflow using subprocess, YAML parsing, and a thread pool. Ruby gives teams another implementation option that can fit existing Ruby-oriented infrastructure and application environments. The practical advantages are familiar blocks and collection transforms, a capable standard library, concise CLI construction, and compatibility with the same JSON, SSH, YAML, and exit-code contracts used elsewhere in DevOps.1 2

A mixed stack is normal. A Python inventory generator can emit the YAML that this Ruby CLI validates. A Ruby fleet check can emit JSON for a Python incident-analysis service. Ansible can remain the approved mutation tool while this small Ruby utility performs read-only verification. Clear interfaces create more flexibility than requiring every automation task to use one language.

Production adaptation checklist

Before adapting this educational example to production, review the inventory source, SSH certificate/agent policy, known_hosts distribution, bastion routing, concurrency limit, timeout, logging retention, and incident response path. Keep the remote command constant or map approved task names to constant argument arrays. Do not accept arbitrary remote shell snippets from a ticket field, webhook, or free-form command-line argument.

For mutating work such as package changes, service restarts, configuration rollout, or instance termination, define a separate protected workflow with least-privilege identities, explicit approvals, preview output, idempotency, audit logs, and a rollback plan. A read-only fleet checker is intentionally not a deployment engine.

Practice exercise

Add a role=worker runbook check that is still read-only. Keep the selector grammar unchanged, add one expected JSON field, and write a failing test before changing the implementation. Then use the existing CLI boundary and JSON result schema instead of adding a second script with a different interface.

The goal is not to make Ruby mandatory. It is to build a small, well-bounded tool that a DevOps team can understand, test, and combine with the rest of its platform.

References

  1. Ruby Standard Library
  2. Chef Infra Client Ruby Guide
  3. Ruby YAML Documentation
  4. OpenSSH ssh_config(5)