curlコマンドによる堅牢なHTTPリクエスト制御とSystemd連携

Tech

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

curlコマンドによる堅牢なHTTPリクエスト制御とSystemd連携

DevOpsの現場では、外部APIとの連携や定期的なデータ収集にHTTPリクエストが頻繁に利用されます。本記事では、汎用性の高いcurlコマンドを用いて堅牢なHTTPリクエストを実装し、jqによるJSON処理、さらにsystemdを活用した自動実行と監視の手法を解説します。安全なBashスクリプトの書き方や権限分離についても触れ、実運用に耐えうるシステム構築を目指します。

1. 要件と前提

1.1. 要件

  • HTTPリクエストの実行(GET, POST, PUT, DELETE)

  • TLSクライアント証明書による認証

  • ネットワークエラー時の再試行と指数バックオフ

  • レスポンスJSONの解析と条件分岐

  • スクリプトの安全な実行(エラーハンドリング、一時ディレクトリ利用)

  • systemdによる定期実行とログ管理

  • root権限の適切な扱いと権限分離

1.2. 前提環境

以下のコマンドがインストールされているLinux環境を前提とします。

  • curl (バージョン7.x以上を推奨)

  • jq (JSONプロセッサ)

  • bash (バージョン4.x以上を推奨)

  • systemd (多くのモダンなLinuxディストリビューションで利用可能)

2. 実装

2.1. 安全なBashスクリプトの基礎

HTTPリクエストを制御するスクリプトは、予期せぬエラーや不正な入力に対して堅牢である必要があります。以下の設定は、Bashスクリプトのベストプラクティスとされています[1]。

#!/bin/bash


# 入力: APIエンドポイントURL


# 出力: 標準出力に処理結果、エラーは標準エラー出力


# 前提: curl, jq がインストールされていること

# 1. set -euo pipefail


# -e: コマンドが失敗した場合、即座にスクリプトを終了


# -u: 未定義の変数を使用した場合、エラーとして終了


# -o pipefail: パイプライン中の任意のコマンドが失敗した場合、パイプライン全体を失敗と見なす

set -euo pipefail

# 2. trap: スクリプト終了時のクリーンアップ処理


# EXIT: スクリプト終了時 (正常/異常問わず)


# ERR: コマンド失敗時 (set -e と併用)

cleanup() {
    echo "スクリプトが終了しました。一時ディレクトリを削除します。" >&2
    if [ -n "${TMP_DIR:-}" ] && [ -d "${TMP_DIR:-}" ]; then
        rm -rf "${TMP_DIR:-}"
    fi
}
trap cleanup EXIT ERR

# 3. 一時ディレクトリの利用


# mktemp -d: 一意で安全な一時ディレクトリを作成

TMP_DIR=$(mktemp -d)
if [ ! -d "${TMP_DIR}" ]; then
    echo "エラー: 一時ディレクトリの作成に失敗しました。" >&2
    exit 1
fi
echo "一時ディレクトリ: ${TMP_DIR}" >&2

# ここからスクリプトのメインロジック


# ...

コメント:

  • set -euo pipefail: 早期エラー検知と停止により、問題を迅速に特定します。

  • trap cleanup EXIT ERR: スクリプトの終了(成功・失敗問わず)時に一時ファイルを確実に削除し、リソースリークを防ぎます。

  • mktemp -d: 予測不能なファイル名の一時ディレクトリを作成し、セキュリティリスクを低減します。

2.2. curl によるHTTPリクエスト制御

2.2.1. 基本的なリクエスト

curlは、-XでHTTPメソッド、-Hでヘッダ、-dでボディデータを指定できます。

API_URL="https://example.com/api/v1/data"
AUTH_TOKEN="your_auth_token"

# GETリクエスト例

echo "GETリクエスト実行..."
GET_RESPONSE=$(curl -sS -X GET \
    -H "Accept: application/json" \
    -H "Authorization: Bearer ${AUTH_TOKEN}" \
    "${API_URL}" \
    --cacert "${TMP_DIR}/ca-bundle.crt") # CA証明書を指定
echo "GETレスポンス: ${GET_RESPONSE}"

# POSTリクエスト例

echo "POSTリクエスト実行..."
POST_DATA='{"key":"value","status":"active"}'
POST_RESPONSE=$(curl -sS -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${AUTH_TOKEN}" \
    -d "${POST_DATA}" \
    "${API_URL}" \
    --cacert "${TMP_DIR}/ca-bundle.crt")
echo "POSTレスポンス: ${POST_RESPONSE}"

コメント:

  • -sS: -sで進捗表示を抑制し、-Sでエラー表示を強制します。

  • --cacert: サーバー証明書の検証に利用するCA証明書バンドルを指定します。これにより、信頼されたルート証明書以外の自己署名証明書などを利用する場合にセキュリティを確保できます[2]。

2.2.2. TLSクライアント証明書認証

Mutual TLS (mTLS) 認証が必要な場合、--cert--keyでクライアント証明書と秘密鍵を指定します[2]。

CLIENT_CERT="${TMP_DIR}/client.crt"
CLIENT_KEY="${TMP_DIR}/client.key"

# 証明書と鍵は事前にTMP_DIRに配置しておくか、安全な方法で取得・生成します。

echo "mTLS認証付きGETリクエスト実行..."
MTLS_RESPONSE=$(curl -sS -X GET \
    -H "Accept: application/json" \
    --cacert "${TMP_DIR}/ca-bundle.crt" \
    --cert "${CLIENT_CERT}" \
    --key "${CLIENT_KEY}" \
    "${API_URL}/mtls" \
    --output "${TMP_DIR}/mtls_response.json") # レスポンスをファイルに保存
echo "mTLSレスポンスが ${TMP_DIR}/mtls_response.json に保存されました。"

コメント:

  • クライアント証明書と秘密鍵は、安全な方法(例: 環境変数、安全なファイルシステム上のパス)で管理し、スクリプト内にハードコードしないことが重要です。

2.2.3. 再試行と指数バックオフ

ネットワークの一時的な障害に対応するため、curlの再試行機能と指数バックオフを組み合わせます[2]。

# curlの再試行オプション:


# --retry <num>: 最大試行回数


# --retry-delay <seconds>: 初回再試行までの待機時間 (秒)


# --retry-max-time <seconds>: 全試行の最大時間 (秒)


# --retry-all-errors: すべてのエラーで再試行 (デフォルトは特定のネットワークエラーのみ)

MAX_RETRIES=5
INITIAL_DELAY=1 # seconds
RETRY_URL="https://unstable.example.com/api/v1/data"

echo "再試行付きGETリクエスト実行..."
RETRY_RESPONSE=$(curl -sS \
    --retry "${MAX_RETRIES}" \
    --retry-delay "${INITIAL_DELAY}" \
    --retry-max-time 60 \
    --retry-all-errors \
    "${RETRY_URL}")

if [ $? -ne 0 ]; then
    echo "エラー: ${RETRY_URL} へのリクエストが ${MAX_RETRIES} 回の試行後も失敗しました。" >&2
    exit 1
fi
echo "再試行後レスポンス: ${RETRY_RESPONSE}"

コメント:

  • --retry-delayは初回再試行までの待機時間であり、その後の待機時間はcurlが指数バックオフ的に自動調整します。

  • --retry-all-errorsは、HTTP 5xxエラーやタイムアウトなど、より広範なエラーで再試行をトリガーします。

カスタム指数バックオフの実装例: curlの組み込み機能だけでは不十分な場合、Bashでカスタムの指数バックオフを実装することも可能です。

attempt=0
max_attempts=5
base_delay=1 # seconds
status_code=0
API_URL="https://example.com/api/fail_occasionally"

while [ ${attempt} -lt ${max_attempts} ]; do
    attempt=$((attempt + 1))
    echo "試行 ${attempt}/${max_attempts}..." >&2

    # レスポンスヘッダとボディを分離して取得

    response=$(curl -sS -w "%{http_code}" \
        -H "Accept: application/json" \
        -o "${TMP_DIR}/response_body.json" \
        "${API_URL}")
    status_code=${response:(-3)} # 最後の3桁がステータスコード

    if [ "${status_code}" -ge 200 ] && [ "${status_code}" -lt 300 ]; then
        echo "リクエスト成功 (HTTP ${status_code})" >&2
        cat "${TMP_DIR}/response_body.json"
        break
    else
        echo "リクエスト失敗 (HTTP ${status_code})" >&2
        if [ "${attempt}" -lt "${max_attempts}" ]; then
            delay=$((base_delay * 2 ** (attempt - 1)))
            echo " ${delay}秒待機して再試行します..." >&2
            sleep "${delay}"
        fi
    fi
done

if [ "${status_code}" -ge 200 ] && [ "${status_code}" -lt 300 ]; then
    echo "最終的に成功しました。" >&2
else
    echo "エラー: ${API_URL} へのリクエストが ${max_attempts} 回の試行後も失敗しました。" >&2
    exit 1
fi

コメント:

  • -w "%{http_code}" オプションで、HTTPステータスコードをボディの直後に出力させ、スクリプトで解析します。

  • -o でレスポンスボディをファイルに保存し、パイプラインの途中でボディが消費されないようにします。

  • base_delay * 2 ** (attempt - 1) で指数バックオフを計算します。

2.3. jq によるJSON処理

jqはJSONデータを効率的に処理するための強力なツールです[3]。

# 前のcurl例で取得したJSONレスポンスをファイルから読み込む

JSON_RESPONSE=$(cat "${TMP_DIR}/response_body.json")

# キー 'data.items' の配列から 'id' と 'name' を抽出

echo "JSON解析例1: 特定のキーと値の抽出"
echo "${JSON_RESPONSE}" | jq -r '.data.items[] | "\(.id): \(.name)"'

# ステータスが 'active' の項目をフィルタリング

echo "JSON解析例2: 条件に基づくフィルタリング"
echo "${JSON_RESPONSE}" | jq '.data.items[] | select(.status == "active")'

# 特定の値に基づいて条件分岐

ITEM_COUNT=$(echo "${JSON_RESPONSE}" | jq '.data.items | length')
if [ "${ITEM_COUNT}" -gt 0 ]; then
    echo "APIレスポンスには ${ITEM_COUNT} 件のアイテムが含まれています。"
else
    echo "APIレスポンスにアイテムはありません。"
fi

コメント:

  • jq -r: raw出力 (引用符なし) で値を出力します。

  • . は現在のオブジェクト/配列、[] は配列の要素を展開、| はパイプです。

  • select() は条件に合致する要素のみをフィルタリングします。

2.4. HTTPリクエスト処理フロー (Mermaid)

graph TD
    A["スクリプト開始"] --> B{"一時ディレクトリ作成
Trap設定"}; B --> C{"CA証明書/クライアント証明書配置"}; C --> D["curl HTTPリクエスト実行"]; D -- エラー発生 --> E{"再試行ポリシー適用?"}; E -- はい --> F["指数バックオフ待機"]; F --> D; E -- いいえ --> G["エラーログ記録
スクリプト終了"]; D -- 成功 --> H["HTTPステータスコード確認"]; H -- 2xx以外 --> G; H -- 2xx --> I["レスポンスJSONをjqで処理"]; I --> J{"処理結果に基づく判定"}; J -- 異常 --> G; J -- 正常 --> K["結果出力/次の処理"]; K --> L["スクリプト正常終了"]; G --> M["Trapによるクリーンアップ"]; L --> M;

コメント:

  • 上記フローは、スクリプトの開始からHTTPリクエスト、再試行、JSON処理、エラーハンドリング、そして終了までの一連の流れを図示しています。これにより、スクリプトの動作概要を視覚的に把握できます。

2.5. systemd によるシステム統合

定期的なHTTPリクエストはsystemdUnitTimerを用いて自動化できます[4], [5]。

2.5.1. サービススクリプト (http-request.sh)

上記で作成したスクリプトを/usr/local/bin/http-request.shとして配置します。

#!/bin/bash

set -euo pipefail
trap cleanup EXIT ERR

cleanup() {
    if [ -n "${TMP_DIR:-}" ] && [ -d "${TMP_DIR:-}" ]; then
        rm -rf "${TMP_DIR:-}"
    fi
}

TMP_DIR=$(mktemp -d)
if [ ! -d "${TMP_DIR}" ]; then
    echo "ERROR: Failed to create temporary directory." >&2
    exit 1
fi

# 一時ディレクトリにCA証明書などを配置 (例としてダミーパス)


# cp /etc/ssl/certs/ca-bundle.crt "${TMP_DIR}/ca-bundle.crt"


# cp /path/to/client.crt "${TMP_DIR}/client.crt"


# cp /path/to/client.key "${TMP_DIR}/client.key"

API_URL="https://httpbin.org/get" # テスト用のURL
MAX_RETRIES=3
INITIAL_DELAY=1

echo "$(date '+%Y-%m-%d %H:%M:%S JST') - Starting HTTP request..." >&2

attempt=0
success=false
while [ "${attempt}" -lt "${MAX_RETRIES}" ]; do
    attempt=$((attempt + 1))
    echo "Attempt ${attempt}/${MAX_RETRIES} to ${API_URL}" >&2

    # curlコマンドを実行し、ステータスコードとボディを取得

    response_output=$(curl -sS -w "\n%{http_code}" --retry 0 \
        -H "User-Agent: systemd-http-agent" \
        -o "${TMP_DIR}/response_body.json" \
        "${API_URL}")

    # ステータスコードは出力の最後の3文字

    status_code="${response_output: -3}"

    if [ "${status_code}" -ge 200 ] && [ "${status_code}" -lt 300 ]; then
        echo "Request successful (HTTP ${status_code})." >&2
        jq_output=$(jq -r '.origin' "${TMP_DIR}/response_body.json")
        echo "Origin IP: ${jq_output}"
        success=true
        break
    else
        echo "Request failed (HTTP ${status_code})." >&2
        if [ "${attempt}" -lt "${MAX_RETRIES}" ]; then
            delay=$((INITIAL_DELAY * 2 ** (attempt - 1)))
            echo "Waiting ${delay} seconds before retry..." >&2
            sleep "${delay}"
        fi
    fi
done

if ! "${success}"; then
    echo "ERROR: HTTP request failed after ${MAX_RETRIES} attempts." >&2
    exit 1
fi

echo "$(date '+%Y-%m-%d %H:%M:%S JST') - HTTP request finished." >&2

コメント:

  • httpbin.org/getはテスト用のエンドポイントです。

  • -o "${TMP_DIR}/response_body.json"-w "\n%{http_code}" を組み合わせて、ステータスコードとレスポンスボディを確実に分離して扱います。

2.5.2. systemd Unitファイル (http-request.service)

/etc/systemd/system/http-request.service を作成します。

[Unit]
Description=Perform periodic HTTP request
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/http-request.sh
User=http_req_user      # スクリプトを実行するユーザーを指定(推奨)
Group=http_req_group    # スクリプトを実行するグループを指定(推奨)
WorkingDirectory=/var/log/http-request # 作業ディレクトリ
StandardOutput=journal  # 標準出力をジャーナルに記録
StandardError=journal   # 標準エラー出力をジャーナルに記録
Restart=on-failure      # サービスが失敗した場合に再起動
RestartSec=5s           # 再起動までの待機時間

[Install]
WantedBy=multi-user.target

コメント:

  • Type=oneshot: スクリプトが一度実行されて終了するタイプです。

  • ExecStart: 実行するスクリプトの絶対パスを指定します。

  • User, Group: セキュリティ上非常に重要です。 サービスはroot権限ではなく、専用の非特権ユーザー(例: http_req_user)で実行すべきです[4]。これは権限分離の原則に則り、スクリプトに脆弱性があった場合の被害を最小限に抑えます。事前にsudo useradd -r -s /bin/false http_req_userなどでユーザーを作成しておく必要があります。

  • StandardOutput, StandardError: 出力をsystemd-journaldに送り、ログの一元管理を可能にします。

2.5.3. systemd Timerファイル (http-request.timer)

/etc/systemd/system/http-request.timer を作成します。

[Unit]
Description=Run http-request.service every 5 minutes

[Timer]
OnBootSec=1min              # システム起動1分後に初回実行
OnUnitActiveSec=5min        # サービスがアクティブになった後、5分ごとに実行
Unit=http-request.service   # 実行するサービスユニットを指定

[Install]
WantedBy=timers.target

コメント:

  • OnBootSec: システム起動後の初回実行タイミングを指定します。

  • OnUnitActiveSec: サービス実行後、指定した間隔で繰り返し実行されます。これにより、サービスが完了した時間から次の実行までの間隔が確保されます[5]。

2.6. systemdの起動とログ確認

UnitファイルとTimerファイルを作成したら、systemdにそれらを認識させ、起動します。

# systemdの設定ファイルをリロード

sudo systemctl daemon-reload

# http-request.serviceを起動(手動実行)

sudo systemctl start http-request.service

# http-request.timerを有効化し、起動(定期実行を開始)

sudo systemctl enable http-request.timer
sudo systemctl start http-request.timer

# タイマーのステータス確認

systemctl list-timers http-request.timer

# サービスのログを確認

journalctl -u http-request.service -f

コメント:

  • daemon-reload: 新しいユニットファイルをsystemdに読み込ませます。

  • start: サービスやタイマーを即座に起動します。

  • enable: システム起動時にサービスやタイマーが自動で起動するように設定します。

  • journalctl -u ... -f: systemd-journaldに記録されたログをリアルタイムで表示します。

3. 検証

スクリプトとsystemdの設定が正しく機能しているかを確認します。

  1. スクリプト単体での動作確認: bash -x /usr/local/bin/http-request.sh を実行し、デバッグ出力を確認しながら、エラーハンドリングや再試行ロジックが期待通りに動作するか検証します。

  2. systemdサービスの手動実行確認: sudo systemctl start http-request.service を実行後、journalctl -u http-request.service でログを確認します。成功メッセージやエラーメッセージが期待通りに出力されているか確認します。

  3. systemdタイマーの動作確認: systemctl list-timers http-request.timer で次回実行時刻を確認します。また、実際に指定した間隔でサービスが起動し、ログが更新されていることをjournalctl -u http-request.service -fで確認します。

  4. 異常系の確認:

    • ネットワークを切断したり、無効なURLを指定して、curlの再試行とスクリプトのエラー終了が正しく行われるか確認します。

    • jqのパースに失敗するような不正なJSONレスポンスをシミュレートし、スクリプトが適切にエラーを処理するか確認します。

4. 運用

  • 監視: systemdのログ(journalctl)を定期的に監視ツール(Prometheus, Grafanaなど)と連携させ、異常を検知できるようにします。

  • 設定管理: スクリプト内のAPIキーや秘密鍵などの機密情報は、環境変数、HashiCorp Vault、またはsystemdEnvironmentFileオプションなどを利用して、スクリプト本体やユニットファイルに直接記述しないようにします。

  • バージョン管理: スクリプトやsystemdユニットファイルはGitなどのバージョン管理システムで管理し、変更履歴を追跡できるようにします。

  • セキュリティアップデート: curl, jq, systemdを含むOSパッケージは常に最新の状態に保ち、セキュリティ脆弱性に対応します。

5. トラブルシュート

問題点 考えられる原因 解決策
curlでTLSエラーが発生する CA証明書が不正/不足、クライアント証明書/秘密鍵のパスが不正、パーミッションエラー curl --verbose (-v) オプションで詳細なエラー情報を確認します。CA証明書(--cacert)、クライアント証明書(--cert)、秘密鍵(--key)のパスとパーミッションを確認します。
スクリプトが途中で終了する set -eによりコマンドが失敗したため journalctl -u http-request.service でエラーログを確認します。bash -x /usr/local/bin/http-request.sh でスクリプトを単体実行し、詳細な実行トレースを追跡します。
jqでJSONパースエラーが発生する curlからのレスポンスが期待するJSON形式ではない curlの出力 (-oでファイルに保存したボディ) を直接jq .に渡して、JSON形式として有効か確認します。APIのレスポンス形式が変更されていないかドキュメントを確認します。
systemdサービスが起動しない ユニットファイルの記述ミス、パーミッションエラー sudo systemctl status http-request.service でステータスを確認します。sudo journalctl -u http-request.service でログを確認し、エラーメッセージから原因を特定します。ExecStartのパスやスクリプトの実行権限(chmod +x)を確認します。
systemdタイマーが実行されない タイマーファイルの記述ミス、タイマーが有効化/開始されていない sudo systemctl status http-request.timer でステータスを確認し、Active: active (waiting)と表示されていることを確認します。sudo systemctl enable http-request.timersudo systemctl start http-request.timer が実行済みか確認します。OnCalendarOnUnitActiveSecの書式を再確認します[5]。
root権限の問題 適切なユーザーでスクリプトが実行されていない、ファイルへのアクセス権がない http-request.serviceUser=Group=ディレクティブが正しく設定されているか確認します。スクリプトがアクセスするファイル(証明書、ログディレクトリなど)の所有者とパーミッションが、Userで指定したユーザー/グループに適切に設定されているか確認します。

6. まとめ

curlコマンドを核として、堅牢なHTTPリクエスト制御を実現するためのDevOpsプラクティスを解説しました。安全なBashスクリプトの記述、jqによるJSON処理、そしてsystemdによる定期実行とログ管理は、日々の運用業務において不可欠なスキルです。特に、root権限の適切な扱いや権限分離の原則を遵守することは、システムのセキュリティを確保する上で極めて重要です。これらの手法を組み合わせることで、安定した自動化システムを構築し、DevOpsプロセスをより効率的かつ安全に進めることができるでしょう。

参考文献

[1] Google. “Shell Style Guide.” Google Shell Style Guide. Accessed 2024年7月26日. https://google.github.io/styleguide/shell.xml [2] curl project. “curl man page.” curl.se. Accessed 2024年7月26日. https://curl.se/docs/manpage.html [3] stedolan. “jq Manual (development version).” stedolan.github.io. Accessed 2024年7月26日. https://stedolan.github.io/jq/manual/ [4] freedesktop.org. “systemd.service(5).” freedesktop.org. Accessed 2024年7月26日. https://www.freedesktop.org/software/systemd/man/systemd.service.html [5] freedesktop.org. “systemd.timer(5).” freedesktop.org. Accessed 2024年7月26日. https://www.freedesktop.org/software/systemd/man/systemd.timer.html

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

コメント

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