Bash `trap` コマンドによる堅牢なエラーハンドリング

Linux・CLI・DevOpsカテゴリを表すパンダのイラスト Linux・CLI・DevOps

本記事は検証済みの業務スクリプトガイドです。

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データの堅牢な処理

  • systemd Unitと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設定が正しく機能するかを検証します。

  1. ユーザーの作成とスクリプトの配置:

    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
    
  2. systemd設定のリロード:

    sudo systemctl daemon-reload
    
  3. サービスの手動実行とログ確認:

    sudo systemctl start data_processor.service
    journalctl -u data_processor.service --since "today" -f
    

    スクリプトが成功したこと、ログが出力されていること、一時ディレクトリがクリーンアップされていることを確認します。

  4. タイマーの有効化と状態確認:

    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日

  • 変更まとめ冒頭の脱字(読点から始まっていた部分)を修正しました。

文書情報

記事タイトル
Bash `trap` コマンドによる堅牢なエラーハンドリング
作成日
更新日
Source URL
https://papanda925.com/?p=4242

ライセンス: 本記事のうち、当サイトが権利を有する本文・自作図表は、特記なき限り CC BY 4.0 で利用できます。生成AIを活用して作成・編集した内容を含みます。コードについて、別途ライセンス表示またはリンク先GitHubリポジトリのライセンスがある場合は、その条件を優先します。引用・第三者資料・画像・商標等は本ライセンスの対象外です。 利用ポリシー

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