シェルスクリプトにおける異常終了時の副作用を防止する堅牢なエラーハンドリング自動化

Tech

<!-- META [STYLE]

  • Technical article for SRE/DevOps ENGINEERS

  • High clarity, practical focus [METADATA]

  • TOPIC: Robust Shell Scripting with set -euo pipefail and trap

  • TARGET: Linux/SRE Engineers --> 本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

    シェルスクリプトにおける異常終了時の副作用を防止する堅牢なエラーハンドリング自動化

    【導入と前提】

    外部APIからのデータ取得と加工処理において、途中でエラーが発生した際の副作用やゴミファイルの残存を防ぐ堅牢なシェルスクリプト構成を構築します。

    前提条件:

    • OS: Ubuntu 22.04 LTS / RHEL 9 (Bash 5.x 以上)

    • 依存ツール: curl (7.68.0以降), jq (1.6以降), systemd (249以降)

    • 権限: 一般ユーザー権限(一部ログ確認やsystemd配置時のみ sudo が必要)


    【処理フローと設計】

    以下のフローに従い、一時ファイルの作成から安全な後処理(クリーンアップ)までを実施します。失敗時には trap が検知して不要な生成物を即座に破棄します。

    graph TD
        A["Start Script"] --> B["Set Safe Flags & Trap"]
        B --> C["Create Temp File"]
        C --> D["Fetch API Data via curl"]
        D -->|Success| E["Parse & Validate via jq"]
        D -->|Failure| H["Trigger Trap Cleanup"]
        E -->|Success| F["Atomically Move Output"]
        E -->|Failure| H
        F --> G["Normal Exit Cleanup"]
        H --> I["Abnormal Exit Log & Cleanup"]
    

    処理概要

    1. シグナル検知と初期化: 失敗時や中断時に特定処理(一時ファイル削除)を呼び出す trap を登録。

    2. API取得: curl でリトライ処理を含めたデータダウンロード。

    3. バリデーション: jq を利用して受領したJSONの健全性を確認。

    4. アトミック移動: 加工済みデータをアトミック(不可分)に本番領域へ配置。

    5. クリーンアップ: 正常時・異常時問わず確実に一時領域を解放。


    【実装:堅牢な自動化スクリプト】

    1. メインシェルスクリプト (/usr/local/bin/fetch_metrics.sh)

    #!/usr/bin/env bash
    
    # set -euo pipefail の解説:
    
    
    # -e: エラー(0以外のステータス)が発生した時点で即座にスクリプトを終了
    
    
    # -u: 未定義の変数を参照した場合にエラーとして終了
    
    
    # -o pipefail: パイプライン内で1つでも失敗したコマンドがあれば、全体の戻り値を失敗として扱う
    
    set -euo pipefail
    
    # スクリプト定数
    
    readonly API_URL="https://api.example.com/v1/metrics"
    readonly OUTPUT_FILE="/var/log/app/metrics.json"
    readonly LOG_TAG="fetch_metrics"
    
    # 一時ディレクトリの作成 (mktemp を使用して推測不可能な領域を確保)
    
    TMP_DIR=$(mktemp -d -t metrics_builder.XXXXXX)
    
    # リソース解放用のクリーンアップ関数
    
    cleanup() {
        local exit_code=$?
    
        # 一時ディレクトリが存在する場合に削除
    
        if [[ -d "${TMP_DIR}" ]]; then
            rm -rf "${TMP_DIR}"
            logger -t "${LOG_TAG}" "INFO: Temporary directory ${TMP_DIR} cleaned up."
        fi
        if [[ ${exit_code} -ne 0 ]]; then
            logger -t "${LOG_TAG}" "ERROR: Script failed with exit code ${exit_code}."
        fi
        exit "${exit_code}"
    }
    
    # EXIT (正常終了・異常終了両方), INT (Ctrl+C), TERM (kill) シグナル発生時に cleanup を実行
    
    trap cleanup EXIT INT TERM
    
    logger -t "${LOG_TAG}" "INFO: Starting metrics collection..."
    
    # 一時ファイルのパス指定
    
    tmp_download_file="${TMP_DIR}/raw_data.json"
    tmp_parsed_file="${TMP_DIR}/parsed_data.json"
    
    # curl による安全なデータ取得
    
    
    # -s: 進捗を表示しない (silent)
    
    
    # -S: -s 使用時でもエラー発生時はメッセージを表示 (show-error)
    
    
    # -f: HTTPレスポンスエラー(4xx/5xx)時に失敗ステータスを返す (fail)
    
    
    # -L: リダイレクトを追跡 (location)
    
    
    # --retry: 通信失敗時の再試行回数
    
    
    # --retry-connrefused: 接続拒否時もリトライ対象に含む
    
    
    # --connect-timeout: 接続タイムアウト秒数
    
    curl -sSfL \
        --connect-timeout 10 \
        --retry 3 \
        --retry-delay 2 \
        --retry-connrefused \
        "${API_URL}" -o "${tmp_download_file}"
    
    # jq による構造チェックとデータ抽出
    
    
    # -e: 出力が null または false の場合に戻り値 1 を返す (exit-status)
    
    jq -e '.status == "success" and (.data | type == "array")' "${tmp_download_file}" > /dev/null
    
    # 必要なフィールドのみ抽出し、整形
    
    jq -c '.data[] | {timestamp: .ts, value: .metric_value}' "${tmp_download_file}" > "${tmp_parsed_file}"
    
    # アトミックなファイル置換 (出力先ディレクトリの存在確認含む)
    
    mkdir -p "$(dirname "${OUTPUT_FILE}")"
    mv -f "${tmp_parsed_file}" "${OUTPUT_FILE}"
    
    logger -t "${LOG_TAG}" "INFO: Successfully updated ${OUTPUT_FILE}"
    

    2. systemd サービスユニット (/etc/systemd/system/fetch-metrics.service)

    [Unit]
    Description=Fetch Metrics and Process JSON
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/bin/fetch_metrics.sh
    User=root
    Group=root
    
    # セキュリティ設定とリソース保護
    
    PrivateTmp=true
    ProtectSystem=full
    ProtectHome=true
    
    [Install]
    WantedBy=multi-user.target
    

    3. systemd タイマーユニット (/etc/systemd/system/fetch-metrics.timer)

    [Unit]
    Description=Run Fetch Metrics Every 5 Minutes
    
    [Timer]
    OnCalendar=*:0/5
    Persistent=true
    
    [Install]
    WantedBy=timers.target
    

    【検証と運用】

    1. スクリプトの検証(正常系および異常系テスト)

    # 実行権限の付与
    
    sudo chmod +x /usr/local/bin/fetch_metrics.sh
    
    # 手動実行テスト (正常系)
    
    /usr/local/bin/fetch_metrics.sh
    
    # 戻り値の確認 (0 であれば正常)
    
    echo $?
    
    # 意図的に未定義変数参照を起こす動作テスト (set -u の確認)
    
    bash -u -c 'echo "$UNDEFINED_VAR"'
    

    2. ログ確認(journalctl & syslogs)

    スクリプト内部で logger コマンドを使用しているため、syslog または journalctl でクリーンアップ状況を含め追跡可能です。

    # syslog メッセージの絞り込み確認
    
    journalctl -t fetch_metrics --since "1 hour ago"
    
    # systemd サービス実行結果のログ確認
    
    sudo journalctl -u fetch-metrics.service -n 50 --no-pager
    

    【トラブルシューティングと落とし穴】

    1. set -eif 構文の相互作用: ifelif の条件式内部、または && / || の左辺で実行されるコマンドは、set -e の対象外となりエラーでスクリプトが止まりません。明示的に戻り値をハンドリングする必要があります。

      # 危険: 単体で失敗しても set -e が効かない場合がある
      
      command_a || command_b
      
      # 安全: 明示的にハンドリング
      
      if ! command_a; then
          logger "command_a failed"
          exit 1
      fi
      
    2. set -u と配列の未定義参照: 空の配列に対して ${array[@]} を参照すると、Bashのバージョンや設定により未定義変数エラーとして扱われます。

      # 解消策: 初期化されているか確認するか、デフォルト値指定構文を使用
      
      echo "${array[@]:-}"
      
    3. sudo 実行時の一時ファイルパーミッション問題: スクリプト内で sudo 権限を使ってファイルを移動すると、作成された一時ファイル(mktemp)が root 所有となり、以降一般ユーザー権限で削除できなくなるリスクがあります。実行ユーザー(User=)を明示してプロセスの全コンテキストを同一ユーザーに統一してください。


    【まとめ】

    シェルスクリプトの冪等性と安全性を保ち、運用の健全性を維持するための3つの重要ポイント:

    1. 厳格な実行モード指定: スクリプト冒頭で set -euo pipefail を定義し、想定外の未定義参照やパイプライン内のサイレントエラーを排除する。

    2. trap による一括後処理: シグナル(EXIT, TERM, INT)に対するクリーンアップ関数を登録し、中間ファイルやプロセスロックの残存を極限まで減らす。

    3. 不可分(アトミック)なリソース更新: データの加工・書き出しはすべて一時ファイルで行い、最終段階で mv コマンド等を用いて不完全な状態のファイルを外部に晒さない。

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

コメント

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