对于非常小的项目,“手动”部署代码是可行的,就是通过远程 shell 手动输入安装新

版代码所必需的一系列命令,并在远程 shell 中执行。不管怎样,即使是中等大小的项目,

这种方法也是容易出错的、乏味的,并且会浪费你最珍贵的资源——你自己的时间。

解决方案就是自动化。简单的经验法则就是,如果你需要手动执行相同的任务至少两

次,那么你应该将它自动化,这样你就不用再做第三次了。有各种工具可以让你将各种事

情自动化。

● 远程执行工具(例如 Fabric),用来按需求自动执行多台远程主机上的代码。

● 配置管理工具(例如 Chef、Puppet、CFEngine、Salt 和 Ansible),用来对远程主机

(执行环境)进行自动化配置。它们可用于设置后端服务(数据库、缓存等)、系统

权限、用户等等。大多数这种工具还可以当作像 Fabric 一样的远程执行工具,但

根据架构不同,其难易程度也不同。

配置管理解决方案是一个复杂的话题,值得单独用一本书介绍。事实上,最简单的远

程执行框架的门槛最低,它也是最常见的选择,至少对小型项目来说是这样。实际上,每

种提供了声明式指定机器配置方法的配置管理工具都在深层某处实现了远程执行层。

此外,由于一些工具的设计,它可能并不是最适合实际的自动化代码部署。一个这样

的例子是 Puppet,它阻止显式地运行任何 shell 命令。这就是许多人选择使用两种解决方案

来互相补充的原因:配置管理工具用于设置系统级的环境,按需的远程执行工具用于应用

部署。

迄今为止,Fabrichttp://www.fabfile.org/)是 Python 开发者用于自动化远程执行的最

常用的解决方案。它是一个 Python 库,也是一个命令行工具,用来提高使用 SSH 进行应

用部署或系统管理的效率。我们将重点介绍它,因为它相对容易上手。注意,它对于你的

问题可能并不是最佳解决方案,这取决于你的需求。不管怎样,如果你还没有任何自动化

的实用程序,它是能够在运营中实现自动化的很好的实用程序例子。

你当然可以只用 Bash 脚本将所有工作自动化,但这是非常乏味的,而且容易出错。Python

有更方便的字符串处理方法,并鼓励代码模块化。事实上,Fabric 只是利用 SSH 将命令执行连

在一起的工具,所以你仍然需要知道命令行界面及其实用程序在你的环境中的工作方式。

要开始使用 Fabric,你需要安装 fabric 包(使用 pip),并创建一个名为 fabfile.py

的脚本,它通常位于项目根目录中。注意,fabfile 可以被看作是项目配置的一部分。因

此,如果你想严格遵守十二要素应用的方法论,那么你不应该在所部署应用的源代码树中

维护其代码。事实上,复杂的项目通常是从作为独立代码库维护的各种组件中构建的,因

此这也是最好所有项目组件配置和 Fabric 脚本有一个单独的仓库的另一个原因。这使得不

同服务的部署更加一致,也鼓励重复使用优秀的代码。

一个定义了简单部署过程的 fabfile 示例如下所示:

# -*- coding: utf-8 -*-

import os

from fabric.api import * # noqa

from fabric.contrib.files import exists

# 假设我们用'devpi'项目创建了一个私有包仓库

PYPI_URL = 'http://devpi.webxample.example.com'

# 这是用于保存已安装版本的任意位置。

# 每个版本都是单独的虚拟环境目录,以项目版本命名。

# 还有一个指向最近部署版本的符号链接'current'。

# 这个符号链接是用于配置进程管理工具的实际路径。例如:

#.

# ├── 0.0.1

# ├── 0.0.2

# ├── 0.0.3

# ├── 0.1.0

# └── current -> 0.1.0/

REMOTE_PROJECT_LOCATION = "/var/projects/webxample"

env.project_location = REMOTE_PROJECT_LOCATION

# roledefs 映射环境类型(预发布环境/生产环境)

env.roledefs = {

'staging': [

'staging.webxample.example.com',

],

'production': [

'prod1.webxample.example.com',

'prod2.webxample.example.com',

],

}

def prepare_release():

""" 通过创建源代码发行版并将其上传至私有包仓库来准备新版本。

"""

local('python setup.py build sdist upload -r {}'.format(

PYPI_URL

))

def get_version():

""" 从 setuptools 获取当前项目版本。"""

return local(

'python setup.py --version', capture=True

).stdout.strip()

def switch_versions(version):

"""通过原子级地(atomically)替换符号链接来切换版本。"""

new_version_path = os.path.join(REMOTE_PROJECT_LOCATION,

version)

temporary = os.path.join(REMOTE_PROJECT_LOCATION, 'next')

desired = os.path.join(REMOTE_PROJECT_LOCATION, 'current')

# 强制使用符号链接(-f),因为可能已经有一个了

run(

"ln -fsT {target} {symlink}"

"".format(target=new_version_path, symlink=temporary)

)

# mv -T 确保该操作的原子性(atomicity)

run("mv -Tf {source} {destination}"

"".format(source=temporary, destination=desired))

@task

def uptime():

"""

在远程主机上运行 uptime 命令——用于测试连接。

"""

run("uptime")

@task

def deploy():

"""利用打包来部署应用。"""

version = get_version()

pip_path = os.path.join(

REMOTE_PROJECT_LOCATION, version, 'bin', 'pip'

)

prepare_release()

if not exists(REMOTE_PROJECT_LOCATION):

# 在新主机上的初次部署,它可能不存在

run("mkdir -p {}".format(REMOTE_PROJECT_LOCATION))

with cd(REMOTE_PROJECT_LOCATION):

# 使用 venv 创建新的虚拟环境

run('python3 -m venv {}'.format(version))

run("{} install webxample=={} --index-url {}".format(

pip_path, version, PYPI_URL

))

switch_versions(version)

# 假设 Circus 是我们选择的进程管理工具。

run('circusctl restart webxample')

每个用@task 装饰的函数都被看作与 fabric 包一起提供的 fab 实用程序的可用子

命令。你可以使用-l 或--list 开关列出所有可用的子命令,代码如下:

$ fab --list

Available commands:

deploy Deploy application with packaging in mind

uptime Run uptime command on remote host - for testing connection.

现在你只有一个 shell 命令就可以将应用部署到给定的环境类型中:

$ fab –R production deploy

注意,前面的 fabfile 只是为了便于说明。在你自己的代码中,你可能要提供大量

的故障处理,并尽量无需重启 web worker 进程就可以重新加载应用。此外,这里介绍的某

些技术可能现在看起来显而易见,但本章后面将对其详细解释。这些技术包括:

● 使用私有包仓库来部署应用。

● 使用 Circus 在远程主机上进行进程管理。