Dark navy and teal Ruby for DevOps graphic reading An Option Beyond Python

Ruby for DevOps — Part 1 of 5

Python deserves its place in DevOps. It is familiar, widely supported, and backed by a huge package ecosystem. But treating Python as the only serious automation language creates an unnecessary constraint. Ruby can solve the same class of day-to-day problems: reading configuration, transforming deployment data, calling APIs, running external commands, generating reports, and returning a useful exit status to CI.

The best reason to learn Ruby is not novelty. It is optionality. A DevOps engineer who can choose between shell, Python, Ruby, and a declarative tool can match the implementation to the team and the system instead of forcing every problem into one default.

Ruby ships with documented libraries for JSON, YAML, HTTP, process control, files, logging, command-line parsing, and testing.1 It also has direct infrastructure history: Chef uses Ruby as its reference language for resources, recipes, and cookbooks, and Vagrantfiles use Ruby syntax.2 3 If your environment already contains those tools or Ruby services, Ruby automation can be the shortest path to code that the owning team understands.

Fork or follow the code

Follow the complete project in the DevOps Coach repository, or open the exact Ruby for DevOps code directory.

Fork it with GitHub CLI:

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

Alternatively, clone the upstream repository:

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

Select Watch on GitHub if you want updates without maintaining a fork. Select Fork if you want to change the examples and keep your own version.

Start with the engineering decision

A language choice affects who can review a script at 2 a.m., how dependencies reach production, which SDKs are available, and how easily the tool can be tested. Syntax is only one factor.

Situation Ruby is a strong option when… Python is a strong option when…
Existing systems The team owns Rails services, Chef code, Vagrantfiles, or Ruby libraries. The team owns Python services, Ansible extensions, or a Python automation platform.
Required libraries Ruby’s standard library or maintained gems cover the task cleanly. A required vendor SDK or specialist library is Python-first.
Team ownership On-call engineers can read, test, and patch Ruby confidently. Python is the shared operational language and Ruby knowledge is limited.
Data work The job is configuration, APIs, text, logs, or orchestration. The job depends heavily on Python’s data-science or machine-learning ecosystem.
Delivery A small CLI with Bundler and a locked dependency set fits the environment. The existing packaging and runtime pipeline is already optimized for Python.

The right answer can be “both.” A Ruby deployment CLI can call a Python analysis service through JSON, while a Python pipeline can invoke a Ruby policy checker as a subprocess. Stable interfaces create more flexibility than a single-language rule.

Install and verify Ruby

Use your operating system’s package manager, a version manager such as mise, asdf, or rbenv, or the installation approach approved by your organization. This series targets Ruby 3.2 or newer.

Verify the runtime and syntax checker:

ruby --version
ruby -c examples/quick_compare.rb

Ruby’s -c option checks syntax without running the file. Chef’s official Ruby guide documents the same pattern for validating cookbook files.2

Install the project dependencies:

bundle install

The production toolkit uses the standard library. The Gemfile adds only Rake and Minitest for repeatable project tasks and tests.

Compare the shape of common automation

Suppose a deployment inventory contains three services. You need the names and combined replica count of the critical services.

A typical Python expression might look like this:

services = [
    {"name": "api", "tier": "critical", "replicas": 3},
    {"name": "worker", "tier": "critical", "replicas": 2},
    {"name": "docs", "tier": "optional", "replicas": 1},
]

critical = [service for service in services if service["tier"] == "critical"]
capacity = sum(service["replicas"] for service in critical)
names = [service["name"] for service in critical]

The Ruby version uses enumerable methods and blocks:

services = [
  { name: "api", tier: "critical", replicas: 3 },
  { name: "worker", tier: "critical", replicas: 2 },
  { name: "docs", tier: "optional", replicas: 1 }
]

critical = services.select { |service| service[:tier] == "critical" }
capacity = critical.sum { |service| service[:replicas] }
names = critical.map { |service| service[:name] }

Neither version is inherently superior. Ruby’s blocks read naturally to many teams, especially when the transformation becomes a chain:

critical_capacity = services
  .select { |service| service[:tier] == "critical" }
  .sum { |service| service[:replicas] }

That style is useful for deployment inventories, incident events, log records, cloud-resource lists, and policy results. It is only a benefit when the chain remains easy to scan. Break complex transformations into named methods rather than pursuing the fewest possible lines.

Environment variables and fail-fast defaults

DevOps tools frequently read environment variables. Ruby’s ENV.fetch can require a value or provide a default:

environment = ENV.fetch("DEPLOY_ENV", "staging")
api_token = ENV.fetch("API_TOKEN")

The first line defaults to staging. The second raises KeyError if API_TOKEN is absent, which is often safer than silently continuing with a blank credential. Do not print secret values in normal or debug output.

Python offers the same choices through os.environ and os.environ.get. The important design principle is explicit behavior, not the language.

Run a child process without a shell string

Ruby’s Open3 library gives access to a child process’s standard input, output, error stream, and exit status.1 Pass the executable and each argument separately:

require "open3"

stdout, stderr, status = Open3.capture3(
  "git",
  "status",
  "--short",
  "--branch"
)

puts stdout
warn stderr unless stderr.empty?
exit status.exitstatus unless status.success?

This is analogous to Python’s subprocess.run(["git", "status", ...], capture_output=True). Avoid interpolating untrusted input into a single shell command. Part 3 turns this idea into a reusable runner with timeout and dry-run support.

Emit JSON as an automation contract

Human-readable output is useful at a terminal. JSON is easier for CI jobs and other programs to consume.

require "json"

report = {
  environment: ENV.fetch("DEPLOY_ENV", "staging"),
  services: %w[api worker],
  ready: true
}

puts JSON.pretty_generate(report)

Symbols such as :environment make Ruby hashes pleasant to write. JSON serialization converts them into string keys. The resulting interface is language-neutral: a shell script can query it with jq, Python can parse it with json.loads, and a pipeline can archive it as an artifact.

Run the complete comparison

The repository combines collection transforms, environment variables, a child Ruby process, and JSON output in one deterministic example:

ruby examples/quick_compare.rb

Expected shape:

{
  "critical_services": [
    "api",
    "worker"
  ],
  "critical_capacity": 5,
  "child_environment": "staging",
  "child_exit_status": 0,
  "child_stderr": ""
}

Change the environment without editing the script:

DEPLOY_ENV=production ruby examples/quick_compare.rb

The output should now report production for child_environment.

The practical benefits

Ruby’s first benefit is choice. You can maintain a Python-first platform and still use Ruby where it aligns with existing Chef, Vagrant, or application code. That reduces forced rewrites and gives teams a better ownership match.2 3

Its second benefit is expressive transformation code. Blocks, enumerables, interpolation, and a consistent object model can make internal tools concise while remaining readable. This is most valuable in scripts that spend their time reshaping structured data rather than performing numerical computing.

Its third benefit is small-tool capability. Standard-library components cover many infrastructure scripting needs.1 The code in this series does not add a third-party HTTP client, YAML parser, CLI framework, or process wrapper.

The final benefit is interface flexibility. Ruby can produce the same JSON, YAML, logs, and process exit codes as Python. When you design around those contracts, replacing or combining implementations later becomes straightforward.

Know the trade-offs

Ruby may require an additional runtime on systems that already standardize on Python. Some cloud and security vendors publish Python examples or SDK features first. The available operations talent matters more than elegance: a script no one can debug is not a flexible script.

Dependency management is also a deliberate responsibility. Use Bundler, commit Gemfile.lock, patch dependencies, and run tests in CI. Part 5 implements that workflow based on the official Bundler and GitHub Actions patterns.4 5

Practice exercise

Open examples/quick_compare.rb and add a total_capacity field for all services. Then add an under_replicated array containing services with fewer than two replicas.

Run the script before and after your change:

ruby -c examples/quick_compare.rb
ruby examples/quick_compare.rb

A good extension keeps the output contract stable and adds tests if the calculation moves into reusable library code.

Next: build a real CLI

Part 2 replaces one-off argument handling with a production-style command-line interface. You will add subcommands, help text, JSON output, and meaningful exit codes using Ruby’s OptionParser.

Continue to Build a Production-Ready DevOps CLI in Ruby.

References

  1. Ruby Standard Library
  2. Chef Infra Client Ruby Guide
  3. HashiCorp Vagrantfile Documentation
  4. RubyGems Guides: Getting Started with Bundler
  5. GitHub Docs: Building and Testing Ruby