Pages

Saturday, 24 November 2012

使用Fabric部署网站应用

以前一直用rsync同步代码到服务器,这种山寨方法用一次两次还可,每天部署10次就麻烦了,最近抽空研究了一下Fabric,发现这个东西部署起来简直太爽了。
Fabric是一个用Python开发的部署工具,最大特点是不用登录远程服务器,在本地运行远程命令,几行Python脚本就可以轻松部署。
花10分钟写了一个部署脚本fabfile.py(名字不能变),放到工程目录下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from datetime import datetime
from fabric.api import *

# 登录用户和主机名:
env.user = 'root'
env.hosts = ['www.example.com'] # 如果有多个主机,fabric会自动依次部署

# 定义一个pack任务:
def pack():
    # 打一个tar包:
    tar_files = ['*.py', 'static/*', 'templates/*', 'favicon.ico']
    local('rm -f example.tar.gz')
    local('tar -czvf example.tar.gz --exclude=\'*.tar.gz\' --exclude=\'fabfile.py\' %s' % ' '.join(tar_files))

# 定义一个部署任务:
def deploy():
    # 远程服务器的临时文件:
    remote_tmp_tar = '/tmp/example.tar.gz'
    tag = datetime.now().strftime('%y.%m.%d_%H.%M.%S')
    run('rm -f %s' % remote_tmp_tar)
    # 上传tar文件至远程服务器:
    put('shici.tar.gz', remote_tmp_tar)
    # 解压:
    remote_dist_dir = '/srv/www.example.com@%s' % tag
    remote_dist_link = '/srv/www.example.com'
    run('mkdir %s' % remote_dist_dir)
    with cd(remote_dist_dir):
        run('tar -xzvf %s' % remote_tmp_tar)
    # 设定新目录的www-data权限:
    run('chown -R www-data:www-data %s' % remote_dist_dir)
    # 删除旧的软链接:
    run('rm -f %s' % remote_dist_link)
    # 创建新的软链接指向新部署的目录:
    run('ln -s %s %s' % (remote_dist_dir, remote_dist_link))
    run('chown -R www-data:www-data %s' % remote_dist_link)
    # 重启fastcgi:
    fcgi = '/etc/init.d/py-fastcgi'
    with settings(warn_only=True):
        run('%s stop' % fcgi)
    run('%s start' % fcgi)
以上定义了pack和deploy两个任务,如果我们用Fabric部署,只需简单地输入两条命令:
$ fab pack
$ fab deploy
Fabric提供几个简单的API来完成所有的部署,最常用的是local()和run(),分别在本地和远程执行命令,put()可以把本地文件上传到远程,当需要在远程指定当前目录时,只需用with cd('/path/to/dir/'):即可。
默认情况下,当命令执行失败时,Fabric会停止执行后续命令。有时,我们允许忽略失败的命令继续执行,比如run('rm /tmp/abc')在文件不存在的时候有可能失败,这时可以用with settings(warn_only=True):执行命令,这样Fabric只会打出警告信息而不会中断执行。
Fabric是如何在远程执行命令的呢?其实Fabric所有操作都是基于SSH执行的,必要时它会提示输入口令,所以非常安全。更好的办法是在指定的部署服务器上用证书配置无密码的ssh连接。
如果是基于团队开发,可以让Fabric利用版本库自动检出代码,自动执行测试、打包、部署的任务。由于Fabric运行的命令都是基本的Linux命令,所以根本不需要用Fabric本身来扩展,会敲Linux命令就能用Fabric部署。
利用Fabric部署Python、Ruby、PHP这样的非编译型网站应用非常方便,而对于编译型的Java、C#等就麻烦了,编译本身就是一个极其复杂的大工程,需要依赖特定工具或者IDE,很难做到自动化。

源码地址: http://docs.fabfile.org/en/1.8/,
https://github.com/fabric/fabric
( Simple, Pythonic remote execution and deployment.
http://fabfile.org)

中文翻译:https://github.com/wrongwaycn/Fabric-Chinese-Docs
------------------------

用 Fabric 实现自动化部署

写完代码测试通过之后,终于松一口气,然后可以愉快的部署上线了。但是问题随之而来:如何部署?或者如何能更自动化的部署?
部署应用是一系列的操作,就环境而言,分为本地和远程服务器,就操作而言,大概包括提交代码、备份代码、更新代码、安装依赖、迁移数据库、重启服务等流程。其中除了提交代码这一步是在本地完成,其余操作都需要在服务器环境执行。
上面的流程当中,有一个很重要的,就是如何同步代码(提交、备份、更新)。就我的经验,了解或用过这些方式:
  • rsync: rsync 是一个文件同步的工具,如果配置好使用起来体验也不错。但是有很多缺点:
    • 配置复杂,命令行参数多
    • 需要在服务器上运行 rsyncd,默认监听 873 端口(可能会有防火墙)
  • scp: scp 底层用的是 SSH 协议,所以只要服务器上运行了 sshd 就可以双向 copy 文件。对于文件传输来说,scp 比 rsync 体验差的地方有:
    • 不能增量更新,每次都是全部传输
    • 不能配置忽略文件(.git 怎么办?)
  • git: 就个人而言,git 是最方便的部署方式了,有版本控制,可以增量更新,可以配置忽略文件,使用简单。实际上只要有可能,都推荐用 git 来发布代码。但问题在于,很多公司的 git 服务器都是在内网的,所以在服务器上无法访问。
很幸运的是,我们有一个公网可以访问的 git 服务器,所以可以用 git 来发布代码。发布完代码后就是后续的一系列操作了,最原始的方式,是登录到服务器,然后一步一步敲命令执行下来。但是如果要频繁部署的话(快速迭代时肯定要经常更新代码),这就变成了繁复的体力劳动,而且容易出错(漏了流程,看花眼了)。于是就想到了脚本,把这些操作写成 shell 脚本,然后执行脚本就好了。这是一个很大的进步,然而仍然存在一个问题:从本地环境到远程环境,需要登录,导致了流程上的阻断。
Fabric 是 Python 编写的一个可以实现自动化部署和系统维护的命令行工具,只需要写一些简单的 Python 代码就能轻松解决上面提到的所有问题。Fabric 底层用的是 SSH 协议,提供了一系列语义清晰的 API 来组合实现部署任务。

安装

Fabric 是 Python 编写的工具,所以可以用 pip 来安装:
sudo pip install fabric
如果是 Ubuntu 系统,还可以用 apt-get 安装:
sudo apt-get install fabric
安装完成后,会生成一个 fab 命令,这个命令会读取当前路径在的 fabfile.py 并执行相应的任务。

Hello, world!

先来看一个简单的例子,用 fab 命令执行一个输出 Hello, world! 的任务。
新建一个文件,fabfile.py:
def hello():
 print 'Hello, world!'
在 fabfile.py 所在的路径执行:
fab hello
可以看到有这样的输出:
Hello, world!

Done.
可以给任务传递参数,修改 fabfile.py:
def hello(name="world"):
    print "Hello, %s!" % name
用 fab 命令执行:
$ fab hello

Hello, world!

Done.

$ fab hello:name=leon

Hello, leon!

Done.
这个例子除了展示 fab 运行任务和传递参数之外,没有什么实际意义,接下来用一个接近真实的场景来展示如何用 Fabric 部署。

部署应用

假设这样一个场景,有个 Python 项目取名 usercenter,用 git 做版本控制,用 supervisor 做进程管理。一次完整的部署过程可能包括这些流程:
# 本地
$ cd /path/to/userenter
$ git pull
$ git add -A
$ git commit -m "commit message"
$ git push

# 远程
$ cd /path/to/usercenter
$ workon usercenter  # virtualenv
$ git pull    # 更新代码
$ pip install -r requirements.txt  # 安装依赖
$ python manage.py db migrate   # 数据库迁移
$ supervisorctl restart usercenter # 重启服务
我们现在用 Fabric 来一次性完成上面所有操作(假设第一次部署是手工执行的,现在只处理更新/升级的任务)。在 usercenter 项目的根目录下新建 fabfile.py 文件:
# -*- coding: utf-8 -*-

from fabric.api import env, local, cd, run
from fabric.context_managers import prefix


def production():
 """ 设置 production 环境 """
 env.hosts = ["production@123.123.123.123:22"]
 env.key_filename = "/path/to/key_file"
 # env.password = "123456" # password 和 keyfile 两者只需要一个就可以


def staging():
 """ 设置 staging 环境 """
 env.hosts = ["staging@111.111.111.111:22"]
 env.password = "123456"  # 如果不写密码,会在 fab 执行时有交互提示输入密码


def prepare():
    """ 本地提交代码,准备部署 """
 local("git pull") # local 用于执行本地命令
 local("pip freeze > requirements.txt")
 local("git add -p && git commit") # 会有交互输入 commit message
 local("git push")


def update():
 """ 服务器上更新代码、依赖和迁移 """
 # cd 用于在服务器上执行 cd 命令,本地环境对应的 api 是 lcd (local cd)
 with cd("/path/to/usercenter"), prefix("workon usercenter"):
  run("git pull")   # run 用于服务器上执行命令
  run("pip install -r requirements.txt")
  run("python manage.py db migrate")
  run("supervisorctl restart usercenter")

def deploy():
 prepare()
 update()
OK, 完成。具体的意义代码里面都有注释,不赘述。需要注意的是 production 和 staging 分别设置了两种不同的环境。
# 部署到 production 环境
$ fab production deploy

# 部署到 staging 环境
$ fab staging deploy
执行过程中可能会有些交互,按提示输入相应信息,然后等着执行完成就好了。如果一切顺利(应该是这样),就完成了 usercenter 的部署了,整个过程只需要敲一行命令,是不是非常方便?

More…

上面的例子基本上是可以在实际环境中使用的,不过还是有很多内容没有覆盖到,比如错误处理,多服务器部署,并行等。Fabric 默认是串行执行的,如果有多个远程服务器,是一个一个顺序执行。执行过程中如果发生异常,任务会直接中断,所以可能需要有错误处理。
上面这些(还有很多)内容都可以在 Fabric 的文档上(非常详细)找到相应的内容,下面给出一些参考链接,结合文档和自己的实际情况,多用几次就能定制出能满足自己需求的 Fabric 任务:

ssh到测试环境pull部署(2分钟),rsync到线上机器A,B,C,D,E(1分钟),分别ssh到ABCDE五台机器,逐一重启(8-10分钟) = 13-15分钟
其中郁闷的是,每次操作都是相同的,命令一样,要命的是在多个机器上,很难在本机一个脚本搞定,主要时间都浪费在ssh,敲命令上了,写成脚本,完全可以一键执行,花两分钟看下执行结果
直到,发现了fabric这货
官方文档 入口

作用

很强大的工具
可以将自动化部署或者多机操作的命令固化到一个脚本里
和某些运维工具很像,用它主要是因为,python…..
简单好用易上手
当然,shell各种命令组合起来也可以,上古神器和现代兵器的区别

环境配置

在本机和目标机器安装对应包(注意,都要有)
sudo easy_install fabric
目前是1.8版本
安装完后,可以查看是否安装成功
[ken@~$] which fab
/usr/local/bin/fab
装完之后,可以浏览下官方文档
然后,可以动手了

hello world

先进行本机简单操作,有一个初步认识,例子来源与官网
新建一个py脚本: fabfile.py
def hello():
    print("Hello world!")
命令行执行:
[ken@~/tmp/fab$] fab hello
Hello world!

Done.
注意,这里可以不用fabfile作为文件名,但是在执行时需指定文件
[ken@~/tmp/fab$] mv fabfile.py test.py
fabfile.py -> test.py
[ken@~/tmp/fab$] fab hello

Fatal error: Couldn't find any fabfiles!

Remember that -f can be used to specify fabfile path, and use -h for help.

Aborting.
[ken@~/tmp/fab$] fab -f test.py hello
Hello world!

Done.
带参数:
修改fabfile.py脚本:
def hello(name, value):
    print("%s = %s!" % (name, value))
执行
[ken@~/tmp/fab$] fab hello:name=age,value=20
age = 20!

Done.
[ken@~/tmp/fab$] fab hello:age,20
age = 20!

Done.

执行本机操作

简单的本地操作:
from fabric.api import local, lcd

def lsfab():
    with lcd('~/tmp/fab'):
        local('ls')
结果:
[ken@~/tmp/fab$] pwd;ls
/Users/ken/tmp/fab
fabfile.py   fabfile.pyc  test.py      test.pyc
[ken@~/tmp/fab$] fab -f test.py lsfab
[localhost] local: cd ~/tmp/fab
[localhost] local: ls
fabfile.py  fabfile.pyc test.py     test.pyc

Done.
实战开始:
假设,你每天要提交一份配置文件settings.py到版本库(这里没有考虑冲突的情况)
如果是手工操作:
cd /home/project/test/conf/
git add settings.py
git commit -m 'daily update settings.py'
git pull origin
git push origin
也就是说,这几个命令你每天都要手动敲一次,所谓daily job,就是每天都要重复的,机械化的工作,让我们看看用fabric怎么实现一键搞定:(其实用shell脚本可以直接搞定,但是fab的优势不是在这里,这里主要位后面本地+远端操作做准备,毕竟两个地方的操作写一种脚本便于维护)
from fabric.api import local, lcd

def setting_ci():
    with lcd('/home/project/test/conf/'):
        local("git add settings.py")
        #后面你懂的,懒得敲了…..

混搭整合远端操作

这时候,假设,你要到机器A的/home/ken/project对应项目目录把配置文件更新下来
#!/usr/bin/env python
# encoding: utf-8

from fabric.api import local,cd,run, env

env.hosts=['user@ip:port',] #ssh要用到的参数
env.password = 'pwd'


def setting_ci():
    local('echo "add and commit settings in local"')
    #刚才的操作换到这里,你懂的

def update_setting_remote():
    print "remote update"
    with cd('~/temp'):   #cd用于进入某个目录
        run('ls -l | wc -l')  #远程操作用run

def update():
    setting_ci()
    update_setting_remote()
然后,执行之:
[ken@~/tmp/fab$] fab -f deploy.py update
[user@ip:port] Executing task 'update'
[localhost] local: echo "add and commit settings in local"
add and commit settings in local
remote update
[user@ip:port] run: ls -l | wc -l
[user@ip:port] out: 12
[user@ip:port] out:


Done.
注意,如果不声明env.password,执行到对应机器时会跳出要求输入密码的交互

多服务器混搭

操作多个服务器,需要配置多个host
#!/usr/bin/env python
# encoding: utf-8

from fabric.api import *

#操作一致的服务器可以放在一组,同一组的执行同一套操作
env.roledefs = {
            'testserver': ['user1@host1:port1',],
            'realserver': ['user2@host2:port2', ]
            }

#env.password = '这里不要用这种配置了,不可能要求密码都一致的,明文编写也不合适。打通所有ssh就行了'

@roles('testserver')
def task1():
    run('ls -l | wc -l')

@roles('realserver')
def task2():
    run('ls ~/temp/ | wc -l')

def dotask():
    execute(task1)
    execute(task2)
结果:
[ken@~/tmp/fab$] fab -f mult.py dotask
[user1@host1:port1] Executing task 'task1'
[user1@host1:port1] run: ls -l | wc -l
[user1@host1:port1] out: 9
[user1@host1:port1] out:

[user2@host2:port2] Executing task 'task2'
[user2@host2:port2] run: ls ~/temp/ | wc -l
[user2@host2:port2] out: 11
[user2@host2:port2] out:


Done.

扩展

1.颜色
可以打印颜色,在查看操作结果信息的时候更为醒目和方便
from fabric.colors import *

def show():
    print green('success')
    print red('fail')
    print yellow('yellow')
#fab -f color.py show
2.错误和异常
默认,一组命令,上一个命令执行失败后,不会接着往下执行
失败后也可以进行不一样的处理, 文档
目前没用到,后续用到再看了
3.密码管理
文档
更好的密码管理方式,哥比较土,没打通,主要是服务器列表变化频繁,我的处理方式是:
3.1 host,user,port,password配置列表,所有的都写在一个文件
或者直接搞到脚本里,当然这个更........
env.hosts = [
        'host1',
        'host2'
]
# 注意: 要使env.passwords生效, host格式必须是  user@ip:port 端口号一定要显式写出来,即使是使用的默认22端口
env.passwords = {
    'host1': "pwdofhost1",
    'host2': "pwdofhost2",
}

或者
env.roledefs = {
'testserver': ['host1:22', 'host2:22'],
'realserver': ['host3:22', ]
}
# 注意: 要使env.passwords生效, host格式必须是  user@ip:port 端口号一定要显式写出来,即使是使用的默认22端口
env.passwords = {
    'host1:22': "pwdofhost1",
    'host2:22': "pwdofhost2",
    'host3:22': "pwdofhost3",
}
3.2 根据key解析成map嵌套,放到deploy中
另外命令其实也可以固化成一个cmds列表的…..
粗略就用到这些,后续有更多需求的时候再去捞文档了,话说文档里好东西真多,就是太多了,看了晕。。。
TODO:
装饰器作用?
@task
@parallel

命令行常用: fab --help
fab -l             -- 显示可用的task(命令)
fab -H             -- 指定host,支持多host逗号分开
fab -R             -- 指定role,支持多个
fab -P             -- 并发数,默认是串行
fab -w             -- warn_only,默认是碰到异常直接abort退出
fab -f             -- 指定入口文件,fab默认入口文件是:fabfile/fabfile.py

状态确认及错误处理

更复杂的操作

update log
2014-10-26 fix error of local/lcd

The end!