the shed // config management

A ~180-line, dependency-free configuration engine that only touches the filesystem when reality doesn’t match what you declared — the Chef/Puppet pattern, without the agent.

Step through the build below:

config_state_engine.rb

Pulling in Chef, Puppet, or Ansible for a handful of config-file guarantees on a small fleet is a lot of machinery — agents, a server, a DSL with its own runtime — for something you can express in a few declarative lines.

config_state_engine.rb shows the pattern underneath all of them in ~180 lines: declare the state you want (“this directory should exist”, “this file should have this exact content”), and the engine only touches the filesystem when reality doesn’t already match it. Run it twice in a row and the second run changes nothing — that’s the whole point.

#!/usr/bin/env ruby
# frozen_string_literal: true
#
# config_state_engine.rb
#
# A minimal, Chef/Puppet/Ansible-style *idempotent* configuration engine in
# ~180 lines of pure Ruby: declare the state you want ("this directory
# should exist", "this file should have this exact content and mode",
# "this line should be present in this file"), and the engine only touches
# the filesystem when reality doesn't already match. Running it twice in a
# row produces zero changes the second time -- that's the whole point.
#
# Why this exists: pulling in Chef, Puppet, or Ansible for a handful of
# config-file guarantees on a small fleet is a lot of machinery (agents,
# a server, a DSL with its own runtime) for something you can express in a
# few declarative lines. This script shows the *pattern* underneath all of
# them -- check current state, diff against desired state, apply only the
# delta, report what changed -- in a form small enough to read end-to-end
# and adapt directly.
#
# Usage:
#   ruby config_state_engine.rb --target-dir /etc/myapp
#   ruby config_state_engine.rb --target-dir /etc/myapp --dry-run
#   ruby config_state_engine.rb --target-dir /etc/myapp --check   # CI-style: exit 1 on drift, no changes made
#
# Requires: Ruby >= 2.7 (stdlib only: fileutils, optparse -- no gems).
require 'fileutils'
require 'optparse'
# --- Resource base class -----------------------------------------------------
#
# Every resource type implements the same two-step contract: #check reports
# whether the current state already matches the desired state (returning a
# human-readable reason when it doesn't), and #apply makes exactly the
# change needed to reach the desired state. The runner (ConfigState) never
# needs to know the difference between a file, a directory, or a line --
# it just calls #check, and #apply only if #check said something was off.
class Resource
  attr_reader :label
  CheckResult = Struct.new(:in_sync, :reason)
  def initialize(label)
    @label = label
  end
  def check
    raise NotImplementedError
  end
  def apply
    raise NotImplementedError
  end
end
# Ensures a directory exists (optionally with a specific permission mode).
class DirectoryResource < Resource
  def initialize(path, mode: nil)
    super("directory #{path}")
    @path = path
    @mode = mode
  end
  def check
    return CheckResult.new(false, 'does not exist') unless Dir.exist?(@path)
    if @mode && (current = File.stat(@path).mode & 0o777) != @mode
      return CheckResult.new(false, format('mode is %04o, want %04o', current, @mode))
    end
    CheckResult.new(true, nil)
  end
  def apply
    FileUtils.mkdir_p(@path)
    File.chmod(@mode, @path) if @mode
  end
end
# Ensures a file exists with exact content (and optionally an exact mode).
# "Exact content" is deliberate: config drift usually means someone hand-
# edited a managed file, and silently merging changes would hide that.
class FileResource < Resource
  def initialize(path, content:, mode: nil)
    super("file #{path}")
    @path = path
    @content = content
    @mode = mode
  end
  def check
    return CheckResult.new(false, 'does not exist') unless File.exist?(@path)
    return CheckResult.new(false, 'content differs') unless File.read(@path) == @content
    if @mode && (current = File.stat(@path).mode & 0o777) != @mode
      return CheckResult.new(false, format('mode is %04o, want %04o', current, @mode))
    end
    CheckResult.new(true, nil)
  end
  def apply
    FileUtils.mkdir_p(File.dirname(@path))
    File.write(@path, @content)
    File.chmod(@mode, @path) if @mode
  end
end
# Ensures a specific line is present somewhere in a file, without touching
# any other line -- the classic "add this one entry to /etc/hosts (or a
# config file) if it's missing" task, done safely and idempotently.
class LineInFileResource < Resource
  def initialize(path, line:)
    super("line in #{path}: #{line.inspect}")
    @path = path
    @line = line
  end
  def check
    return CheckResult.new(false, 'file does not exist') unless File.exist?(@path)
    lines = File.readlines(@path, chomp: true)
    return CheckResult.new(true, nil) if lines.include?(@line)
    CheckResult.new(false, 'line not present')
  end
  def apply
    FileUtils.touch(@path) unless File.exist?(@path)
    File.open(@path, 'a') { |f| f.puts(@line) }
  end
end
# --- Runner ------------------------------------------------------------------
#
# Collects resources declared via the ensure_* DSL methods, then runs them
# in declaration order, printing one line per resource: OK (already in the
# desired state), CHANGED (drift found and fixed), WOULD CHANGE (drift
# found, --dry-run so nothing was touched), or DRIFT (drift found, --check
# mode so nothing was touched and the run will exit non-zero).
class ConfigState
  Mode = Struct.new(:dry_run, :check_only)
  def self.run(dry_run: false, check_only: false)
    engine = new(Mode.new(dry_run, check_only))
    yield engine
    engine.execute
  end
  def initialize(mode)
    @mode = mode
    @resources = []
  end
  def ensure_directory(path, mode: nil)
    @resources << DirectoryResource.new(path, mode: mode)
  end
  def ensure_file(path, content:, mode: nil)
    @resources << FileResource.new(path, content: content, mode: mode)
  end
  def ensure_line_in_file(path, line:)
    @resources << LineInFileResource.new(path, line: line)
  end
  # Runs every declared resource and returns a summary hash. Also prints a
  # one-line status per resource as it goes, so a long run still shows
  # partial progress if interrupted.
  def execute
    counts = Hash.new(0)
    @resources.each do |resource|
      result = resource.check
      if result.in_sync
        counts[:ok] += 1
        puts "OK          #{resource.label}"
        next
      end
      if @mode.check_only
        counts[:drift] += 1
        puts "DRIFT       #{resource.label} (#{result.reason})"
      elsif @mode.dry_run
        counts[:would_change] += 1
        puts "WOULD CHANGE #{resource.label} (#{result.reason})"
      else
        resource.apply
        counts[:changed] += 1
        puts "CHANGED     #{resource.label} (#{result.reason})"
      end
    end
    counts
  end
end
# --- CLI entry point -----------------------------------------------------
if $PROGRAM_NAME == __FILE__
  options = { target_dir: '/tmp/config-state-demo', dry_run: false, check: false }
  OptionParser.new do |opts|
    opts.banner = 'Usage: config_state_engine.rb [options]'
    opts.on('--target-dir DIR', 'Directory the demo manifest manages (default /tmp/config-state-demo)') { |v| options[:target_dir] = v }
    opts.on('--dry-run', 'Report what would change without touching the filesystem') { options[:dry_run] = true }
    opts.on('--check', 'CI mode: exit 1 if anything is out of sync, without applying changes') { options[:check] = true }
  end.parse!
  dir = options[:target_dir]
  # This is the "manifest" -- the declarative part you'd customize per
  # project. Everything above this line is the reusable engine; everything
  # below is what a sysadmin actually writes day to day.
  counts = ConfigState.run(dry_run: options[:dry_run], check_only: options[:check]) do |c|
    c.ensure_directory dir, mode: 0o755
    c.ensure_directory File.join(dir, 'conf.d'), mode: 0o755
    c.ensure_file File.join(dir, 'app.yml'),
                  content: "env: production\nlog_level: info\n",
                  mode: 0o644
    # Deliberately a *different* file than app.yml above: a single resource
    # should own a given file's exact content, or append single lines to an
    # unmanaged file -- never both, or the two resources will fight over
    # the same bytes and the run will never settle (see Troubleshooting).
    c.ensure_line_in_file File.join(dir, 'hosts.local'), line: '127.0.0.1 myapp.local'
  end
  puts '---'
  puts "ok=#{counts[:ok]} changed=#{counts[:changed]} would_change=#{counts[:would_change]} drift=#{counts[:drift]}"
  exit(1) if options[:check] && counts[:drift].positive?
end

Every resource type implements the same contract: #check reports whether current state already matches desired state, and #apply makes exactly the change needed — called only when #check found drift.

The runner never needs to know the difference between a file, a directory, or a line; it just calls #check on each resource in order, and #apply only if that check reported something out of sync.

One deliberate design note: a single file should be owned by exactly one resource type. Using both ensure_file (exact-content) and ensure_line_in_file (append-if-missing) on the same file makes them fight over the same bytes and the run never settles — see Troubleshooting.

$ ruby config_state_engine.rb --target-dir /tmp/cs-sample
CHANGED     directory /tmp/cs-sample (does not exist)
CHANGED     directory /tmp/cs-sample/conf.d (does not exist)
CHANGED     file /tmp/cs-sample/app.yml (does not exist)
CHANGED     line in /tmp/cs-sample/hosts.local: "127.0.0.1 myapp.local" (file does not exist)
---
ok=0 changed=4 would_change=0 drift=0
$ ruby config_state_engine.rb --target-dir /tmp/cs-sample   # run again: fully idempotent
OK          directory /tmp/cs-sample
OK          directory /tmp/cs-sample/conf.d
OK          file /tmp/cs-sample/app.yml
OK          line in /tmp/cs-sample/hosts.local: "127.0.0.1 myapp.local"
---
ok=4 changed=0 would_change=0 drift=0
Get the code

Full script + README on GitHub: ruby-devops-toolkit/config-state-engine

Getting started

Prerequisites

You will need
  • Ruby ≥ 2.7 (stdlib only: fileutils, optparse — no gems required)
  • Write access to whatever directory the manifest targets
  • Linux or macOS for the exact commands below (the resource classes themselves are pure Ruby file I/O and work on Windows too)
Reference

Full source

config_state_engine.rbruby
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# config_state_engine.rb
#
# A minimal, Chef/Puppet/Ansible-style *idempotent* configuration engine in
# ~180 lines of pure Ruby: declare the state you want ("this directory
# should exist", "this file should have this exact content and mode",
# "this line should be present in this file"), and the engine only touches
# the filesystem when reality doesn't already match. Running it twice in a
# row produces zero changes the second time -- that's the whole point.
#
# Why this exists: pulling in Chef, Puppet, or Ansible for a handful of
# config-file guarantees on a small fleet is a lot of machinery (agents,
# a server, a DSL with its own runtime) for something you can express in a
# few declarative lines. This script shows the *pattern* underneath all of
# them -- check current state, diff against desired state, apply only the
# delta, report what changed -- in a form small enough to read end-to-end
# and adapt directly.
#
# Usage:
#   ruby config_state_engine.rb --target-dir /etc/myapp
#   ruby config_state_engine.rb --target-dir /etc/myapp --dry-run
#   ruby config_state_engine.rb --target-dir /etc/myapp --check   # CI-style: exit 1 on drift, no changes made
#
# Requires: Ruby >= 2.7 (stdlib only: fileutils, optparse -- no gems).
require 'fileutils'
require 'optparse'
# --- Resource base class -----------------------------------------------------
#
# Every resource type implements the same two-step contract: #check reports
# whether the current state already matches the desired state (returning a
# human-readable reason when it doesn't), and #apply makes exactly the
# change needed to reach the desired state. The runner (ConfigState) never
# needs to know the difference between a file, a directory, or a line --
# it just calls #check, and #apply only if #check said something was off.
class Resource
  attr_reader :label
  CheckResult = Struct.new(:in_sync, :reason)
  def initialize(label)
    @label = label
  end
  def check
    raise NotImplementedError
  end
  def apply
    raise NotImplementedError
  end
end
# Ensures a directory exists (optionally with a specific permission mode).
class DirectoryResource < Resource
  def initialize(path, mode: nil)
    super("directory #{path}")
    @path = path
    @mode = mode
  end
  def check
    return CheckResult.new(false, 'does not exist') unless Dir.exist?(@path)
    if @mode && (current = File.stat(@path).mode & 0o777) != @mode
      return CheckResult.new(false, format('mode is %04o, want %04o', current, @mode))
    end
    CheckResult.new(true, nil)
  end
  def apply
    FileUtils.mkdir_p(@path)
    File.chmod(@mode, @path) if @mode
  end
end
# Ensures a file exists with exact content (and optionally an exact mode).
# "Exact content" is deliberate: config drift usually means someone hand-
# edited a managed file, and silently merging changes would hide that.
class FileResource < Resource
  def initialize(path, content:, mode: nil)
    super("file #{path}")
    @path = path
    @content = content
    @mode = mode
  end
  def check
    return CheckResult.new(false, 'does not exist') unless File.exist?(@path)
    return CheckResult.new(false, 'content differs') unless File.read(@path) == @content
    if @mode && (current = File.stat(@path).mode & 0o777) != @mode
      return CheckResult.new(false, format('mode is %04o, want %04o', current, @mode))
    end
    CheckResult.new(true, nil)
  end
  def apply
    FileUtils.mkdir_p(File.dirname(@path))
    File.write(@path, @content)
    File.chmod(@mode, @path) if @mode
  end
end
# Ensures a specific line is present somewhere in a file, without touching
# any other line -- the classic "add this one entry to /etc/hosts (or a
# config file) if it's missing" task, done safely and idempotently.
class LineInFileResource < Resource
  def initialize(path, line:)
    super("line in #{path}: #{line.inspect}")
    @path = path
    @line = line
  end
  def check
    return CheckResult.new(false, 'file does not exist') unless File.exist?(@path)
    lines = File.readlines(@path, chomp: true)
    return CheckResult.new(true, nil) if lines.include?(@line)
    CheckResult.new(false, 'line not present')
  end
  def apply
    FileUtils.touch(@path) unless File.exist?(@path)
    File.open(@path, 'a') { |f| f.puts(@line) }
  end
end
# --- Runner ------------------------------------------------------------------
#
# Collects resources declared via the ensure_* DSL methods, then runs them
# in declaration order, printing one line per resource: OK (already in the
# desired state), CHANGED (drift found and fixed), WOULD CHANGE (drift
# found, --dry-run so nothing was touched), or DRIFT (drift found, --check
# mode so nothing was touched and the run will exit non-zero).
class ConfigState
  Mode = Struct.new(:dry_run, :check_only)
  def self.run(dry_run: false, check_only: false)
    engine = new(Mode.new(dry_run, check_only))
    yield engine
    engine.execute
  end
  def initialize(mode)
    @mode = mode
    @resources = []
  end
  def ensure_directory(path, mode: nil)
    @resources << DirectoryResource.new(path, mode: mode)
  end
  def ensure_file(path, content:, mode: nil)
    @resources << FileResource.new(path, content: content, mode: mode)
  end
  def ensure_line_in_file(path, line:)
    @resources << LineInFileResource.new(path, line: line)
  end
  # Runs every declared resource and returns a summary hash. Also prints a
  # one-line status per resource as it goes, so a long run still shows
  # partial progress if interrupted.
  def execute
    counts = Hash.new(0)
    @resources.each do |resource|
      result = resource.check
      if result.in_sync
        counts[:ok] += 1
        puts "OK          #{resource.label}"
        next
      end
      if @mode.check_only
        counts[:drift] += 1
        puts "DRIFT       #{resource.label} (#{result.reason})"
      elsif @mode.dry_run
        counts[:would_change] += 1
        puts "WOULD CHANGE #{resource.label} (#{result.reason})"
      else
        resource.apply
        counts[:changed] += 1
        puts "CHANGED     #{resource.label} (#{result.reason})"
      end
    end
    counts
  end
end
# --- CLI entry point -----------------------------------------------------
if $PROGRAM_NAME == __FILE__
  options = { target_dir: '/tmp/config-state-demo', dry_run: false, check: false }
  OptionParser.new do |opts|
    opts.banner = 'Usage: config_state_engine.rb [options]'
    opts.on('--target-dir DIR', 'Directory the demo manifest manages (default /tmp/config-state-demo)') { |v| options[:target_dir] = v }
    opts.on('--dry-run', 'Report what would change without touching the filesystem') { options[:dry_run] = true }
    opts.on('--check', 'CI mode: exit 1 if anything is out of sync, without applying changes') { options[:check] = true }
  end.parse!
  dir = options[:target_dir]
  # This is the "manifest" -- the declarative part you'd customize per
  # project. Everything above this line is the reusable engine; everything
  # below is what a sysadmin actually writes day to day.
  counts = ConfigState.run(dry_run: options[:dry_run], check_only: options[:check]) do |c|
    c.ensure_directory dir, mode: 0o755
    c.ensure_directory File.join(dir, 'conf.d'), mode: 0o755
    c.ensure_file File.join(dir, 'app.yml'),
                  content: "env: production\nlog_level: info\n",
                  mode: 0o644
    # Deliberately a *different* file than app.yml above: a single resource
    # should own a given file's exact content, or append single lines to an
    # unmanaged file -- never both, or the two resources will fight over
    # the same bytes and the run will never settle (see Troubleshooting).
    c.ensure_line_in_file File.join(dir, 'hosts.local'), line: '127.0.0.1 myapp.local'
  end
  puts '---'
  puts "ok=#{counts[:ok]} changed=#{counts[:changed]} would_change=#{counts[:would_change]} drift=#{counts[:drift]}"
  exit(1) if options[:check] && counts[:drift].positive?
end
config_state_engine.rb idempotency loop diagram

The idempotency loop: check, then only apply on drift — a resource already in sync is always a no-op.
How it works

Step-by-step walkthrough

Core pieces
  • Resource — abstract base defining the #check / #apply contract every resource type implements.
  • DirectoryResource / FileResource / LineInFileResource — the three concrete resource types: ensure a directory exists (optionally with a mode), ensure a file has exact content (and mode), and ensure one specific line is present in a file without touching any other line.
  • ConfigState.run — the DSL entry point; yields an engine instance so a manifest can call ensure_file/ensure_directory/ensure_line_in_file, then runs every declared resource in order.
  • Three run modes — a real run applies fixes and prints CHANGED; --dry-run reports WOULD CHANGE without touching anything; --check reports DRIFT and exits 1 if anything is out of sync, for wiring into CI or a monitoring check.
Verified in the sandbox

Example output

ruby config_state_engine.rb –target-dir /etc/myapp (then run again)
$ ruby config_state_engine.rb –target-dir /tmp/cs-sample
CHANGED directory /tmp/cs-sample (does not exist)
CHANGED directory /tmp/cs-sample/conf.d (does not exist)
CHANGED file /tmp/cs-sample/app.yml (does not exist)
CHANGED line in /tmp/cs-sample/hosts.local: "127.0.0.1 myapp.local" (file does not exist)
ok=0 changed=4 would_change=0 drift=0
$ ruby config_state_engine.rb –target-dir /tmp/cs-sample # run again: fully idempotent
OK directory /tmp/cs-sample
OK directory /tmp/cs-sample/conf.d
OK file /tmp/cs-sample/app.yml
OK line in /tmp/cs-sample/hosts.local: "127.0.0.1 myapp.local"
ok=4 changed=0 would_change=0 drift=0
How this was tested

Ran the demo manifest against a scratch directory six times in a row, covering every code path: a fresh run correctly reported CHANGED for all 4 resources; an immediate second run reported all 4 as OK with zero changes (true idempotency); after hand-editing a managed file, --dry-run correctly reported WOULD CHANGE and left the file untouched; --check correctly reported DRIFT and exited with status 1; a real run then fixed the drift; and a final run confirmed the fix was idempotent too.

When things go wrong

Troubleshooting

Common issues
  • A resource shows CHANGED on every single run, never settling to OK — almost always means two resources are managing the same file with incompatible strategies (e.g. an exact-content ensure_file and an append-only ensure_line_in_file on the same path). Give each managed file exactly one owning resource.
  • Permission denied on apply — the process needs write access to the target path; on a real box this often means running with appropriate privileges for the directories being managed (e.g. /etc).
  • –check always exits 0 even with drift — double check the flag is actually being read; some shells/wrappers swallow flags passed after a script name depending on how they invoke Ruby.
Where to take it next

Extending this script

Ideas
  • Add a SymlinkResource (ensure a symlink points at a specific target — handy alongside release-directory deploy patterns).
  • Add a PackageResource that shells out to dpkg -l/rpm -q to check (and optionally install) a package idempotently.
  • Load the manifest from an external .rb file via load so different hosts/roles can share the engine but declare different desired state.
  • Add a machine-readable --json output mode for feeding run results into a compliance dashboard.