云 CLI 自动化脚本实践

系统讲解 aws、az、gcloud 三大云 CLI 的脚本化用法:结构化输出与 jq 解析、分页与等待收敛、幂等资源管理、凭据与区域配置的安全实践。

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 自动化的本质是把最终一致的系统当成不可靠组件来对待:不假设调用立即生效、不假设一次拿到全部数据、不假设重复调用无害。把这三条写进脚本,云上运维才不会在深夜出事故。云资源管好之后,最后一环是把一台空白机器从零初始化到可用状态。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「shell」更多文章

  1. 任务编排与 Makefile 实战
  2. 文件监控与事件驱动流水线实战
  3. 结构化数据清洗与报表生成实战