fabric,一个神奇的 Python 库!
凌晨一点,三台机器,只改一个配置。
手上已经敲了十几遍:
ssh app01
scp xxx app01:/tmp
systemctl restart xxx
tail -f xxx.log
敲到第三台的时候,我第一反应不是累,是不太信自己。人肉操作这种东西,越简单越容易翻车,尤其是发布、重启、改 Nginx 配置这种活。
Fabric 就是专门收拾这类烂摊子的。
它不是那种很重的自动化平台,也不是 Ansible 那种一套完整体系。Fabric 更像一个“带 SSH 能力的 Python 脚本”。官方对它的定位也很直接:高层 Python SSH 命令执行库,底层基于 Invoke 和 Paramiko 做命令执行和 SSH 连接。安装也没花头,一行
pip install fabric
就能用。
我一般不用它写特别宏大的运维平台。那种东西最后八成会变成另一个没人敢动的系统。
Fabric 最舒服的地方,是把这些零碎动作收进一个 Python 文件里:传包、备份、切软链、重启、验活、失败回滚。脚本不长,但比人手敲稳多了。
比如一个很常见的发布脚本,我会这么写:
from pathlib import Path
from fabric import Connection, task
APP = "order-api"
BASE_DIR = "/opt/order-api"
PKG_DIR = Path("release")
SERVERS = [
"[email protected]",
"[email protected]",
"[email protected]",
]
def open_conn(host):
return Connection(
host=host,
connect_timeout=8,
connect_kwargs={
"key_filename": str(Path.home() / ".ssh" / "id_ed25519")
}
)
def run_checked(conn, cmd):
ret = conn.run(cmd, hide=True, warn=True)
if ret.failed:
raise RuntimeError(f"{conn.host} 执行失败: {cmd}\n{ret.stderr}")
return ret.stdout.strip()
def deploy_one(host, version):
pkg_name = f"{APP}-{version}.tar.gz"
local_pkg = PKG_DIR / pkg_name
remote_pkg = f"/tmp/{pkg_name}"
release_dir = f"{BASE_DIR}/releases/{version}"
if not local_pkg.exists():
raise FileNotFoundError(f"本地包不存在: {local_pkg}")
conn = open_conn(host)
print(f"[{host}] 上传包")
conn.put(str(local_pkg), remote_pkg)
print(f"[{host}] 解压")
run_checked(conn, f"mkdir -p {release_dir}")
run_checked(conn, f"tar -xzf {remote_pkg} -C {release_dir}")
print(f"[{host}] 切软链")
run_checked(conn, f"ln -sfn {release_dir} {BASE_DIR}/current")
print(f"[{host}] 重启服务")
conn.sudo(f"systemctl restart {APP}", hide=True)
print(f"[{host}] 验活")
health = conn.run(
"curl -fsS http://127.0.0.1:8080/health",
hide=True,
warn=True
)
if health.failed:
print(f"[{host}] 验活失败,准备回滚")
conn.sudo(f"systemctl restart {APP}", hide=True)
raise RuntimeError(f"{host} 发布后健康检查失败")
print(f"[{host}] 发布完成: {health.stdout.strip()}")
@task
def deploy(ctx, version):
for host in SERVERS:
deploy_one(host, version)
执行的时候:
fab deploy --version=2026.06.29
这段代码没什么炫技的地方,但我喜欢这种脚本。
它有几个点比较实在。
第一个,命令都能看见。
有些自动化工具把动作藏得太深,失败了只给你一个红色结果。Fabric 不一样,本质还是 Python 调 SSH。你想打印什么就打印什么,想在哪一步停就在哪一步停。
第二个,异常能自己管。
我最烦发布脚本里一堆:
cmd1 && cmd2 && cmd3
看着简洁,真挂了还得倒回去猜是哪一步。Fabric 里拿到的是命令结果对象,
stdout
、
stderr
、退出码都在,想判断就判断。
比如清日志这种动作,我不会直接删:
def clean_remote_logs(conn):
size = run_checked(
conn,
"du -sm /var/log/order-api 2>/dev/null | awk '{print $1}'"
)
if int(size or 0) < 500:
print(f"[{conn.host}] 日志不到 500MB,不清")
return
conn.run(
"find /var/log/order-api -name '*.log.*' -mtime +7 -delete",
hide=True,
warn=True
)
这地方我一般会加阈值。不是因为技术多复杂,是因为线上机器上“顺手删一下”这四个字,出过太多事。
第三个,Fabric 适合做那种半自动化的脏活。
比如批量查服务状态:
from fabric import Connection
hosts = ["app01.internal", "app02.internal", "app03.internal"]
for host in hosts:
conn = Connection(f"deploy@{host}")
ret = conn.run(
"systemctl is-active order-api && ss -lntp | grep 8080",
hide=True,
warn=True
)
if ret.failed:
print(f"[{host}] 异常")
print(ret.stderr.strip())
continue
print(f"[{host}] 正常")
print(ret.stdout.strip())
这种脚本放在平时不显眼。
但真到了线上接口抖动、某台机器 CPU 飙高、负载均衡摘不干净的时候,它比你开三个终端挨个 ssh 快太多。
我还有个习惯,Fabric 脚本里尽量别写太多“智能逻辑”。
比如自动发现机器、自动判断环境、自动拼发布路径,看着高级,后面维护的人会骂。发布脚本最怕聪明过头。机器列表写清楚,路径写清楚,失败就停,日志打出来,这就够了。
Fabric 也不是没坑。
sudo
密码、SSH key、跳板机、远程环境变量,这些地方都可能卡一下。尤其是远程执行命令时,不要默认它和你登录 shell 看到的环境完全一样。很多人本地手敲没问题,Fabric 一跑就报:
python: command not found
或者:
java: No such file or directory
这种我一般不猜,直接在脚本里打出来:
conn.run("whoami; pwd; echo $PATH; which java || true")
先看现场,再改脚本。不要上来就怀疑 Fabric,这库没那么玄,大部分时候是远程 shell 环境和你想的不一样。
Fabric 神奇的地方不在于它能替你做多少事,而是它允许你用 Python 把 SSH 这件事管起来。
一台机器,手敲可以。
三台机器,勉强可以。
十几台机器,还靠手敲,那就不是勤快,是给事故留入口了。
我现在看到重复第三遍的 SSH 命令,基本就会停一下。别急着敲了,写个 Fabric 脚本,后面少背锅。