Remote Automation in Python with Paramiko and Fabric
When Ansible is too heavy and a bash for-loop over SSH is too fragile, Paramiko and Fabric hit the sweet spot. Here's how to drive remote hosts from Python safely.
- #bash
- #python
- #ssh
- #paramiko
- #fabric
- #automation
There’s a gap in the automation toolbox between “loop over hostnames calling ssh in bash” and “stand up a full Ansible control node with inventory and playbooks.” A lot of real ops work lives in that gap: run a diagnostic across 20 boxes, pull a config file off each, restart a service in a controlled order. For that middle ground, Python’s Paramiko and Fabric are exactly the right size.
I reach for them when I need real logic — conditionals, error handling, parsing command output and acting on it — that gets ugly fast in a bash SSH loop. Here’s how to use them without creating a new class of footguns.
Paramiko vs Fabric: which one
Paramiko is the low-level SSH library: it speaks the protocol, manages keys, opens channels, runs commands, transfers files. Fabric is a friendlier layer built on top of it — connection objects, command runners, and helpers for running the same task across many hosts.
Rule of thumb: use Fabric unless you need the low-level control Paramiko gives you. Fabric’s ergonomics are better for everyday “run this command over there” work. Drop to Paramiko when you need fine-grained channel handling, custom auth flows, or SFTP behavior Fabric doesn’t expose cleanly.
A first Fabric connection
from fabric import Connection
conn = Connection(
host="web01.internal",
user="deploy",
connect_kwargs={"key_filename": "/home/deploy/.ssh/id_ed25519"},
)
result = conn.run("uptime", hide=True, warn=True)
print(result.stdout.strip())
print("exit code:", result.exited)
Two flags you’ll use constantly. hide=True suppresses the live echo so you can capture output yourself. warn=True turns a non-zero exit code into a result you inspect rather than an exception that aborts the run — critical when you’re surveying a fleet and some hosts are expected to differ.
Running across a fleet, in order
The whole reason to script this is to act on many hosts. Fabric has a SerialGroup and a ThreadingGroup:
from fabric import SerialGroup, ThreadingGroup
hosts = ["web01.internal", "web02.internal", "web03.internal"]
# Parallel survey — read-only, order doesn't matter
pool = ThreadingGroup(*hosts, user="deploy")
for conn, result in pool.run("systemctl is-active nginx", hide=True, warn=True).items():
print(f"{conn.host}: {result.stdout.strip()}")
For a survey — gathering state, no changes — parallel is fine and fast. For a change — restarting services, deploying code — go serial and add your own gate between hosts:
group = SerialGroup(*hosts, user="deploy")
for conn in group:
print(f"--- {conn.host} ---")
health = conn.run("curl -fsS localhost:8080/healthz", warn=True, hide=True)
if not health.ok:
print(f" unhealthy before change, skipping {conn.host}")
continue
conn.run("sudo systemctl restart myapp")
# verify it came back before moving on
after = conn.run("curl -fsS localhost:8080/healthz", warn=True, hide=True)
if not after.ok:
raise SystemExit(f"{conn.host} failed health check after restart — STOPPING")
That pattern — check, change, verify, and stop the rollout on the first failure — is the difference between a controlled deploy and taking down your whole fleet in parallel because one host had a bad config.
File transfer with Paramiko SFTP
Pulling files back is where Paramiko shines. Say you want each host’s effective nginx config:
import paramiko
def fetch_file(host, remote_path, local_path):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.RejectPolicy())
client.load_system_host_keys()
client.connect(host, username="deploy", key_filename="/home/deploy/.ssh/id_ed25519")
try:
sftp = client.open_sftp()
sftp.get(remote_path, local_path)
finally:
client.close()
fetch_file("web01.internal", "/etc/nginx/nginx.conf", "./web01-nginx.conf")
Note RejectPolicy() plus load_system_host_keys(). The examples you’ll find online use AutoAddPolicy(), which silently trusts any host key it sees — that defeats the entire point of host-key verification and opens you to man-in-the-middle attacks on your own network. Use RejectPolicy and pre-seed your known_hosts. If a key changes, you want the connection to fail loudly.
The footguns
- Never embed passwords or run with password auth in scripts. Use SSH keys, ideally with an agent. If you must use a password, pull it from a secret manager at runtime, never a literal in the file.
- Always set timeouts.
conn.run(..., timeout=30)and a connect timeout. A hung host should fail, not freeze your whole fleet loop. Without timeouts, one wedged box stalls the entire run. - Don’t parallelize changes by default. Parallel reads are fine. Parallel writes mean a bug hits every host simultaneously with no chance to catch it after host one.
- Capture and check exit codes.
warn=Trueplus an explicit.okcheck beats hoping nothing failed. Silent non-zero exits are how “the script ran fine” turns into a 2am page. - Mind sudo. If commands need root, configure passwordless sudo for the specific commands on the remote side, or use Fabric’s
sudo()with a securely supplied password — never hardcode it.
When to graduate to Ansible
Paramiko and Fabric are perfect for imperative, one-shot, or logic-heavy tasks. The moment you find yourself reimplementing idempotency — “is this package already installed? is this line already in the file?” — you’ve outgrown them. That’s Ansible’s whole job. Use the right tool: scripts for procedures, configuration management for desired state.
For more on writing automation that’s safe to re-run and easy to reason about, see the other Bash & Python automation guides, or start from a ready-made prompt.
Remote-execution scripts run real commands on real hosts. Test against a staging fleet, and gate any state-changing run behind health checks before trusting it in production.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.