
这两年经手的不少项目都是“一台物理服务器上开一堆虚拟机每台虚拟机跑一个独立服务”的状态。VMware Workstation里挂三五台Linux开发机是常态开发、测试、演示全依赖这批虚拟机。但很长一段时间里我们的上线流程却停留在“手动复制文件再重启服务”的原始操作上改几行代码先scp到临时目录再ssh上去kill旧进程、替换文件、把服务拉起来一晚上能重复好几遍。标题里的“虚拟机-持续部署流水线最简工具yunedit-ssh”就是我从这个痛点里憋出来的一个偷懒方案——只用一个轻量工具通过SSH通道把本地代码推到虚拟机在远程触发部署命令完整走完一条最简单的持续部署流水线。这个方案适合谁适合个人开发者、小团队适合还在用虚拟机跑原型、跑测试环境、跑内部系统的人。你不一定需要一套完整的CI/CD平台但你需要“代码更新之后系统自动替换、自动重启”的稳定体感。yunedit-ssh解决的问题就是在不引入额外复杂中间件的前提下把代码同步、远程执行、服务重启这几个动作串成一个可复现、可自动触发的流水线。1. 为什么虚拟机场景需要一条“最简”持续部署流水线1.1 虚拟机上的服务部署最痛的不是装环境很多人一提到虚拟机就想到安装、配置、克隆镜像觉得把Linux装起来、软件跑起来就算完事。但真正到了维护阶段部署环节会暴露最多问题。虚拟机项目有几个特点第一它通常不止一台同一台物理机上可能并排跑着好几台虚机每台环境还不完全一样第二它对应的服务往往迭代频率很高尤其是演示环境和联调环境基本是早上改完下午就要上第三虚机网络层多了一层虚拟化手动部署时经常出现“文件传上去了但服务没起来”“端口被占了”“配置文件忘了改时区”之类的问题。这些问题的根子其实是部署过程没有沉淀成固定的、可重复的操作。手动部署的优势是灵活但代价是每个人的操作方式和顺序都不一样。今天你记得先传文件再重启明天他可能只传了文件忘重启后天又有人把整个目录权限改乱了。持续部署流水线的本质不是让你“自动化”三个字看起来很酷而是把人的不确定性从部署过程里拿掉。1.2 为什么我没有直接上 Jenkins 或 GitLab CI也许你会问持续部署用现成平台不就行了我一度也这么想但试过之后发现在纯虚拟机环境下引入一套完整CI/CD平台代价比收益大。首先是资源问题。跑Jenkins要额外占一台虚机的内存和磁盘很多开发机配置并不富裕多一个常驻Java服务可能就吃掉1G内存。其次是一致性问题。CI平台通常在自己的构建机上拉代码、跑脚本、出产物再把产物推给目标机器。如果构建机Windows的工具链和目标机Linux不一致反而要花大量时间适配。最现实的问题还是维护成本团队里如果有两三个人大家都要学习插件的安装配置出问题后排查链路非常长。所以我当时的判断是虚拟机和目标服务器是同一台机或者虚拟机就是目标环境的直接承载者这中间的部署距离根本没有长到需要一套CI/CD平台来管。我真正需要的是三个能力把本地产物传上去、在远程执行命令、能定时或收到通知后自动干这件事。这三个能力一条SSH链路就够用了。1.3 yunedit-ssh 的定位SSH 是虚拟机唯一必需的通道这也解释了yunedit-ssh这个工具为什么叫这么个名字。它本质上是一个CLI工具内部封装了两类操作一是通过SSH协议把本地目录或文件增量同步到远程虚拟机二是在同步完成之后按配置顺序在远程执行若干部署命令。整个过程里没有任何额外端口、额外服务也不需要专门的监控代理只要虚拟机能SSH连接就够了。我觉得这是它在虚拟机场景下的最大价值——SSH是Linux虚拟机默认就有的能力几乎不需要额外配置。而持续部署最核心的“推送代码”和“拉起服务”两个动作全都能基于SSH完成。工具不需要“驻留”在虚拟机里不需要常驻进程用完即走。这也意味着只要你的虚拟机还能通过SSH登录这个流水线就始终可用。2. 动手前先搭好的虚拟机基础环境2.1 网络模式与固定IP决定了流水线能不能找到目标很多流水线搭到一半突然跑不通问题就出在虚拟机网络环境上。如果你的虚机使用NAT模式那它通常只有一个虚拟机内部的私有地址对外访问受限而且DHCP分配的IP还可能随重启变化。持续部署的首要前提是“本地机器能稳定访问到目标的IP和端口”所以网络模式建议优先考虑桥接模式或者至少要设置端口映射。这里有个实用对照我按常见的使用方式梳理一下网络模式能否被宿主机/局域网直接访问典型问题NAT默认不能直接访问需配置端口映射每次部署地址不固定配置复杂桥接可以虚拟机像局域网内独立主机需要物理路由器可用IP需确认IP未冲突仅主机仅宿主机能访问不能用于外部演示环境我自己的习惯是开发机用NAT但会手动把虚拟机的IP绑定成固定地址而真正对外演示或准备长期使用的虚拟机直接改桥接分配一个固定IP。不管哪种模式一定要确保虚拟机IP不会因为物理机重启而漂移。做完这一步后续的SSH配置才有意义。2.2 安装并开启 SSH 服务设置专用部署用户目标虚拟机至少要具备SSH服务端。Ubuntu/Debian系列用apt install openssh-server就行CentOS/RHEL系列默认就带sshd安装完记得systemctl enable --now sshd。这一步别偷懒一定要确认端口是通的再继续。我不建议直接用root账号跑部署。生产环境里root权限过大出问题不好回溯而日常开发环境如果你习惯用root很容易在部署脚本里写出用root才跑得通、换普通用户就报错的命令。更合理的做法是创建一个专用部署账号比如deployer然后把sudo权限收紧到必需的命令范围内。这样做既安全也逼着自己把部署脚本写得规范一点。# 在虚拟机上创建 deployer 用户并加入到 sudo 组以 Ubuntu 为例 sudo adduser deployer sudo usermod -aG sudo deployer2.3 配置 SSH 密钥并写好本地 config搭建虚拟机和本地的 SSH密钥认证 时很多人习惯记密码但密码登录在自动化部署里就是个灾难。因为流水线脚本没办法每次手动输入密码总得借助sshpass这类工具把明文密码存到配置里既不安全体验也很差。我会先在本机生成一对专用的部署密钥然后把公钥写到虚拟机的authorized_keys文件里。ssh-keygen -t ed25519 -f ~/.ssh/id_deploy -C cd-vm ssh-copy-id -i ~/.ssh/id_deploy.pub deployer10.20.3.15这里有个细节~/.ssh目录权限要设置为700authorized_keys、私钥文件的权限要设置为600。权限过宽时SSH会直接拒绝加载密钥。另外给不同的虚拟机准备不同的密钥文件后续加机器时管理起来会清晰很多。在~/.ssh/config里加一段别名配置部署时连端口、用户、密钥都不用额外指定Host vm-staging HostName 10.20.3.15 User deployer Port 22 IdentityFile ~/.ssh/id_deploy2.4 VMware 场景下的几个前置坑搜热词时看到不少人遇到“vmware workstation无法连接到虚拟机。请确保您有权运行该程序、访问该程序”这类提示我在这也要提醒一句在VMware里跑持续部署虚拟机的状态必须稳定。如果虚拟机是在VMware中“已暂停”的状态SSH是连不通的流水线必然报错。另一个高频坑是宿主机重启之后虚拟机的IP变了导致流水线找不到目标。解决办法是上面说的固定IP绑定或者在/etc/hosts、虚拟网络编辑器里做好静态映射。还有一点如果你是在VMware里克隆出来的虚拟机克隆之后一定要重配网卡和hostname不然可能出现两台虚机IP冲突部署内容被推送到错误的机器上去。这个坑我踩过一次现象就是代码经常更新到旧虚拟机新虚拟机一直没更新排查了半小时才发现两台机器用的IP是相同的。3. 用 yunedit-ssh 搭一条最简 CD 流水线3.1 把工具装到本机yunedit-ssh是一个单文件CLI工具没有任何运行时依赖也不需要独立安装服务端。它在本地工作通过SSH协议和远程虚拟机交互。安装方式很简单下载对应平台的二进制文件放到系统PATH目录下重开一个终端即可使用。mv yunedit-ssh /usr/local/bin/ chmod x /usr/local/bin/yunedit-ssh yunedit-ssh --version为什么强调单文件因为虚拟机的部署环境经常需要在不同电脑之间切换单文件工具容易复制、上传、集成到现有脚本里不用考虑依赖库冲突。尤其对小团队来说一个人装了工具就能把整个部署流程跑起来学习门槛很低。3.2 编写 .yunedit.yml 配置文件yunedit-ssh的核心用法是先用一个YAML配置文件描述部署规则然后执行yunedit-ssh deploy。这样设计的好处是配置即记录谁来执行行为都是一样的。下面是我一个前端项目用的最小配置# .yunedit.yml host: vm-staging port: 22 user: deployer key: ~/.ssh/id_deploy sync: local: ./dist remote: /srv/webapp exclude: - .git - *.log - node_modules deploy: - cd /srv/webapp npm ci --omitdev - systemctl restart webapp这个配置文件里要特别注意两个部分。第一是sync块它定义“本地哪个目录推到虚拟机哪个目录”exclude列表用来排除日志、依赖目录、临时文件避免把一堆无用垃圾传到服务器。第二是deploy块它按顺序执行远程命令一般包含依赖安装、配置生成、服务重启三个动作。3.3 第一次执行部署命令配置写好后直接跑yunedit-ssh deploy -c .yunedit.yml工具会先检查SSH连通性然后执行目录同步一般输出里能看到每个文件的对比和传输结果。等同步完成就会看到deploy列表里的命令一条条被执行。我习惯把日志实时打到终端同时重定向到本地文件排查问题时有据可查。[10:12:01] connect vm-staging ... ok [10:12:02] sync ./dist - /srv/webapp [10:12:05] sync 128 files, 0 skipped [10:12:05] run: cd /srv/webapp npm ci --omitdev [10:12:30] run: systemctl restart webapp [10:12:31] deploy finished这次执行其实就是整条持续部署流水线的手动版。手动执行的意义很大因为自动触发之前你至少要验证一遍配置和脚本是否正确。等这一步稳定之后再叠加自动触发机制才不会把错误批量暴露出来。3.4 把部署接到“代码提交”这个动作上最简的自动触发方式其实就是Git的post-merge或post-commit钩子。以Git仓库为例在本地仓库的.git/hooks/post-commit里加一行执行命令就可以在每次提交后自动触发部署#!/bin/sh yunedit-ssh deploy -c .yunedit.yml注意要让钩子文件可执行chmod x .git/hooks/post-commit。如果是团队协作所有人拉到代码后不会每个人都配好钩子这就要求代码库里放一个deploy.sh脚本里面调用同一份配置然后引导大家手动执行脚本。更稳妥的做法是结合定时任务在开发机上设置一个每5分钟跑一次的cron调用yunedit-ssh deploy有更新就同步没有更新就跳过。这样团队任何一个人push代码几分钟内虚拟机上的演示环境就会自动更新。4. 关键参数与部署脚本的核心细节4.1 同步策略增量优先别每次全量覆盖持续部署如果每次把整个目录重新传一遍很快你就会嫌它慢。yunedit-ssh内部对目录同步做了增量处理默认只传输变化过的文件减少网络开销。但增量同步也有前提文件时间戳和大小要可靠。如果你本地的构建工具每次生成的文件时间戳都不稳定建议在配置里开启基于内容校验的同步方式否则可能漏传内容变了、但大小和时间没变化的文件。同步时还要用exclude把绝对不需要传到虚拟机的目录排除掉常见的有.git、node_modules、.idea、*.log。.git目录经常占几十兆完全没必要上传node_modules也该在虚拟机上通过npm ci重新生成而不是从Windows或macOS同步过去否则很容易出现平台相关的二进制兼容问题。4.2 部署脚本必须做到“可以反复执行”很多人写部署脚本时默认服务是第一次部署的全新状态结果再执行一次就报错。比如mkdir目录已经存在npm install重复执行systemctl restart又依赖前序命令的成功退出码。真正稳定的部署脚本应该具备幂等性——无论执行一次还是十次最终的系统状态都一致。我常用的一套姿势是先用-p或--parents保证目录存在不报错命令之间用连接只要有一步失败就中断所有中间产物放到固定的临时目录最后再原子性地替换正式目录。以Node项目为例set -e cd /srv/webapp npm ci --omitdev npm run build systemctl restart webapp脚本里的每一条命令在部署日志里都要能看到退出码这样出了问题才能知道具体是哪一步挂了。这一点看起来不起眼但真的能为后续省下大量排查时间。4.3 回滚方案不要实时覆盖正式目录持续部署流水线的最大隐患是部署了一个坏版本想退回上一个版本却发现现场已经被覆盖了。我建议在虚拟机里用“版本目录软链接”的方式组织部署目录。每次同步和构建都生成带时间戳的新目录然后让服务指向最新的软链接。# 部署脚本示例 DEPLOY_DIR/srv/webapp/releases/$(date %Y%m%d%H%M%S) ln -s -T $DEPLOY_DIR /srv/webapp/current这样如果新版本启动失败回滚只需要把软链接指向上一个版本然后重启服务即可。yunedit-ssh本身不强制这种结构但你的部署命令里完全可以加入这些动作让流水线从“能自动部署”升级成“能安全地自动部署”。4.4 连接参数和代理设置别忽略超时虚拟机部署偶尔会因为网络抖动而失败所以SSH连接需要合理的超时设置。yunedit-ssh默认支持在配置里提供连接超时、命令执行超时以及SSH Agent转发开关。如果虚拟机所在网络比较慢建议把连接超时放到10到15秒如果本地使用跳板机或需要经过有代理的网络还要检查SSH的代理配置保证本地流量可以到达虚拟机的22端口。另外在哪台机器上执行部署也值得思考。通常从笔记本直接部署到虚拟机OK但如果有两台开发机需要轮流部署最好把部署命令和执行环境固定到一台“部署入口机”上避免两边配置不一致。流水的配置方式永远比靠人脑记得干净。5. 高频问题与排错实录5.1 连接被拒绝或超时最常见的情况是虚拟机没开机或者SSH服务没启动。物理机重启后虚拟网卡没加载也会导致IP连不上。先在本地ping虚拟机IP能通再继续。如果ping能通但ssh -p 22 userhost报Connection refused大概率是sshd没起来或者监听端口不是22。这个排查顺序能让你快速定位是网络问题还是服务问题而不是一上来就怀疑工具配置。5.2 密钥权限导致认证失败系统日志里常见的报错是Permissions 0644 for id_deploy are too open。原因基本是私钥文件权限太宽SSH拒绝使用。修一下权限即可chmod 600 ~/.ssh/id_deploy chmod 700 ~/.ssh还有一种是公钥没有追加到虚拟机的authorized_keys里尤其当你手动复制时换行符或者属主不对SSH也会认证失败。用ssh-copy-id是最稳的它会自动处理权限和追加逻辑。5.3 文件同步了但服务没正常启动这类问题最坑因为它不会直接报“同步失败”而是表现为“服务状态异常”或“容器起不来”。最常见的原因有三个一是部署脚本里缺了必要的环境变量二是远程目录的文件属主不是运行服务账号导致无权限创建临时文件三是服务本来就在依赖某个旧文件路径同步后路径变了。我的习惯是在部署命令里增加一行systemctl --no-pager status或健康检查命令提前暴露问题。5.4 虚拟机IP变了流水线失联之前提过宿主机重启后虚拟机IP漂移是虚拟机场景最典型的问题。如果每次开机IP都变可以把虚拟网络配置从自动获取改成静态IP或者在~/.ssh/config里把 HostName 改成域名通过DNS解析获取地址。遇到“刚才还能连突然连不上”的情况时先确认虚拟机是否因为系统升级自动重启了再看VMware里虚拟机的IP是否改变不要一上来就重装环境。5.5 常见问题速查表症状可能原因处理方式Connection refusedsshd未启动/端口不对检查虚拟机sshd状态和监听端口Permission denied密钥权限过宽或公钥未配置修正密钥权限重新ssh-copy-id同步慢大目录未排除增加exclude排除依赖目录与日志服务未重启部署脚本执行中断用set -e检查每步退出码部署成功但页面无变化同步目录与服务目录不对应检查remote路径和软链接是否指错VMware连接失败虚拟机关机或用户权限不足确认虚拟机已开机检查当前账号权限6. 我实际用下来的体会与后续扩展这套“虚拟机 yunedit-ssh”的组合目前在我这边承担了至少三套演示环境的日常发布。每到一个新项目先把虚机和SSH环境搭好写一个.yunedit.yml再把部署钩子加到Git操作上整个流程半小时内能跑通。相比Jenkins那套体系它最大的优势就是拿起来就能用不用维护额外服务最大的劣势则是缺少对构建过程的可视化追踪不适合几十人团队沉淀复杂的发布审批流程。如果你也想把这个方案延伸一步可以在后面加一个webhook服务当Git仓库收到push事件时由webhook调用yunedit-ssh deploy这样就不依赖本地Git钩子也能在团队成员没有配钩子的情况下完成自动部署。再往后如果想做多虚拟机批量发布用循环批处理调用同一个配置模板就可以。我个人没有把部署做成复杂系统的习惯。越简单的流水线越容易稳定运行虚拟机上留太多常驻服务往往是故障的源头。这次踩过坑之后我也意识到一个更通用的道理部署工具的核心价值不是功能多而是让眼前这条部署链路可靠、可查、可复现。yunedit-ssh就是个只做SSH这一件事的小工具但正是这种克制让它成了我日常开发里用得最稳的部署手段。