Python技术迷

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 脚本,后面少背锅。