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 memorycase/when /pattern/— regex matching in switch statements$1— captures the first regex group from the last=~matchsort_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
| Task | Ruby? | Better choice |
|---|---|---|
| Quick shell scripts | ✅ Great | Bash (simpler) |
| Log parsing / text processing | ✅ Excellent | — |
| API clients / HTTP checks | ✅ Great | Python (more libraries) |
| Infrastructure as Code | ✅ Chef, Puppet | Terraform (declarative) |
| CLI tools with complex logic | ✅ Excellent | Go (single binary) |
| High-performance / concurrent | ⚠️ Slower | Go, Rust |
| Data science / ML | ❌ Limited | Python |
| 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-sshgem 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.
