<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">curlを用いたREST API連携の安全な自動化とsystemdによる定期実行</h1>
<p><code>curl</code>コマンドは、REST API連携における強力かつ汎用的なツールである。本記事では、<code>curl</code>と<code>jq</code>を組み合わせ、安全で冪等なスクリプトを記述し、<code>systemd</code>による定期実行の仕組みを解説する。</p>
<h2 class="wp-block-heading">要件と前提</h2>
<p>本記事の要件は、外部のREST APIに対しデータを送信し、応答を処理する自動化されたプロセスを構築することである。前提として、Linux環境で<code>bash</code>、<code>curl</code>、<code>jq</code>が利用可能であること、および対象のREST APIエンドポイントが存在することを想定する。スクリプトは冪等性を保ち、エラー発生時の再試行メカニズムを含み、一時ファイルの安全な取り扱いを徹底する。</p>
<h2 class="wp-block-heading">実装</h2>
<p>API連携スクリプトは、TLS通信、JSONデータの送受信、応答の検証、そしてエラー時の再試行を実装する。一時ディレクトリの使用とクリーンアップは必須である。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/usr/bin/env bash
set -euo pipefail # エラー時に即座に終了、未定義変数を使用禁止、パイプライン失敗を検出
# 一時ディレクトリの作成とクリーンアップ
# mktemp -dは安全な一時ディレクトリを作成し、所有者のみアクセス可能
TMP_DIR=$(mktemp -d -t api_call_XXXXXXXX)
log_file="${TMP_DIR}/api_output.log"
# スクリプト終了時に一時ディレクトリを削除するトラップ
cleanup() {
echo "INFO: Cleaning up temporary directory: ${TMP_DIR}" >&2
rm -rf "${TMP_DIR}"
}
trap cleanup EXIT INT TERM
# 変数定義
API_ENDPOINT="https://httpbin.org/post" # テスト用POSTエンドポイント
API_KEY="your_secure_api_key_here" # 実際のAPIキーは環境変数やsecrets managerから取得推奨
MAX_RETRIES=5
RETRY_DELAY=5 # 秒
# APIリクエストペイロード
# ここではテスト用に静的なJSONを使用するが、実運用では動的に生成
REQUEST_BODY='{"id": "unique-transaction-id-001", "status": "processed", "data": {"value": 123}}'
echo "INFO: Starting API request to ${API_ENDPOINT}" >&2
# API呼び出し関数 (再試行ロジックを含む)
perform_api_call() {
local attempt=1
while [[ ${attempt} -le ${MAX_RETRIES} ]]; do
echo "INFO: Attempt ${attempt}/${MAX_RETRIES}..." >&2
# curlコマンド:
# -s: サイレントモード (プログレスメーター非表示)
# -S: エラー表示を強制 (サイレントモードでもエラーは表示)
# -X POST: POSTリクエスト
# -H: ヘッダー追加 (Content-Type, Authorizationなど)
# -d: リクエストボディ
# --retry, --retry-delay, --retry-max-time: curl自身の再試行機能 (今回はカスタム実装)
# -w: レスポンス後に情報を出力 (HTTPステータスコードなどを取得可能)
# -k: 証明書検証を無効化 (テスト環境のみ推奨、本番では使用しない)
# --cacert /path/to/ca.pem: 信頼するCA証明書を指定 (本番環境推奨)
response=$(curl -sS -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${API_KEY}" \
-d "${REQUEST_BODY}" \
"${API_ENDPOINT}" \
--connect-timeout 10 \
--max-time 30 \
|| { echo "ERROR: curl command failed." >&2; return 1; })
# HTTPステータスコードの取得 (ここではレスポンス全体をjqで処理)
# jqで処理できない場合はcurlの-w "%{http_code}" を利用
if echo "${response}" | jq -e '.json.id == "unique-transaction-id-001"' > /dev/null; then
echo "INFO: API call successful." >&2
echo "${response}" | jq . > "${log_file}"
return 0 # 成功
else
echo "WARN: API call failed or unexpected response. Response:" >&2
echo "${response}" | jq . || echo "${response}" >&2 # JSONパース失敗時も表示
attempt=$((attempt + 1))
if [[ ${attempt} -le ${MAX_RETRIES} ]]; then
echo "INFO: Retrying in ${RETRY_DELAY} seconds..." >&2
sleep "${RETRY_DELAY}"
fi
fi
done
echo "ERROR: Max retries (${MAX_RETRIES}) reached. API call failed permanently." >&2
return 1 # 最大再試行回数を超過
}
perform_api_call
exit_code=$?
if [[ ${exit_code} -eq 0 ]]; then
echo "INFO: Script completed successfully. Output in ${log_file}" >&2
else
echo "ERROR: Script failed." >&2
fi
exit "${exit_code}"
</pre>
</div>
<p>このスクリプトは、<code>httpbin.org</code>の <code>/post</code> エンドポイントに対しJSONデータを送信し、レスポンス内の特定のキーを<code>jq</code>で検証する。失敗時には複数回再試行する。</p>
<h2 class="wp-block-heading">検証</h2>
<p>スクリプトが正しく動作することを確認する。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># スクリプトを /usr/local/bin/my_api_script.sh として保存し、実行権限を付与
chmod +x /usr/local/bin/my_api_script.sh
# スクリプトの実行
/usr/local/bin/my_api_script.sh
# 成功時のログ確認
# cat /tmp/api_call_XXXXXX/api_output.log のようなパス
# jqで処理されたJSONレスポンスが表示される
</pre>
</div>
<p>成功すれば、スクリプトの出力に成功メッセージと、一時ファイルにJSONレスポンスが記録される。失敗時には、エラーメッセージと再試行のログが出力される。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["スクリプト開始"] --> B{"一時ディレクトリ作成"};
B --> C["APIリクエストペイロード準備"];
C --> D{"API呼び出し (試行 #1)"};
D -- 成功 --> E["レスポンスをjqで検証"];
D -- 失敗 --> F{"再試行上限に達したか?"};
F -- いいえ --> G["待機し、再試行"];
F -- はい --> H["エラー終了"];
E -- 成功 --> I["ログファイルに保存"];
E -- 失敗 --> F;
I --> J["正常終了"];
G --> D;
</pre></div>
<h2 class="wp-block-heading">運用</h2>
<p>本スクリプトを定期的に実行するため、<code>systemd</code>のユニットとタイマーを使用する。セキュリティのため、専用の非特権ユーザーで実行することが推奨される。</p>
<p>まず、スクリプト実行用の専用ユーザーを作成する。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo useradd --system --no-create-home --shell /sbin/nologin api_user
</pre>
</div>
<p>次に、<code>systemd</code>サービスユニットファイルを作成する。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /etc/systemd/system/example-api-call.service
[Unit]
Description=Periodically call external API
After=network.target
[Service]
# Root権限の回避: 専用の非特権ユーザーで実行
User=api_user
Group=api_user
# WorkingDirectoryはスクリプトの実行場所
# 環境変数などでAPIキーを渡す場合は、ここに追加するか、スクリプト内でsecrets managerから取得
# Environment="API_KEY=your_secure_api_key_here"
ExecStart=/usr/local/bin/my_api_script.sh
# 失敗時に自動再起動しない (タイマーで制御するため)
Restart=no
Type=oneshot
# 標準出力/エラー出力をjournaldに送る
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
</pre>
</div>
<p>続いて、タイマーユニットファイルを作成する。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /etc/systemd/system/example-api-call.timer
[Unit]
Description=Run example-api-call service daily
[Timer]
# システム起動1分後に一度実行
OnBootSec=1min
# 毎日午前3時に実行
OnCalendar=*-*-* 03:00:00
# サービス実行後、次回実行までの間隔 (OnCalendarと併用可能)
# OnUnitActiveSec=24h
# タイマーが有効化された際にすぐに実行 (デバッグ時などに便利)
# Persistent=true
[Install]
WantedBy=timers.target
</pre>
</div>
<p><code>systemd</code>の設定をリロードし、タイマーを有効化して起動する。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">sudo systemctl daemon-reload
sudo systemctl enable example-api-call.timer
sudo systemctl start example-api-call.timer
</pre>
</div>
<p>ログの確認は<code>journalctl</code>を使用する。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">journalctl -u example-api-call.service -f
journalctl -u example-api-call.timer
</pre>
</div>
<p><strong>root権限の扱いと権限分離の注意点:</strong>
<code>systemd</code>サービスを<code>root</code>以外のユーザー (<code>api_user</code>) で実行することで、スクリプトに起因する潜在的なセキュリティリスクを最小限に抑えることができる。<code>api_user</code>は最小限の権限のみを持つべきであり、スクリプトが必要とするファイルやディレクトリへのアクセス権限のみを付与する。<code>/usr/local/bin</code>に置かれたスクリプトは通常、システム全体の実行権限を持つため、<code>api_user</code>はそのスクリプトを実行できる。機密情報は環境変数や<code>systemd</code>のSecureBoot/TPM連携、または専用のシークレット管理サービスから取得し、決してスクリプト内にハードコードしない。</p>
<h2 class="wp-block-heading">トラブルシュート</h2>
<ul class="wp-block-list">
<li><strong>スクリプトの実行失敗:</strong> <code>journalctl -u example-api-call.service</code> で詳細なエラーログを確認する。<code>set -euo pipefail</code> により、エラー箇所が特定しやすい。</li>
<li><strong>APIからの応答が不正:</strong> <code>jq</code>のフィルタ条件を見直す。<code>echo "${response}" | jq .</code> で実際の応答構造を確認し、期待するJSONパスを特定する。</li>
<li><strong>TLS/SSL証明書エラー:</strong> <code>--cacert</code>オプションで信頼するCA証明書パスを指定するか、テスト環境でのみ<code>-k</code>(危険)を使用する。本番環境では証明書チェーンが正しく設定されているか確認する。</li>
<li><strong>systemdタイマーが起動しない:</strong> <code>systemctl status example-api-call.timer</code> および <code>journalctl -u example-api-call.timer</code> でタイマーの稼働状況を確認する。<code>OnCalendar</code>の記述ミスがないか確認する。</li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p><code>curl</code>と<code>jq</code>を組み合わせることで、REST APIとの堅牢な連携スクリプトを構築できる。さらに、<code>set -euo pipefail</code>や<code>trap</code>を用いた安全なbashスクリプトの記述、<code>mktemp -d</code>による一時ファイルの管理は、スクリプトの信頼性を高める。<code>systemd</code>サービスとタイマーを利用し、専用の非特権ユーザーでスクリプトを定期実行することは、運用上の効率性とセキュリティを両立させるDevOpsのベストプラクティスである。</p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
curlを用いたREST API連携の安全な自動化とsystemdによる定期実行
curl
コマンドは、REST API連携における強力かつ汎用的なツールである。本記事では、curl
とjq
を組み合わせ、安全で冪等なスクリプトを記述し、systemd
による定期実行の仕組みを解説する。
要件と前提
本記事の要件は、外部のREST APIに対しデータを送信し、応答を処理する自動化されたプロセスを構築することである。前提として、Linux環境でbash
、curl
、jq
が利用可能であること、および対象のREST APIエンドポイントが存在することを想定する。スクリプトは冪等性を保ち、エラー発生時の再試行メカニズムを含み、一時ファイルの安全な取り扱いを徹底する。
実装
API連携スクリプトは、TLS通信、JSONデータの送受信、応答の検証、そしてエラー時の再試行を実装する。一時ディレクトリの使用とクリーンアップは必須である。
#!/usr/bin/env bash
set -euo pipefail # エラー時に即座に終了、未定義変数を使用禁止、パイプライン失敗を検出
# 一時ディレクトリの作成とクリーンアップ
# mktemp -dは安全な一時ディレクトリを作成し、所有者のみアクセス可能
TMP_DIR=$(mktemp -d -t api_call_XXXXXXXX)
log_file="${TMP_DIR}/api_output.log"
# スクリプト終了時に一時ディレクトリを削除するトラップ
cleanup() {
echo "INFO: Cleaning up temporary directory: ${TMP_DIR}" >&2
rm -rf "${TMP_DIR}"
}
trap cleanup EXIT INT TERM
# 変数定義
API_ENDPOINT="https://httpbin.org/post" # テスト用POSTエンドポイント
API_KEY="your_secure_api_key_here" # 実際のAPIキーは環境変数やsecrets managerから取得推奨
MAX_RETRIES=5
RETRY_DELAY=5 # 秒
# APIリクエストペイロード
# ここではテスト用に静的なJSONを使用するが、実運用では動的に生成
REQUEST_BODY='{"id": "unique-transaction-id-001", "status": "processed", "data": {"value": 123}}'
echo "INFO: Starting API request to ${API_ENDPOINT}" >&2
# API呼び出し関数 (再試行ロジックを含む)
perform_api_call() {
local attempt=1
while [[ ${attempt} -le ${MAX_RETRIES} ]]; do
echo "INFO: Attempt ${attempt}/${MAX_RETRIES}..." >&2
# curlコマンド:
# -s: サイレントモード (プログレスメーター非表示)
# -S: エラー表示を強制 (サイレントモードでもエラーは表示)
# -X POST: POSTリクエスト
# -H: ヘッダー追加 (Content-Type, Authorizationなど)
# -d: リクエストボディ
# --retry, --retry-delay, --retry-max-time: curl自身の再試行機能 (今回はカスタム実装)
# -w: レスポンス後に情報を出力 (HTTPステータスコードなどを取得可能)
# -k: 証明書検証を無効化 (テスト環境のみ推奨、本番では使用しない)
# --cacert /path/to/ca.pem: 信頼するCA証明書を指定 (本番環境推奨)
response=$(curl -sS -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${API_KEY}" \
-d "${REQUEST_BODY}" \
"${API_ENDPOINT}" \
--connect-timeout 10 \
--max-time 30 \
|| { echo "ERROR: curl command failed." >&2; return 1; })
# HTTPステータスコードの取得 (ここではレスポンス全体をjqで処理)
# jqで処理できない場合はcurlの-w "%{http_code}" を利用
if echo "${response}" | jq -e '.json.id == "unique-transaction-id-001"' > /dev/null; then
echo "INFO: API call successful." >&2
echo "${response}" | jq . > "${log_file}"
return 0 # 成功
else
echo "WARN: API call failed or unexpected response. Response:" >&2
echo "${response}" | jq . || echo "${response}" >&2 # JSONパース失敗時も表示
attempt=$((attempt + 1))
if [[ ${attempt} -le ${MAX_RETRIES} ]]; then
echo "INFO: Retrying in ${RETRY_DELAY} seconds..." >&2
sleep "${RETRY_DELAY}"
fi
fi
done
echo "ERROR: Max retries (${MAX_RETRIES}) reached. API call failed permanently." >&2
return 1 # 最大再試行回数を超過
}
perform_api_call
exit_code=$?
if [[ ${exit_code} -eq 0 ]]; then
echo "INFO: Script completed successfully. Output in ${log_file}" >&2
else
echo "ERROR: Script failed." >&2
fi
exit "${exit_code}"
このスクリプトは、httpbin.org
の /post
エンドポイントに対しJSONデータを送信し、レスポンス内の特定のキーをjq
で検証する。失敗時には複数回再試行する。
検証
スクリプトが正しく動作することを確認する。
# スクリプトを /usr/local/bin/my_api_script.sh として保存し、実行権限を付与
chmod +x /usr/local/bin/my_api_script.sh
# スクリプトの実行
/usr/local/bin/my_api_script.sh
# 成功時のログ確認
# cat /tmp/api_call_XXXXXX/api_output.log のようなパス
# jqで処理されたJSONレスポンスが表示される
成功すれば、スクリプトの出力に成功メッセージと、一時ファイルにJSONレスポンスが記録される。失敗時には、エラーメッセージと再試行のログが出力される。
graph TD
A["スクリプト開始"] --> B{"一時ディレクトリ作成"};
B --> C["APIリクエストペイロード準備"];
C --> D{"API呼び出し (試行 #1)"};
D -- 成功 --> E["レスポンスをjqで検証"];
D -- 失敗 --> F{"再試行上限に達したか?"};
F -- いいえ --> G["待機し、再試行"];
F -- はい --> H["エラー終了"];
E -- 成功 --> I["ログファイルに保存"];
E -- 失敗 --> F;
I --> J["正常終了"];
G --> D;
運用
本スクリプトを定期的に実行するため、systemd
のユニットとタイマーを使用する。セキュリティのため、専用の非特権ユーザーで実行することが推奨される。
まず、スクリプト実行用の専用ユーザーを作成する。
sudo useradd --system --no-create-home --shell /sbin/nologin api_user
次に、systemd
サービスユニットファイルを作成する。
# /etc/systemd/system/example-api-call.service
[Unit]
Description=Periodically call external API
After=network.target
[Service]
# Root権限の回避: 専用の非特権ユーザーで実行
User=api_user
Group=api_user
# WorkingDirectoryはスクリプトの実行場所
# 環境変数などでAPIキーを渡す場合は、ここに追加するか、スクリプト内でsecrets managerから取得
# Environment="API_KEY=your_secure_api_key_here"
ExecStart=/usr/local/bin/my_api_script.sh
# 失敗時に自動再起動しない (タイマーで制御するため)
Restart=no
Type=oneshot
# 標準出力/エラー出力をjournaldに送る
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
続いて、タイマーユニットファイルを作成する。
# /etc/systemd/system/example-api-call.timer
[Unit]
Description=Run example-api-call service daily
[Timer]
# システム起動1分後に一度実行
OnBootSec=1min
# 毎日午前3時に実行
OnCalendar=*-*-* 03:00:00
# サービス実行後、次回実行までの間隔 (OnCalendarと併用可能)
# OnUnitActiveSec=24h
# タイマーが有効化された際にすぐに実行 (デバッグ時などに便利)
# Persistent=true
[Install]
WantedBy=timers.target
systemd
の設定をリロードし、タイマーを有効化して起動する。
sudo systemctl daemon-reload
sudo systemctl enable example-api-call.timer
sudo systemctl start example-api-call.timer
ログの確認はjournalctl
を使用する。
journalctl -u example-api-call.service -f
journalctl -u example-api-call.timer
root権限の扱いと権限分離の注意点:
systemd
サービスをroot
以外のユーザー (api_user
) で実行することで、スクリプトに起因する潜在的なセキュリティリスクを最小限に抑えることができる。api_user
は最小限の権限のみを持つべきであり、スクリプトが必要とするファイルやディレクトリへのアクセス権限のみを付与する。/usr/local/bin
に置かれたスクリプトは通常、システム全体の実行権限を持つため、api_user
はそのスクリプトを実行できる。機密情報は環境変数やsystemd
のSecureBoot/TPM連携、または専用のシークレット管理サービスから取得し、決してスクリプト内にハードコードしない。
トラブルシュート
- スクリプトの実行失敗:
journalctl -u example-api-call.service
で詳細なエラーログを確認する。set -euo pipefail
により、エラー箇所が特定しやすい。
- APIからの応答が不正:
jq
のフィルタ条件を見直す。echo "${response}" | jq .
で実際の応答構造を確認し、期待するJSONパスを特定する。
- TLS/SSL証明書エラー:
--cacert
オプションで信頼するCA証明書パスを指定するか、テスト環境でのみ-k
(危険)を使用する。本番環境では証明書チェーンが正しく設定されているか確認する。
- systemdタイマーが起動しない:
systemctl status example-api-call.timer
および journalctl -u example-api-call.timer
でタイマーの稼働状況を確認する。OnCalendar
の記述ミスがないか確認する。
まとめ
curl
とjq
を組み合わせることで、REST APIとの堅牢な連携スクリプトを構築できる。さらに、set -euo pipefail
やtrap
を用いた安全なbashスクリプトの記述、mktemp -d
による一時ファイルの管理は、スクリプトの信頼性を高める。systemd
サービスとタイマーを利用し、専用の非特権ユーザーでスクリプトを定期実行することは、運用上の効率性とセキュリティを両立させるDevOpsのベストプラクティスである。
コメント