0Pricing
DevOps Bootcamp · レッスン

CLIとjqによるクラウドリソースのスクリプト操作

クラウドプロバイダーのCLIを冪等に操作し、JSONレスポンスを解析してリソースをプロビジョニング・削除します。

「CLIとjqによるクラウドリソースのスクリプト操作」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全4レッスンが含まれています。

冪等なクラウドスクリプトが重要な理由

C2レベルでは、クラウドスクリプトはボタンをクリックすることではありません。重複するリソースを作成したり、2回目の実行で失敗したりすることなく、何度でも安全に実行できるコードを書くことです。

冪等なスクリプトは、リソースを作成する前にそのリソースがすでに存在するかどうかを確認します。これは、信頼性の高いインフラストラクチャ自動化の基盤です。

  • クラウドCLI(AWS、GCP、Azure)はJSONを返すため、その出力を解析することが不可欠です。
  • jq は、シェルスクリプトからJSONを切り出し、フィルタリングし、変換するための標準的なUnixツールです。
  • CLI、jq、条件分岐を組み合わせることで、堅牢で繰り返し実行可能なプロビジョニングスクリプトを作成できます。

このレッスンでは、AWS CLIを基準としてS3バケット、EC2インスタンス、IAMロールをプロビジョニングします。ここで扱うパターンは gcloud や az にもそのまま応用できます。

クラウドCLIのインストールと検証

スクリプトを作成する前に、必要なツールが揃っていることを確認してください。環境間の差異を避けるため、CIでは必ずバージョンを固定します。

以下のスニペットは、AWS CLI v2、jq、GCP SDKを確認し、不足しているものだけをインストールします。新しいVMやコンテナ用のブートストラップスクリプトで役立つパターンです。

#!/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' — 生の文字列を出力し、1行につき1つのバケット名を表示します。
  • jq -e — 式の結果が null または false の場合に終了コード1で終了するため、if のガードに適しています。
  • jq '.[] | select(.Name == env.BUCKET)' — env を使ってシェル変数でフィルタリングします。

以下のスニペットは、すべての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バケットの作成

存在確認を用意したら、作成処理をガードで囲みます。適切に構成されたクラウド用関数は、次のパターンに従います。

  1. APIから現在の状態を読み取ります。
  2. 望ましい状態と実際の状態を比較します。
  3. 差分がある場合にのみ処理を実行します。

--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 によるパスの走査と、AWS CLIに標準搭載されたJMESPathの --query はどちらも使えますが、複雑なロジックでは 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を扱う場合、このコードを捕捉する方法は、事前に一覧を取得して確認するよりもすっきりした冪等性のパターンです。

以下のスクリプトでは、次の処理を示します。

  • set -e による中止を防ぐため、|| true を使ってCLIの終了コードを取得する
  • プロセス置換を使い、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応用:変換、map、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."

エンドツーエンド:冪等なインフラストラクチャブートストラップスクリプト

すべてを組み合わせると、プロダクション品質のブートストラップスクリプトは、複数のリソースを正しい順序でオーケストレーションし、完全に冪等で、CIシステムが解析できる構造化ログを出力します。

実践している主なプラクティス:

  • 構造化ロギング:log()ヘルパーを使用し、[INFO]、[WARN]、[ERROR]を先頭に付けます。
  • 状態ファイル — 作成したリソースIDをJSON状態ファイルに書き込み、後続の実行とteardownスクリプトで同じ参照を共有します。
  • エラートラップ — 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によるクラウドリソースのスクリプティング

このレッスンでは、シェルスクリプト、クラウドCLI、jqを使った冪等なクラウド自動化の全ライフサイクルを扱いました。

基本原則:

  • 書き込む前に読み取る — 常に最初に既存の状態を照会し、差分に対してのみ操作します。
  • ガードにはjq -eを使う — 終了ステータスモードを使い、JSONレスポンスからif分岐を制御します。
  • エラーコードの意味 — 事前に一覧を取得するより効率的な場合は、プロバイダー固有のエラーコード(例:EntityAlreadyExists)を捕捉します。
  • 非同期状態をポーリングする — タイムアウト付きのuntilループを使い、作成直後にリソースの準備が整っていると決めつけないようにします。
  • 状態ファイル — リソースIDを共有JSONファイルに書き込み、スクリプトの各フェーズとteardownで同じ参照を共有します。
  • ドライランフラグ — 破壊的な操作の前にオペレーターが安全に確認できるよう、常に--dry-runをサポートします。

これらのパターンは、厳格なset -euo pipefailヘッダーとERRトラップを組み合わせることで、C2レベルのプロダクション品質なインフラストラクチャスクリプティングの基盤になります。

よくある質問

「CLIとjqによるクラウドリソースのスクリプト操作」レッスンは無料ですか?

はい。「CLIとjqによるクラウドリソースのスクリプト操作」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。

「CLIとjqによるクラウドリソースのスクリプト操作」で何を学びますか?

クラウドプロバイダーのCLIを冪等に操作し、JSONレスポンスを解析してリソースをプロビジョニング・削除します。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

DevOps Bootcampを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのDevOps Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「CLIとjqによるクラウドリソースのスクリプト操作」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このDevOps Bootcampレッスンでコードを書いて実行できますか?

はい。すべてのDevOps Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 軽量なDockerfileとシェルエントリポイントの作成
  2. envsubstとheredocによる設定のテンプレート化
  3. CLIとjqによるクラウドリソースのスクリプト操作
  4. ヘルスプローブ、準備完了ゲート、待機ループ
← DevOps Bootcampに戻る