使用Fabric自动化部署Django项目的实现


Posted in Python onSeptember 27, 2019

文中涉及的示例代码,已同步更新到HelloGitHub-Team 仓库

在上一篇教程中,我们通过手工方式将代码部署到了服务器。整个过程涉及到十几条命令,输了 N 个字符。一旦我们本地的代码有更新,整个过程又得重复来一遍,这将变得非常繁琐。

使用 Fabric 可以在服务器中自动执行命令。因为整个代码部署过程都是相同的,只要我们用 Fabric 写好部署脚本,以后就可以通过运行脚本自动完成部署了。

首先在本地安装 Fabric:

$ pipenv install fabric --dev

因为 Fabric 只需在本地使用,因此使用 --dev 选项,让 Pipenv 将 Fabric 依赖写到 dev-packages 配置下,线上环境就不会安装 Fabric。

部署过程回顾

在写 Fabric 脚本之前,先来回顾一下当我们在本地开发环境下更新了代码后,在服务器上的整个部署过程。

  • 远程连接服务器。
  • 进入项目根目录,从远程仓库拉取最新的代码。
  • 如果项目引入了新的依赖,需要执行 pipenv install --deploy --ignore-pipfile 安装最新依赖。
  • 如果修改或新增了项目静态文件,需要执行 pipenv run python manage.py collectstatic 收集静态文件。
  • 如果数据库发生了变化,需要执行 pipenv run python manage.py migrate 迁移数据库。
  • 重启 Nginx 和 Gunicorn 使改动生效。

整个过程就是这样,把每一步操作翻译成 Fabric 对应的脚本代码,这样一个自动化部署脚本就完成了。

完善项目配置

分离 settings 文件

为了安全,线上环境我们将 debug 改为了 False,但开发环境要改为 True,改来改去将很麻烦。此外,django 的 SECRET_KEY 是很私密的配置,django 的很多安全机制都依赖它,如果不慎泄露,网站将面临巨大安全风险,像我们现在这样直接写在配置文件中,万一不小心公开了源代码,SECRET_KEY 就会直接泄露,好的实践是将这个值写入环境变量,通过从环境变量取这个值。

解决以上问题的一个方案就是拆分 settings.py 文件,不同环境对应不同的 settings 文件,django 在启动时会从环境变量中读取 DJANGO_SETTINGS_MODULE 的值,以这个值指定的文件作为应用的最终配置。

我们来把 settings.py 拆分,首先在 blogproject 目录下新建一个 Python 包,名为 settings,然后创建一个 common.py,用于存放通用配置,local.py 存放开发环境的配置,production.py 存放线上环境的配置:

blogproject\
  settings\
    __init__.py
    local.py
    production.py
  settings.py

将 settings.py 文件中的内容全部复制到 common.py 里,并将 SECRET_KEY、DEBUG、ALLOWED_HOSTS 这些配置移到 local.py 和 production.py 中(common.py 中这些项可以删除)。

开发环境的配置 local.py 内容如下:

from .common import *

SECRET_KEY = 'development-secret-key'
DEBUG = True
ALLOWED_HOSTS = ['*']

线上环境的配置:

from .common import *

SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DEBUG = False
ALLOWED_HOSTS = ['hellodjango-blog-tutorial.zmrenwu.com']

注意这里我们在顶部使用 from .common import * 将全部配置从 common.py 导入,然后根据环境的不同,在下面进行配置覆盖。

线上环境和开发环境不同的是,为了安全,DEBUG 模式被关闭,SECRET_KEY 从环境变量获取,ALLOWED_HOSTS 设置了允许的 HTTP HOSTS(具体作用见后面的讲解)。

以上操作完成后,一定记得删除 settings.py

现在我们有了两套配置,一套是 local.py,一套是 production.py,那么启动项目时,django 怎么知道我们使用了哪套配置呢?答案是在运行 manage.py 脚本时,django 默认帮我们指定了。在使用 python manage.py 执行命令时,django 可以接收一个 --settings-module 的参数,用于指定执行命令时,项目使用的配置文件,如果参数未显示指定,django 会从环境变量 DJANGO_SETTINGS_MODULE 里获取。看到 manage.py 的源码:

def main():
  os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blogproject.settings')
  try:
    from django.core.management import execute_from_command_line
  except ImportError as exc:
    raise ImportError(
      "Couldn't import Django. Are you sure it's installed and "
      "available on your PYTHONPATH environment variable? Did you "
      "forget to activate a virtual environment?"
    ) from exc
  execute_from_command_line(sys.argv)

可以看到这个 main 函数,第一行的 setdefault 为我们设置了环境变量 DJANGO_SETTINGS_MODULE 的值,这句代码的作用是,如果当前环境中 DJANGO_SETTINGS_MODULE 的值没有被设置,就将其设置为 blogproject.settings,所以我们使用 python manage.py 执行命令时,django 默认为我们使用了 settings.py 这个配置。

所以我们可以通过设置环境变量,来指定 django 使用的配置文件。

对于 manage.py,通常在开发环境下执行,因此将这里的 DJANGO_SETTINGS_MODULE 的值改为 blogproject.settings.local,这样运行开发服务器时 django 会加载 blogproject/settings/local.py 这个配置文件。

另外看到 wsgi.py 文件中,这个文件中有一个 application,是在线上环境时 Gunicorn 加载运行的,将这里面的 DJANGO_SETTINGS_MODULE 改为 blogproject.settings.production

这样,在使用 manage.py 执行命令时,加载的是 local.py 的设置,而使用 gunicorn 运行项目时,使用的是 production.py 的设置。

修改 BASE_DIR 配置项

还有需要注意的一点,看到存放通用配置的 common.py 文件,里面有一个配置项为:

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

这个 BASE_DIR 指向项目根目录,其获取方式为根据所在的配置文件向上回溯,找到项目根目录。因为此前的目录结构为 HelloDjango-blog-tutorial/blogproject/settings.py,因此向上回溯 2 层就到达项目根目录。而现在目录结构变为 HelloDjango-blog-tutorial/blogproject/settings/common.py,需向上回溯 3 层才到达项目根目录,因此需将 BASE_DIR 进行一个简单修改,修改如下:

BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

即再在外面包一层 os.path.dirname,再向上回退一层,到达项目根目录。

设置 Supervisor 环境变量

此外,由于线上环境配置中的 secret_key 从环境变量获取,因此我们改一下 supervisor 的配置,将环境变量导入,打开 supervisor 的配置文件 ~/etc/supervisor/conf.d/hellodjango-blog-tutorial.ini,添加环境变量的配置语句:

environment=DJANGO_SECRET_KEY=2pe8eih8oah2_2z1=7f84bzme7^bwuto7y&f(#@rgd9ux9mp-3

因为此前可能将代码传过公开的代码仓库,所以最好把线上使用的 SECRET_KEY换一下。这个网站可以自动生成 SECRET_KEY:Django Secret Key Generator。

保存配置,然后要执行 update 命令更新配置。

$ supervisorctl -c ~/etc/supervisord.conf update

编写 Fabric 脚本

一切准备工作均已就绪,现在就来使用 Fabric 编写自动部署脚本。

Fabric 脚本通常位于 fabfile.py 文件里,因此先在项目根目录下建一个 fabfile.py 文件。

根据上述过程编写的脚本代码如下:

from fabric import task
from invoke import Responder
from ._credentials import github_username, github_password


def _get_github_auth_responders():
  """
  返回 GitHub 用户名密码自动填充器
  """
  username_responder = Responder(
    pattern="Username for 'https://github.com':",
    response='{}\n'.format(github_username)
  )
  password_responder = Responder(
    pattern="Password for 'https://{}@github.com':".format(github_username),
    response='{}\n'.format(github_password)
  )
  return [username_responder, password_responder]


@task()
def deploy(c):
  supervisor_conf_path = '~/etc/'
  supervisor_program_name = 'hellodjango-blog-tutorial'

  project_root_path = '~/apps/HelloDjango-blog-tutorial/'

  # 先停止应用
  with c.cd(supervisor_conf_path):
    cmd = 'supervisorctl stop {}'.format(supervisor_program_name)
    c.run(cmd)

  # 进入项目根目录,从 Git 拉取最新代码
  with c.cd(project_root_path):
    cmd = 'git pull'
    responders = _get_github_auth_responders()
    c.run(cmd, watchers=responders)

  # 安装依赖,迁移数据库,收集静态文件
  with c.cd(project_root_path):
    c.run('pipenv install --deploy --ignore-pipfile')
    c.run('pipenv run python manage.py migrate')
    c.run('pipenv run python collectstatic --noinput')

  # 重新启动应用
  with c.cd(supervisor_conf_path):
    cmd = 'supervisorctl start {}'.format(supervisor_program_name)
    c.run(cmd)

来分析一下部署代码。

deploy 函数为部署过程的入口,加上 task 装饰器将其标注为一个 fabric 任务。

然后定义了一些项目相关的变量,主要是应用相关代码和配置所在服务器的路径。

deploy 函数被调用时会传入一个 c 参数,这个参数的值是 Fabric 在连接服务器时创建的 ssh 客户端实例,使用这个实例可以在服务器上运行相关命令。

接着就是执行一系列部署命令了,进入某个目录使用 ssh 客户端实例的 cd 方法,运行命令使用 run 方法。

需要注意的是,每次 ssh 客户端实例执行新的命令是无状态的,即每次都会在服务器根目录执行新的命令,而不是在上一次执行的命令所在目录,所以要在同一个目录下连续执行多条命令,需要使用 with c.cd 上下文管理器。

最后,如果服务器没有加入代码仓库的信任列表,运行 git pull 一般会要求输入密码。我们代码托管使用了 GitHub,所以写了一个 GitHub 账户密码响应器,一旦 Fabric 检测到需要输入 GitHub 账户密码,就会调用这个响应器,自动填写账户密码。

由于响应器从 _credentials.py 模块导入敏感信息,因此在 fabfile.py 同级目录新建一个 _credentials.py文件,写上 GitHub 的用户名和密码:

github_username = your-github-username
github_password = your-github-password

当然,这个文件包含账户密码等敏感信息,所以一定记得将这个文件加入 .gitignore 文件,将其排除在版本控制系统之外,别一不小心提交了公开仓库,导致个人 GitHub 账户泄露。

执行 Fabric 自动部署脚本

进入 fabfile.py 文件所在的目录,用 fab 命令运行这个脚本文件(将 server_ip 换为你线上服务器的 ip 地址):

fab -H server_ip --prompt-for-login-password -p deploy

这时 Fabric 会自动检测到 fabfile.py 脚本中的 deploy 函数并运行,输入服务器登录密码后回车,然后你会看到命令行输出了一系列字符串,最后看到部署完毕的消息。

如果脚本运行中出错,检查一下命令行输出的错误信息,修复问题后重新运行脚本即可。以后当你在本地开发完相关功能后,只需要执行这一个脚本文件,就可以自动把最新代码部署到服务器了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
介绍Python的@property装饰器的用法
Apr 28 Python
python 中split 和 strip的实例详解
Jul 12 Python
Python中的is和==比较两个对象的两种方法
Sep 06 Python
微信跳一跳自动运行python脚本
Jan 08 Python
python3使用flask编写注册post接口的方法
Dec 28 Python
Python3.5文件修改操作实例分析
May 01 Python
基于Python检测动态物体颜色过程解析
Dec 04 Python
Python利用PyExecJS库执行JS函数的案例分析
Dec 18 Python
如何基于python实现脚本加密
Dec 28 Python
pycharm中使用request和Pytest进行接口测试的方法
Jul 31 Python
python使用numpy中的size()函数实例用法详解
Jan 29 Python
python sleep和wait对比总结
Feb 03 Python
Win10+GPU版Pytorch1.1安装的安装步骤
Sep 27 #Python
opencv调整图像亮度对比度的示例代码
Sep 27 #Python
详解Django将秒转换为xx天xx时xx分
Sep 27 #Python
pytorch多GPU并行运算的实现
Sep 27 #Python
Python使用matplotlib 模块scatter方法画散点图示例
Sep 27 #Python
python利用openpyxl拆分多个工作表的工作簿的方法
Sep 27 #Python
Python绘制热力图示例
Sep 27 #Python
You might like
如何给phpadmin一个保护
2006/10/09 PHP
ADODB的数据库封包程序库
2006/12/31 PHP
PHP实现可自定义样式的分页类
2016/03/29 PHP
php使用文本统计访问量的方法
2016/05/12 PHP
ThinkPHP自定义Redis处理SESSION的实现方法
2016/05/16 PHP
postfixadmin忘记密码后的修改密码方法详解
2016/07/20 PHP
php使用ftp实现文件上传与下载功能
2017/07/21 PHP
PHP面向对象程序设计之构造方法和析构方法详解
2019/06/13 PHP
laravel config文件配置全局变量的例子
2019/10/13 PHP
PHP pthreads v3使用中的一些坑和注意点分析
2020/02/21 PHP
jQuery渐变发光导航菜单的实例代码
2013/03/27 Javascript
jquery $.each 和for怎么跳出循环终止本次循环
2013/09/27 Javascript
推荐10 款 SVG 动画的 JavaScript 库
2015/03/24 Javascript
javascript基础练习之翻转字符串与回文
2017/02/20 Javascript
Vue实现一个无限加载列表功能
2018/11/13 Javascript
JS实现的图片选择顺序切换和循环切换功能示例【测试可用】
2018/12/28 Javascript
使用pkg打包ThinkJS项目的方法步骤
2019/12/30 Javascript
v-slot和slot、slot-scope之间相互替换实例
2020/09/04 Javascript
[05:20]2018DOTA2亚洲邀请赛主赛事第三日战况回顾 LGD率先挺进胜者组决赛
2018/04/06 DOTA
用virtualenv建立多个Python独立虚拟开发环境
2017/07/06 Python
Python MySQLdb 使用utf-8 编码插入中文数据问题
2018/03/13 Python
TensorFlow实现卷积神经网络
2018/05/24 Python
python占位符输入方式实例
2019/05/27 Python
python统计指定目录内文件的代码行数
2019/09/19 Python
PyQt5中多线程模块QThread使用方法的实现
2020/01/31 Python
Django-migrate报错问题解决方案
2020/04/21 Python
Python操作MySQL数据库的示例代码
2020/07/13 Python
Python grpc超时机制代码示例
2020/09/14 Python
电脑租赁公司创业计划书
2014/01/08 职场文书
门诊手术室工作制度
2014/01/30 职场文书
《中国梦我的梦》中学生演讲稿
2014/08/20 职场文书
当幸福来敲门英文观后感
2015/06/01 职场文书
房贷工资证明范本
2015/06/12 职场文书
会议简讯范文
2015/07/20 职场文书
tp5使用layui实现多个图片上传(带附件选择)的方法实例
2021/11/17 PHP
详解如何使用Nginx解决跨域问题
2022/05/06 Servers