堅牢なシェルスクリプト設計:set -euo pipefailとtrapによる副作用の最小化

Tech
[META:VERSION] 2024-05-22-SRE-BASH
[META:ROLE] Senior SRE / DevOps Engineer
[META:FOCUS] Robustness, Error Handling, Idempotency, Clean Code
[META:LOG] Shell automation design pattern using Bash fail-safes and traps.

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

堅牢なシェルスクリプト設計:set -euo pipefailとtrapによる副作用の最小化

【導入と前提】 APIから構成データを取得し、一時的な加工を経てシステムへ適用する処理を、エラー発生時のゴミを残さず安全に自動化します。

  • OS: GNU/Linux (Bash 4.4+)

  • Tools: curl, jq, systemd

【処理フローと設計】

graph TD
    A["スクリプト開始"] --> B["シェルオプション設定/trap登録"]
    B --> C["一時ディレクトリ作成"]
    C --> D["外部APIからJSON取得: curl"]
    D --> E["データ抽出/バリデーション: jq"]
    E --> F["メイン処理の実行"]
    F --> G["正常終了"]
    G --> H["trapによる一時ファイル削除"]
    D -- エラー時 --> H
    E -- 異常値検出 --> H
    F -- 処理失敗 --> H
    H --> I["スクリプト終了"]

エラーが発生した際、どのフェーズであってもtrapが介在し、作成された一時ファイルや中間データを確実に破棄してシステムをクリーンな状態に保ちます。

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

#!/usr/bin/env bash

# -e: エラー時に即終了, -u: 未定義変数でエラー, -o pipefail: パイプ途中のエラーを拾う

set -euo pipefail

# ログ出力用関数

log() { echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*"; }

# 終了時のクリーンアップ処理(正常・異常を問わず実行)

cleanup() {
  local exit_status=$?
  if [ -d "${TMP_DIR:-}" ]; then
    rm -rf "$TMP_DIR"
    log "INFO: Temporary directory deleted."
  fi
  log "INFO: Script finished with status $exit_status"
  exit "$exit_status"
}

# EXITシグナルを捕捉してcleanupを実行

trap cleanup EXIT

# 初期設定

API_URL="https://api.example.com/v1/config"
TMP_DIR=$(mktemp -d)
CONFIG_FILE="$TMP_DIR/config.json"

log "INFO: Starting process..."

# curlによるデータ取得


# -s: 進捗非表示, -S: エラー時のみ表示, -f: HTTPエラー(4xx/5xx)で失敗扱い, -L: リダイレクト追従

curl -sSfL "$API_URL" -o "$CONFIG_FILE" || {
  log "ERROR: Failed to fetch configuration from $API_URL"
  exit 1
}

# jqによるパースとバリデーション


# -r: raw output, -e: 結果がnull/falseなら終了コード1

TARGET_VERSION=$(jq -re '.version' "$CONFIG_FILE")
log "INFO: Target configuration version: $TARGET_VERSION"

# メインの処理(例:設定の反映)


# ... ここに具体的なロジックを記述 ...

log "SUCCESS: All tasks completed successfully."

補足:systemd タイマー設定例

定期実行が必要な場合は、以下のユニットファイルで管理することでログ管理をjournaldに集約できます。

# /etc/systemd/system/config-sync.service

[Unit]
Description=Robust Config Sync Service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/config-sync.sh
User=app-user
Group=app-user

【検証と運用】

  1. 正常系の確認:

    bash -n config-sync.sh  # 構文チェック
    ./config-sync.sh        # 実行
    echo $?                 # 終了コード0を確認
    
  2. 異常系の確認: APIのURLを無効なものに変更して実行し、TMP_DIRが残っていないこと、およびログにエラーが記録されていることを確認します。

  3. ログ確認:

    journalctl -u config-sync.service
    

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

  • pipefailの罠: grepでマッチしない場合に終了コード1が返り、スクリプトが停止することがあります。必要に応じて grep ... || true で許容するか検討してください。

  • 一時ファイルの肥大化: mktemp を使用する際、ディスク容量不足(/tmpのフル)に注意してください。

  • 環境変数の漏洩: APIキーなどの機密情報を扱う際は、exportせず、必要なコマンドにのみ渡すか、jq--argなどで安全に処理してください。

【まとめ】

  1. フェイルファースト: set -e により、予期せぬ挙動を放置せず即座に停止させる。

  2. リソースの自己完結: trap を使い、異常時でも一時ファイルやロックファイルを確実に回収する。

  3. ステータスの可視化: パイプラインの各段階で jqcurl -f を活用し、論理的なエラーを終了コードに変換する。

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

コメント

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