curlコマンドでREST APIを操作する実践例

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

curlコマンドでREST APIを操作する実践例

要件と前提

DevOpsの現場では、システム連携や自動化のためにREST APIを操作する機会が頻繁にあります。本記事では、汎用性の高いcurlコマンドを使い、REST APIを安全かつ冪等に操作する実践的な手法を解説します。

前提ツール

  • curl: HTTP/HTTPSリクエストを送信するためのコマンドラインツール。

  • jq: JSONデータを整形・フィルタリングするためのコマンドラインJSONプロセッサ。

  • bash: スクリプト実行環境。

  • systemd: Linuxにおけるシステムおよびサービスマネージャー。

対象とするAPIの想定

、架空の「Product Management API」を操作する例を考えます。

  • ベースURL: https://api.example.com/products

  • 認証: ヘッダーに X-API-KEY を含める。

  • データ形式: JSON

冪等性 (Idempotence) の重要性

冪等性とは、同じ操作を何度繰り返してもシステムの状態が同じになる特性です。自動化スクリプトにおいては、途中でエラーが発生してリトライした場合でも、意図しない副作用(データの重複作成など)を防ぐために非常に重要です。

実装

REST API操作を安全かつ冪等に行うためのBashスクリプトの実装例を示します。

共通スクリプトの骨格

#!/bin/bash

set -euo pipefail # エラー発生時、未定義変数使用時、パイプ失敗時に即座に終了

# 一時ディレクトリの作成と削除


# スクリプト内で一時ファイルが必要な場合に使用

tmp_dir=$(mktemp -d -t curl_api_XXXXXX)
function cleanup {
    if [ -d "$tmp_dir" ]; then
        rm -rf "$tmp_dir"
        echo "INFO: Temporary directory $tmp_dir removed." >&2
    fi
}
trap cleanup EXIT SIGINT SIGTERM # スクリプト終了時にクリーンアップ関数を実行

# 設定

API_BASE_URL="https://api.example.com/products"
API_KEY="${API_KEY:-your_default_api_key_if_not_set}" # 環境変数からの取得を推奨

# 本番環境では環境変数やシークレットマネージャーを使用


# export API_KEY="your_actual_api_key_from_vault_or_env"

# curl共通オプション

CURL_COMMON_OPTS=(
    -sS # サイレントモード (-s) とエラー表示 (-S)
    --fail-with-body # HTTPエラーコードが返された場合でもレスポンスボディを表示
    --retry 5 # 5回までリトライ
    --retry-delay 2 # 2秒間隔でリトライ
    --retry-max-time 30 # 合計30秒までリトライ
    --connect-timeout 10 # 接続タイムアウト10秒
    --max-time 60 # 全体タイムアウト60秒

    # --cacert /etc/ssl/certs/my_custom_ca.pem # 特定のCA証明書を使用する場合


    # --insecure # TLS証明書検証を無効にする (開発・検証用途のみ、本番では非推奨)

)

# APIリクエスト関数


# 引数: HTTPメソッド, パス, [ボディ]

function api_request {
    local method="$1"
    local path="$2"
    local body="${3:-}"
    local url="${API_BASE_URL}${path}"
    local http_code=0
    local response_body=""
    local headers_file="${tmp_dir}/headers.txt"

    echo "INFO: Requesting ${method} ${url}" >&2

    # ヘッダーファイルに書き出すために -D オプションを使用

    if [ -n "$body" ]; then
        response_body=$(curl "${CURL_COMMON_OPTS[@]}" \
            -X "${method}" \
            -H "X-API-KEY: ${API_KEY}" \
            -H "Content-Type: application/json" \
            -d "${body}" \
            -D "$headers_file" \
            "${url}" 2>&1)
    else
        response_body=$(curl "${CURL_COMMON_OPTS[@]}" \
            -X "${method}" \
            -H "X-API-KEY: ${API_KEY}" \
            -D "$headers_file" \
            "${url}" 2>&1)
    fi

    # HTTPステータスコードをヘッダーファイルから抽出

    http_code=$(awk '/^< HTTP\// {print $2; exit}' "$headers_file" || echo "000")

    if [ $? -ne 0 ] || [ "$http_code" -ge 400 ]; then
        echo "ERROR: API request failed. HTTP Status: ${http_code}" >&2
        echo "ERROR: Response: ${response_body}" >&2
        return 1
    fi

    echo "$response_body"
    return 0
}

GETリクエスト(データ取得)

冪等性: GETリクエストは本質的に冪等です。

# 全製品リストの取得

echo "--- GET All Products ---"
products_json=$(api_request GET "/list")

if [ $? -eq 0 ]; then
    echo "Successfully fetched products."
    echo "$products_json" | jq -r 'map(.name) | .[]' # 製品名だけを抽出して表示
else
    echo "Failed to fetch products."
    exit 1
fi

# 特定の製品の取得 (ID: 123)

PRODUCT_ID="123"
echo "--- GET Product ${PRODUCT_ID} ---"
product_detail_json=$(api_request GET "/${PRODUCT_ID}")

if [ $? -eq 0 ]; then
    echo "Successfully fetched product ${PRODUCT_ID}."
    echo "$product_detail_json" | jq -r '.name, .price' # 名前と価格を抽出
else
    echo "Failed to fetch product ${PRODUCT_ID}."

    # 存在しない場合はエラーではなく、情報がないとして処理する方が冪等性を高める

    if echo "$product_detail_json" | grep -q "Product not found"; then
        echo "INFO: Product ${PRODUCT_ID} not found, which is expected for some workflows."
    else
        exit 1
    fi
fi

POSTリクエスト(データ作成)

冪等性: POSTは通常冪等ではありませんが、作成前に存在チェックを行うことで冪等性を確保できます。

# 製品の作成 (冪等性考慮)

NEW_PRODUCT_NAME="New Gadget Pro"
NEW_PRODUCT_PRICE=99.99
NEW_PRODUCT_DESCRIPTION="An advanced gadget for professionals."
PRODUCT_TO_CREATE=$(jq -n \
    --arg name "$NEW_PRODUCT_NAME" \
    --argjson price "$NEW_PRODUCT_PRICE" \
    --arg description "$NEW_PRODUCT_DESCRIPTION" \
    '{name: $name, price: $price, description: $description}')

echo "--- POST New Product (Idempotent) ---"

# 1. 存在チェック (GETリクエスト)

echo "INFO: Checking if product '${NEW_PRODUCT_NAME}' already exists..." >&2
existing_product=$(api_request GET "/search?name=$(echo "$NEW_PRODUCT_NAME" | urlencode)") # urlencode関数は別途定義が必要

if [ $? -eq 0 ] && echo "$existing_product" | jq -e '.[] | select(.name == "'"$NEW_PRODUCT_NAME"'")' > /dev/null; then
    echo "INFO: Product '${NEW_PRODUCT_NAME}' already exists. Skipping creation." >&2

    # 既存の製品IDを取得するなど、後続処理に役立つ情報を得ることも可能


    # existing_product_id=$(echo "$existing_product" | jq -r '.[] | select(.name == "'"$NEW_PRODUCT_NAME"'") | .id')


    # echo "INFO: Existing product ID: ${existing_product_id}" >&2

else
    echo "INFO: Product '${NEW_PRODUCT_NAME}' does not exist. Creating..." >&2
    create_response=$(api_request POST "/" "$PRODUCT_TO_CREATE")
    if [ $? -eq 0 ]; then
        echo "Successfully created product:"
        echo "$create_response" | jq .
    else
        echo "Failed to create product."
        exit 1
    fi
fi

: 上記の例では urlencode が必要です。簡単な例は次の通りです。

function urlencode() {
    local string="$1"
    local strlen=${#string}
    local encoded=""
    local char
    for (( i=0; i<strlen; i++ )); do
        char=${string:$i:1}
        case "$char" in
            [a-zA-Z0-9.~_-]) encoded+="$char" ;;
            *) encoded+=$(printf '%%%02X' "'$char") ;;
        esac
    done
    echo "$encoded"
}

PUT/PATCHリクエスト(データ更新)

冪等性: PUTはリソース全体を置き換えるため冪等です。PATCHは部分更新ですが、同じ部分を同じ値で更新する限りは冪等とみなせます。

# 製品の更新 (ID: 123)

PRODUCT_ID_TO_UPDATE="123" # 既存の製品ID
UPDATED_PRODUCT_PRICE=109.99
UPDATE_PAYLOAD=$(jq -n \
    --argjson price "$UPDATED_PRODUCT_PRICE" \
    '{price: $price}')

echo "--- PATCH Product ${PRODUCT_ID_TO_UPDATE} ---"
update_response=$(api_request PATCH "/${PRODUCT_ID_TO_UPDATE}" "$UPDATE_PAYLOAD")

if [ $? -eq 0 ]; then
    echo "Successfully updated product ${PRODUCT_ID_TO_UPDATE}:"
    echo "$update_response" | jq .
else
    echo "Failed to update product ${PRODUCT_ID_TO_UPDATE}."
    exit 1
fi

DELETEリクエスト(データ削除)

冪等性: 削除前に存在チェックを行うことで冪等性を確保できます。

# 製品の削除 (冪等性考慮)

PRODUCT_ID_TO_DELETE="124" # 削除したい製品ID

echo "--- DELETE Product ${PRODUCT_ID_TO_DELETE} (Idempotent) ---"

# 1. 存在チェック (GETリクエスト)

echo "INFO: Checking if product '${PRODUCT_ID_TO_DELETE}' exists for deletion..." >&2
delete_check_response=$(api_request GET "/${PRODUCT_ID_TO_DELETE}")

if [ $? -eq 0 ] && echo "$delete_check_response" | jq -e 'has("id")' > /dev/null; then # idフィールドが存在すれば存在する
    echo "INFO: Product '${PRODUCT_ID_TO_DELETE}' exists. Deleting..." >&2
    delete_response=$(api_request DELETE "/${PRODUCT_ID_TO_DELETE}")
    if [ $? -eq 0 ]; then
        echo "Successfully deleted product ${PRODUCT_ID_TO_DELETE}."
        echo "$delete_response" | jq .
    else
        echo "Failed to delete product ${PRODUCT_ID_TO_DELETE}."
        exit 1
    fi
else
    echo "INFO: Product '${PRODUCT_ID_TO_DELETE}' does not exist or already deleted. Skipping deletion." >&2
fi

API操作フロー

graph TD
    A["開始"] --> B{"製品の存在確認?"};
    B --|はい| C["製品情報取得 (GET)"];
    B --|いいえ| D["製品作成 (POST)"];
    C --> E{"既存製品の更新が必要?"};
    E --|はい| F["製品更新 (PUT/PATCH)"];
    E --|いいえ| G["処理完了"];
    D --> G;
    F --> G;
    G --> H["終了"];

検証

上記スクリプトは、実際に存在しないAPIエンドポイントに対して実行するとエラーになるため、適切なモックAPIやテスト環境で実行する必要があります。

  1. 手動実行:

    chmod +x my_api_script.sh
    ./my_api_script.sh
    

    実行後、出力メッセージ、HTTPステータスコード、jqによる整形結果が期待通りかを確認します。

  2. エラーケースの確認:

    • APIキーをわざと間違えて実行し、認証エラーが適切に処理されるか。

    • 存在しないIDを指定して取得/更新/削除を試み、その場合の冪等な挙動(エラーとして終了しない、または特定のエラーメッセージで処理が分岐するなど)を確認します。

    • ネットワークを一時的に切断し、curlのリトライ機能が動作するか確認します。

運用

定期的なAPI操作やバックグラウンドでの処理にはsystemdのUnitとTimerを組み合わせるのが最適です。

systemd Unit ファイル (/etc/systemd/system/product-api-poller.service)

[Unit]
Description=Product API Poller Service
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot

# 権限分離: 専用のユーザーを作成し、そのユーザーでスクリプトを実行する


# 例えば `sudo useradd -r -s /usr/sbin/nologin product_api_user` で作成

User=product_api_user
Group=product_api_user
WorkingDirectory=/opt/product-api-poller # スクリプトを配置するディレクトリ
ExecStart=/opt/product-api-poller/my_api_script.sh

# 環境変数でAPIキーを渡すのは簡易的だが、本番ではSecrets Managerなどを推奨

Environment="API_KEY=YOUR_ACTUAL_API_KEY_HERE"

# セキュリティ強化のためのオプション

ProtectSystem=full
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ReadOnlyPaths=/
ReadWritePaths=/opt/product-api-poller/tmp # 一時ファイル書き込みが必要な場合
CapabilityBoundingSet=~CAP_SYS_ADMIN # 不要な権限を剥奪
Restart=on-failure # エラー終了時に再起動 (oneshotの場合は通常不要だが、念のため)
RestartSec=5
TimeoutSec=300 # サービスのタイムアウト時間

[Install]
WantedBy=multi-user.target

root権限の扱いと権限分離の注意点: systemdサービスは、root権限でインストールされますが、User=およびGroup=ディレクティブを使用することで、実際にスクリプトを実行するユーザー/グループを制限できます。これにより、スクリプトが誤動作した場合でもシステム全体への影響を最小限に抑えられます。product_api_userのような専用の非特権ユーザーを作成し、最小限の権限のみを与えることがベストプラクティスです。Environment=でAPIキーを渡すのは単純ですが、よりセキュアな方法としてはsystemd-credsやVaultなどのシークレットマネージャーを使用することを強く推奨します。

systemd Timer ファイル (/etc/systemd/system/product-api-poller.timer)

[Unit]
Description=Run Product API Poller every 10 minutes

[Timer]
OnCalendar=*:0/10 # 10分ごとに実行
Persistent=true # サービスが停止している間に発生したタイマーイベントも実行

# RandomSec=30s # 起動時刻を最大30秒ずらして、一時的な負荷集中を避ける

[Install]
WantedBy=timers.target

systemd の起動と確認

  1. スクリプトを /opt/product-api-poller/my_api_script.sh に配置し、実行権限を付与。

  2. Unit/Timer ファイルを配置。

  3. systemdをリロードし、Timerを有効化・開始。

    sudo systemctl daemon-reload
    sudo systemctl enable --now product-api-poller.timer
    
  4. ステータスの確認。

    sudo systemctl status product-api-poller.timer
    sudo systemctl status product-api-poller.service
    
  5. ログの確認。

    sudo journalctl -u product-api-poller.service -f
    

トラブルシュート

  • curlエラー:

    • --verbose または -v: 詳細なリクエスト/レスポンスヘッダーを表示し、通信の過程を確認できます。

    • --trace-ascii <file>: 送受信されるすべてのデータをASCII形式でファイルに記録し、詳細なデバッグ情報を提供します。

    • HTTPステータスコードを確認し、APIのドキュメントと照らし合わせます(例: 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error)。

    • jqでレスポンスボディを整形し、APIからのエラーメッセージを確認します。

  • jqエラー:

    • jq: parse error: Expected another character, got: ... などはJSON構文エラーを示します。jqに渡す前のJSON文字列を確認してください。

    • jq -e: 結果が空の場合にエラーコード1を返します。スクリプトでの条件分岐に有用です。

  • スクリプトのエラー (set -euo pipefail):

    • set -euo pipefail により、エラー発生箇所が明確になります。echo "ERROR: ${FUNCNAME[0]} failed at line ${LINENO}" >&2 のようにエラーログを追加すると特定が容易です。

    • 未定義変数エラーは、変数名をダブルクォートで囲んでいない ($VAR -> "$VAR") か、初期値が設定されていない場合に発生しやすいです。

  • systemdエラー:

    • sudo journalctl -u <service_name>.service -f: サービスログをリアルタイムで監視し、スクリプトからの標準出力/エラー出力、およびsystemd自身のメッセージを確認します。

    • systemctl status <service_name>.service: サービスの現在の状態、最後の終了コードなどを確認します。

    • ExecStartパスの誤り、権限不足、Environment変数の設定ミスなどが一般的な原因です。

まとめ

curlコマンドはREST API操作の強力なツールですが、本記事で示したようにjqとの連携、set -euo pipefailtrapを用いた安全なシェルスクリプトの記述、そして冪等性を考慮したロジックの実装が不可欠です。さらに、systemd unit/timerを活用することで、これらの操作を自動化し、安定した運用が可能になります。DevOpsエンジニアとして、これらのプラクティスを習得することは、堅牢で信頼性の高いシステムを構築する上で非常に重要です。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました