perl.gg / modern-perl

<!-- category: modern-perl -->

Rex: Infrastructure Automation in Perl

2026-03-23

You manage servers. Multiple servers. You SSH into each one, run the same commands, check the same things, fix the same problems. Over and over.

What if you could write that in Perl?

task 'check_disk', group => 'webservers', sub { say run 'df -h'; };
$ rex check_disk
That runs df -h on every server in the webservers group. Over SSH. In parallel. With one command.

This is Rex. Remote Execution. Infrastructure automation written in Perl, configured in Perl, extended in Perl. Think Ansible, but your playbooks are real code, not YAML pretending to be code.

Part 1: WHAT REX IS

Rex is a CPAN module. Install it:
cpanm Rex
You write a Rexfile (like a Makefile, but Perl). Define your servers. Define your tasks. Run them from the command line.
rex <task_name>
That's the whole workflow. No agents on remote servers. No daemon process. Just SSH. Rex connects, runs your commands, reports back.
┌─────────┐ │ Your │ │ machine │ rex check_disk │ (Rexfile)│ └────┬─────┘ │ SSH ┌────┼────────────────┐ │ │ │ v v v ┌────┐ ┌────┐ ┌────┐ │web1│ │web2│ ... │webN│ └────┘ └────┘ └────┘ No agents. No daemons. Just SSH.

Part 2: THE REXFILE

A Rexfile is just Perl with Rex functions imported. Here's a minimal one:
use Rex -base; user 'deploy'; private_key '~/.ssh/id_ed25519'; group webservers => ( '10.0.1.10', '10.0.1.11', '10.0.1.12', ); desc 'Check disk usage on all web servers'; task 'check_disk', group => 'webservers', sub { my $out = run 'df -h'; say $out; }; 1;
Save it as Rexfile in your project directory. Run it:
$ rex check_disk
Rex SSHs into each server in the webservers group and runs df -h. Output comes back to your terminal. Done.

Part 3: SERVER GROUPS

Group your servers by function:
group webservers => ('10.0.1.10', '10.0.1.11'); group databases => ('10.0.2.10', '10.0.2.11'); group monitoring => ('10.0.3.10'); group all_prod => ('10.0.1.10', '10.0.1.11', '10.0.2.10', '10.0.2.11', '10.0.3.10');
Each task targets a group:
task 'check_web', group => 'webservers', sub { ... }; task 'check_db', group => 'databases', sub { ... }; task 'check_all', group => 'all_prod', sub { ... };
Or override on the command line:
$ rex -H 10.0.1.10 check_disk
Run a task on a specific host, regardless of its group.

List all available tasks:

$ rex -T Tasks: check_disk Check disk usage on all web servers check_nginx Check if nginx is running update Apply security updates
The desc line before each task becomes its description in the task list. Document your tasks. Future you will thank present you.

Part 4: RUNNING COMMANDS

The run function is your workhorse. It executes a command on the remote server and returns the output:
my $output = run 'uptime'; say $output;
Check exit status:
run 'systemctl restart nginx', on_success => sub { say "nginx restarted OK"; }, on_error => sub { say "FAILED to restart nginx"; die "Critical failure"; };
The callbacks give you clean error handling. No checking $? manually. No parsing exit codes.

Run with sudo:

my $output = run 'sudo apt-get update -q';
Or set it globally:
sudo TRUE;
Now every run command uses sudo automatically.

Part 5: PACKAGE MANAGEMENT

Rex has built-in functions for managing packages. No need to shell out to apt or yum:
use Rex::Commands::Pkg; task 'install_essentials', group => 'webservers', sub { pkg 'nginx', ensure => 'present'; pkg 'curl', ensure => 'present'; pkg 'htop', ensure => 'present'; };
Rex detects the OS and uses the right package manager. Debian gets apt. Red Hat gets yum or dnf. You write it once.

Update all packages:

task 'update_all', group => 'all_prod', sub { update_package_db; # apt update / yum check-update run 'sudo apt-get upgrade -y'; # or use Rex's update functions };

Part 6: SERVICE MANAGEMENT

Start, stop, restart, check status:
use Rex::Commands::Service; task 'restart_nginx', group => 'webservers', sub { service nginx => 'restart'; say "nginx restarted on " . connection->server; };
Check if a service is running:
task 'check_services', group => 'webservers', sub { my $hostname = run 'hostname'; say "> $hostname"; my $status = run 'systemctl is-active nginx'; if ($status eq 'active') { say " nginx: OK"; } else { say " nginx: $status (PROBLEM)"; } };
Nothing magical here. It's just Perl. if statements, string comparison, say. All the language features you already know, applied to remote servers.
.--. |o_o | "It's just Perl. That's the whole point." |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/

Part 7: FILE OPERATIONS

Push files to remote servers:
use Rex::Commands::Upload; use Rex::Commands::File; task 'deploy_config', group => 'webservers', sub { upload 'files/nginx.conf', '/etc/nginx/nginx.conf'; file '/etc/nginx/nginx.conf', owner => 'root', group => 'root', mode => '644'; service nginx => 'reload'; };
Upload. Set permissions. Reload. Three lines of intention, zero lines of SSH and SCP plumbing.

Template files with embedded Perl:

file '/etc/nginx/conf.d/myapp.conf', content => template('templates/myapp.conf.tpl', server_name => 'app.example.com', upstream => '127.0.0.1:8080', );
Your template uses Rex's template syntax (or plain Perl string interpolation). Config management without learning a template DSL.

Part 8: A REAL REXFILE

Here's a practical Rexfile for managing a small web fleet:
use Rex -base; use Rex::Commands::Pkg; use Rex::Commands::Service; use Rex::Commands::Upload; use Rex::Commands::File; user 'deploy'; private_key '~/.ssh/id_ed25519'; group webservers => ('10.0.1.10', '10.0.1.11', '10.0.1.12'); desc 'Show hostname and uptime'; task 'hello', group => 'webservers', sub { say run 'hostname'; say run 'uptime'; }; desc 'Check disk usage, warn if above 80%'; task 'check_disk', group => 'webservers', sub { my $host = run 'hostname'; my @lines = split /\n/, run 'df -h'; for my $line (@lines) { if ($line =~ m~(\d+)%~ && $1 > 80) { say "WARNING [$host]: $line"; } } }; desc 'Apply security updates'; task 'security_updates', group => 'webservers', sub { my $host = run 'hostname'; say "Updating $host..."; run 'sudo apt-get update -q', on_error => sub { die "apt update failed on $host" }; run 'sudo apt-get upgrade -y -q', on_error => sub { die "upgrade failed on $host" }; say "$host updated OK"; }; desc 'Deploy nginx config and reload'; task 'deploy_nginx', group => 'webservers', sub { upload 'files/nginx.conf', '/etc/nginx/nginx.conf'; file '/etc/nginx/nginx.conf', owner => 'root', group => 'root', mode => '644'; run 'sudo nginx -t', on_error => sub { die "nginx config test FAILED" }; service nginx => 'reload'; say "nginx deployed and reloaded on " . run('hostname'); }; 1;
That's your entire fleet management in one file. Run any task:
$ rex hello $ rex check_disk $ rex security_updates $ rex deploy_nginx

Part 9: WHY NOT ANSIBLE?

Fair question. Ansible is the 800-pound gorilla of config management. Why would you pick Rex?

Your Rexfile is real code. Not YAML with Jinja2 templates and weird indentation rules. Perl. With variables, loops, conditionals, error handling, CPAN modules, and everything else you already know.

No new language to learn. If you know Perl, you know Rex. Ansible requires learning YAML structure, Jinja2 templating, module arguments, playbook organization, roles, galaxy. Rex requires learning about ten functions.

Lightweight. No control server. No inventory database. No tower UI. Just a Rexfile and SSH keys.

Perfect for small fleets. If you have 3 to 50 servers, Rex is exactly the right size tool. Ansible works too, but it's like driving a semi truck to the grocery store.

Ansible: YAML → Jinja2 → Python → SSH → Remote Rex: Perl → SSH → Remote Fewer layers. Fewer surprises.

Part 10: BEYOND BASICS

Rex can do more than run commands. It has modules for:
CATEGORY WHAT IT DOES ---------------- ------------------------------------------ Rex::Commands::Fs File system operations (mkdir, symlink, etc.) Rex::Commands::Cron Manage cron jobs Rex::Commands::User Create/manage users and groups Rex::Commands::Iptables Firewall rules Rex::Commands::Gather System info (OS, memory, CPU) Rex::Commands::Cloud AWS, OpenStack integration
Gather system info:
use Rex::Commands::Gather; task 'inventory', group => 'all_prod', sub { my $os = operating_system(); my $ver = operating_system_version(); my $mem = memory(); my $host = run 'hostname'; say "$host: $os $ver, ${\ int($mem->{total}/1024) }MB RAM"; };
Run rex inventory and get a fleet-wide hardware report. No agents, no monitoring tool, no dashboard. Just Perl asking questions over SSH.

Part 11: TESTING LOCALLY

Rex can run tasks on localhost too:
task 'local_check', sub { LOCAL { say run 'df -h /'; }; };
The LOCAL block runs commands on your machine, not remote. Great for testing task logic before deploying to the fleet.

Or use the -H localhost flag:

$ rex -H localhost check_disk

Part 12: GETTING STARTED

Install Rex:
cpanm Rex
Create a Rexfile in your project directory. Start with one server, one task:
use Rex -base; user 'youruser'; private_key '~/.ssh/id_ed25519'; group myserver => ('10.0.0.50'); desc 'Hello from Rex'; task 'hello', group => 'myserver', sub { say run 'hostname'; say run 'uptime'; }; 1;
Run it:
$ rex hello
If that works, you have infrastructure automation. Add more servers, add more tasks, and grow from there. Your Rexfile is version controlled, peer reviewable, and testable. It's just Perl.

The Rex documentation lives at https://www.rexify.org/. The API reference covers every built-in command. The cookbook has patterns for common tasks.

But honestly, the best way to learn Rex is to take that thing you SSH into three servers to do every Tuesday morning and put it in a Rexfile. You'll never go back.

Rex ┌───────────┐ │ │ │ Rexfile │ "It's just Perl." │ │ └─────┬──────┘ │ ┌───────┼───────┐ │ │ │ v v v [web1] [web2] [web3] Infrastructure as code. Real code.
perl.gg