通过 CLI 和 jq 编写云资源脚本
以幂等方式调用云服务商的 CLI,并解析 JSON 响应来创建和销毁资源。
通过 CLI 和 jq 编写云资源脚本 是 CoddyKit 上的免费 DevOps Bootcamp 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DevOps Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DevOps Bootcamp 课程共包含 4 节课。
幂等云脚本为何重要
在 C2 级别,云脚本编写并不是点击按钮,而是编写能够安全地多次运行的代码,不会创建重复资源,也不会在第二次运行时失败。
幂等脚本会在创建资源前检查资源是否已经存在。这是可靠基础设施自动化的基石。
- 云 CLI(AWS、GCP、Azure)会返回 JSON,因此解析这些输出至关重要。
jq是 Unix 中用于从 Shell 脚本生成的 JSON 里切分、筛选和转换数据的标准工具。- 将 CLI、jq 和条件逻辑结合起来,可以编写稳健且可重复运行的配置脚本。
在本课程中,您将以 AWS CLI 为参考,配置 S3 存储桶、EC2 实例和 IAM 角色;这些模式也可以直接迁移到 gcloud 和 az。
安装并验证云 CLI 工具
编写脚本之前,请确认所需工具已经存在。在 CI 中始终固定版本,以避免环境之间发生漂移。
下面的代码片段会检查 AWS CLI v2、jq 和 GCP SDK,只安装缺失的工具——这种模式适用于新建虚拟机或容器的引导脚本。
#!/usr/bin/env bash
set -euo pipefail
check_or_install() {
local cmd="$1"
local install_cmd="$2"
if ! command -v "$cmd" &>/dev/null; then
echo "[INFO] $cmd not found — installing..."
eval "$install_cmd"
else
echo "[OK] $cmd $("$cmd" --version 2>&1 | head -1)"
fi
}
# AWS CLI v2
check_or_install aws \
'curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o /tmp/awscliv2.zip && unzip -q /tmp/awscliv2.zip -d /tmp && sudo /tmp/aws/install'
# jq
check_or_install jq \
'sudo apt-get install -y jq 2>/dev/null || sudo yum install -y jq'
# gcloud (optional)
check_or_install gcloud \
'echo "Install gcloud SDK manually from https://cloud.google.com/sdk"'
echo "All prerequisites satisfied."使用 jq 查询现有资源
任何幂等脚本的第一步都是读取——向 API 询问资源是否已经存在,然后据此进行分支处理。
AWS CLI 始终返回 JSON。jq 可以让您准确提取所需字段:
jq -r '.Buckets[].Name'— 原始字符串输出,每行一个存储桶名称。jq -e— 如果表达式产生null或false,则以代码 1 退出,因此非常适合用作if守卫条件。jq '.[] | select(.Name == env.BUCKET)'— 通过env使用 Shell 变量进行筛选。
下面的代码片段会列出所有 S3 存储桶,并检查目标存储桶是否已经存在。
#!/usr/bin/env bash
set -euo pipefail
BUCKET="my-devops-artifacts-$(date +%Y%m)"
echo "Fetching existing S3 buckets..."
EXISTING=$(aws s3api list-buckets --output json)
# Extract names as newline-separated list
echo "$EXISTING" | jq -r '.Buckets[].Name'
# Check if target bucket exists
if echo "$EXISTING" | jq -e --arg b "$BUCKET" '.Buckets[] | select(.Name == $b)' > /dev/null 2>&1; then
echo "[EXISTS] Bucket $BUCKET already present — skipping creation."
else
echo "[MISSING] Bucket $BUCKET not found — will create."
fi幂等地创建 S3 存储桶
完成存在性检查后,请将创建操作放入守卫条件中。结构良好的云函数遵循以下模式:
- 从 API 读取当前状态。
- 将期望状态与实际状态进行比较。
- 只针对差异采取操作。
请注意 --create-bucket-configuration 标志——除 us-east-1 外的所有区域都需要它。在脚本中将区域硬编码,可以避免 AWS_DEFAULT_REGION 未设置时出现无提示的失败。
#!/usr/bin/env bash
set -euo pipefail
REGION="eu-west-1"
BUCKET="my-devops-artifacts-$(date +%Y%m)"
ensure_bucket() {
local bucket="$1"
local region="$2"
local existing
existing=$(aws s3api list-buckets --query 'Buckets[].Name' --output json)
if echo "$existing" | jq -e --arg b "$bucket" 'index($b) != null' > /dev/null 2>&1; then
echo "[SKIP] Bucket $bucket already exists."
return 0
fi
echo "[CREATE] Creating bucket $bucket in $region..."
aws s3api create-bucket \
--bucket "$bucket" \
--region "$region" \
--create-bucket-configuration LocationConstraint="$region"
# Enable versioning immediately after creation
aws s3api put-bucket-versioning \
--bucket "$bucket" \
--versioning-configuration Status=Enabled
echo "[DONE] Bucket $bucket created with versioning enabled."
}
ensure_bucket "$BUCKET" "$REGION"解析嵌套 JSON:EC2 实例状态
EC2 响应的嵌套层次很深。jq 路径遍历和 --query(AWS CLI 原生支持的 JMESPath)都可以使用,但对于复杂逻辑而言,jq 更加强大。
EC2 中的关键 jq 模式:
.Reservations[].Instances[]— 展平双重数组结构。select(.State.Name == "running")— 按状态筛选。.Tags[] | select(.Key == "Name") | .Value— 提取标签值。
下面的代码片段会根据 Name 标签查找运行中的实例,并返回其 ID 和私有 IP。
#!/usr/bin/env bash
set -euo pipefail
INSTANCE_NAME="web-server-prod"
RESULT=$(aws ec2 describe-instances \
--filters \
"Name=tag:Name,Values=${INSTANCE_NAME}" \
"Name=instance-state-name,Values=running" \
--output json)
# Extract instance ID and private IP using jq
INSTANCE_ID=$(echo "$RESULT" | jq -r \
'.Reservations[].Instances[] | .InstanceId')
PRIVATE_IP=$(echo "$RESULT" | jq -r \
'.Reservations[].Instances[] | .PrivateIpAddress')
if [[ -z "$INSTANCE_ID" ]]; then
echo "[WARN] No running instance named '$INSTANCE_NAME' found."
exit 1
fi
echo "Instance ID : $INSTANCE_ID"
echo "Private IP : $PRIVATE_IP"幂等地配置 IAM 角色
IAM 资源是全局性的,不得重复创建。尝试创建已存在的角色时,AWS 会返回特定错误代码——EntityAlreadyExists。在大规模处理 IAM 时,捕获该代码比预先调用列表 API 更加简洁,是一种更好的幂等模式。
下面的脚本演示了以下操作:
- 使用
|| true捕获 CLI 退出代码,防止set -e中止执行。 - 使用进程替换解析 AWS 写入标准错误的错误消息 JSON。
- 仅当策略尚未附加时才附加策略。
#!/usr/bin/env bash
set -euo pipefail
ROLE_NAME="DevOpsDeployRole"
POLICY_ARN="arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
TRUST_POLICY='{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}'
# Attempt creation; ignore EntityAlreadyExists
CREATE_OUTPUT=$(aws iam create-role \
--role-name "$ROLE_NAME" \
--assume-role-policy-document "$TRUST_POLICY" \
--output json 2>&1) || {
if echo "$CREATE_OUTPUT" | grep -q 'EntityAlreadyExists'; then
echo "[SKIP] Role $ROLE_NAME already exists."
else
echo "[ERROR] Unexpected error: $CREATE_OUTPUT" >&2
exit 1
fi
}
# Attach policy (attach-role-policy is idempotent by default)
aws iam attach-role-policy \
--role-name "$ROLE_NAME" \
--policy-arn "$POLICY_ARN"
echo "[OK] Role $ROLE_NAME ready with policy $POLICY_ARN."jq 高级用法:转换、映射与 toentries
真实的基础设施响应包含几十个字段。jq 转换可以重塑输出,供下游工具、日志或配置文件使用。
重要的高级模式:
map(select(...))— 筛选数组,同时保留数组外层结构。to_entries | map(select(.value != null))— 在写入配置前移除 null 字段。[.[] | {id: .InstanceId, ip: .PrivateIpAddress}]— 转换为新的数据结构。@csv、@tsv、@base64— 内置格式转换器。
下面的代码片段会提取所有运行中的实例,并写入 TSV 清单文件。
#!/usr/bin/env bash
set -euo pipefail
OUTPUT_FILE="/tmp/ec2_inventory.tsv"
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--output json \
| jq -r '
["InstanceId", "Name", "PrivateIp", "Type", "AZ"],
[
.Reservations[].Instances[] | [
.InstanceId,
(.Tags // [] | map(select(.Key == "Name")) | .[0].Value // "(none)"),
(.PrivateIpAddress // "N/A"),
.InstanceType,
.Placement.AvailabilityZone
]
][]
| @tsv' > "$OUTPUT_FILE"
echo "Inventory written to $OUTPUT_FILE:"
column -t "$OUTPUT_FILE"等待异步操作:使用 jq 轮询
云操作是异步的。创建 EC2 实例后会立即返回,此时状态为 pending。可靠的脚本必须轮询,直到达到所需状态,然后才能继续执行。
下面的模式使用带指数退避的 until 循环。AWS CLI 也提供 wait 子命令(例如 aws ec2 wait instance-running),但自行编写轮询可以提供自定义的超时控制和更详细的日志记录。
#!/usr/bin/env bash
set -euo pipefail
INSTANCE_ID="i-0abcdef1234567890"
MAX_WAIT=300 # seconds
INTERVAL=10
ELAPSED=0
echo "Waiting for instance $INSTANCE_ID to reach 'running' state..."
while true; do
STATE=$(aws ec2 describe-instances \
--instance-ids "$INSTANCE_ID" \
--output json \
| jq -r '.Reservations[0].Instances[0].State.Name')
echo " [$(date +%T)] state = $STATE"
[[ "$STATE" == "running" ]] && break
if [[ "$STATE" == "terminated" || "$STATE" == "shutting-down" ]]; then
echo "[FATAL] Instance entered terminal state: $STATE" >&2
exit 1
fi
if (( ELAPSED >= MAX_WAIT )); then
echo "[TIMEOUT] Instance did not reach 'running' after ${MAX_WAIT}s." >&2
exit 1
fi
sleep "$INTERVAL"
(( ELAPSED += INTERVAL ))
done
echo "[OK] Instance $INSTANCE_ID is running."多云模式:GCP 与 Azure 的对应实现
幂等的先读后做模式可以直接迁移到其他云 CLI。gcloud 和 az 都会返回 JSON,并支持筛选:
- GCP:
gcloud ... --format='json'— 与 AWS 完全一样,将结果通过管道传给jq。在脚本中使用gcloud ... --quiet可禁止提示。 - Azure:
az ... --output json— 模式相同。az group exists会返回纯布尔字符串(true/false),因此在简单情况下无需使用 jq。
下面的代码片段并排展示了 Azure 中幂等地创建资源组,以及 GCP 中创建 GCS 存储桶;两者使用相同的守卫模式。
#!/usr/bin/env bash
set -euo pipefail
# --- Azure: idempotent resource group ---
RG="devops-rg"
LOCATION="westeurope"
if [[ $(az group exists --name "$RG") == "true" ]]; then
echo "[SKIP] Azure resource group $RG already exists."
else
echo "[CREATE] Creating Azure resource group $RG..."
az group create --name "$RG" --location "$LOCATION" --output json \
| jq '{name: .name, location: .location, provisioningState: .properties.provisioningState}'
fi
# --- GCP: idempotent GCS bucket ---
GCS_BUCKET="gs://devops-artifacts-prod"
PROJECT="my-gcp-project"
if gcloud storage buckets describe "$GCS_BUCKET" \
--project="$PROJECT" --format='value(name)' &>/dev/null; then
echo "[SKIP] GCS bucket $GCS_BUCKET already exists."
else
echo "[CREATE] Creating GCS bucket $GCS_BUCKET..."
gcloud storage buckets create "$GCS_BUCKET" \
--project="$PROJECT" \
--location=EU \
--uniform-bucket-level-access
fi拆除:安全销毁资源
销毁脚本与创建脚本同样重要。安全的拆除流程应当:
- 在删除任何内容之前列出资源,并打印摘要供人工检查。
- 接受
--dry-run标志,以便操作人员在执行前确认计划。 - 按照正确的依赖顺序删除资源(例如,先终止实例,再删除安全组)。
下面的代码片段带有试运行保护,会终止所有标记为 Env=staging 的 EC2 实例。
#!/usr/bin/env bash
set -euo pipefail
DRY_RUN="${1:-}"
echo "Finding staging EC2 instances..."
INSTANCE_IDS=$(aws ec2 describe-instances \
--filters \
"Name=tag:Env,Values=staging" \
"Name=instance-state-name,Values=running,stopped" \
--output json \
| jq -r '[.Reservations[].Instances[].InstanceId] | @sh')
if [[ -z "$INSTANCE_IDS" ]]; then
echo "[INFO] No staging instances found. Nothing to do."
exit 0
fi
echo "Instances to terminate: $INSTANCE_IDS"
if [[ "$DRY_RUN" == "--dry-run" ]]; then
echo "[DRY-RUN] No changes made."
exit 0
fi
read -rp "Terminate these instances? [yes/N]: " CONFIRM
[[ "$CONFIRM" != "yes" ]] && { echo "Aborted."; exit 0; }
# shellcheck disable=SC2086
aws ec2 terminate-instances --instance-ids $INSTANCE_IDS --output json \
| jq '.TerminatingInstances[] | {id: .InstanceId, state: .CurrentState.Name}'
echo "[DONE] Termination initiated."端到端:幂等基础设施引导脚本
将所有内容整合起来:一个适用于生产环境的引导脚本按正确顺序协调多个资源,完全具备幂等性,并输出持续集成系统可以解析的结构化日志。
展示的关键实践:
- 结构化日志记录:通过
log()辅助函数为日志添加[INFO]、[WARN]、[ERROR]前缀。 - 状态文件——将已创建资源的标识符写入 JSON 状态文件,以便后续运行和拆除脚本共享相同的引用。
- 错误捕获——
trap捕获意外退出并报告失败的行号。
#!/usr/bin/env bash
set -euo pipefail
STATE_FILE="/tmp/infra_state.json"
REGION="eu-west-1"
BUCKET="devops-bootstrap-$(date +%Y%m)"
ROLE="BootstrapRole"
log() { echo "[$(date -u +%T)] [$1] ${*:2}"; }
trap 'log ERROR "Script failed at line $LINENO"' ERR
# Initialize state
[[ -f "$STATE_FILE" ]] || echo '{}' > "$STATE_FILE"
# --- Step 1: S3 bucket ---
EXISTING_BUCKETS=$(aws s3api list-buckets --query 'Buckets[].Name' --output json)
if echo "$EXISTING_BUCKETS" | jq -e --arg b "$BUCKET" 'index($b) != null' > /dev/null; then
log INFO "Bucket $BUCKET exists — skipping."
else
aws s3api create-bucket --bucket "$BUCKET" --region "$REGION" \
--create-bucket-configuration LocationConstraint="$REGION" > /dev/null
log INFO "Bucket $BUCKET created."
fi
# Update state file
jq --arg b "$BUCKET" '.bucket = $b' "$STATE_FILE" > /tmp/_state_tmp && mv /tmp/_state_tmp "$STATE_FILE"
# --- Step 2: IAM role ---
if aws iam get-role --role-name "$ROLE" &>/dev/null; then
log INFO "Role $ROLE exists — skipping."
else
aws iam create-role --role-name "$ROLE" \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}' \
--output json | jq '{RoleName: .Role.RoleName, Arn: .Role.Arn}'
log INFO "Role $ROLE created."
fi
jq --arg r "$ROLE" '.role = $r' "$STATE_FILE" > /tmp/_state_tmp && mv /tmp/_state_tmp "$STATE_FILE"
log INFO "Bootstrap complete. State: $(cat "$STATE_FILE" | jq -c .)"知识检查:jq 幂等性保护
使用 jq 检验您对幂等云脚本的理解。
回顾:通过 CLI 和 jq 编写云资源脚本
本课介绍了使用 Shell 脚本、云 CLI 和 jq 实现幂等云自动化的完整生命周期。
核心原则:
- 先读后写——始终先查询现有状态;只对差异执行操作。
- 使用 jq -e 进行条件保护——使用退出状态模式,根据 JSON 响应控制
if分支。 - 错误代码语义——在更高效的情况下,捕获提供商特定的错误代码(例如
EntityAlreadyExists),而不是预先列出资源。 - 轮询异步状态——使用带超时的
until循环;不要假定资源在创建后立即就绪。 - 状态文件——将资源标识符写入共享 JSON 文件,使脚本的每个阶段和拆除过程共享相同的引用。
- 试运行标志——始终支持
--dry-run,以便操作人员在执行破坏性操作前安全地进行检查。
这些模式与严格的 set -euo pipefail 文件头及 ERR 捕获机制相结合,构成了 C2 级别生产级基础设施脚本编写的基础。
常见问题解答
「通过 CLI 和 jq 编写云资源脚本」课时是免费的吗?
是的 — 「通过 CLI 和 jq 编写云资源脚本」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DevOps Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 DevOps Bootcamp 课程共包含 4 节课。
「通过 CLI 和 jq 编写云资源脚本」这节课中我会学到什么?
以幂等方式调用云服务商的 CLI,并解析 JSON 响应来创建和销毁资源。 你通过在浏览器中直接运行的动手代码来练习 DevOps Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 DevOps Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 DevOps Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「通过 CLI 和 jq 编写云资源脚本」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 DevOps Bootcamp 课中编写并运行代码吗?
能。每节 DevOps Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 编写精简的 Dockerfile 与 Shell 入口点
- 使用 envsubst 和 heredoc 创建配置模板
- 通过 CLI 和 jq 编写云资源脚本
- 健康探针、就绪门禁与等待循环