如何高效地远程部署?自动化运维利器 Fabric 教程

关于 Python 自动化的话题,在上一篇文章中,我介绍了 Invoke 库,它是 Fabric 的最重要组件之一。Fabric 也是一个被广泛应用的自动化工具库,是不得不提的自动化运维利器,所以,本文将来介绍一下它。

Fabric 主要用在应用部署与系统管理等任务的自动化,简单轻量级,提供有丰富的 SSH 扩展接口。在 Fabric 1.x 版本中,它混杂了本地及远程两类功能;但自 Fabric 2.x 版本起,它分离出了独立的 Invoke 库,来处理本地的自动化任务,而 Fabric 则聚焦于远程与网络层面的任务。

为了做到这点,Fabric 主要依赖另一大核心组件 Paramiko,它是基于 SSH 协议的远程控制模块,Fabric 在其基础上封装出了更加友好的接口,可以远程执行 Shell 命令、传输文件、批量操作服务器、身份认证、多种配置与设置代理,等等。

一、Fabric 的版本区分

Python 2 版本已经被官宣在今年元旦“退休”了,未来只会是 Python 3 的舞台。为了适应 Python 版本的非兼容性迁移,很多项目也必须推出自己的新版本(兼容或只支持 Python 3),其中就包括本文的主角 Fabric。

Fabric 自身存在着 2 个大版本:Fabric 1 和 Fabric 2,而在这个库的基础上,还有两个很容易混淆的相关库:Fabric2 和 Fabric3(注意这里的数字是库名的一部分)。

它们的区分如下:

  • Fabric 1.x:支持 Python 2.5-2.7,但不支持 Python 3
  • Fabric 2.x:支持 Python 2.7 与 3.4+,但不兼容 Fabric 1.x 的 fabfile
  • Fabric2:等同于 Fabric 2.x,为了使不同版本共存(装一个 1.x 旧版本,再装它作为新版本)
  • Fabric3:一个基于 Fabric 1.x 的 fork(非官方),兼容 Python 2&3,兼容 Fabric1.x 的 fabfile

综上可见,我们推荐使用官方的 Fabric 2.x 系列版本,但同时要注意,某些过时的教程可能是基于早期版本的(或非官方的 Fabric3,也是基于 Fabric 1.x),需要注意识别。

例如,在 Fabric 1.x 系列中这么写导入:from fabric.api import run;在新版本中将报错:“ImportError: No module named api”(PS:可根据是否有 fabric.api 来判断 Fabric 的版本,就像在 Python 中根据 print 语句或 print 函数来判断版本一样)。同时,由于新版本不支持老版本的 fabfile,在使用时就可能报错:“No idea what 'xxx' is!”

Fabric 2 是非兼容性版本,相比于前个版本,它主要改进的点有:

  • 支持 Python 2.7 与 3.4+
  • 线程安全,取消了多进程的并发实现
  • API 围绕 fabric.connection.Connection 进行了重组
  • 全面修改了命令行解析器,允许在每个任务的基础上使用规则的 GNU/POSIX 风格的标志和选项(不再需要 fab mytask:weird = custom,arg = format)
  • 可以声明前置任务与后置任务
  • ……(官方列了10几条 [1],本文不一一罗列)

之前介绍过的 invoke,就是在开发 Fabric 2 时被分离出来的,具体的原因可参见这个回答 [2]。总而言之,在使用 Fabric 时,应该注意版本差异的问题。

二、Fabric 的基本用法

1、安装

首先是安装:pip intall fabric ,安装后,可在命令行窗口查看版本信息:

>>> fab -V
Fabric 2.5.0
Paramiko 2.7.1
Invoke 1.4.0

执行“fab -V”,以上结果可看出我安装的是 Fabric 2.5.0 版本,同时可看到它的两个核心依赖库 Paramiko 及 Invoke 的版本信息。

2、一个简单的例子

Fabric 主要用于远程任务,即要对远程服务器进行操作,下面是一个简单的例子:

# 可使用任意的文件名
from fabric import Connection

host_ip = '47.xx.xx.xx'  # 服务器地址
user_name = 'root' # 服务器用户名
password = '****'  # 服务器密码
cmd = 'date'  # shell 命令,查询服务器上的时间

con = Connection(host_ip, user_name, connect_kwargs={'password': password})
result = con.run(cmd, hide=True)

print(result)

以上代码,通过账号+密码登录到远程服务器,然后执行date命令,查看服务器的时间,执行结果:

Command exited with status 0.
=== stdout ===
Fri Feb 14 15:33:05 CST 2020

(no stderr)

现在打印的结果中,除了服务器时间,还有一些无关的信息。这是因为它打印的“result”是一个"fabric.runners.Result"类,我们可以把其中的信息解析出来:

print(result.stdout)  # Fri Feb 14 15:33:05 CST 2020
print(result.exited)  # 0
print(result.ok)      # True
print(result.failed)  # False
print(result.command) # date
print(result.connection.host) # 47.xx.xx.xx

上述代码使用了 Connection 类及其 run() 方法,可在连接的服务器上运行 shell 命令。如果需要用管理员权限,则需替换成 sudo() 方法。如果要在本地执行 shell 命令,则需替换成 local() 方法。

除此之外,还有 get()、put() 等方法,详见下文介绍。

3、命令行用法

上例代码可写在任意的 .py 脚本中,然后运行该脚本,或者稍微封装下再导入到其它脚本中使用。

另外,Fabric 还是个命令行工具,可以通过fab命令来执行任务。我们稍微改造一下上例的代码:

# 文件名:fabfile.py
from fabric import Connection
from fabric import task

host_ip = '47.xx.xx.xx'  # 服务器地址
user_name = 'root' # 服务器用户名
password = '****'  # 服务器密码
cmd = 'date'  # shell 命令,查询服务器上的时间

@task
def test(c):
    """
    Get date from remote host.
    """
    con = Connection(host_ip, user_name, connect_kwargs={'password': password})
    result = con.run(cmd, hide=True)
    print(result.stdout)  # 只打印时间

解释一下,主要的改动点有:

  • fabfile.py 文件名:入口代码的脚本名必须用这个名字
  • @task 装饰器:需要从 fabric 中引入这个装饰器,它是对 invoke 的 @task 装饰器的封装,实际用法跟 invoke 一样(注意:它也需要有上下文参数“c”,但实际上它并没有在代码块中使用,而是用了 Connection 类的实例)

然后,在该脚本同级目录的命令行窗口中,可以查看和执行相应的任务:

>>> fab -l
Available tasks:
  test   Get date from remote host.

>>> fab test
Fri Feb 14 16:10:24 CST 2020

fab 是 Invoke 的扩展实现,继承了很多原有功能,所以执行“fab --help”,与之前介绍的“inv --help”相比,你会发现它们的很多参数与解释都是一模一样的。

fab 针对远程服务的场景,添加了几个命令行选项(已标蓝),其中:

  • --prompt-for-login-password:令程序在命令行中输入 SSH 登录密码(上例在代码中指定了 connect_kwargs.password 参数,若用此选项,可要求在执行时再手工输入密码)
  • --prompt-for-passphrase:令程序在命令行中输入 SSH 私钥加密文件的路径
  • -H 或 --hosts:指定要连接的 host 名
  • -i 或 --identity:指定 SSH 连接所用的私钥文件
  • -S 或 --ssh-config:指定运行时要加载的 SSH 配置文件

关于 Fabric 的命令行接口,更多内容可查看文档 [3]。

4、交互式操作

远程服务器上若有交互式提示,要求输入密码或“yes”之类的信息,这就要求 Fabric 能够监听并作出回应。

以下是一个简单示例。引入 invoke 的 Responder,初始化内容是一个正则字符串和回应信息,最后赋值给 watchers 参数:

from invoke import Responder
from fabric import Connection
c = Connection('host')
sudopass = Responder(
     pattern=r'\[sudo\] password:',
     response='mypassword\n')
c.run('sudo whoami', pty=True, watchers=[sudopass])

5、传输文件

本地与服务器间的文件传输是常见用法。Fabric 在这方面做了很好的封装,Connection 类中有以下两个方法可用:

  • get(*args, **kwargs):拉取远端文件到本地文件系统或类文件(file-like)对象
  • put(*args, **kwargs):推送本地文件或类文件对象到远端文件系统

在已建立连接的情况下,示例:

# (略)
con.get('/opt/123.txt', '123.txt')
con.put('test.txt', '/opt/test.txt')

第一个参数指的是要传输的源文件,第二个参数是要传输的目的地,可以指定成文件名或者文件夹(为空或 None 时,使用默认路径):

# (略)
con.get('/opt/123.txt', '')  # 为空时,使用默认路径
con.put('test.txt', '/opt/') # 指定路径 /opt/

get() 方法的默认存储路径是os.getcwd ,而 put() 方法的默认存储路径是 home 目录。

6、服务器批量操作

对于服务器集群的批量操作,最简单的实现方法是用 for 循环,然后逐一建立 connection 和执行操作,类似这样:

for host in ('web1', 'web2', 'mac1'):
    result = Connection(host).run('uname -s')

但有时候,这样的方案会存在问题:

  • 如果存在多组不同的服务器集群,需要执行不同操作,那么需要写很多 for 循环
  • 如果想把每组操作的结果聚合起来(例如字典形式,key-主机,value-结果),还得在 for 循环之外添加额外的操作
  • for 循环是顺序同步执行的,效率太低,而且缺乏异常处理机制(若中间出现异常,会导致跳出后续操作)

对于这些问题,Fabric 提出了 Group 的概念,可将一组主机定义成一个 Group,它的 API 方法跟 Connection 一样,即一个 Group 可简化地视为一个 Connection。

然后,开发者只需要简单地操作这个 Group,最后得到一个结果集即可,减少了自己在异常处理及执行顺序上的工作。

Fabric 提供了一个 fabric.group.Group 基类,并由其派生出两个子类,区别是:

  • SerialGroup(*hosts, **kwargs):按串行方式执行操作
  • ThreadingGroup(*hosts, **kwargs):按并发方式执行操作

Group 的类型决定了主机集群的操作方式,我们只需要做出选择即可。然后,它们的执行结果是一个fabric.group.GroupResult类,它是 dict 的子类,存储了每个主机 connection 及其执行结果的对应关系。

>>> from fabric import SerialGroup
>>> results = SerialGroup('web1', 'web2', 'mac1').run('uname -s')
>>> print(results)
<GroupResult: {
    <Connection 'web1'>: <CommandResult 'uname -s'>,
    <Connection 'web2'>: <CommandResult 'uname -s'>,
    <Connection 'mac1'>: <CommandResult 'uname -s'>,
}>

另外,GroupResult 还提供了 failed 与 succeeded 两个属性,可以取出失败/成功的子集。由此,也可以方便地批量进行二次操作。 原文

三、Fabric 的进阶用法

1、身份认证

Fabric 使用 SSH 协议来建立远程会话,它是一种相对安全的基于应用层的加密传输协议。

基本来说,它有两种级别的安全认证方式:

  • 基于口令的身份认证:使用账号与密码来登录远程主机,安全性较低,容易受到“中间人”攻击
  • 基于密钥的身份认证:使用密钥对方式(公钥放服务端,私钥放客户端),不会受到“中间人”攻击,但登录耗时较长

前文在举例时,我们用了第一种方式,即通过指定 connect_kwargs.password 参数,使用口令来登录。

Fabric 当然也支持采用第二种方式,有三种方法来指定私钥文件的路径,优先级如下:

  • 优先查找 connect_kwargs.key_filename 参数,找到则用作私钥
  • 其次查找命令行用法的 --identify 选项
  • 最后默认使用操作系统的 ssh_config 文件中的IdentityFile 的值

如果私钥文件本身还被加密过,则需要使用 connect_kwargs.passphrase 参数。

2、配置文件

Fabric 支持把一些参数项与业务代码分离,即通过配置文件来管理它们,例如前面提到的密码和私钥文件,可写在配置文件中,避免与代码耦合。

Fabric 基本沿用了 Invoke 的配置文件体系(官方文档中列出了 9 层),同时增加了一些跟 SSH 相关的配置项。支持的文件格式有 .yaml、.yml、.json 与 .py(按此次序排优先级),推荐使用 yaml 格式(后缀可简写成 yml)。

其中,比较常用的配置文件有:

  • 系统级的配置文件:/etc/fabric.yml
  • 用户级的配置文件:~/.fabric.yml(Windows 在 C:\Users\xxx 下)
  • 项目级的配置文件:/myproject/fabric.yml

以上文件的优先级递减,由于我本机是 Windows,为了方便,我在用户目录建一个".fabric.yml"文件,内容如下:

# filename:.fabric.yml

user: root
connect_kwargs:
  password: xxxx
# 若用密钥,则如下
#  key_filename:
#    - your_key_file

我们把用户名和密码抽离出来了,所以 fabfile 中就可以删掉这些内容:

# 文件名:fabfile.py
from fabric import Connection
from fabric import task

host_ip = '47.xx.xx.xx'  # 服务器地址
cmd = 'date'  # shell 命令,查询服务器上的时间

@task
def test(c):
    """
    Get date from remote host.
    """
    con = Connection(host_ip)
    result = con.run(cmd, hide=True)
    print(result.stdout) 

然后,在命令行中执行:

>>> fab test
Tue Feb 18 10:33:38 CST 2020

配置文件中还可以设置很多参数,详细可查看文档 [4]。

3、网络网关

如果远程服务是网络隔离的,无法直接被访问到(处在不同局域网),这时候需要有网关/代理/隧道,这个中间层的机器通常被称为跳板机或堡垒机。

Fabric 中有两种网关解决方案,对应到 OpenSSH 客户端的两种选项:

  • ProxyJump:简单,开销少,可嵌套
  • ProxyCommand:开销大,不可嵌套,更灵活

在创建 Fabric 的 Connection 对象时,可通过指定 gateway 参数来应用这两种方案:

ProxyJump 方式就是在一个 Connection 中嵌套一个 Connection 作为前者的网关,后者使用 SSH 协议的direct-tcpip 为前者打开与实际远程主机的连接,而且后者还可以继续嵌套使用自己的网关。

from fabric import Connection

c = Connection('internalhost', gateway=Connection('gatewayhost'))

ProxyCommand 方式是客户端在本地用 ssh 命令(类似“ssh -W %h:%p gatewayhost”),创建一个子进程,该子进程与服务端进行通信,同时它能读取标准输入和输出。

这部分的实现细节分别在paramiko.channel.Channelparamiko.proxy.ProxyCommand,除了在参数中指定,也可以在 Fabric 支持的配置文件中定义。更多细节,请查阅文档 [5]。

四、小结

Fabric 的非兼容版本造成了一定程度的社区分裂,这无疑跟 Python 3 的推行脱不开关系,但是我们有理由相信,新版本优胜于老版本。

网上关于 Fabric 的文章,很多已过时了。本文针对最新的官方文档,梳理出了较为全面的知识点,可以带大家很好地入门 Fabric。

读完本文,相信读者们只需要几分钟就能轻松上手使用。如若有所疑问,欢迎通过以下方式联系我。

--------------

公众号:Python猫

头条号:Python猫

知乎:豌豆花下猫

掘金:豌豆花下猫

--------------

相关链接:

Invoke教程:https://mp.weixin.qq.com/s/up8lxuRhJQAXRzxkirF3nA

1、http://www.fabfile.org/upgrading.html#upgrading

2、http://www.pyinvoke.org/faq.html#invoke-split-from-fabric

3、http://docs.fabfile.org/en/2.5/cli.html

4、http://docs.fabfile.org/en/2.5/concepts/configuration.html

5、http://docs.fabfile.org/en/2.5/concepts/networking.html

公众号【Python猫】, 本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写作、优质英文推荐与翻译等等,欢迎关注哦。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容