A migration-like DSL for server provisioning via SSH. Provides a change-based approach to server configuration, similar to how Rails database migrations work but for infrastructure.
- Idempotency: Changes can be run multiple times safely
- Auditability: All changes are tracked in version control
- Consistency: All servers in an environment receive the same configuration
- Incremental updates: Only pending changes are applied
- Change tracking: State stored on each server, automatically re-applied on rebuild
Add to your Gemfile:
gem "simple_infrastructure"Then run:
bundle install
rails generate simple_infrastructure:installThis creates:
bin/infrastructure— CLI toolconfig/infrastructure/inventory.yml— server inventoryconfig/infrastructure/changes/— directory for change files
For Rails apps, the gem auto-configures via Railtie (sets project_root to Rails.root and logger to Rails.logger).
For standalone use:
require "simple_infrastructure"
SimpleInfrastructure.configure do |config|
config.project_root = "/path/to/project"
endconfig/infrastructure/
├── inventory.yml # Server inventory
└── changes/ # Change files
├── 20250121000100_install_essentials.rb
├── 20250121000200_configure_storage.rb
└── ...
Servers are defined in config/infrastructure/inventory.yml, grouped by environment:
defaults:
user: civo
servers:
production:
- hostname: web1.production.example.com
- hostname: web2.production.example.com
- hostname: db.production.example.com
staging:
- hostname: app1.staging.example.comThe defaults section provides default values for all servers. Each server entry can override these defaults.
View change status for all servers:
bin/infrastructure statusOutput shows which servers have pending changes:
Production -------------------------------------------------------------
✗ web1.production.example.com (6 pending)
✔︎ web2.production.example.com (up to date)
Staging ----------------------------------------------------------------
✗ app1.staging.example.com (2 pending)
Use -v or --verbose to see the list of pending changes:
bin/infrastructure status -vPreview what changes would be made without executing them:
bin/infrastructure --dry-run staging
bin/infrastructure --dry-run production
bin/infrastructure --dry-run web1.production.example.comApply pending changes:
# All servers in an environment
bin/infrastructure staging
bin/infrastructure production
# Specific server
bin/infrastructure web1.production.example.comCreate a new change file:
bin/infrastructure new setup_redis
# Or using the Rails generator:
rails generate simple_infrastructure:change setup_redisThis creates a timestamped file like config/infrastructure/changes/20250121143000_setup_redis.rb.
# Target specific servers (omit target line for all servers)
target env: :production # All production servers
target env: :staging # All staging servers
target hostname: "db.production.example.com" # Specific server
# Run shell commands
run "apt update", sudo: true
run "systemctl restart nginx", sudo: true
# Manage file contents
file "/etc/ssh/sshd_config", sudo: true do
contains "PermitRootLogin no" # Ensure line exists
remove "PermitRootLogin yes" # Remove line if present
on_change { run "systemctl restart sshd", sudo: true }
end
# Manage YAML files
yaml "/etc/config.yml", sudo: true do
set "server.port", 8080
remove "deprecated.setting"
end
# Manage TOML files
toml "/etc/config.toml", sudo: true do
set "database.host", "localhost"
end
# Upload local files
upload "config/backup/script.sh", "/root/bin/script.sh", sudo: true, mode: "700"By default, changes apply to all servers. Use target to restrict to specific servers:
# All servers in production
target env: :production
# Specific hostname
target hostname: "db.production.example.com"
# Hostname pattern (regex)
target hostname: /^web\d+\.production\./Execute a shell command on the remote server.
run "apt update", sudo: true
run "docker compose up -d"Manage plain text file contents.
file "/etc/fstab", sudo: true do
contains "/swapfile none swap sw 0 0" # Add if missing
remove "/old/swap none swap sw 0 0" # Remove if present
on_change { run "mount -a", sudo: true } # Run only if file changed
endManage YAML configuration files.
yaml "/etc/app/config.yml", sudo: true do
set "database.host", "localhost"
set "database.port", 3306
remove "deprecated_key"
endManage TOML configuration files.
toml "/etc/app/config.toml", sudo: true do
set "server.bind", "0.0.0.0"
set "server.port", 8080
endUpload a local file to the remote server.
upload "bin/backup", "/root/bin/backup", sudo: true, mode: "700"
upload "config/nginx.conf", "/etc/nginx/nginx.conf", sudo: trueThe file DSL supports an on_change callback that only executes when the file was actually modified:
file "/etc/ssh/sshd_config", sudo: true do
remove "PermitRootLogin yes"
contains "PermitRootLogin no"
on_change { run "systemctl restart sshd", sudo: true }
endThis is useful for restarting services only when their configuration changes, avoiding unnecessary service restarts.
Alternative to the CLI:
# Show status
rake infrastructure:status
# Run changes
rake infrastructure:run[production]
rake infrastructure:run[staging]
# Dry run
rake infrastructure:dry_run[production]
# Generate change
rake infrastructure:generate[setup_redis]The system tracks which changes have been applied by storing log files on each server in ~/.infrastructure/:
~/.infrastructure/
├── 20250121000100_install_essentials.log
├── 20250121000200_configure_storage.log
├── 20250121000300_configure_swap.log
└── ...
Each log file contains the timestamp when the change was applied and any output from the commands.
This approach means:
- State lives on the server itself, not in your local repository
- If a server is deleted and recreated, changes will be re-applied automatically
- Log files provide debugging information if something goes wrong
- Make changes idempotent: Use
containsinstead of blindly appending, check if files exist before creating them - Use
on_changefor service restarts: Avoid unnecessary restarts by only restarting when config actually changes - Test with dry-run first: Always preview changes before applying to production
- Target narrowly when appropriate: Use specific hostnames for server-specific configuration (like database backups)
- Keep changes small and focused: One concern per change makes troubleshooting easier
MIT License. See LICENSE.txt.