1. 云 CLI 自动化的挑战
一句话总结: 云 CLI 的问题不在「调用」,而在「输出格式不稳定、操作是异步的、重复执行会报错」这三件事。
云 API 本质是分布式的最终一致系统:创建一台机器返回的只是「已接受请求」,资源真正可用要等几十秒;同一个命令跑两次,第二次可能因为资源已存在而失败。脚本必须处理这些,否则只能在演示里跑通。
# 危险:依赖人类可读输出做判断
aws ec2 describe-instances | grep running # 输出格式随时可能变
# 正确:结构化输出 + 显式查询
aws ec2 describe-instances \
--query 'Reservations[].Instances[].State.Name' \
--output text
1.1 三条工程原则
一句话总结: 结构化输出、幂等设计、显式等待,是云脚本可靠性的三根支柱。
# 全局约定:一律 JSON 输出,一律显式区域,一律禁用交互分页
export AWS_PAGER=""
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-cn-north-1}"
export AWS_OUTPUT=json
# gcloud 关闭交互提示
export CLOUDSDK_CORE_DISABLE_PROMPTS=1
# az 关闭动态安装与提示
export AZURE_CORE_ONLY_SHOW_ERRORS=true
1.2 三大 CLI 的输出差异
一句话总结: aws 用
--query(JMESPath)与--output,az 用--query(JMESPath)+-o tsv,gcloud 用--format加--filter。
# AWS:JMESPath 查询
aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId' --output text
# Azure:同样的 JMESPath,但输出格式参数不同
az vm list --query '[].name' -o tsv
# GCP:format 与 filter 分离
gcloud compute instances list --format='value(name)' --filter='status=RUNNING'
2. 结构化输出与 jq 解析
一句话总结:
--output json加 jq 是云脚本的通用解析层,-r去引号、-c紧凑、// empty兜底空值。
# 提取字段
aws ec2 describe-instances --output json \
| jq -r '.Reservations[].Instances[] | "\(.InstanceId)\t\(.State.Name)"'
# 过滤后再取
aws ec2 describe-instances --output json \
| jq -r '.Reservations[].Instances[]
| select(.State.Name == "running")
| .InstanceId'
2.1 jq 常用模式
一句话总结: 云 API 返回普遍是「多层嵌套 + 可能为 null」,
?、//、select三个符号覆盖大部分解析需求。
# 安全访问可能不存在的字段
aws elbv2 describe-load-balancers --output json \
| jq -r '.LoadBalancers[]? | .DNSName // "无"'
# 转成 TSV 便于后续 awk 处理
aws ec2 describe-volumes --output json \
| jq -r '.Volumes[] | [.VolumeId, .Size, .State] | @tsv'
# 聚合成一张表
aws ec2 describe-instances --output json \
| jq -r '[.Reservations[].Instances[] | {id: .InstanceId, az: .Placement.AvailabilityZone}]'
2.2 避免解析人类输出
一句话总结: 只要命令支持
--output json,就永远不要用awk去切表格,格式变化会让脚本静默出错。
# 反面教材:切表格列
aws ec2 describe-instances | awk '/running/ {print $8}'
# 正确:显式字段
aws ec2 describe-instances --output json \
| jq -r '.Reservations[].Instances[] | select(.State.Name=="running") | .InstanceId'
3. 分页与结果完整性
一句话总结: 云 API 默认只返回一页,脚本不处理分页就会静默漏数据——这是最隐蔽的 bug。
# AWS:--no-paginate 关闭自动分页(危险!)
# 默认 AWS CLI v2 会自动翻页,但如果用了 --max-items 就只返回一页
aws s3api list-objects-v2 --bucket mybucket --max-items 100 --output json \
| jq -r '.Contents[]?.Key'
# 需要全部时:显式翻页或去掉 max-items
aws s3api list-objects-v2 --bucket mybucket --output json \
| jq -r '.Contents[]?.Key'
3.1 手工翻页
一句话总结: 拿到 NextToken 就继续请求,直到为空,这是所有云 API 分页的通用模型。
# 通用翻页骨架
list_all_logs() {
local token="" args=()
while :; do
args=(--output json)
[[ -n "$token" ]] && args+=(--next-token "$token")
resp=$(aws logs describe-log-groups "${args[@]}")
jq -r '.logGroups[].logGroupName' <<< "$resp"
token=$(jq -r '.nextToken // empty' <<< "$resp")
[[ -z "$token" ]] && break
done
}
list_all_logs
3.2 其它 CLI 的分页
一句话总结: az 用
--query后仍可能截断,gcloud 用--page-size与--format配合,思路一致。
# gcloud:显式分页
gcloud compute instances list --page-size=500 --format='value(name)'
# az:部分命令支持 --top,需要全量时循环 offset
az vm list --query '[].name' -o tsv
4. 异步操作与等待
一句话总结: 创建类操作是异步的,脚本必须显式等待到目标状态,否则后续步骤会因资源未就绪而失败。
# AWS:用 wait 子命令
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"
# 自定义轮询(无内置 wait 时)
wait_for_state() {
local id="$1" want="$2" i=0
while (( i < 60 )); do
cur=$(aws ec2 describe-instances --instance-ids "$id" \
--query 'Reservations[0].Instances[0].State.Name' --output text)
[[ "$cur" == "$want" ]] && return 0
sleep 5
i=$(( i + 1 ))
done
echo "等待超时: $id 当前 $cur" >&2
return 1
}
4.1 带退避的等待
一句话总结: 指数退避减少 API 压力,同时设最大次数防止无限等待。
# 指数退避等待
wait_backoff() {
local id="$1" want="$2" delay=2 i=0
while (( i < 10 )); do
cur=$(aws ec2 describe-instances --instance-ids "$id" \
--query 'Reservations[0].Instances[0].State.Name' --output text 2>/dev/null || echo unknown)
[[ "$cur" == "$want" ]] && { echo "$id 已达 $want"; return 0; }
sleep "$delay"
delay=$(( delay * 2 > 60 ? 60 : delay * 2 ))
i=$(( i + 1 ))
done
return 1
}
4.2 资源泄漏防护
一句话总结: 创建中途失败要清理已创建的资源,用 trap 登记「已创建清单」再统一回收。
# 登记并清理
CREATED=()
cleanup() {
for r in "${CREATED[@]:-}"; do
[[ -n "$r" ]] && aws ec2 terminate-instances --instance-ids "$r" >/dev/null 2>&1 || true
done
}
trap cleanup EXIT INT TERM
id=$(aws ec2 run-instances --image-id "$AMI" --instance-type t3.micro \
--query 'Instances[0].InstanceId' --output text)
CREATED+=("$id")
5. 幂等脚本设计
一句话总结: 「先查后建」是幂等的核心:存在就复用,不存在才创建,绝不依赖「重复调用会失败」来判断。
# 幂等创建安全组
get_or_create_sg() {
local name="$1" vpc="$2" sg
sg=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=$name" "Name=vpc-id,Values=$vpc" \
--query 'SecurityGroups[0].GroupId' --output text 2>/dev/null)
[[ -z "$sg" || "$sg" == "None" ]] && sg=$(aws ec2 create-security-group \
--group-name "$name" --description "managed by script" --vpc-id "$vpc" \
--query 'GroupId' --output text)
printf '%s' "$sg"
}
5.1 声明式与命令式取舍
一句话总结: 单次运维用命令式幂等脚本,长期存在的资源交给 Terraform 这类声明式工具,两者边界要清晰。
# 适合脚本:临时资源、一次性运维、跨账号批量操作
# 适合 Terraform:长期存在、需要状态追踪、需要变更预览的基础设施
5.2 幂等的 idempotency token
一句话总结: 部分 API 支持客户端 token,同一 token 重复请求只会生效一次,是强幂等的最优解。
# 用 uuid 作为幂等 token,重试时复用同一个
token=$(uuidgen)
aws ec2 run-instances --client-token "$token" \
--image-id "$AMI" --instance-type t3.micro
6. 凭据与区域管理
一句话总结: 云凭据的优先级链必须搞清:环境变量 > profile 文件 > 实例角色,脚本要显式指定而不是靠猜。
# 显式指定 profile 与区域,不依赖环境默认值
AWS_PROFILE=prod AWS_DEFAULT_REGION=cn-north-1 \
aws s3 ls s3://prod-bucket/
# 查看当前生效的身份(调试第一招)
aws sts get-caller-identity --output json
az account show --output json
gcloud auth list --format='value(account)'
6.1 多账号切换
一句话总结: 用 profile 或
az account set显式切换,脚本入口打印当前身份,避免「在错误的账号上执行」。
# 入口处打印身份并确认
whoami_cloud() {
case "$1" in
aws) aws sts get-caller-identity --query Arn --output text ;;
az) az account show --query id --output tsv ;;
gcp) gcloud config get-value project 2>/dev/null ;;
esac
}
echo "当前身份: $(whoami_cloud aws)"
6.2 临时凭据
一句话总结: 生产脚本一律用 STS 临时凭据或 OIDC 联邦,绝不在 CI 里放长期 Access Key。
# 假设角色获取临时凭据
creds=$(aws sts assume-role \
--role-arn "arn:aws:iam::123456789012:role/DeployRole" \
--role-session-name "deploy-$(date +%s)" \
--output json)
export AWS_ACCESS_KEY_ID=$(jq -r '.Credentials.AccessKeyId' <<< "$creds")
export AWS_SECRET_ACCESS_KEY=$(jq -r '.Credentials.SecretAccessKey' <<< "$creds")
export AWS_SESSION_TOKEN=$(jq -r '.Credentials.SessionToken' <<< "$creds")
6.3 区域与配额校验
一句话总结: 脚本开头校验目标区域与配额,能在真正创建前拦下大部分低级错误。
# 校验区域是否在允许清单内
ALLOWED_REGIONS=(cn-north-1 cn-northwest-1)
region="${AWS_DEFAULT_REGION:-}"
[[ " ${ALLOWED_REGIONS[*]} " == *" $region "* ]] \
|| { echo "不允许的区域: $region" >&2; exit 1; }
# 查询配额
aws service-quotas get-service-quota \
--service-code ec2 --quota-code L-1216C47A \
--query 'Quota.Value' --output text
7. 实战:跨账号资源巡检脚本
一句话总结: 把身份校验、分页列举、结构化输出、汇总报表串起来,就是一个可复用的云巡检骨架。
#!/usr/bin/env bash
set -euo pipefail
export AWS_PAGER=""
REGIONS=("cn-north-1" "cn-northwest-1")
REPORT="inventory-$(date +%Y%m%d).tsv"; : > "$REPORT"
printf '账号: %s\n' "$(aws sts get-caller-identity --query Arn --output text)" >&2
# 遍历区域,分页拉全量
for region in "${REGIONS[@]}"; do
token=""
while :; do
args=(ec2 describe-instances --region "$region" --output json)
[[ -n "$token" ]] && args+=(--next-token "$token")
resp=$(aws "${args[@]}")
jq -r --arg r "$region" '
.Reservations[].Instances[]
| [$r, .InstanceId, .InstanceType, .State.Name,
(.Tags // [] | map(select(.Key=="Name")) | .[0].Value // "-")]
| @tsv' <<< "$resp" >> "$REPORT"
token=$(jq -r '.NextToken // empty' <<< "$resp")
[[ -z "$token" ]] && break
done
done
echo "总计: $(wc -l < "$REPORT") 台实例"
awk -F'\t' '{c[$4]++} END {for (s in c) print s, c[s]}' "$REPORT" | sort
7.1 报表与告警
一句话总结: 巡检结果落地成 TSV,再按阈值判断是否告警,脚本就能接入监控体系。
# 找出长期停止的实例
awk -F'\t' '$4 == "stopped" {print $2}' "$REPORT" | while read -r id; do
echo "告警: 实例 $id 处于 stopped 状态"
done
# 输出 JSON 供上游消费
awk -F'\t' '{print $2}' "$REPORT" | jq -R . | jq -s .
7.2 失败与重试
一句话总结: 云 API 的限流(Throttling)要用退避重试,脚本层面统一封装比每个调用单独处理更可靠。
# 限流退避封装
aws_retry() {
local i=0 delay=2 out
while (( i < 5 )); do
out=$(aws "$@" 2>&1) && { printf '%s' "$out"; return 0; }
grep -qiE 'Throttling|RequestLimitExceeded|TooManyRequests' <<< "$out" \
|| { printf '%s\n' "$out" >&2; return 1; }
sleep "$delay"; delay=$(( delay * 2 )); i=$(( i + 1 ))
done
echo "重试耗尽" >&2; return 1
}
aws_retry ec2 describe-instances --output json > /dev/null
8. 总结
| 环节 | 要点 |
|---|---|
| 输出 | 一律 JSON,禁止解析人类可读表格 |
| 解析 | jq 的 -r、?、//、select、@tsv 五件套 |
| 分页 | 拿到 token 继续请求,不处理分页会静默漏数据 |
| 等待 | 异步操作显式轮询到目标状态,配指数退避 |
| 幂等 | 先查后建,或使用 client token 强幂等 |
| 凭据 | STS 临时凭据或 OIDC,绝不放长期 Access Key |
| 区域 | 显式指定并在入口校验,避免误操作 |
| 重试 | 只对限流类错误退避重试,逻辑错误立即失败 |
云 CLI 自动化的本质是把最终一致的系统当成不可靠组件来对待:不假设调用立即生效、不假设一次拿到全部数据、不假设重复调用无害。把这三条写进脚本,云上运维才不会在深夜出事故。云资源管好之后,最后一环是把一台空白机器从零初始化到可用状态。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。