使用 grunt-ssh 将子应用分别部署到不同 bucket
解读
面试官抛出这道题,并不是想听你背诵 grunt-ssh 的 API,而是考察你在“多子应用 + 多 bucket”场景下,如何用 Grunt 做自动化部署、保证密钥安全、避免任务串扰、可灰度可回滚。国内真实环境通常还要兼顾阿里云 OSS/腾讯云 COS 的权限模型、CI 机无固定公网 IP、子应用独立版本号等痛点,因此答案必须体现“配置隔离 + 并发安全 + 失败重试 + 日志追溯”四项能力。
知识点
- grunt-ssh 与 sftp 原理:基于 ssh2 协议,sftp 上传是流式写入,大文件需分片并发控制。
- bucket 映射策略:国内云厂商 bucket 名称全局唯一,建议用“项目-环境-子应用”三段式命名,如
web-proj-pay。 - 多任务并发:Grunt 默认串行,需用 grunt-concurrent 或自定义 this.async() 实现 bucket 级并发,但并发数 ≤ 5,否则会被云厂商限流。
- 密钥托管:CI 环境禁止硬编码,通过环境变量注入,本地开发用
~/.ssh/config的 IdentityFile 指向只读 pem,权限 400。 - 差分上传:grunt-ssh 本身无 diff,需前置 grunt-newer 或自定义 md5 清单文件,实现“增量上传 + 秒级回滚”。
- 国内加速:阿里云 OSS 上传域名与下载域名分离,上传走内网 Endpoint(如 oss-cn-shanghai-internal.aliyuncs.com)可省 80% 流量费。
- 合规审计:金融类项目要求留痕 6 个月,需在 Gruntfile 里把上传日志同时写到
logs/deploy-${timestamp}.log并转存到审计 bucket。
答案
以下是一份可直接落地的 Gruntfile 片段,演示“三个子应用 → 三个 bucket”的完整流程,兼顾并发、安全、失败重试、灰度。
module.exports = function(grunt) {
// 1. 密钥与 bucket 配置完全抽离,符合国内合规要求
const cfg = {
pay: {
host: process.env.DEPLOY_HOST_PAY, // 仅 CI 可见
bucket: 'web-proj-pay',
path: '/'
},
admin: {
host: process.env.DEPLOY_HOST_ADMIN,
bucket: 'web-proj-admin',
path: '/'
},
h5: {
host: process.env.DEPLOY_HOST_H5,
bucket: 'web-proj-h5',
path: '/'
}
};
// 2. 动态生成 ssh 任务,避免手工复制
Object.keys(cfg).forEach(app => {
grunt.config.set(`sshexec.upload-${app}`, {
command: [
`ossutil cp -r -f dist/${app}/ oss://${cfg[app].bucket}/${cfg[app].path}`
].join(' && '),
options: {
host: cfg[app].host,
username: process.env.DEPLOY_USER,
privateKey: grunt.file.read(process.env.DEPLOY_KEY),
readyTimeout: 20000,
maxRetries: 3
}
});
});
// 3. 并发上传,但限制并发数 3,防止被云厂商限流
grunt.config.set('concurrent.deploy', {
tasks: ['sshexec:upload-pay', 'sshexec:upload-admin', 'sshexec:upload-h5'],
limit: 3,
logConcurrentOutput: true
});
// 4. 灰度:先上传,再切换流量
grunt.registerTask('deploy:canary', function(target) {
const done = this.async();
grunt.util.spawn({
cmd: 'ossutil',
args: ['ls', `oss://${cfg[target].bucket}/version.txt`]
}, (e, res) => {
if (e) return grunt.fail.fatal('bucket 不存在,请先创建');
// 写入灰度版本号
grunt.file.write(`dist/${target}/version.txt`, Date.now());
grunt.task.run(`sshexec:upload-${target}`);
done();
});
});
// 5. 回滚:利用 ossutil 的版本管理
grunt.registerTask('rollback', function(target, version) {
grunt.config.set(`sshexec.rollback-${target}`, {
command: `ossutil cp -r -f oss://${cfg[target].bucket}/.archive/${version}/ oss://${cfg[target].bucket}/`,
options: grunt.config.get(`sshexec.upload-${target}.options`)
});
grunt.task.run(`sshexec:rollback-${target}`);
});
grunt.loadNpmTasks('grunt-ssh');
grunt.loadNpmTasks('grunt-concurrent');
grunt.registerTask('deploy-all', ['build', 'concurrent:deploy']);
};
使用方式
本地灰度:DEPLOY_KEY=~/.ssh/deploy.pem grunt deploy:canary:pay
CI 全量:Jenkinsfile 里 sh 'grunt deploy-all'
回滚:grunt rollback:pay:20240625120000
拓展思考
- 无服务器化:如果公司全面迁到 Serverless,可改用 grunt-aws-lambda-invoke 直接触发函数计算部署,不再走 ssh,节省堡垒机成本。
- 多云容灾:国内监管要求“两地三中心”,可在 Gruntfile 里再包一层
ossutil sync --delete,把主 bucket 同步到异地容灾 bucket,RPO ≤ 15 min。 - 权限最小化:对 CI 机只授予
oss:PutObject与oss:GetObject,关闭 ListBucket 权限,防止黑客列文件。 - 大前端 monorepo:子应用数 > 20 时,用 lerna + grunt 动态生成任务,避免 Gruntfile 膨胀到上千行。
- 审计闭环:把每次上传的 etag、耗时、CI 构建号写入阿里云 SLS 日志库,对接 Grafana 大盘,实现“一键定位哪次部署导致现网白屏”。