運用事故を防ぐ堅牢なシェルスクリプト:set -euo pipefail と trap による障害耐性の構築

Tech

role: SRE_DevOps_Expert tone: Professional, Technical, Practical composition: Metadata, Badge, H1, Introduction, Flow, Implementation, Verification, Troubleshooting, Summary logic: Error-handling first, idempotency, minimalist footprint

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

運用事故を防ぐ堅牢なシェルスクリプト:set -euo pipefail と trap による障害耐性の構築

【導入と前提】 外部API連携や設定ファイル更新を伴う定型タスクにおいて、異常終了時のゴミ残存や連鎖的な不具合を防止する安全な実行基盤を構築します。

  • OS: Linux (bash 4.0+)

  • ツール: curl, jq, systemd

【処理フローと設計】

graph TD
A["スクリプト開始"] --> B["環境変数・依存関係チェック"]
B --> C["一時ファイルの作成 mktemp"]
C --> D["APIデータ取得 curl"]
D --> E["JSONパース・検証 jq"]
E --> F["設定ファイル更新/反映"]
F --> G["systemd サービス再起動"]
G --> H["正常終了: 一時ファイル削除"]
D -- エラー発生 --> I["trap: ロールバック/ログ出力"]
E -- 不正データ --> I
F -- 権限エラー等 --> I
I --> J["異常終了: クリーンアップ"]

APIからの動的なデータ取得に基づき、ローカルのサービス設定を更新するワークフローを想定しています。異常発生時は、どのフェーズであっても trap が介入し、システムに不要な中間ファイルを残さない設計です。

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

#!/usr/bin/env bash

# --- 安全のためのマジックコマンド ---


# -e: コマンドが失敗したら即終了


# -u: 未定義の変数があればエラー


# -o pipefail: パイプライン途中のエラーも検知

set -euo pipefail

# --- グローバル変数 ---

readonly API_URL="https://api.example.com/v1/config"
readonly CONFIG_PATH="/etc/myapp/config.json"
readonly TEMP_FILE=$(mktemp /tmp/myapp_conf.XXXXXX)
readonly LOG_TAG="myapp-updater"

# --- 終了処理 (trap) ---


# 正常・異常に関わらず終了時に一時ファイルを削除

cleanup() {
    local exit_code=$?
    rm -f "${TEMP_FILE}"
    if [ "${exit_code}" -ne 0 ]; then
        logger -t "${LOG_TAG}" -p user.error "Update failed with exit code ${exit_code}."
    fi
}
trap cleanup EXIT

# --- メイン処理 ---

logger -t "${LOG_TAG}" "Starting configuration update..."

# 1. APIからデータ取得 (リトライ処理付き)


# -s: 進捗非表示, -S: エラー表示, -L: リダイレクト追従, --retry: 失敗時リトライ

curl -sSL --retry 3 --retry-delay 2 "${API_URL}" -o "${TEMP_FILE}"

# 2. JSONの検証と加工


# jq -e: 抽出結果が空またはfalseなら終了コード1を返す

if ! jq -e '.version' "${TEMP_FILE}" > /dev/null; then
    echo "Error: Invalid JSON received from API" >&2
    exit 1
fi

# 3. 設定ファイルの更新 (アトミックな操作)


# 直接編集せず、検証済みのファイルをmvで上書き(ファイルシステムレベルでの安全性を確保)

if ! diff -q "${TEMP_FILE}" "${CONFIG_PATH}" > /dev/null 2>&1; then
    sudo cp "${TEMP_FILE}" "${CONFIG_PATH}"
    logger -t "${LOG_TAG}" "Configuration updated."

    # 4. サービスの再起動

    sudo systemctl restart myapp.service
else
    logger -t "${LOG_TAG}" "No changes detected. Skipping restart."
fi

echo "Successfully completed."

補足:systemd ユニットファイル例

スクリプトを定期実行する場合は、systemd timerを活用します。

# /etc/systemd/system/myapp-updater.service

[Unit]
Description=Update myapp config from API
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/update-config.sh
User=deploy-user
Group=deploy-group

【検証と運用】

  1. 正常系確認:

    • スクリプトを手動実行し、echo $?0 が返ることを確認。

    • journalctl -t myapp-updater でログが出力されているか確認。

  2. 異常系確認:

    • API_URL を無効なURLに書き換え、スクリプトが即座に停止し、/tmp にゴミが残らないことを確認。

    • jq のフィルタでエラーが出るデータを流し込み、後続の systemctl restart が実行されないことを確認。

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

  • パイプラインとgrepの挙動: set -o pipefail を有効にしている場合、grep でヒットがないと終了コードが非ゼロになり、スクリプトが止まります。意図的にヒットなしを許容する場合は grep ... || true のように記述してください。

  • sudoのパスワード要件: 自動化スクリプト内で sudo を使う場合、/etc/sudoers.d/ で特定のコマンド(例:systemctl restart)に対して NOPASSWD を設定する必要があります。

  • 環境変数の露出: APIキーなどをスクリプトに直書きせず、EnvironmentFile= (systemd) やシークレット管理ツールから読み込むようにしてください。

【まとめ:運用の冪等性を維持するための3つのポイント】

  1. 一時ファイルの活用: 本番設定ファイルを直接編集(リダイレクト等)せず、mktemp で作成したファイルを検証後に cp または mv することで、不完全な書き込みを防止する。

  2. 早期リターン (Fail Fast): set -euo pipefail により、問題発生箇所で即座に処理を止めることで、破損したデータがシステム全体に波及するのを防ぐ。

  3. クリーンアップの強制: trap を使用して、スクリプトの出口を一箇所に統合し、リソースの解放やログ記録を確実に行う。

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

コメント

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