我的博客是如何自动部署的
没错,利用现成的 CD(Continuous Deployment,持续部署)工具就可以实现自动部署。流行的 CI / CD 工具有 Jenkins、Travis CI 和 Drone……然而,它们对我来说都太笨重了(服务器资源有限),因此我自己实现了一个简单的自动化部署程序。
先来看一下一个自动部署程序需要具备哪些基本功能:
- 能够检测到指定代码仓库有代码提交,并执行后续一系列的部署命令
- 能够拉取指定项目的仓库代码
- 能够构建并重启指定项目
- 能够把部署结果通知给我
下面我重点介绍一下在这整个过程中我使用的一些关键工具:
Webhook
GitHub、Bitbucket 等都提供了 Webhook 功能,第一个功能迎刃而解,但这就意味着我们需要起一个 web 服务来作为 Webhook 事件发送的目标地址。使用 template-tornado 很快就可以写出处理方法:
class DeployHandler(BasicHandler):
async def post(self):
params = self.validate_argument({"repository": {"id": Use(int)}})
message = await DeployService.deploy(repo_id=params["repository"]["id"])
result = {"message": message}
return self.success(result)
这里,我们只需要取出参数中的仓库 id 即可,我们再配一个仓库 id 与信息的映射就可以了:
xxxxxxxxxx
REPO_MAP = {
83530215: {
"name": "typora-blog",
"path": "/app/typora-blog",
"url": "https://www.jackeriss.com/",
"deployer": Deployer.GULP,
},
}
注意 deployer 我配的是一个枚举,这样就可以支持多种部署程序了。
之后就可以根据仓库 id 在对应的目录下执行git pull
并使用对应的部署程序进行部署了。
GitPython
你可以用 subprocess.Popen 直接执行git pull
命令,但我更推荐使用 GitPython 这个第三方库来简化操作:
ximport git
git_repo = git.cmd.Git("/app/typora-blog")
git_repo.checkout(".")
git_repo.pull()
当然,它底层也是用 subprocess.Popen() 来执行 Git 命令的 。
gulp
不同的项目可能是用不同的脚本或守护进程管理的。我们的目标是支持任意项目的构建与重启。因此,构建与重启命令可以定义成一个字典,类型可以用枚举来定义:
xxxxxxxxxx
class Deployer(StrEnum):
NGINX = "nginx"
GULP = "gulp"
NONE = "none"
DEPLOYER_MAP = {
Deployer.NGINX: ["/usr/sbin/nginx -s reload"],
Deployer.GULP: ["/usr/local/bin/gulp"],
Deployer.NONE: [""],
}
这里的StrEnum
是我定义的一个字符串枚举类(这是 Python 官方文档中推荐的用法):
xxxxxxxxxx
import Enum
class StrEnum(str, Enum):
"""字符串类型的枚举"""
我的项目统一使用 gulp 来管理,它可以定义一系列的构建流程,而且有丰富的第三方插件可以使用。
举个例子,对于普通的 Python 项目,使用下面的脚本就足以应付:
x
const gulp = require('gulp');
const cp = require("child_process")
function install() {
return cp.exec('pipenv install --deploy')
}
function start() {
return cp.exec('/usr/local/bin/pm2 startOrReload pm2.json')
}
exports.default = gulp.series(install, start)
而对于前端项目 gulp 就更能大显身手了,你可以用它来构建压缩前端静态文件并上传到 CDN,还可以把 HTML 里所有的静态文件路径替换为 CDN 上对应的地址。
pm2
用过很多进程管理工具,比如用 python 写的 supervisor 和 circus,也试过自己手写 shell 脚本,但是它们用起来都不如 pm2 让人省心。pm2 一般被用来管理 JS 的项目,但其实管理其他项目也很好用。分享一下通过 pm2 管理使用 pipenv 的 python 项目的配置:
xxxxxxxxxx
{
"apps": [
{
"name": "typora-blog",
"script": "/bin/bash",
"args": [
"-c",
"pipenv run serve"
],
"exec_mode": "fork_mode",
"instances": "1",
"autorestart": false,
"log_file": "/app/log/typora-blog/typora-blog.log",
"time": true,
"merge_logs": true,
"increment_var": "PORT",
"env": {
"ENV": "prod",
"PORT": 10200
}
}
]
}
注意:使用 pipenv 启动 python 项目或者要启动多个进程在不同的端口的话exec_mode
必须是fork_mode
。increment_var
是自增变量,但是当前版本(4.2.1)的这项配置有个 bug:使用 pm2 重启项目后这项配置会丢失,导致所有进程都会使用同一个端口,后面启动的进程都会报端口冲突错误(详见:https://github.com/Unitech/pm2/issues/4502)。
subprocess 模块
subprocess 模块被推荐用来替换一些老的模块和函数,如:os.system()、os.spawn() 和 os.popen() 等。
subprocess 模块引入了许多新的方法,如:subprecess.call()、subprocess.check_call() 和 subprocess.check_output() 等,而这些方法在 Python 3.5 之后又可以用 subprecess.run() 来替代。实际上这些方法都是对 subprocess.Popen() 的封装,这些封装的目的是为了让我们容易使用子进程。对于更进阶的用例,就要使用底层的 subprocess.Popen() 接口。
直接调用 subprocess.Popen() 时父进程是不会等待子进程结束的,需要调用 wait() 方法等待子进程结束,而调用 subprocess.run() 则会等待子进程结束。这里我们用 subprocess.run() 就行了:
xxxxxxxxxx
completed_process = subprocess.run(
constant.DEPLOYER_MAP[self.deployer],
cwd=self.path,
capture_output=True,
check=True
)
bark
至此,我们其实已经实现了代码自动部署的功能,但是我们并不知道部署是否成功。重启命令执行成功并不意味着服务重启后没问题,我们可以通过在服务中预留一个健康检查接口的方式来检查服务是否启动正常。
那如何把结果通知给我呢?使用邮件作为通知媒介是一个 比较普遍的做法。以前我也比较倾向于使用邮件,甚至还做过一个通过邮件远程监控电脑的桌面软件 Email My PC。但是对于这种即时性的通知我会更推荐使用 bark。通过 bark 可以很方便的给 iOS 设备发送消息通知。不足之处是只有 iOS 设备可以使用,以及无法查看历史消息(为了弥补这个缺陷,对于重要消息我通常会同时发送 bark 消息和邮件)。
进阶功能
一般 CD 工具都会提供诸如发布回滚、自动化测试、流程可视化以及集群部署等功能。这些就稍微有些复杂了,如果真的需要完美支持这些功能的话建议还是直接搭建一套 Jenkins 吧……