使用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获取apk文件URL地址实例
Nov 01 Python
Python中字典的基础知识归纳小结
Aug 19 Python
使用Python读写及压缩和解压缩文件的示例
Jul 08 Python
详解python进行mp3格式判断
Dec 23 Python
Python3非对称加密算法RSA实例详解
Dec 06 Python
Python图像处理之颜色的定义与使用分析
Jan 03 Python
Python实现的对一个数进行因式分解操作示例
Jun 27 Python
python递归法解决棋盘分割问题
Jul 17 Python
python对Excel按条件进行内容补充(推荐)
Nov 24 Python
浅谈python出错时traceback的解读
Jul 15 Python
Python发送邮件实现基础解析
Aug 14 Python
python urllib和urllib3知识点总结
Feb 08 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下用GD生成生成缩略图的两个选择和区别
2007/04/17 PHP
生成ubuntu自动切换壁纸xml文件的php代码
2010/07/17 PHP
详解WordPress开发中用于获取分类及子页面的函数用法
2016/01/08 PHP
Yii2验证器(Validator)用法分析
2016/07/23 PHP
PHP生成随机字符串实例代码(字母+数字)
2019/09/11 PHP
php判断目录存在的简单方法
2019/09/26 PHP
js创建对象的几种常用方式小结(推荐)
2010/10/24 Javascript
JQuery 图片的展开和伸缩实例讲解
2013/04/18 Javascript
图片动画横条广告带上下滚动可自定义图片、链接等等
2013/10/20 Javascript
jQuery学习笔记之toArray()
2014/06/09 Javascript
简单纯js实现点击切换TAB标签实例
2015/08/23 Javascript
JavaScript ES6中CLASS的使用详解
2016/11/22 Javascript
纯html+css+javascript实现楼层跳跃式的页面布局(实例代码)
2017/10/25 Javascript
用Fundebug插件记录网络请求异常的方法
2019/02/21 Javascript
详解JavaScript的内存空间、赋值和深浅拷贝
2019/04/17 Javascript
详解keep-alive + vuex 让缓存的页面灵活起来
2019/04/19 Javascript
vue swipe自定义组件实现轮播效果
2019/07/03 Javascript
浅析TypeScript 命名空间
2020/03/19 Javascript
python正则表达式及使用正则表达式的例子
2018/01/22 Python
python3的输入方式及多组输入方法
2018/10/17 Python
对python指数、幂数拟合curve_fit详解
2018/12/29 Python
Python提取视频中图片的示例(按帧、按秒)
2020/10/22 Python
python 爬取英雄联盟皮肤并下载的示例
2020/12/04 Python
解决pytorch 数据类型报错的问题
2021/03/03 Python
如何开发一款堪比APP的微信小程序(腾讯内部团队分享)
2016/12/22 HTML / CSS
Maisons du Monde德国:法国家具和装饰的市场领导者
2019/07/26 全球购物
法国购买二手电子产品网站:Asgoodasnew
2020/03/27 全球购物
汉语言文学毕业生求职信
2013/10/01 职场文书
施工资料员的岗位职责
2013/12/22 职场文书
职工代表大会主持词
2014/04/01 职场文书
五四青年节比赛演讲稿
2015/03/18 职场文书
Python爬虫进阶之Beautiful Soup库详解
2021/04/29 Python
用python画城市轮播地图
2021/05/28 Python
springboot layui hutool Excel导入的实现
2022/03/31 Java/Android
《我的美好婚事》动画化决定纪念插画与先导PV公开
2022/04/06 日漫
零基础学java之带返回值的方法的定义和调用
2022/04/10 Java/Android