轻量化运维之一 Fabric

其实在Glow的技术团队中是没有全职的Ops或是Sys admin,我想很多小的创业团队也是如此。但是运维是整个产品发布过程中必不可少的一环,所以想写一个针对开发工程师(特别是Python工程师)的运维入门教程。如果你是专业的运维工程师,请不要浪费你自己的时间。

先说一下,什么是“轻量化运维”?简而言之,就是用最快最省事的方法做好最基本的运维工作。这里的工作主要包括以下几个部分

  1. 生产环境服务器集群的日常管理
  2. 自动化系统配置与代码发布
  3. 系统状态与性能监控

今天先来聊聊服务器的日常管理。这里包括各种琐碎的任务,例如重启服务,安装或是更新软件包,备份日志文件或是数据库等等。通常对于这类任务,我们要解决两个问题

  • 针对每项任务编写脚本,避免重复劳动。虽然许多运维都是Shell脚本达人,但大部分程序员看到Shell脚本都是一个脑袋两个大。
  • 需要在许多台机器上执行同一项任务。

如果你碰巧对Python略知一二,那么恭喜你,从此以后你只要使用Fabric这个神器,就再不用为Shell脚本头痛了。

Fabric初探 - 执行单条指令

Fabric可以通过pip来安装

pip install fabric

它的功能是将一个任务通过ssh在多台服务器上执行,而每个任务可以是单条shell指令或是一段python脚本。一个最基本的例子

fab -H web0,web1 -- sudo apt-get update

在web0和web1两台机器上以root来运行apt-get update指令。当然前提是,当前用户可以通过本机ssh到web0和web1两台机器。如果你嫌一台一台服务器按序执行太慢,你可以加上-P参数,让所有服务器同时执行。

用Role对服务器分组

在之前的例子中,我们已经知道Fabric可以在多台服务器上执行任务。但当服务器数量较多时,手动指定一组服务器就比较麻烦了。Fabric提供了Role的概念,你可以将一组功能相同的服务器定义为一个Role,Role的定义需要写在名为fabfile.py文件里,fabric在执行时会默认读取当前目录下的fabfile.py文件,例如:

from fabric.api import env

env.roledefs = {
    'web': ['web%d.example.com' % i for i in xrange(10)],
    'db': ['db%d.example.com' % i for i in xrange(4)]
}

这里我们定义了webdb两个Role,其中web包括了web0.example.com到web9.example.com共10台服务器,db包括了4台服务器。例如,我们需要在所有的web服务器上安装nginx,在所有的db服务器上安装mysql,则只需下面两行命令

fab -P -R web -- sudo apt-get -y install nginx
fab -P -R db -- sudo apt-get -y install mysql-server

这里用-P也使得安装过程在这些服务器上并行执行。

Fabric的核心 - 执行任务

单条指令能做的事情非常有限,Fabric中可以定义任务。通常一个任务对应于fabfile.py中的一个函数方法,配合Fabric自身提供的API,可以对远程的服务器执行一组指令。既可以保留Shell指令简小精悍的优点,又不失Python语言出色的可读性。我们来看一个代码发布的例子。

在这个例子中,我们假设目标服务器上已经有代码的git repo,并且我们已经为将要部署的代码生成了tag并push到了remote。整个代码发布的流程如下,

  1. 显示将要部署的版本与线上版本的code diff,让发布人员做最后的确认。
  2. 清理git repo中所有的untracked文件,这其中可能包括一些编译后的代码(如 *.pyc),或是其他临时生成的文件。保证当前的git repo处在一个干净的状态
  3. 将代码切换到tag对应的版本
  4. 重新启动Webserver,这里我们用supervisor来管理webserver进程。
  5. 向网站发送一个http请求,确保网站在代码更新后处于正常状态。
import requests
from fabric.api import task, sudo, prompt

@task
def deploy(tag):
    """
    Deploy new code version and reload the webserver
    Version: 1.0
    """
    with cd('/repos/example'):
        sudo('git diff --stat HEAD..{}'.format(tag))
        if not prompt('Does the code diff look good to you? [y/N]').lower() == 'y':
            print 'Abort'
            return
        sudo('git clean -xdf')
        sudo('git fetch --all')
        sudo('git checkout {}'.format(tag))
    sudo('supervisorctl restart webserver')
    resp = requests.get('http://www.example.com/ping')
    print 'Web server health check: {}'.format('OK' if resp.status_code == 200 else 'FAILED')

用下面的命令来执行该任务

fab -H web0.example.com deploy:v0.1.0

以上的代码看上去还不错,但如果你需要在多台服务器上部署代码的话,则会碰到一些问题。比如code diff和http request都会被执行多次,而实际上它们只需要被执行一次。在下面的版本中我们用到了Fabric的execute方法,它可以在一个任务中调用另一个任务,并指定在哪些服务器上执行这个子任务。以下是改进后的版本,

import requests
import sys
from fabric.api import task, sudo, prompt

@task
def deploy(tag):
    """
    Deploy new code version and reload the webserver
    Version: 2.0
    """
    execute(check_code_diff, hosts=['web0.example.com'])
    execute(update_code, roles=['www'])
    execute(restart_webserver, roles=['www'])
    resp = requests.get('http://www.example.com/ping')
    print 'Web server health check: {}'.format('OK' if resp.status_code == 200 else 'FAILED')

@task
def check_code_diff(tag):
    with cd('/repos/example'):
        sudo('git diff --stat HEAD..{}'.format(tag))
        if not prompt('Does the code diff look good to you? [y/N]').lower() == 'y':
            sys.exit(1)

@task
@parallel
def update_code(tag):
    sudo('git clean -xdf')
    sudo('git fetch --all')
    sudo('git checkout {}'.format(tag) 

@task
@parallel(pool_size=4)
def restart_webserver():
    sudo('supervisorctl restart webserver')

执行该任务时不需要指定服务器,因为已经在任务代码中指定了。、

fab deploy:v0.1.0

这个版本解决了上面提到的两个问题。另外值得注意的是update_code可以在所有服务器上并行执行,但我们不能对所有的服务器同时重启webserver,这样做会使得整个网站在短时间内无法响应任何请求。在这个例子中我们一共有10台服务器,将@parallel(pool_size=4)保证同一时间最多只有4台服务器在重启webserver进程,确保了发布过程中web服务的可用性。

小结

Fabric是将Python, Shell和SSH的功能很优雅地结合在了一起,同时自身又非常的轻量,适合大部分服务器群的日常管理工作。下次和大家分享一个更重量级的武器Ansible,它的强项在于多环境下服务器的自动化配置,用同一套代码管理development, sandbox和production环境。