Linux Command Line & Bash Scripting Mastery · レッスン

安全な一時ファイルとロックディレクトリ

mktempとflockを使って競合状態のない一時リソースを作成し、スクリプトの同時実行を防ぎます。

レッスン 3/413 ステップ

「安全な一時ファイルとロックディレクトリ」はCoddyKit上の無料Linux Command Line & Bash Scripting Masteryレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLinux Command Line & Bash Scripting Mastery学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。

一時ファイルがセキュリティリスクになる理由

Bashスクリプトでは、中間結果やロックの目印、ステージング領域など、一時的な保存場所が頻繁に必要になります。しかし、不注意に一時ファイルを作成すると、深刻な脆弱性につながります。

  • 競合状態:別のプロセスがファイル名を予測して先にファイルを作成し、書き込み先を変更させる可能性があります。
  • シンボリックリンク攻撃:攻撃者が想定されるパスに、/etc/passwdのような機密ファイルを指すシンボリックリンクを作成します。
  • 残存ファイル:スクリプトがクラッシュすると一時ファイルが蓄積し、機密データが露出する可能性があります。

これらの問題を解消する中核的なツールがmktempとflockです。このレッスンでは、両方を安全かつ防御的に使用する方法を説明します。

mktempで安全な一時ファイルを作成する

mktempは、ランダムで予測できない名前の一時ファイルを作成し、そのパスを返します。ファイルはアトミックに作成されるため、別のプロセスが先にその名前を取得することはできません。

  • 構文:mktemp [TEMPLATE] — テンプレートは、3文字以上のXで終わっている必要があります。
  • 各Xはランダムな文字に置き換えられ、/tmp/script.aB3kQzのような一意の名前になります。
  • ファイルは自動的にパーミッション0600(所有者だけが読み取り可能)で作成されます。

後で参照してクリーンアップできるよう、返されたパスは必ずただちに変数へ保存してください。

#!/usr/bin/env bash
set -euo pipefail

# Create a secure temp file
TMPFILE=$(mktemp /tmp/myapp.XXXXXX)
echo "Temp file created at: $TMPFILE"

# Write data to it
echo "some intermediate result" > "$TMPFILE"

# Read it back
cat "$TMPFILE"

# Clean up
rm -f "$TMPFILE"

trapで必ずクリーンアップする

スクリプトがエラーやシグナル、またはset -eの発動によって予期せず終了すると、クリーンアップハンドラーを登録していない限り、一時ファイルが残されます。

trapビルトインは、シェルがシグナルを受信したとき、または終了したときにコマンドを実行します。一時ファイルをクリーンアップする標準的なパターンは次のとおりです。

  • 一時ファイルを作成した直後にトラップを登録します。
  • EXITにトラップを設定し、正常終了時と異常終了時の両方でクリーンアップが実行されるようにします。
  • スクリプトの実行時間が長い場合や対話型の場合は、INTとTERMにもトラップを設定します。

これにより、スクリプトが実行途中で強制終了されても、孤立したファイルが残らないことが保証されます。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/report.XXXXXX)

# Register cleanup before doing any real work
cleanup() {
    rm -f "$TMPFILE"
    echo "Cleaned up $TMPFILE" >&2
}
trap cleanup EXIT

# Do work — even if this fails, cleanup() will run
echo "Processing..." > "$TMPFILE"
grep "result" "$TMPFILE" || true

echo "Done. File will be removed on exit."

mktemp -d で一時ディレクトリを作成する

複数のファイルを一時的に配置するために、ディレクトリ全体が必要になることがあります。たとえば、アーカイブを作成したり、tarball を展開してから処理したりする場合です。mktemp -d を使用すると、安全な一時ディレクトリを作成できます。

  • ディレクトリは権限 0700(所有者のみアクセス可能)で作成されます。
  • trap 内で rm -rf を使用して削除します。ただし、ハードコードしたパスではなく、変数だけを削除するよう十分注意してください。
  • さらに安全性を高めるため、ダブルクォートを使用し、rm -rf を呼び出す前に変数が空でないことを確認してください。
#!/usr/bin/env bash
set -euo pipefail

TMPDIR=$(mktemp -d /tmp/extract.XXXXXX)

cleanup() {
    # Guard: only rm if variable is set and non-empty
    [[ -n "${TMPDIR:-}" ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT

echo "Working in $TMPDIR"

# Simulate staging files
echo "file one" > "$TMPDIR/part1.txt"
echo "file two" > "$TMPDIR/part2.txt"

ls "$TMPDIR"
echo "All done."

スクリプトの同時実行という問題

Cron ジョブ、systemd タイマー、手動で起動したスクリプトによって、同じスクリプトの複数のインスタンスが同時に実行されることは珍しくありません。これにより、次の問題が発生します。

  • 重複処理: 同じデータベースレコードやファイルが二重に処理されます。
  • 出力の破損: 2つのインスタンスが同じ出力ファイルに同時に書き込みます。
  • デッドロックや不完全な状態: 両方のインスタンスが共有リソースを、予測できない順序で交互に変更します。

従来の対策は、PID ファイルを書き込んで起動時に確認する方法でした。しかし、この方法には確認と書き込みの間に競合が発生する隙間があります。現在の正しい解決策は flock です。flock はカーネルのアドバイザリロック機構を使用するため、競合のないロックを保証できます。

flock によるロック: ワンライナーパターン

flock は、コマンドを実行する前にファイルディスクリプターに対するアドバイザリロックを取得します。最も簡単な使い方は、コマンドラインからスクリプト全体をラップする方法です。

flock -n /var/lock/myscript.lock bash myscript.sh

  • -n(ノンブロッキング): ロックがすでに取得されている場合、待機せずにステータス 1 で直ちに終了します。
  • -n を指定しない場合、flock はロックが利用可能になるまでブロックします。キューイングに便利です。
  • ロックファイル自体は単なる目印であり、内容は重要ではありません。実行のたびに残しておいても安全です。
  • ロックを保持しているプロセスが終了すると、カーネルが自動的にロックを解放するため、手動で削除する必要はありません。
#!/usr/bin/env bash
# launcher.sh — prevents concurrent runs of worker.sh
set -euo pipefail

LOCKFILE="/tmp/myworker.lock"

if ! flock -n "$LOCKFILE" bash -c 'echo "Running worker..."; sleep 2; echo "Done."'; then
    echo "Another instance is already running. Exiting." >&2
    exit 1
fi

ファイルディスクリプターを使用してスクリプト内で flock を使う

スクリプトを外部からラップするのではなく、スクリプト内でロックする場合は、exec でファイルディスクリプターを開き、そのディスクリプターに対して flock を呼び出します。これは本番用スクリプトで使われる標準的なパターンです。

  • exec 200>"$LOCKFILE" は、ファイルをディスクリプター 200 で書き込み用に開きます(必要に応じて作成します)。
  • flock -n 200 は、ディスクリプター 200 のロックをノンブロッキングで試みます。
  • ロックはファイル名ではなくファイルディスクリプターに関連付けられるため、シェルプロセスの終了時に自動的に解放されます。
  • 標準入力・標準出力・標準エラーとの衝突を避けるため、ディスクリプター番号 200~299 を慣例的に使用します。
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myjob.lock"

# Open lock file on FD 200
exec 200>"$LOCKFILE"

# Attempt non-blocking lock
if ! flock -n 200; then
    echo "ERROR: Another instance of this script is running." >&2
    exit 1
fi

echo "Lock acquired. Starting work..."
sleep 1
echo "Work complete. Lock will be released on exit."

mktemp と flock を1つのスクリプトで組み合わせる

実際の防御的なスクリプトには、同時実行を防ぐロックと、中間データ用の安全な一時ファイルの両方が必要です。ここでは、両方の手法を組み合わせた完全なパターンを示します。

  • まずロックを取得します。一時ファイルを作成する前に取得することで、作業を行うインスタンスを常に1つだけにします。
  • ロックが確認できた後で、一時リソースを作成します。
  • 一時リソースを作成した直後に trap を登録し、スクリプトがどのように終了しても確実にクリーンアップされるようにします。
  • ロックファイルを一時ディレクトリに置いてはいけません。flock が参照できるよう、実行の間も残しておく必要があります。
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/report_builder.lock"
exec 200>"$LOCKFILE"

if ! flock -n 200; then
    echo "Already running — aborting." >&2
    exit 1
fi

# Now safe to create temp resources
TMPDIR=$(mktemp -d /tmp/report.XXXXXX)
TMPLOG=$(mktemp /tmp/report_log.XXXXXX)

cleanup() {
    rm -rf "${TMPDIR:-}"
    rm -f  "${TMPLOG:-}"
}
trap cleanup EXIT

echo "Building report in $TMPDIR" | tee "$TMPLOG"
echo "Step 1 complete"             >> "$TMPLOG"
cat "$TMPLOG"

代替のロック機構としてのロックディレクトリ

flock を利用できないシステム(組み込みシステムや NFS などのネットワークファイルシステム)では、代わりにロックディレクトリを使用できます。POSIX システムでは mkdir はアトミックに動作し、ディレクトリがまだ存在しない場合にのみ成功します。

  • mkdir /tmp/myscript.lock.d でロックディレクトリを作成します。別のインスタンスがすでに作成している場合、mkdir は直ちに失敗します。
  • 診断に役立つメタデータ(PID など)をディレクトリ内に保存します。
  • EXIT 時の trap で、必ずディレクトリを削除します。
  • 注意: flock とは異なり、プロセスが -9 で強制終了された場合やマシンが再起動した場合、ディレクトリロックは自動的に解放されません。古いロックを検出するチェックを追加してください。
#!/usr/bin/env bash
set -euo pipefail

LOCKDIR="/tmp/myscript.lock.d"

# Atomic mkdir — fails if directory already exists
if ! mkdir "$LOCKDIR" 2>/dev/null; then
    # Check if the holding PID is still alive
    HOLDER_PID=$(cat "$LOCKDIR/pid" 2>/dev/null || echo "")
    if [[ -n "$HOLDER_PID" ]] && kill -0 "$HOLDER_PID" 2>/dev/null; then
        echo "Locked by PID $HOLDER_PID. Exiting." >&2
        exit 1
    else
        echo "Stale lock detected. Removing and continuing." >&2
        rm -rf "$LOCKDIR"
        mkdir "$LOCKDIR"
    fi
fi

echo "$$" > "$LOCKDIR/pid"
trap 'rm -rf "$LOCKDIR"' EXIT

echo "Lock acquired via directory. Running..."
sleep 1
echo "Done."

flock のタイムアウト付き待機

ロックをすぐに取得できなくても失敗せずに待ちたいものの、永遠に待機させたくない場合があります。flock では、-w フラグを使用してタイムアウトを指定できます。

  • flock -w 10 200 はロックを最大10秒間待機し、それでも利用できない場合はステータス 1 で終了します。
  • 短時間で終了する先行プロセスの後ろにキューとして並びつつ、先行プロセスが停止した場合は諦めたいスクリプトに適しています。
  • -w と、ロックファイルのパスや待機時間などの状況を含む意味のあるエラーメッセージを組み合わせてください。これにより、オペレーターはハングをすばやく診断できます。
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/data_sync.lock"
TIMEOUT=15

exec 200>"$LOCKFILE"

echo "Waiting up to ${TIMEOUT}s for lock on $LOCKFILE..."

if ! flock -w "$TIMEOUT" 200; then
    echo "ERROR: Could not acquire lock after ${TIMEOUT}s." \
         "Another instance may be hung." >&2
    exit 1
fi

echo "Lock acquired. Syncing data..."
sleep 1
echo "Sync complete."

防御的なチェックリスト: 安全な一時リソース

一時ファイルやロックを使用するスクリプトをリリースする前に、次のチェックリストを確認してください。

  • mktemp を使用し、ハードコードしたパスは使わない — /tmp/myapp.tmp のようなパスは予測可能で、悪用されるおそれがあります。
  • パスを直ちに変数へ保存する — 他のコマンドを実行する前に TMPFILE=$(mktemp ...) を実行します。
  • 作成直後に trap cleanup EXIT を登録する — スクリプトの末尾で登録してはいけません。
  • すべての変数の使用をダブルクォートする — rm -f "$TMPFILE" とし、rm -f $TMPFILE とは書きません。
  • PID ファイルより flock を優先する — カーネルが管理し、クラッシュ時にも自動的に解放されます。
  • デフォルトではノンブロッキングの -n を使用する — 何も知らせずにブロックするロックは、パフォーマンスの問題を隠してしまいます。
  • ロックファイルを一時ディレクトリの外に置く — クリーンアップ用の trap の影響を受けずに残せるようにします。
  • クリーンアップの動作をテストする — スクリプトの実行途中で kill -9 を実行し、残ったファイルがないことを確認します(flock ベースのスクリプトの場合。ディレクトリロックには追加の対策が必要です)。

理解度チェック: flock のフラグの動作

Cron ジョブが毎分実行され、共有ファイルを処理しています。前回の実行がまだ続いている場合、新しい起動は待機せずに直ちにエラー終了させたいとします。これを正しく実装する flock の呼び出しはどれでしょうか。

まとめ: 安全な一時ファイルとロックディレクトリ

このレッスンでは、Bash で防御的にリソースを管理するための2つの重要なツールを学びました。

  • mktemp は、予測困難で安全な権限が設定された一時ファイル(0600)と一時ディレクトリ(0700)を作成し、ハードコードしたパスで発生しやすい競合状態やシンボリックリンク攻撃を排除します。
  • trap cleanup EXIT を作成直後に登録すると、通常終了、エラーによる終了、シグナルによる終了のいずれの場合でも、一時ファイルが確実に削除されます。
  • flock はカーネルが強制するアドバイザリロックを提供します。競合時にすぐ失敗するには -n、タイムアウト付きで待機するには -w N、スクリプト内でロックしプロセス終了時にカーネルが自動解放するには exec 200>file パターンを使用します。
  • ロックディレクトリ(mkdir)は、flock を利用できない環境向けの移植性の高い代替手段ですが、古いロックを明示的に検出する必要があります。
  • ロックファイルは常に一時ディレクトリの外側に置き、クリーンアップで使用するすべての変数をダブルクォートしてください。

mktemp + flock + trap を組み合わせることで、同時起動、予測できないクラッシュ、悪意のあるファイルシステム操作に対して安全なスクリプトを作成できます。

無料で開始

AI チューターと学ぶ Bash — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
22
レッスン
88

よくある質問

「安全な一時ファイルとロックディレクトリ」レッスンは無料ですか?

はい。「安全な一時ファイルとロックディレクトリ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Linux Command Line & Bash Scripting Masteryコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。

「安全な一時ファイルとロックディレクトリ」で何を学びますか?

mktempとflockを使って競合状態のない一時リソースを作成し、スクリプトの同時実行を防ぎます。 ブラウザで直接実行するハンズオンコードでLinux Command Line & Bash Scripting Masteryを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Linux Command Line & Bash Scripting Masteryを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのLinux Command Line & Bash Scripting Masteryは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「安全な一時ファイルとロックディレクトリ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このLinux Command Line & Bash Scripting Masteryレッスンでコードを書いて実行できますか?

はい。すべてのLinux Command Line & Bash Scripting Masteryレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. set -euo pipefailによる厳格モード
  2. クリーンアップとシグナル処理のためのtrapハンドラー
  3. 安全な一時ファイルとロックディレクトリ
  4. 冪等なスクリプトと指数バックオフによるリトライ
← Linux Command Line & Bash Scripting Masteryに戻る