自动化部署工具Fabric简介


    Fabric就是一个帮助我们在上线时减少重复/繁琐操作的自动化部署利器,对于缺乏成熟运维平台的众多小公司的运维或开发人员来说,掌握这个工具是有必要的。

1. Fabric是什么

Fabric官方文档的描述如下: 
     Fabric is a Python (2.5-2.7) library and command-line tool for streamlining the use of SSH for application deployment or systems administration tasks. 
具体来说,Fabric是一个Python库,只要目标机器支持ssh访问,就可以借助fabric来进行远程操作(如在host1上对host2远程运行shell命令),显然,由于fabric是个Python package,故其它Python package都可以被import到fabric特有的fabfile.py脚本中,这使得fabric如虎添翼,在功能的丰富程度和运维脚本的可维护性上,远远超过用shell实现的自动化部署脚本,更不要说与纯手工敲命令的上线方式相比所体现出的巨大优势了。

在系统运维和部署自动化领域,与fabric类似的工具还有很多(如Puppet, Chef),感兴趣的话,可以参考这篇文章48 Best Cloud Tools for Infrastructure Automation的介绍。

Fabric的安装非常方便,pip install fabric就可以搞定,这里不赘述。

2. Fabric支持的操作

Fabric支持的常用命令列出如下: 
1)local Run a command on the local system. 
它是对subprocess模块的封装(Shell=True)可以通过设置Capture = True/False来捕获其执行结果。 
2)run  Run a shell command on a remote host. 
该命令的返回值包含了远程命令是否执行成功以及远程命令的返回码等信息。通过run执行命令时,通常会要求输入目标机器密码,如果对多台机器进行部署,可以通过设置env.passwords来避免手动输入密码,具体的设置方法会在下篇笔记中介绍。 
3)get  Download one or more files from a remote host. 
4)put  Upload one or more files to a remote host. 
5)sudo  Run a shell command on a remote host, with superuser privileges. 
功能与run操作类似,它可以对当前用户临时提权来执行某些需要root权限的命令。

此外,还有些不常用的命令(如:prompt, reboot, open_shell, require)这里没有列出,感兴趣的话,可以参考Fabric Operations文档

需要特别注意的是,fabric通过run或sudo执行远程任务时,每次都会新建ssh连接,也即任务之间是不会耦合状态的,所以在实现需要多步操作的任务时,需要把多个命令放入同一行,命令间用逗号隔开。实例说明如下: 
假设要在远程机器上cd至/home/work/tmp目录后创建test目录,则下面的命令无法实现预期目的:

run('cd /home/work/tmp')
run('mkdir test')  ## 第2次run会重新创建ssh连接,且不会记忆上次cd到的目录!!12

需要用下面的命令来实现:

run('cd /home/work/tmp; mkdir test')1
run('cd /home/work/tmp && mkdir test')1


当然,还可以借助fabric提供的context manager来实现:

with cd('/home/work/tmp'):
    run('mkdir test')12


上面介绍了fabric支持的元操作,那么如何基于这些操作实现复杂功能呢? 
在fabric中,一组具有逻辑关系的操作通常被封装成一个task,fabric以task为粒度来执行命令,下面开始介绍如何定义task。

3. 在fabfile中定义tasks

3.1 fabfile是什么

 根据fabric的约定,当运行例如”fab deploy”这样的命令时,fab会默认搜索名为fabfile.py的python文件或名为fabfile的package,故基于fabric的部署脚本通常以fabfile.py命名且应该位于当期工作目录下以便于fab进行搜索,在该文件中实现我们想要的任务即可。当然,如果要实现的部署任务比较复杂,这些任务也可以写在多个脚本中,统一置于fabric package下。关于fabfile的细节,可以参考官方文档Fabfile construction and use。

3.2 定义task

在语法约定上,fabric有两种定义task的方式: 
1)经典方式(classic method) 
所有定义在fabfile中的可调用对象(如函数、类)均可被当作task被fab执行,这种方式不支持嵌套,也即:若fabfile.py中import了其它模块,则即使这些模块中定义了可调用对象,这些不是直接定义在fabfile中的可调用对象也不会被当作fab task。

以classic方式定义的task示例下(摘自Fabric Overview and Tutorial):

from fabric.api import localdef prepare_deploy():
    local("./manage.py test my_app")
    local("git add -p && git commit")
    local("git push")

上述示例代码在fabfile.py中定义了一个普通函数prepare_deploy,不难看出,其功能是在本地执行代码测试后,将本地的最新codebase更新到版本管理系统中以便后续以该codebase进行部署。

2)基于Task类的新风格task 
从fabric 1.1开始,这种new-style的task定义方式被引入。该方式约定,所有的fab任务必须定义成Task类的实例或子类,其最大的优点是支持嵌套namespaces,也即,task可以定义在其它文件,fabfile.py通过import引入该文件后,定义在该文件的task也是可以被fab识别并支持的。

在new-style方式定义task的具体实现上,由2种方法:a. 定义一个继承自Task的子类并为其实现run()方法; b. 借助@task装饰器。示例分别如下:

class MyTask(Task):
    name = "deploy"  ## 指定task name,会在fab --list输出中显示
    def run(self, environment, domain="whatever.com"):
        run("git clone foo")
        sudo("service apache2 restart")

instance = MyTask()

上述示例与借助@task定义task的方式等价:

@taskdef deploy(environment, domain="whatever.com"):
    run("git clone foo")
    sudo("service apache2 restart")

被@task装饰的函数默认继承自Task类,我们可以让函数继承自定义的类,具体的用法可以参考文档Defining tasks的”Using custom subclasses with @task”部分,这里只是抛砖引玉,不再赘述。

需要特别注意的:

两种task的定义方式是互斥的!具体而言,如果fabric在fabfile或它import的文件中发现了基于Task类的new-style定义,那么,所有以classic方式定义的task(s)均会被fabric忽略。个人认为,如果要用fabric实现复杂系统的自动化部署,最好以new-style定义任务,因为这种方式支持嵌套namespace,可以用不同的脚本文件分层组织不同的任务,更方便维护。

备注:可以运行”fab –list”来查看fabric可以识别的任务。

完成task定义后,fabric是如何执行的?尤其是远程部署多台机器时,如何更好地管理这些机器(如角色、密码等)?

这些问题会在下篇笔记中进行说明。

参考资料

[1] 48 Best Cloud Tools for Infrastructure Automation 
[2] Deployment Management Tools: Chef vs. Puppet vs. Ansible vs. SaltStack vs. Fabric 
[3] Fabric Doc: Overview and Tutorial 
[4] Fabric Doc: Operations 
[5] Fabric Doc: Context Managers 
[6] Fabric Doc: Defining tasks






1. Fabric的任务运行规则

根据Fabric Execution model的说明,fabric默认以串行方式运行tasks,具体而言: 
1)在fabfile及其import文件中定义的task对象依次被创建(只是创建对象,并未真正执行),任务之间保持其定义的先后顺序。 
2)对于每个task,生成将要运行该task的目标机器列表。 
3)fab执行tasks时,按任务被指定的顺序依次执行这些任务;针对每个任务,依次在其指定的目标机器运行且只运行一次。 
4)未指定目标机器的task被当作本地任务运行,且只会被运行一次。

假设在fabfile.py中定义了如下tasks:

from fabric.api import run, env

env.hosts = ['host1', 'host2']
def taskA():
    run('ls')
def taskB():
    run('whoami')

在终端运行fab –list时,我们会看到taskA和taskB两个任务,运行之:

$ fab taskA taskB1

结果示例如下:

taskA executed on host1taskA executed on host2
taskB executed on host1taskB executed on host2

通过上面的实例,大家应该可以明白fabric默认的串行执行策略是怎么回事。

Fabric还允许我们指定以并行方式(借助multiprocessing模块实现多个进程并行执行)在多台机器上并行地运行任务,甚至还可在同一个fabfile文件中指定某些task以并行方式运行,而某些task以默认的串行方式运行。具体地,可以借助@parallel或@serial指定任务的运行模式,还可以在命令行中通过-P参数指定任务是否要并性执行。示例如下:

from fabric.api import *
@parallel
def runs_in_parallel():
    pass
def runs_serially():
    pass

当运行如下命令时:

fab -H host1,host2,host3 runs_in_parallel runs_serially1

执行结果示例如下:

runs_in_parallel on host1, host2, and host3
runs_serially on host1
runs_serially on host2
runs_serially on host3

此外,还可以通过对@parallel传入pool_size参数来控制并行进程数以防并行进程太多把机器拖垮。

2. 为task指定目标机器

有多种方式可以指定任务的将要运行的目标机器,下面分别进行说明。 
1)通过env.hosts或env.roles进行全局指定 
Fabric的env模块中定义了一系列全局变量,可以将其理解为可以控制fabric行为的环境变量。其中env.hosts和env.roles可以用来全局指定task的目标机器列表,这两个“环境变量”的默认值都是空列表[]。

env.hosts的元素是fabric约定的”host strings”,每个host strings由username@hostname:port三部分构成,其中username和port部分可以缺省。本篇笔记前面的第1个代码实例其实已经说明了如何用env.hosts全局地指定task的目标机器列表,这里不再赘述。

env.roles则是在配置了env.roledefs的情况下才有用武之地。在很多时候,不同的机器有着不同的角色,如有些是接入层,有些是业务层,有些是数据存储层。env.roledefs可以用来组织这些机器列表以体现其角色,示例如下:

from fabric.api import env

env.roledefs = {    'web': {        
                 'hosts': ['www1', 'www2', 'www3'],
    },           'db': {        
                 'hosts': ['db1', 'db2'],
    }
}
@roles('web')
def mytask():
    run('ls /var/www')

上例通过env.roledefs配置了两个角色web和db,分别包含3台、2台机器,并借助@roles为mytask指定了目标机器列表。

2)通过命令行进行全局指定

$ fab -H host1,host2 mytask1

需要注意的是,命令行通过-H参数指定的机器列表在fabfile脚本load前被解释,故如果fabfile中重新配置了env.hosts或env.roles,则命令行指定的机器列表会被覆盖。为了避免fabfile覆盖命令行参数,在fabfile中应该借助list.extend()指定env.hosts或env.roles,示例如下:

from fabric.api import env, run

env.hosts.extend(['host3', 'host4'])
def mytask():
    run('ls /var/www')123456

此时,当我们运行”fab -H host1,host2 mytask”时,env.hosts包含来自命令行和fabfile的4台机器。

3)通过命令行为每个任务指定机器列表

$ fab mytask:hosts="host1;host2"1

上述方式会覆盖全局指定的机器列表,确保mytask只会在host1, host2上执行。

4)借助装饰器@hosts为每个任务指定目标机器

from fabric.api import env, run

env.hosts.extend(['host3', 'host4'])
def mytask():
    run('ls /var/www')
    
    
   ##or#
my_hosts = ('host1', 'host2')
@hosts(my_hosts)
def mytask():
    # ...


每个任务的@hosts装饰器指定的机器列表会覆盖全局目标机器列表,但不会覆盖通过命令行为该任务单独指定的目标机器列表

上述4种为task指定目标机器列表的方式之间的优先级规则总结如下: 
1) Per-task, command-line host lists (fab mytask:host=host1) override absolutely everything else. 
2) Per-task, decorator-specified host lists (@hosts(‘host1’)) override the env variables. 
3) Globally specified host lists set in the fabfile (env.hosts = [‘host1’]) can override such lists set on the command-line, but only if you’re not careful (or want them to.) 
4) Globally specified host lists set on the command-line (–hosts=host1) will initialize the env variables, but that’s it.

截止目前,我们可以看到,fabric允许我们混合使用上面列出的几种目标机器指定方式,但是我们要明白混合的结果是否符合预期。

此外,fabric默认会对通过不同来源出现多次的同一个目标机器做去重,当然,可以通过设置env.dedupe_hosts为False来关闭默认的去重策略。甚至还可以指定任务需要跳过的机器列表。具体细节可以参考Fabric Execution model的说明,这里不赘述。

3. 任务执行时,目标机器的密码管理

如果你亲自运行上面的示例代码,就会发现,每次在目标机器远程执行task时,fabric均会要求输入目标机器的登录名及密码。如果要在多台机器上执行task,那这些密码输入的过程可以自动化吗?

答案是肯定的。实现方式有两种,下面分别进行说明。

1)通过env.password或env.passwords配置目标机器的登录信息 
下面的示例说明了如何通过env.passwords配置多台机器的登录信息:

#!/bin/env python
#-*- encoding: utf-8 -*-
from fabric.api import run, env, hosts
## 需要注意的是,这里的host strings必须由username@host:port三部分构成,缺一不可,否则运行时还是会要求输入密码
env.passwords = {    'slvher@10.123.11.209:22': 'xxx',    
              'work@10.123.11.210:23': 'yyy',
}

@hosts('10.123.11.209', '10.123.11.210')
def host_os_type():
    run('uname -a')



        但是,这种明文指定登录名/密码的方式存在安全性问题,所以,fabric还支持以ssh key认证的方式免密在远程机器执行任务

在具体实现上,需要事先在目标机器上生成ssh public key并配置在~/.ssh/config文件中,然后在定义任务的fabfile中将env.use_ssh_config设置为True来启用基于ssh public key方式的身份认证,以便实现免密码远程执行任务。