这两年经手的不少项目,都是“一台物理服务器上开一堆虚拟机,每台虚拟机跑一个独立服务”的状态。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 deployer@10.20.3.15这里有个细节:~/.ssh目录权限要设置为700,authorized_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 --omit=dev" - "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 --omit=dev [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 --omit=dev 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 user@host报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 refused | sshd未启动/端口不对 | 检查虚拟机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这一件事的小工具,但正是这种克制,让它成了我日常开发里用得最稳的部署手段。