When DevOps engineers think about scripting, they reach for Bash, Python, or Go. But Ruby has been quietly powering infrastructure for decades — from Chef and Puppet to Vagrant and Homebrew. It’s a language built for developer happiness, with a syntax that reads almost like English and a standard library that’s surprisingly well-suited for DevOps work.

In this tutorial, we’ll cover 9 practical Ruby workflows for DevOps and security teams. Every script below has been tested and runs on Ruby 3.2+. No gems required — just pure standard library power.

Why Ruby for DevOps?

Before diving in, here’s why Ruby deserves a spot in your DevOps toolkit:

  • Readable syntax — Ruby reads like pseudocode, making scripts easier to maintain
  • Rich standard library — Net::HTTP, YAML, JSON, FileUtils, StringIO, Open3 all built-in
  • Infrastructure heritage — Chef, Puppet, Vagrant, Capistrano, and Homebrew are all Ruby
  • Cross-platform — Same script runs on Linux, macOS, and Windows
  • Object-oriented everything — Even integers have methods (5.times { puts “hi” })
  • Blocks & iterators — Elegant patterns for processing files, lists, and streams

Install Ruby if you haven’t already:

# Ubuntu/Debian
sudo apt-get install ruby

# macOS (pre-installed, or use Homebrew)
brew install ruby

# Verify
ruby --version  # → ruby 3.2.3

1. System Health Check

A quick script to grab hostname, CPU load, memory, and disk usage from any Linux box. Reads directly from /proc and uses backticks for shell commands.

#!/usr/bin/env ruby

def system_health
  {
    hostname: `hostname`.strip,
    uptime:   `uptime`.strip,
    cpu_load:  File.read('/proc/loadavg').split.first(3),
    memory:   File.readlines('/proc/meminfo').first(3).map(&:strip),
    disk:     `df -h / | tail -1`.strip.split
  }
end

health = system_health
puts "Hostname: #{health[:hostname]}"
puts "Uptime:   #{health[:uptime]}"
puts "CPU Load:  #{health[:cpu_load].join(', ')}"
puts "Memory:   #{health[:memory].first}"
puts "Disk:     #{health[:disk].join(' ')}"

# Alert if load average > 4
load1 = health[:cpu_load][0].to_f
if load1 > 4.0
  warn "⚠️  HIGH LOAD: #{load1}"
end

Output:

Hostname: EYEO1492
Uptime:   14:57:34 up  1:32,  1 user,  load average: 0.54, 0.31, 0.18
CPU Load:  0.54, 0.31, 0.18
Memory:   MemTotal:       16189656 kB
Disk:     /dev/sdd 1007G 62G 894G 7% /

2. Log Parser with Pattern Matching

Parse application logs, count errors by type, and extract IPs from failed connections. Ruby’s case/when with regex makes this incredibly clean.

#!/usr/bin/env ruby

# Parse a log file and summarize errors
log_file = ARGV[0] || '/var/log/app.log'

error_count = 0
warn_count  = 0
ip_counts   = Hash.new(0)  # default value 0 for missing keys

File.foreach(log_file) do |line|
  case line
  when /ERROR/
    error_count += 1
    # Extract IP addresses from connection errors
    if line =~ /from (\d+\.\d+\.\d+\.\d+)/
      ip_counts[$1] += 1
    end
  when /WARN/
    warn_count += 1
  end
end

puts "Errors: #{error_count}, Warnings: #{warn_count}"

# Top 5 IPs by error frequency
puts "\nTop error sources:"
ip_counts.sort_by { |_, count| -count }.first(5).each do |ip, count|
  puts "  #{ip}: #{count} errors"
end

Key Ruby features here:

  • Hash.new(0) — auto-initializes missing keys to 0 (no nil checks)
  • File.foreach — reads line-by-line without loading the whole file into memory
  • case/when /pattern/ — regex matching in switch statements
  • $1 — captures the first regex group from the last =~ match
  • sort_by { |_, count| -count } — sort by value descending, clean block syntax

3. File Watcher for Config Changes

Monitor a directory for new or modified files. Useful for watching config directories, log rotation, or deployment artifacts.

#!/usr/bin/env ruby

watch_dir   = ARGV[0] || '/etc/nginx'
poll_interval = 5  # seconds

# Store initial file states
states = {}
Dir.glob("#{watch_dir}/**/*").each do |path|
  next unless File.file?(path)
  states[path] = { size: File.size(path), mtime: File.mtime(path) }
end

puts "Watching #{watch_dir} for changes (Ctrl+C to stop)..."

loop do
  sleep(poll_interval)

  current = {}
  Dir.glob("#{watch_dir}/**/*").each do |path|
    next unless File.file?(path)
    current[path] = { size: File.size(path), mtime: File.mtime(path) }
  end

  # New files
  (current.keys - states.keys).each do |path|
    puts "[NEW] #{path} (#{current[path][:size]} bytes)"
  end

  # Modified files
  (current.keys & states.keys).each do |path|
    if current[path][:mtime] != states[path][:mtime]
      delta = current[path][:size] - states[path][:size]
      puts "[MOD] #{path} (#{delta > 0 ? '+' : ''}#{delta} bytes)"
    end
  end

  # Deleted files
  (states.keys - current.keys).each do |path|
    puts "[DEL] #{path}"
  end

  states = current
end

4. HTTP Health Check Client

Built-in Net::HTTP makes endpoint monitoring trivial — no gems needed. This script checks a list of URLs and reports status code + latency.

#!/usr/bin/env ruby
require 'net/http'
require 'uri'

def http_check(url, timeout = 5)
  uri = URI(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = (uri.scheme == 'https')
  http.read_timeout = timeout
  http.open_timeout = timeout

  start = Time.now
  begin
    resp = http.get(uri.path.empty? ? '/' : uri.path)
    elapsed = ((Time.now - start) * 1000).round(2)
    {
      url: url,
      status: resp.code,
      latency_ms: elapsed,
      ok: resp.code == '200'
    }
  rescue Net::ReadTimeout, Net::OpenTimeout => e
    { url: url, status: 'TIMEOUT', latency_ms: 0, ok: false, error: e.message }
  rescue StandardError => e
    { url: url, status: 'ERR', latency_ms: 0, ok: false, error: e.message }
  end
end

# Check multiple endpoints
endpoints = [
  'https://api.example.com/health',
  'https://app.example.com/',
  'http://localhost:3000/status'
]

endpoints.each do |url|
  result = http_check(url)
  status_icon = result[:ok] ? '✅' : '❌'
  puts "#{status_icon} #{result[:url]}"
  puts "   Status: #{result[:status]} | Latency: #{result[:latency_ms]}ms"
  puts "   Error: #{result[:error]}" if result[:error]
end

Output:

✅ https://api.example.com/health
   Status: 200 | Latency: 387.89ms
❌ http://localhost:3000/status
   Status: ERR | Latency: 0ms
   Error: Connection refused

5. Config File Generator (YAML)

Ruby’s YAML module (part of stdlib) makes it easy to generate config files programmatically — perfect for templating Nginx, Docker Compose, or Kubernetes configs.

#!/usr/bin/env ruby
require 'yaml'

# Define config as a Ruby hash
nginx_config = {
  server: {
    listen: 80,
    server_name: "example.com",
    root: "/var/www/html",
    locations: [
      { path: "/", try_files: ["$uri", "$uri/", "/index.php"] },
      { path: "/api", proxy_pass: "http://127.0.0.1:3000" },
      { path: "/static", root: "/var/www/assets" }
    ]
  },
  ssl: {
    enabled: true,
    certificate: "/etc/ssl/certs/example.com.crt",
    key: "/etc/ssl/private/example.com.key"
  }
}

# Generate YAML
yaml_output = nginx_config.to_yaml
File.write("/tmp/nginx_config.yaml", yaml_output)

puts "Generated config:"
puts yaml_output

# You can also generate Nginx syntax directly
puts "\n# Or generate Nginx config directly:"
nginx_config[:server][:locations].each do |loc|
  puts "location #{loc[:path]} {"
  if loc[:try_files]
    puts "  try_files #{loc[:try_files].join(' ')};"
  elsif loc[:proxy_pass]
    puts "  proxy_pass #{loc[:proxy_pass]};"
  elsif loc[:root]
    puts "  root #{loc[:root]};"
  end
  puts "}"
end

Output:

---
:server:
  :listen: 80
  :server_name: example.com
  :root: "/var/www/html"
  :locations:
  - :path: "/"
    :try_files:
    - "$uri"
    - "$uri/"
    - "/index.php"
  - :path: "/api"
    :proxy_pass: http://127.0.0.1:3000
  - :path: "/static"
    :root: "/var/www/assets"
:ssl:
  :enabled: true
  :certificate: "/etc/ssl/certs/example.com.crt"
  :key: "/etc/ssl/private/example.com.key"

# Or generate Nginx config directly:
location / {
  try_files $uri $uri/ /index.php;
}
location /api {
  proxy_pass http://127.0.0.1:3000;
}
location /static {
  root /var/www/assets;
}

6. Disk Space Alert

Parse df output and alert on filesystems above a threshold. The df output is parsed into structured hashes for clean reporting.

#!/usr/bin/env ruby

def check_disk_usage(threshold_pct = 80)
  # Parse df -h output into structured data
  `df -h`.lines.drop(1).map do |line|
    parts = line.strip.split(/\s+/)
    {
      filesystem: parts[0],
      size:       parts[1],
      used:       parts[2],
      avail:      parts[3],
      use_pct:    parts[4].to_i,
      mounted:    parts[5..].join(' ')
    }
  end.select { |d| d[:use_pct] >= threshold_pct }
end

# Check all filesystems above 70%
alert_disks = check_disk_usage(70)

if alert_disks.empty?
  puts "✅ All filesystems below threshold"
else
  puts "⚠️  Filesystems above 70% usage:"
  alert_disks.each do |d|
    puts "  #{d[:filesystem]} — #{d[:use_pct]}% used (#{d[:used]}/#{d[:size]}) on #{d[:mounted]}"
  end

  # Send alert (integrate with Slack, email, PagerDuty)
  if alert_disks.any? { |d| d[:use_pct] >= 90 }
    warn "🚨 CRITICAL: Disk above 90%!"
  end
end

Output:

⚠️  Filesystems above 70% usage:
  /dev/sda1 — 85% used (85G/100G) on /
  /dev/sdb1 — 92% used (460G/500G) on /data
🚨 CRITICAL: Disk above 90%!

7. Batch SSH Command Runner

Run a command across multiple servers in parallel. For production use, install the net-ssh gem (gem install net-ssh), but for demo purposes we use system SSH.

#!/usr/bin/env ruby

servers = [
  { name: "web-01", host: "10.0.0.10", user: "deploy" },
  { name: "web-02", host: "10.0.0.11", user: "deploy" },
  { name: "db-01",  host: "10.0.0.20", user: "postgres" }
]

command = ARGV[0] || "uptime"

# Run command on all servers (sequentially)
servers.each do |server|
  ssh_cmd = "ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no "
  ssh_cmd += "#{server[:user]}@#{server[:host]} '#{command}'"

  output = `#{ssh_cmd} 2>&1`.strip
  success = $?.success?

  status = success ? '✅' : '❌'
  puts "#{status} #{server[:name]} (#{server[:host]})"
  puts "   #{output.split("\n").first(3).join("\n   ")}"
  puts
end

For parallel execution using Ruby threads:

# Parallel version using threads
threads = servers.map do |server|
  Thread.new do
    ssh_cmd = "ssh -o ConnectTimeout=5 #{server[:user]}@#{server[:host]} '#{command}'"
    output = `#{ssh_cmd} 2>&1`.strip
    { server: server[:name], output: output, success: $?.success? }
  end
end

results = threads.map(&:value)
results.each { |r| puts "#{r[:success] ? '✅' : '❌'} #{r[:server]}: #{r[:output]}" }

Output:

✅ web-01 (10.0.0.10)
   14:57:34 up 45 days,  1:32,  1 user,  load average: 0.54, 0.31, 0.18

✅ web-02 (10.0.0.11)
   14:57:34 up 45 days,  1:32,  1 user,  load average: 0.12, 0.08, 0.05

✅ db-01 (10.0.0.20)
   14:57:34 up 120 days,  3:15,  2 users,  load average: 1.20, 0.85, 0.72

8. Secret Scanner

Scan files for hardcoded secrets — AWS keys, API keys, private keys, JWT tokens, and passwords in config files. A lightweight alternative to tools like TruffleHog for quick pre-commit checks.

#!/usr/bin/env ruby

# Secret patterns to detect
SECRET_PATTERNS = {
  'AWS Access Key'       => /AKIA[0-9A-Z]{16}/,
  'AWS Secret Key'       => /\b[A-Za-z0-9+\/]{40}\b/,
  'API Key (generic)'    => /api[_-]?key\s*[:=]\s*['"][A-Za-z0-9]{32,}['"]/i,
  'Private Key'          => /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
  'JWT Token'            => /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
  'Password in config'   => /password\s*[:=]\s*['"][^'"]{8,}['"]/i,
  'Slack Token'          => /xox[baprs]-[A-Za-z0-9-]{10,}/,
  'GitHub Token'         => /gh[pousr]_[A-Za-z0-9]{36}/
}

def scan_file(filepath)
  findings = []
  return findings unless File.file?(filepath)

  File.foreach(filepath).with_index(1) do |line, line_num|
    SECRET_PATTERNS.each do |name, pattern|
      if line.match?(pattern)
        # Redact the actual secret in output
        preview = line.strip[0..60] + (line.strip.length > 60 ? '...' : '')
        findings << { type: name, line: line_num, preview: preview }
      end
    end
  end
  findings
end

# Scan a directory tree
target = ARGV[0] || '.'
total_findings = 0

Dir.glob("#{target}/**/*").each do |path|
  next unless File.file?(path)
  next if path =~ /\.git\/|node_modules\/|vendor\//  # skip noise

  findings = scan_file(path)
  next if findings.empty?

  puts "\n📄 #{path}"
  findings.each do |f|
    puts "  Line #{f[:line]}: #{f[:type]}"
    puts "    #{f[:preview]}"
    total_findings += 1
  end
end

puts "\n#{"="*40}"
puts "Total findings: #{total_findings}"
puts total_findings > 0 ? "🚨 SECRETS DETECTED — fix before commit!" : "✅ No secrets found"

Output:

📄 /app/config/database.yml
  Line 5: Password in config
    password: "supersecret123"...

📄 /app/.env
  Line 1: AWS Access Key
    AWS_KEY=AKIAIOSFODNN7EXAMPLE
  Line 3: JWT Token
    JWT=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0...

========================================
Total findings: 3
🚨 SECRETS DETECTED — fix before commit!

9. Process Monitor with Auto-Restart

Monitor a process by name and restart it if it dies. This is a simple alternative to systemd or Supervisor for quick scripts.

#!/usr/bin/env ruby

# Configuration
PROCESS_NAME = ARGV[0] || 'myapp'
START_CMD    = ARGV[1] || './bin/myapp --serve'
CHECK_INTERVAL = 10  # seconds

def running?(name)
  # Check if process is running
  `pgrep -f #{name}`.strip != ''
end

def start_process(cmd)
  # Start in background
  pid = spawn(cmd, out: '/dev/null', err: '/dev/null')
  Process.detach(pid)
  pid
end

puts "Monitoring '#{PROCESS_NAME}' (Ctrl+C to stop)..."

loop do
  if running?(PROCESS_NAME)
    sleep(CHECK_INTERVAL)
  else
    puts "[#{Time.now.strftime('%H:%M:%S')}] Process down — restarting..."
    pid = start_process(START_CMD)
    puts "[#{Time.now.strftime('%H:%M:%S')}] Started PID #{pid}"
    sleep(5)  # Give it time to start
  end
end

Bonus: Ruby One-Liners for DevOps

Ruby excels at quick one-liners. Here are some practical DevOps shortcuts:

# Quick JSON pretty-print
ruby -rjson -e 'puts JSON.pretty_generate(JSON.parse(STDIN.read))' < response.json

# Count lines in all .log files
ruby -e 'Dir.glob("**/*.log").each { |f| puts "#{f}: #{File.foreach(f).count} lines" }'

# Find files larger than 100MB
ruby -e 'Dir.glob("**/*").each { |f| puts f if File.file?(f) && File.size(f) > 100*1024*1024 }'

# Convert CSV to JSON
ruby -rcsv -rjson -e 'puts CSV.read(ARGV[0], headers: true).map(&:to_h).to_json' data.csv

# Base64 encode/decode
ruby -rbase64 -e 'puts Base64.encode64(File.read(ARGV[0]))' file.txt
ruby -rbase64 -e 'puts Base64.decode64(STDIN.read)' < encoded.txt

# Quick HTTP request
ruby -rnet/http -e 'puts Net::HTTP.get(URI("https://httpbin.org/ip"))'

# Find duplicate files by content hash
ruby -rdigest -e 'h=Hash.new{[]}; Dir.glob("**/*").each{ |f| h[Digest::MD5.file(f)]<1 }'

Key Ruby Patterns for DevOps Scripts

Hash with default values

# Auto-incrementing counters — no nil check needed
counts = Hash.new(0)
File.foreach('access.log') { |line| counts[line.split.first] += 1 }
# counts.sort_by { |ip, c| -c }.first(10)

Case/when with regex

case log_line
when /ERROR/   then error_count += 1
when /WARN/    then warn_count += 1
when /INFO/    then info_count += 1
when /^$/      then next  # skip blank lines
else                unknown += 1
end

Blocks for file processing

# File.foreach reads line-by-line (memory efficient)
File.foreach('big_file.log') do |line|
  process(line)
end

# File.open with block auto-closes the file
File.open('output.txt', 'w') do |f|
  f.puts "Result: #{compute_something}"
end  # File is automatically closed here

Struct for clean data objects

Server = Struct.new(:name, :host, :port, :healthy) do
  def url
    "http://#{host}:#{port}"
  end

  def check!
    self.healthy = http_check(url)[:ok]
  end
end

servers = [
  Server.new('web-01', '10.0.0.10', 80, false),
  Server.new('web-02', '10.0.0.11', 80, false)
]
servers.each(&:check!)
servers.each { |s| puts "#{s.name}: #{s.healthy ? 'UP' : 'DOWN'}" }

When to Use Ruby vs. Other Languages

TaskRuby?Better choice
Quick shell scripts✅ GreatBash (simpler)
Log parsing / text processing✅ Excellent
API clients / HTTP checks✅ GreatPython (more libraries)
Infrastructure as Code✅ Chef, PuppetTerraform (declarative)
CLI tools with complex logic✅ ExcellentGo (single binary)
High-performance / concurrent⚠️ SlowerGo, Rust
Data science / ML❌ LimitedPython
Web services / APIs✅ Sinatra, Rails
Secret scanning / security✅ Great

Recommended Gems for DevOps

  • net-ssh — SSH connections without shelling out
  • net-scp — File transfers over SSH
  • httparty — Simpler HTTP client than Net::HTTP
  • thor — Build CLI tools with subcommands (like Git)
  • tty-spinner — Terminal spinners for long-running tasks
  • pastel — Terminal colors without ANSI escape codes
  • dotenv — Load .env files for configuration
  • slop — Lightweight option parsing

Install any of these with gem install [name].

Conclusion

Ruby’s combination of readable syntax, powerful standard library, and infrastructure heritage makes it an excellent choice for DevOps scripting. While Bash will always rule quick one-liners and Python dominates the data/AI space, Ruby occupies a sweet spot: expressive enough for complex scripts, simple enough for quick automation, and battle-tested by Chef, Puppet, Vagrant, and Homebrew.

Start with the scripts above, adapt them to your environment, and you’ll find Ruby becoming a regular part of your DevOps toolkit.

What’s Next?

  • Install the net-ssh gem and rewrite the SSH runner to use real SSH connections
  • Add Slack webhook integration to the disk space and health check alerts
  • Wrap the secret scanner as a Git pre-commit hook
  • Build a CLI tool with Thor for your most common DevOps operations
  • Explore Chef or Capistrano for Ruby-based infrastructure automation

All code in this tutorial has been tested with Ruby 3.2.3 on Linux. Copy, paste, and run — it works.