服务器没配 SSH 密钥,怎么部署和运维?阿里云的云助手(ECS RunCommand)是答案:命令经 OpenAPI 下发,由机器上的 agent 执行。听起来简单,实际用起来我踩了六个坑——每个都附真实命令,拿走就能用。
坑 0:先验证身份和权限
aliyun sts GetCallerIdentity --auto-plugin-install false
# 期待:AccountId + Arn,退出码 0。一切操作前先跑这个
坑 1:InstanceId 是 RepeatList,不是普通参数
# ❌ MissingParam.InstanceId —— 传了但不认
aliyun ecs RunCommand --InstanceId i-2ze7... --Type RunShellScript ...
# ✅ 列表参数要带序号
aliyun ecs RunCommand --RegionId cn-beijing \
--InstanceId.1 i-2ze7t8fmi6ap6ke2rylr \
--Type RunShellScript \
--CommandContent "$(printf '%s' "$SCRIPT" | base64)" \
--ContentEncoding base64 \
--Name my_task --Timeout 120
# → {"InvokeId": "t-bj0709..."} 用它查结果
报错信息只说"参数缺失",不提示格式——这类 RepeatList 参数(.N 后缀)是阿里云 CLI 的常见暗礁。
坑 2:执行器是 dash,不是 bash
第一版脚本开头写了 set -euo pipefail,直接失败:
/usr/local/share/aliyun-assist/work/script/t-xxx.sh: 1: set: Illegal option -o pipefail
云助手用 /bin/sh(Ubuntu 上是 dash)执行。修法:脚本只用 POSIX 语法(set -eu,不要 pipefail;管道后取 $? 判断)。
坑 3:命令要 base64,输出也是 base64
传入:--ContentEncoding base64 + printf '%s' "$SCRIPT" | base64(用 printf 不用 echo,避免尾部换文差异)。取回:
aliyun ecs DescribeInvocationResults --RegionId cn-beijing \
--InvokeId t-bj0709... | python3 -c "
import sys, json, base64
r = json.load(sys.stdin)['Invocation']['InvocationResults']['InvocationResult'][0]
print('exit:', r['ExitCode'], r['InvocationStatus'])
print(base64.b64decode(r['Output']).decode())"
Output 字段是 base64 的,不解码你只会看到一串乱码。
坑 4:heredoc 写文件,$ 要转义
通过云助手写 nginx 配置,$uri 这类 nginx 变量会被 shell 展开:
cat > /etc/nginx/sites-available/blog.conf <<'NGINX' # 引号锁住,不展开
server {
location / { try_files $uri $uri/ =404; }
}
NGINX
heredoc 定界符加单引号是最省事的锁;不加引号时就要逐个 \$uri 转义(我第一版就这么写的,能跑但难读)。
坑 5:分页拉全量资源
资源中心 SearchResources 有 NextToken 分页,一条 while 循环拉完:
TOKEN=""; PAGE=1
while :; do
if [ -z "$TOKEN" ]; then
RESP=$(aliyun resourcecenter SearchResources --MaxResults 100 \
--endpoint resourcecenter.aliyuncs.com)
else
RESP=$(aliyun resourcecenter SearchResources --MaxResults 100 \
--NextToken "$TOKEN" --endpoint resourcecenter.aliyuncs.com)
fi
echo "$RESP" >> raw.json
TOKEN=$(echo "$RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('NextToken',''))")
[ -z "$TOKEN" ] && break
done
注意 resourcecenter 要显式传全局 endpoint,默认地域的 endpoint 是查不到的。
坑 6:文件分发走 OSS 内网签名 URL
云助手命令有 16KB 上限,大文件(如 5.5MB 的 JS)塞不进去。解法:OSS 中转。
# 本地:上传 + 生成内网签名 URL(签名是本地计算,不需要能访问内网)
aliyun oss cp site.tar.gz oss://my-bucket/ --endpoint oss-cn-beijing.aliyuncs.com
URL=$(aliyun oss sign oss://my-bucket/site.tar.gz \
--endpoint oss-cn-beijing-internal.aliyuncs.com --timeout 3600 | head -1)
# 服务器上:curl 内网拉取,免流量费、秒级
组装:一个可靠的执行模板
run_remote() {
local name=$1 script=$2
local inv=$(aliyun ecs RunCommand --RegionId cn-beijing \
--InstanceId.1 "$INSTANCE" --Type RunShellScript \
--CommandContent "$(printf '%s' "$script" | base64)" \
--ContentEncoding base64 --Name "$name" --Timeout 300 \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["InvokeId"])')
sleep 8
aliyun ecs DescribeInvocationResults --RegionId cn-beijing --InvokeId "$inv" \
| python3 -c "..." # 解码输出 + 检查 ExitCode
}
这套模板在本博客的部署里跑了几十次:装 certbot、改 nginx、验证 HTTPS,全程没碰过 SSH。没有密钥不是障碍,不知道 API 的坑才是。