本記事は検証済みの業務スクリプトガイドです。
Bash trap コマンドによる堅牢なエラーハンドリング
DevOps環境における自動化スクリプトは、予期せぬエラーや中断に対して堅牢である必要があります。Bashのtrapコマンドは、シグナルやスクリプト終了時の処理を定義することで、この堅牢性を大幅に向上させます。本記事では、trapを核として、set -euo pipefail、安全な一時ディレクトリ管理、curlとjqによるデータ処理、そしてsystemd連携を組み合わせた、本番運用に耐えうるBashスクリプトの実装方法を解説します。
要件と前提
本記事の目的は、運用環境で信頼性の高いBashスクリプトを構築するための実践的なガイドを提供することです。以下の要素を網羅し、冪等性 (idempotent) と安全な書き方を重視します。
前提条件:
Bashが動作するLinux環境
curl、jqがインストールされていることsystemdが利用可能なこと
カバーする内容:
set -euo pipefailによるエラーチェックの厳格化trapコマンドを使ったシグナルハンドリングとクリーンアップ処理mktempによる安全な一時ディレクトリの作成と管理curlの再試行、タイムアウト、TLS検証設定jqを用いたJSONデータの堅牢な処理systemdUnitとTimerファイルによる定期実行設定root権限の扱いと権限分離の注意点
実装
全体フロー
まず、スクリプトの処理とエラーハンドリングの全体像をMermaidフローチャートで示します。
graph TD
A["スクリプト開始"] --> B{"環境設定と一時ディレクトリ作成
(set -euo pipefail, mktemp, trap EXIT)"};
B --> C{"curlで外部APIからデータ取得
(TLS検証, 再試行設定)"};
C -- 成功 --> D{"jqでJSONデータを処理
(結果検証)"};
C -- 失敗/中断 --> F["エラーハンドリングとクリーンアップ
(trap ERR/INT/TERM)"];
D -- 成功 --> E["結果出力とログ記録"];
D -- 失敗/中断 --> F;
E --> G["一時ディレクトリクリーンアップ
(trap EXITにより自動実行)"];
F --> G;
G --> H["スクリプト終了"];
B -- 中断 (INT/TERM) --> F;
安全なBashスクリプトの基礎
堅牢なスクリプトの第一歩は、厳格なエラーチェックと安全な一時ファイル管理です。
set -euo pipefail の活用
スクリプトの冒頭で以下のオプションを設定することで、予期せぬエラーや未定義変数を早期に検出し、パイプラインでのエラー伝播を保証します。
set -e: コマンドが非ゼロの終了ステータスで終了した場合、スクリプトを即座に終了させます。set -u: 未定義の変数を参照しようとした場合、エラーとしてスクリプトを終了させます。set -o pipefail: パイプライン内で一つでも非ゼロ終了するコマンドがあった場合、パイプライン全体の終了ステータスがその非ゼロの終了ステータスになります。
mktemp による一時ディレクトリの安全な管理
一時ファイルやディレクトリは、予測可能な名前や場所で作るとセキュリティリスクや競合の問題を引き起こす可能性があります。mktemp を使うことで、一意かつ安全な一時ディレクトリを作成できます。
# 一時ディレクトリを作成し、変数にパスを格納 tmpdir=$(mktemp -d -t data-processor-XXXXXXXX) || exit 1 # スクリプト終了時に一時ディレクトリを削除するトラップを設定 trap 'rm -rf "$tmpdir"' EXIT
mktemp -d: ディレクトリを作成します。-t data-processor-XXXXXXXX: テンプレートを指定します。XXXXXXXX部分はランダムな文字列に置換されます。trap 'rm -rf "$tmpdir"' EXIT: スクリプトが正常終了するか、エラーで終了するかに関わらず、EXITシグナルが発行されたときに$tmpdirを削除するよう設定します。
trap コマンドによるエラーハンドリング
trapコマンドは、特定のシグナル(EXIT, ERR, INT, TERMなど)が発行されたときに実行するコマンドや関数を定義します。これにより、クリーンアップやエラーログ記録などの処理を確実に行えます。
#!/bin/bash
# process_data.sh
# 厳格なエラーチェック
set -euo pipefail
# ログファイルパス
LOG_FILE="/var/log/data_processor.log"
# APIエンドポイントの例
API_URL="https://jsonplaceholder.typicode.com/posts/1"
# --- 関数定義 ---
# クリーンアップ関数
cleanup_on_exit() {
local exit_code="$?"
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Script finished with exit code: $exit_code. Cleaning up temporary directory..." | tee -a "$LOG_FILE"
if [[ -d "$tmpdir" ]]; then
rm -rf "$tmpdir"
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Temporary directory '$tmpdir' removed." | tee -a "$LOG_FILE"
fi
exit "$exit_code"
}
# エラーハンドラ関数 (ERRシグナル用)
error_handler() {
local last_command="$BASH_COMMAND"
local line_number="$LINENO"
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] An error occurred on line $line_number, command: '$last_command'." | tee -a "$LOG_FILE"
exit 1
}
# 中断ハンドラ関数 (INT/TERMシグナル用)
interrupt_handler() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [WARN] Script interrupted by signal (INT/TERM). Initiating graceful shutdown..." | tee -a "$LOG_FILE"
exit 130
}
# --- trap 設定 ---
trap 'cleanup_on_exit' EXIT
trap 'error_handler' ERR
trap 'interrupt_handler' INT TERM
# --- メイン処理 ---
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Script started." | tee -a "$LOG_FILE"
tmpdir=$(mktemp -d -t data-processor-XXXXXXXX)
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Created temporary directory: $tmpdir" | tee -a "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Fetching data from $API_URL..." | tee -a "$LOG_FILE"
if ! curl -sS --fail-with-body --retry 5 --retry-delay 3 --retry-max-time 30 \
--output "$tmpdir/data.json" "$API_URL"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] Failed to fetch data from $API_URL." | tee -a "$LOG_FILE"
exit 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Data fetched successfully to $tmpdir/data.json." | tee -a "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Processing JSON data..." | tee -a "$LOG_FILE"
if ! jq -e '.title' < "$tmpdir/data.json" > "$tmpdir/processed_title.txt"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] Failed to process JSON data with jq." | tee -a "$LOG_FILE"
exit 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] JSON data processed. Extracted title:" | tee -a "$LOG_FILE"
cat "$tmpdir/processed_title.txt" | tee -a "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Script completed successfully." | tee -a "$LOG_FILE"
curl と jq を用いた堅牢なデータ処理
上記のメインスクリプト例で、curl と jq の安全な利用法を組み込んでいます。
curl の設定
-sS(--silent --show-error): プログレスメーターを非表示にし、エラーメッセージのみ表示します。--fail-with-body: HTTPステータスコードが400以上の場合に終了ステータスを非ゼロにし、レスポンスボディも出力します。--retry 5: 失敗した場合、最大5回までリトライします。--retry-delay 3: リトライ間隔を3秒に設定します。--retry-max-time 30: リトライを含む総時間を30秒までに制限します。--output "$tmpdir/data.json": 取得したデータを一時ファイルに保存します。
jq の設定
jq -e '.title': JSONから.titleフィールドを抽出します。-eまたは--exit-statusオプションは、出力がnullの場合やエラーが発生した場合に非ゼロの終了ステータスを返します。これにより、set -eとtrap ERRが適切に機能します。
systemd Unitファイルの定義
このスクリプトをsystemdで管理するために、Unitファイルを作成します。例えば、/etc/systemd/system/data_processor.service として保存します。
# /etc/systemd/system/data_processor.service [Unit] Description=Daily data processing script After=network.target [Service] Type=oneshot ExecStart=/usr/local/bin/data_processor.sh WorkingDirectory=/opt/data-processor User=data_user Group=data_user StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target
Type=oneshot: コマンドが実行され、終了するとサービスも終了する単発のスクリプトに適しています。ExecStart: 実行するスクリプトのフルパスを指定します。WorkingDirectory: スクリプトが実行される作業ディレクトリを指定します。User=data_user,Group=data_user: セキュリティのために、スクリプトを特定の非特権ユーザーとグループで実行させます。StandardOutput=journal,StandardError=journal: スクリプトの標準出力と標準エラー出力をjournaldにリダイレクトし、集中ログ管理を可能にします。
systemd Timerファイルの定義
このサービスを定期的に実行するために、Timerファイルを作成します。例えば、/etc/systemd/system/data_processor.timer として保存します。
# /etc/systemd/system/data_processor.timer [Unit] Description=Run data_processor script daily at 3 AM RefuseManualStart=no RefuseManualStop=no [Timer] OnCalendar=*-*-* 03:00:00 Persistent=true [Install] WantedBy=timers.target
OnCalendar=*-*-* 03:00:00: 毎日午前3時00分にサービスを起動するよう設定します。Persistent=true: システムがダウンしている間にタイマーの実行時間が過ぎた場合、システム起動後に即座にサービスが起動されます。
検証
作成したスクリプトとsystemd設定が正しく機能するかを検証します。
ユーザーの作成とスクリプトの配置:
sudo useradd -r -s /sbin/nologin data_user sudo mkdir -p /opt/data-processor /usr/local/bin sudo chown data_user:data_user /opt/data-processor sudo chmod +x /usr/local/bin/data_processor.sh
systemd設定のリロード:
sudo systemctl daemon-reload
サービスの手動実行とログ確認:
sudo systemctl start data_processor.service journalctl -u data_processor.service --since "today" -f
スクリプトが成功したこと、ログが出力されていること、一時ディレクトリがクリーンアップされていることを確認します。
タイマーの有効化と状態確認:
sudo systemctl enable --now data_processor.timer systemctl list-timers data_processor.timer
タイマーが有効化され、次回実行時刻が正しく設定されていることを確認します。
運用
ログ監視
systemdのStandardOutputとStandardErrorをjournalに設定することで、スクリプトのログはjournalctlで一元的に管理されます。
journalctl -u data_processor.service: サービスのログを表示します。journalctl -u data_processor.service -f: リアルタイムでログを追跡します。
まとめ
本記事では、Bashのtrapコマンドを核として、set -euo pipefail、mktemp、curl、jq、そしてsystemdを組み合わせた堅牢なエラーハンドリングとスクリプト実行環境の構築方法を解説しました。
DevOpsエンジニアとして、自動化されたスクリプトが運用環境で安定して動作することは極めて重要です。trapを使ったクリーンアップ処理やエラーハンドリング、systemdによる適切な権限分離とログ管理を導入することで、システムの信頼性と保守性を大幅に向上させることができます。
この記事の更新履歴
この記事は、生成AIを活用した自動レビュー・更新フローにより内容を見直し、必要な修正を反映しています。
2026年9月15日
- 変更まとめ冒頭の脱字(読点から始まっていた部分)を修正しました。

