ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Fabric自动化部署实战:基于SSH的远程命令、批量操作与失败回滚

Fabric自动化部署实战:基于SSH的远程命令、批量操作与失败回滚 不是写教程就是想把这几年部署这件事从“手忙脚乱复制命令”变成“一条命令跑完”的过程原原本本讲清楚。如果你也受够了每次上线都要SSH开好几个终端、盯着屏幕一条一条敲命令或者明明上次没问题、这次却漏了哪一步那这篇就很对你的胃口。Fabric这个名字Python圈子里的老人都熟。它是一个基于SSH的自动化部署与系统管理工具用Python写任务脚本然后通过fab命令一键执行。说白了就是把“连上服务器、拉代码、装依赖、重启服务”这一串重复劳动变成几个可复用、可回放的函数。这篇文章我会从实际场景讲起先说清楚为什么选择Fabric然后给出一套能直接抄的完整部署方案包括远程命令执行、文件上传下载、多服务器并行操作、出错回滚还有我踩过的坑。不管你是刚接触自动化部署的运维新人还是想给自己项目加料的后端工程师看完都能直接用起来。1. 为什么我会在部署中选Fabric设计思路与定位1.1 部署自动化想解决的三个痛点在没有工具化之前我的上线流程大概是这样先登录服务器进到项目目录手动git pull然后激活虚拟环境更新依赖如果有数据库变更还要单独跑迁移脚本最后重启服务还要等几秒再curl一下看有没有起来。听着不复杂但一旦服务器多了或者某种服务依赖前置条件问题就来了。第一个痛点是步骤遗漏。人不是机器三步五步可能记得住一二十步就难说了。我有一次连新服务器部署漏了创建静态资源目录结果页面能打开但所有图片都是404调试了半天才发现是本地习惯的目录结构和服务端不一致。第二个痛点是环境差异。同样一份代码本机跑得好好的一上服务器就报依赖版本不对因为本地和线上Python版本、系统库版本都不一样。第三个痛点是不可审计。手工敲过的命令没有留痕出了问题很难复盘当时到底执行了什么用的哪个版本有没有漏掉某个参数这三个痛点其实是一个共同本质部署流程是确定的、可重复的却被当成了一次性手工活。自动化的思路就是把这些确定步骤翻译成代码让机器去执行人来负责维护这个翻译结果本身。1.2 Fabric和Ansible、Shell脚本、CI/CD的关系不少人一开始会问部署自动化不是有Ansible吗不是有Shell脚本吗不是有Jenkins吗为什么还要用Fabric我先说Shell脚本。Shell脚本当然能做部署但它的问题在于每台服务器要单独SSH进去执行脚本本身不会帮你管理连接要处理多台服务器就得自己写循环和后台进程错误处理的逻辑也要完全自己实现。如果是三五台机器偶尔跑一次Shell够用但一旦涉及不同环境的分支逻辑脚本会越来越难维护。再说Ansible。Ansible是配置管理工具它用声明式Playbook描述“系统最终应该是什么状态”擅长的是基础设施层面的批量管理装软件、改配置、下发文件、管理服务状态。但Ansible有YAML的学习成本和执行机制上的抽象对于“用Python写一段带业务逻辑的部署流程”这件事反而显得笨重。CI/CD平台Jenkins、GitLab CI等更像是流程调度器它们负责在代码提交后触发流水线、跑测试、做构建。但流水线最后一步往往还是要调用某个工具去操作远程服务器——这个“操作远程服务器”的环节就是Fabric的用武之地。所以我的定位是Fabric是部署阶段的“搬运工和装配工”它把CI/CD生下来的产物以可靠的方式放到服务器上并完成启动、检查、回滚操作。换个生活化的类比CI/CD是后厨的菜单和配菜流程Fabric就是那个按顺序把菜炒好、端上桌的服务员它不一定管食材采购但保证出菜顺序不乱。1.3 Fabric的工作原理SSH连接 Python任务Fabric 2.x的架构其实很清爽底层是ParamikoPython的SSH协议库上层把SSH连接封装成Connection对象再结合Invoke的任务执行机制。可以简单理解成你写一个Python函数函数第一个参数c就是一个已经建好的远程连接。在这个连接上你调用c.run()就是在远程服务器跑命令c.put()就是把本地的文件传上去。而fab命令的任务就是找到这些函数帮你建立连接、执行函数、返回结果。这个设计相比直接写Shell脚本有个巨大的优势你可以在部署脚本里写循环、条件判断、异常处理甚至import你自己的工具库。比如需要根据日期生成备份文件名或者检测某端口通不通再决定后续步骤这些都用Python的常规语法就能完成完全不需要学另一门DSL。我会在后面的章节逐步展开连接、执行、上传这些细节先把这个基础认知立住Fabric不是银弹但它是在“远程执行命令”这个场景里最贴近程序员思维的工具。2. 环境准备与上手5分钟跑通第一个Fabric任务2.1 环境准备和安装先确认Python版本Fabric 2.x要求Python 3.8以上我建议至少3.10主要原因不是跑不起来而是Fabric依赖的cryptography、paramiko这些库在新版本上兼容性更好。安装很简单我一般建虚拟环境再装mkdir -p ~/deploy-tools cd ~/deploy-tools python3 -m venv venv source venv/bin/activate pip install fabric装完验证一下fab --version如果能看到版本号说明已经装了主要的命令行工具。Fabric的Python包会同时给你一个fab命令这个命令默认会读取当前目录下的fabfile.py或fabfile/包中的任务。这里有个容易踩坑的细节网上很多教程讲的是Fabric 1.x的写法比如from fabric.api import env, run, sudo那是老版本API2.x已经完全改了。现在官方推荐直接from fabric import task连接对象作为函数第一个参数传入。如果照着老教程写大概率会报ImportError。我也不能免俗当年就是从1.x迁移到2.x花了不少时间调整脚本逻辑。2.2 第一个fabfile.py从本地到远程在项目根目录创建一个fabfile.pyfrom fabric import task task def hello(c): print(hello from local) c.run(echo hello from remote)然后执行fab --hosts root192.168.1.10 hello你会先看到本地打印一行然后看到远程服务器执行echo的结果。这短短一个任务背后发生了这些事情fab命令读取fabfile.py找到hello这个带task装饰器的函数--hosts root192.168.1.10告诉fab要连哪台服务器函数里的c.run会在远程以root身份执行命令返回结果后打印到当前终端。这一步跑通说明SSH连接、任务注册、远程执行整个链路都是通的。接下来几乎所有的部署脚本都是在这个链路上做文章。2.3 运行任务的核心参数和传参方式实际使用中fab命令的参数比第一眼看起来更讲究。我常用的几个-H或--hosts指定目标主机支持逗号分隔多台例如-H host1,host2-u或--user指定SSH用户名也可以在hosts里带user--prompt-for-login-password交互式输入密码适合偶尔手动跑避免密码进历史记录--set覆盖配置项例如--set sudo-passwordxxx-f指定任务文件默认fabfile.py-l列出当前所有找到的任务。再说任务函数本身的参数。Fabric的任务参数可以直接从命令行传非常方便。比如task def deploy(c, branchmain): c.run(fgit pull origin {branch})运行时fab --hosts webserver deploy --branchrelease-1.2注意Fabric 2.x的命令行语法是任务名在前面任务参数用--放在后面中间没有等号。这个细节写文档时没人提醒但在命令行实际操作时挺容易搞混。2.4 工程化组织fabfile项目一复杂把几十个任务堆在一个fabfile.py里就很难读了。我的习惯是把fabfile升级成包fabfile/ __init__.py deploy.py utils.py db.py然后在__init__.py里用Invoke的Collection把各模块汇总from invoke import Collection from fabfile import deploy, utils, db ns Collection() ns.add_collection(Collection.from_module(deploy, namedeploy)) ns.add_collection(Collection.from_module(utils, nameutils)) ns.add_collection(Collection.from_module(db, namedb))这样fab -l能列出分组任务命令也可以写成fab deploy.prod这种带前缀的形式。不要小看这个组织步骤等你的部署任务增长到几十个函数没有分组就只能靠命名约定强行维护早晚会乱。3. 核心API与部署实操远程命令、文件传输、并行执行3.1 远程命令的三板斧run、sudo、localFabric 2.x里最常用的三个方法c.run()在远程执行普通命令c.sudo()在远程提权执行c.local()在本地执行。from fabric import task task def check(c): # 远程普通用户执行 result c.run(whoami, hideTrue) print(remote user:, result.stdout.strip()) # 远程提权执行需要sudo密码 sudo_result c.sudo(cat /var/log/nginx/access.log | tail -3, hideTrue) print(sudo_result.stdout) # 本地执行比如打包代码 c.local(tar czf release.tgz ./app, hideTrue)hideTrue的作用是让命令的输出不再直接打到屏幕上而是收进result.stdout里方便你写逻辑判断。比如健康检查就靠这个把curl输出捕获下来判断里面是不是包含了预期内容。sudo的处理是个高频纠结点。在生产环境我更推荐给部署账号配置sudo免密但权限精细控制只在指定命令上放行。如果实在要密码可以用Invoke的Responder来自动应答from invoke import Responder task def update_cache(c): responder Responder( patternr\[sudo\] password for, responsec.config.sudo.password \n, ) c.sudo(apt-get update, ptyTrue, watchers[responder])这个Responder会监听终端输出当出现“password for”的提示时自动把密码输入进去。不过要注意密码不要直接写在代码里优先从环境变量读取或者用--set sudo-passwordxxx传入避免顺手把密钥提交到仓库。3.2 文件传输put、get与动态配置生成部署不只是跑命令还要把文件传上去。最典型的就是配置文件。c.put()把本地上传到远程c.get()反过来下载。举几个实际场景场景一上传nginx配置task def upload_nginx(c): c.put(deploy/nginx.conf, /tmp/nginx.conf) c.sudo(mv /tmp/nginx.conf /etc/nginx/conf.d/myapp.conf) c.sudo(nginx -t) c.sudo(systemctl reload nginx)注意先传到/tmp再mv为什么因为直接put到/etc/nginx/conf.d/下面如果目标目录权限受限或者路径不存在会直接报错。先放到/tmp再sudo mv既避免了权限问题又给nginx -t留了一个检查机会。场景二动态生成配置文件。很多时候每个环境的配置内容不一样比如数据库地址、域名、密钥。我不建议在服务器上手工改配置文件而是用Python的字符串模板生成后上传from io import StringIO import os ENV_TEMPLATE \ SECRET_KEY{secret} DATABASE_URL{db_url} DEBUG0 task def upload_env(c): content ENV_TEMPLATE.format( secretos.environ[APP_SECRET_KEY], db_urlos.environ[DATABASE_URL], ) c.put(StringIO(content), /var/www/myapp/.env) c.sudo(chown appuser:appuser /var/www/myapp/.env)这个模式的好处是服务器上不会出现一个人一个样的配置文件所有环境差异都收口在模板里改一处就是改全部。c.get()我一般用来拉日志或备份数据库文件task def backup_db(c): c.run(mysqldump mydb /tmp/backup.sql) c.get(/tmp/backup.sql, fbackups/mydb-{datetime.date.today()}.sql)3.3 多服务器批量操作SerialGroup和ThreadingGroup部署两三台服务器一台一台跑也行但如果有八台十台逐个等就很浪费时间。Fabric提供了两个组合类SerialGroup串行批量和ThreadingGroup线程并行批量。from fabric import SerialGroup, ThreadingGroup # 串行一台接一台 group SerialGroup(web1.example.com, web2.example.com) group.run(uptime) # 并行多台同时执行 group ThreadingGroup(web1.example.com, web2.example.com, web3.example.com) group.run(hostname)实际部署中我不建议一开始就让所有机器并行重启。记得某次我用ThreadingGroup一次性重启了六台Nginx结果由于服务起动时间不一致部分机器还在恢复期就被后面命令调到健康检查误报了一堆告警。后来我改成“先一台等健康检查通过再启动其余服务器”的灰度逻辑过程安静了很多。如果需要在任务函数里处理多台服务器还有一个更优雅的写法直接给task指定hosts列表task(hosts[web1, web2, web3]) def deploy_all(c): with c.cd(/var/www/myapp): c.run(git pull)这时函数里的c会自动绑定到hosts列表中的每一台主机fab会依次执行。配合--parallel参数还能并行。3.4 错误处理与回滚设计默认情况下Fabric检测到远程命令返回非零退出码会立刻抛异常中断执行。这其实是好事比Shell脚本“命令失败但脚本还在蒙头跑”要安全得多。但有些场景不想让它直接抛错比如健康检查curl失败一次不能就崩了应该重试几轮。这时候用warnTruetask def health_check(c): result c.run( curl -s -o /dev/null -w %{http_code} http://127.0.0.1:8080/health, warnTrue, hideTrue, ) if 200 not in result.stdout: raise SystemExit(health check failed)生产中真正实用的回滚不是简单把异常吞掉而是做“备份-变更-失败恢复”三段式。以数据库迁移为例task def migrate_with_rollback(c): backup_file f/tmp/myapp_backup_{datetime.datetime.now():%Y%m%d%H%M%S}.sql c.run(fmysqldump myapp {backup_file}) try: c.run(python manage.py db upgrade, warnTrue) except Exception: c.run(fmysql myapp {backup_file}) raise SystemExit(migration failed, restored from backup)这里的思路是变更是有风险的操作跑之前先留一个可恢复的底稿变更失败时用底稿还原现场然后中止后续流程。不要等出了问题再想着“手工处理一下”越慌越容易出错事先写进代码里才是正解。4. 完整部署流程实战用一个Flask应用把流程串起来4.1 场景设定与服务器规划从零讲配置很虚我拿一个具体场景来串假设有一个Flask应用用Gunicorn启动由systemd管理代码托管在Git仓库。服务器假设是一台Ubuntu 22.04部署用户叫deploy项目路径是/var/www/myapp。整个部署流程的目标是本地执行fab deploy后服务器自动完成代码更新、依赖安装、数据库迁移、服务重启、健康检查。整个过程不需要人再介入。为什么要这个顺序代码更新放最前面因为依赖变更和迁移都是基于新代码的依赖安装在迁移之前因为迁移脚本可能用到新的第三方库数据库迁移要放在重启服务之前否则服务起米后可能带着旧数据结构去连数据库分分钟报错。这个顺序是我踩过几次坑才固定下来的。4.2 部署步骤拆解从拉代码到健康检查第一步拉取最新代码with c.cd(/var/www/myapp): c.run(git pull origin main)with c.cd()相当于在远程会话中切目录注意它不是一个持久的shell状态而是在这条run命令前自动补上cd的前缀。这个行为很多人会误解以为进入目录后会一直生效其实不是每条run都是独立shell必须用cd块包起来才有效。第二步安装依赖with c.cd(/var/www/myapp): c.run(./venv/bin/pip install -r requirements.txt)我用项目内的虚拟环境路径而不用系统Python原因很简单系统Python是系统库和业务库共用的一旦升级系统包就可能弄坏业务依赖。用venv隔离部署回滚也更方便旧版本的虚拟环境不受影响。第三步数据库迁移with c.cd(/var/www/myapp): c.run(./venv/bin/python manage.py db upgrade)如果应用没有迁移脚本这一步可以跳过但只要是数据库结构有演进我都会把它独立成一个任务方便单独执行。第四步重启服务并等待健康检查c.sudo(systemctl restart myapp) c.run(sleep 3) # 最多重试5次每次间隔2秒 for i in range(5): result c.run( curl -s -o /dev/null -w %{http_code} http://127.0.0.1:8080/health, warnTrue, hideTrue, ) if result.stdout.strip() 200: print(health check passed) break if i 4: raise SystemExit(service not healthy after restart) c.run(sleep 2)这里判断的是HTTP 200更严格的话还可以检查响应体内容比如JSON里的status字段。curl的重试逻辑其实就是“readiness probe”自己实现比依赖外部探针更灵活而且完全在代码里可读。4.3 敏感信息处理密钥、密码和环境变量部署必然涉及敏感信息这是我的原则代码仓库里只留模板不碰真实值。首先是服务器的SSH登录。最好的方式是用SSH密钥而不是密码。在你的本地机器上生成密钥后把公钥加到服务器的~/.ssh/authorized_keys里然后指定密钥文件fab --hosts deployserver -i ~/.ssh/deploy_key deploy其次是sudo密码。前面说过可以用Responder自动应答但更稳妥的做法是给deploy用户配置部分命令的sudo免密比如deploy ALL(ALL) NOPASSWD: /usr/bin/systemctl restart myapp, /usr/bin/systemctl reload nginx这样既不需要保存sudo密码又不至于给整个账户完全放开sudo权限。第三是应用本身的敏感配置。比如数据库连接串、加密密钥我统一放到环境变量里部署脚本运行时从本地环境读取再通过StringIO模板渲染上传而不是把真实值写死。本地环境变量可以放到.env文件里并严格加入.gitignore。4.4 完整脚本deploy、reload、rollback把上面的步骤整合成一个完整的fabfile.pyfrom io import StringIO import os import time from fabric import task REMOTE_DIR /var/www/myapp HEALTH_URL http://127.0.0.1:8080/health ENV_TEMPLATE \ SECRET_KEY{secret} DATABASE_URL{db_url} DEBUG0 task def upload_env(c): content ENV_TEMPLATE.format( secretos.environ[APP_SECRET_KEY], db_urlos.environ[DATABASE_URL], ) c.put(StringIO(content), f{REMOTE_DIR}/.env) c.sudo(fchown deploy:deploy {REMOTE_DIR}/.env) task def update_code(c): with c.cd(REMOTE_DIR): c.run(git pull origin main) task def install_deps(c): with c.cd(REMOTE_DIR): c.run(./venv/bin/pip install -r requirements.txt) task def migrate(c): with c.cd(REMOTE_DIR): c.run(./venv/bin/python manage.py db upgrade) task def restart(c): c.sudo(systemctl restart myapp) task def health_check(c): for i in range(5): result c.run( fcurl -s -o /dev/null -w %{{http_code}} {HEALTH_URL}, warnTrue, hideTrue, ) if result.stdout.strip() 200: print(health check passed) return time.sleep(2) raise SystemExit(service not healthy) task def rollback(c): with c.cd(REMOTE_DIR): c.run(git log -1 --oneline) c.run(git checkout -f HEAD~1) c.sudo(systemctl restart myapp) task def deploy(c): update_code(c) install_deps(c) upload_env(c) migrate(c) restart(c) health_check(c)rollback这个任务我没写得特别复杂核心是切到上一个提交并重启服务。生产环境的回滚还可以做得更精细比如打好版本tag或者通过符号链接切换版本目录但思路一样快速恢复到上一个可用状态服务恢复优先于一切。4.5 执行演示与结果解读执行过程长这样fab --hosts deploy192.168.1.10 -i ~/.ssh/deploy_key deploy如果一切顺利你会看到[192.168.1.10] Executing task deploy [192.168.1.10] run: git pull origin main [192.168.1.10] out: Already up to date. [192.168.1.10] run: ./venv/bin/pip install -r requirements.txt [192.168.1.10] out: Requirement already satisfied... [192.168.1.10] run: python manage.py db upgrade [192.168.1.10] run: systemctl restart myapp [192.168.1.10] run: curl ... health check passed整个过程不到一分钟。此时你想验证服务器状态可以直接在远程跑curl -I http://127.0.0.1:8080/看到200或者systemctl status myapp看到active (running)。这个流程虽然以Flask为例但框架本身不是重点。Gunicorn、Nginx、uWSGI、甚至Django项目结构完全一样更通用一点只要是“代码变更-依赖变更-服务重启”这种形态都能照这个模板改。5. 常见问题与排查技巧实录5.1 高频问题速查表我整理了这些年用Fabric遇到过的高频问题按频率排个序现象可能原因解决方法fab命令找不到Python环境没激活或安装失败which fab确认在venv下重装pip install fabricNo hosts matched命令行没传-H或任务没配置hosts补上--hosts userhost或在task(hosts[...])指定Authentication failed用户名写错、密钥不对或账号权限受限检查authorized_keys用ssh -i key userhost手动验证Could not connect to server主机IP/端口不对、防火墙或网络不通先手动ssh测试再看安全组和SSH端口sudo: no tty presentsudo需要终端但pty没开启run中用ptyTrue或给部署账号配sudo免密put报No such file or directory远程目标目录不存在先执行c.run(mkdir -p 目录)再put命令执行了但没效果部署目录不对或服务没重启用pwd和ls -l确认位置确保restart执行成功中文乱码终端编码和服务端语言环境不一致设置LC_ALLC.UTF-8或代码里做编码转换5.2 排查思路与调试技巧遇到问题不要慌我一般按这个顺序排查第一步先验证最基础的链路。直接在本地用ssh命令连一遍目标服务器确认用户名、密钥、网络都没有问题。Fabric只是包装了SSHSSH本身不通的话再怎么调脚本都没用。第二步给任务加上可见的输出。在任务里临时用echoTrue或hideFalse让Fabric把执行的每一条命令和输出都打出来。很多部署问题就是命令和参数没对齐比如目录路径少了个斜杠、分支名不对等日志一眼就能看出来。第三步用dry-run检查参数解析。fab --dry-run -H host deploy --branchmain可以只看任务函数名和参数不会实际执行。如果发现参数没传进去多半是命令行命名规则写错了。第四步逐步拆任务。把完整deploy拆成fab update_code、fab migrate这种子任务先一个个单独跑确认每个任务都没问题再合起来跑完整流程。尽量不要一开始就全链路调试不然出错也分不清是哪个环节挂了。5.3 避坑经验最后分享几个我踩过多次的坑都是代码之外才能体会到的。第一个坑不要在fab脚本里写死服务器路径。换个环境部署就要改代码那是给自己找麻烦。我现在的做法是路径写成配置项支持命令行覆盖比如fab --set remote-dir/opt/myapp deploy这样测试环境和生产环境用同一套脚本只是参数不同。第二个坑大量文件传输时别一个一个put。Fabric的put是基于SFTP的传很多小文件时开销很高。我传前端构建产物时习惯先在本地tar再put到服务器上远程再解压快好几倍task def upload_dist(c): c.local(tar czf dist.tgz -C dist .) c.put(dist.tgz, /tmp/dist.tgz) with c.cd(REMOTE_DIR): c.run(sudo -u appuser tar xzf /tmp/dist.tgz -C static)第三个坑sudo里用Responder时注意密码提示的pattern要匹配准确。有时候因为终端输出带了转义符pattern匹配不上代码里就卡在那。这种情况我宁愿配nopasswd或者用--set sudo-password传环境变量也别在代码里写死了正则维护成本很高。第四个坑远程shell源文件和换行符问题。如果项目里有Windows下编辑过的脚本传到服务器上可能会因为\r导致bad interpreter: /usr/bin/python3^M这样的错误。写部署脚本时所有脚本文件尽量统一用LF换行或者上传后做一个sed -i s/\r$//清理这个坑特别隐蔽。第五个坑多主机并行部署时注意超时控制。ThreadingGroup虽然方便但如果其中某台机器网络缓慢整个任务的完成时间会被那台拖住。建议给连接设置合理的连接超时比如Connection(host, connect_kwargs{connect_timeout: 10})或者干脆在代码里限制每台机器的执行时间。结尾说到底Fabric给我的最大的改变不是省了多少手工操作而是我终于敢把部署这件事当成代码来对待可以Review、可以测试、可以回滚。以前最怕上线现在反而觉得出问题了先跑一次deploy的rollback大部分情况都能稳住。如果你准备在自己的项目里上自动化部署我建议不要一上来就写一个几百行的完整脚本。先从一个最简单的任务开始比如fab restart只是帮你远程重启一下服务然后慢慢加代码更新、加依赖安装、加健康检查。等这些原子任务都有了再组合成一个deploy流水线——这个过程本身就是一次很好的项目管理练习。最后分享一个小技巧把常用的服务器列表和参数放进Invoke的配置文件比如invoke.yaml再配合任务分组你最终的命令可能短到只是fab prod deploy到这一步部署就从“操作步骤”变成了“一个按钮”。希望这篇文章也能帮你走到这一步。
返回列表