本記事は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やテスト環境で実行する必要があります。
手動実行:
chmod +x my_api_script.sh ./my_api_script.sh
実行後、出力メッセージ、HTTPステータスコード、
jqによる整形結果が期待通りかを確認します。エラーケースの確認:
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 の起動と確認
スクリプトを
/opt/product-api-poller/my_api_script.shに配置し、実行権限を付与。Unit/Timer ファイルを配置。
systemdをリロードし、Timerを有効化・開始。sudo systemctl daemon-reload sudo systemctl enable --now product-api-poller.timer
ステータスの確認。
sudo systemctl status product-api-poller.timer sudo systemctl status product-api-poller.service
ログの確認。
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 pipefailやtrapを用いた安全なシェルスクリプトの記述、そして冪等性を考慮したロジックの実装が不可欠です。さらに、systemd unit/timerを活用することで、これらの操作を自動化し、安定した運用が可能になります。DevOpsエンジニアとして、これらのプラクティスを習得することは、堅牢で信頼性の高いシステムを構築する上で非常に重要です。

コメント