使用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除法运算小技巧
Apr 05 Python
python访问mysql数据库的实现方法(2则示例)
Jan 06 Python
Python的Flask框架中的Jinja2模板引擎学习教程
Jun 30 Python
python中int与str互转方法
Jul 02 Python
Python利用itchat库向好友或者公众号发消息的实例
Feb 21 Python
cProfile Python性能分析工具使用详解
Jul 22 Python
Python操作多维数组输出和矩阵运算示例
Nov 28 Python
numpy ndarray 取出满足特定条件的某些行实例
Dec 05 Python
python如何使用代码运行助手
Jul 03 Python
python 爬取小说并下载的示例
Dec 07 Python
python中使用asyncio实现异步IO实例分析
Feb 26 Python
理解python中装饰器的作用
Jul 21 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
php获取服务器端mac和客户端mac的地址支持WIN/LINUX
2014/05/15 PHP
详解WordPress中分类函数wp_list_categories的使用
2016/01/04 PHP
Nigma vs Liquid BO3 第一场2.14
2021/03/10 DOTA
JavaScript入门教程(2) JS基础知识
2009/01/31 Javascript
javascript new后的constructor属性
2010/08/05 Javascript
图片上传插件jquery.uploadify详解
2013/11/15 Javascript
js原型继承的两种方法对比介绍
2014/03/30 Javascript
JavaScript实现从数组中选出和等于固定值的n个数
2014/09/03 Javascript
详谈javascript中的cookie
2015/06/03 Javascript
在其他地方你学不到的jQuery小贴士和技巧(欢迎收藏)
2016/01/20 Javascript
基于JS判断iframe是否加载成功的方法(多种浏览器)
2016/05/13 Javascript
js判断复选框是否选中及选中个数的实现代码
2016/05/30 Javascript
深入理解React中es6创建组件this的方法
2016/08/29 Javascript
jquery遍历标签中自定义的属性方法
2016/09/17 Javascript
简单实现JS上传图片预览功能
2017/04/14 Javascript
mac下的nodejs环境安装的步骤
2017/05/24 NodeJs
JS实现为动态创建的元素添加事件操作示例
2018/03/17 Javascript
移动端(微信等使用vConsole调试console的方法
2019/03/05 Javascript
Vue实现商品详情页的评价列表功能
2019/09/04 Javascript
JavaScript常用进制转换及位运算实例解析
2020/10/14 Javascript
[04:52]DOTA2亚洲邀请赛附加赛 TOP10精彩集锦
2015/01/29 DOTA
[58:32]EG vs Liquid 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
Python实现PS滤镜的万花筒效果示例
2018/01/23 Python
pycharm: 恢复(reset) 误删文件的方法
2018/10/22 Python
Django 开发环境配置过程详解
2019/07/18 Python
详解python内置常用高阶函数(列出了5个常用的)
2020/02/21 Python
Python新手学习函数默认参数设置
2020/06/03 Python
浅析Python中字符串的intern机制
2020/10/03 Python
合同专员岗位职责
2013/12/18 职场文书
高中学校对照检查材料
2014/08/31 职场文书
师德标兵先进事迹材料
2014/12/19 职场文书
2015年酒店工作总结范文
2015/04/07 职场文书
商场营业员岗位职责
2015/04/14 职场文书
社区活动总结范文
2015/05/07 职场文书
学校食堂管理制度
2015/08/04 职场文书
商业计划书范文
2019/04/24 职场文书