現場で起きる課題:深夜のディスク容量枯渇障害
日曜日の深夜2時、突如アラート通知が鳴り響きます。本番環境のメインAPIが応答を停止しました。調査のためレスキューコンソールにSSH接続を試みたものの、bashから即座にNo space left on deviceというエラーが返されます。PostgreSQLは書き込みトランザクションを停止し、Redisはクラッシュ、システム監視エージェントも沈黙しました。df -hで原因を調査すると、/var/log/app/output.logという単一のアプリケーションログが72時間で85GBに達し、ルートパーティションの空き容量を100%消費していたことが判明しました。
適切に管理されていないログの肥大化は、日々本番クラスタを静かに停止させています。Webサーバー、バックグラウンドワーカー、コンテナランタイムは1時間あたり数百万行ものログを生成します。自動ローテーションポリシーを導入していなければ、これらのファイルはいずれディスクを埋め尽くし、ファイルシステムのinodeを枯渇させてしまいます。
根本原因の分析:野放しのログがストレージとinodeを圧迫する仕組み
Linuxのプロセスは、アクティブなファイルディスクリプタを介してファイルへの書き込みを行います。デーモンが継続してログを出力し続けると、以下のようなプロセスでストレージが圧迫されます。
- ブロックストレージの枯渇:詳細なデバッグトレースやWebへのブルートフォース攻撃により、1時間足らずで500MB以上のプレーンテキストが出力されることがあります。プレーンテキストのままでは、サイズを85%〜90%削減できるgzip圧縮ファイルと比較して、大量のブロックストレージを浪費します。
- ファイルディスクリプタの保持(ロック):稼働中のログファイルに対して
rm /var/log/app/output.logを実行しても、ディスク容量は解放されません。アプリケーションプロセスが開いているファイルハンドルを保持している限り、カーネルはアンリンクされたinodeを維持し続けます。そのため、デーモンを再起動するか/proc経由で手動でハンドルを切り詰める(truncate)まで、ストレージ使用率は100%のままロックされます。 - パーミッションと所有者の不整合:ファイルを移動するアドホックな自作スクリプトでは、新しく作成されたファイルが
www-dataなどのアプリケーションユーザーではなくroot:root権限になってしまうケースが多発します。その結果、次の書き込み時に権限拒否(Permission denied)が発生し、アプリケーションが即座にクラッシュします。
対策の比較:手動切り詰め、自作シェルスクリプト、Logrotate
エンジニアがログの肥大化に対処する方法は、主に以下の3つに分かれます。
| 手法 | メリット | デメリット |
|---|---|---|
手動切り詰め(> file.log) |
ダウンタイムゼロ。プロセスを停止することなく即座にディスクブロックを解放可能。 | 完全な手動作業。過去のログ履歴がすべて消去され、次回以降の障害防止にはならない。 |
| Cronによる自作シェルスクリプト | find -mtimeやgzip、独自のS3アップロードフックなどを組み合わせた柔軟なパイプラインを構築可能。 |
脆弱性が高い。アトミックなファイル切り替え、シグナル送信、Cron重複時の状態復帰などのハンドリングが困難。 |
| 標準ユーティリティ「Logrotate」 | Debian、Ubuntu、RHEL、Rocky Linuxに標準搭載。パーミッション管理、シグナルによるリロード、世代管理、圧縮を標準でサポート。 | 設定ディレクティブの構文規則と、対象サービスがファイルディスクリプタをどのように扱うかの理解が必要。 |
急ごしらえのシェルスクリプトは、エッジケースに直面して破綻するまで問題なく動いているように見えがちです。標準のlogrotateツールを使用すれば、実績のある宣言的な仕組みにより、独自のメンテナンスコストを完全に削減できます。
最適なアプローチ:Logrotateによる自動ログローテーションの実装
多くのLinuxディストリビューションでは、systemdタイマー(logrotate.timer)または/etc/cron.daily/logrotate内のcronスクリプトによって、logrotateが1日1回実行されます。logrotateは/etc/logrotate.confから基本デフォルト設定を読み込み、/etc/logrotate.d/内の個別ファイルからモジュール化されたルールを読み込みます。
ステップ1:本番環境に対応したLogrotate設定の構造
設定はモジュール単位で管理するのがベストプラクティスです。グローバルなデフォルト設定を直接編集するのではなく、/etc/logrotate.d/内に対象サービス専用のファイルを作成します。
sudo nano /etc/logrotate.d/my-app
以下の設定ブロックを記述します。
/var/log/my-app/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 myappuser myappgroup
sharedscripts
postrotate
/usr/bin/systemctl reload my-app.service > /dev/null 2>&1 || true
endscript
}
各ディレクティブの内部的な役割は以下の通りです。
daily:ログの状態を24時間ごとにチェックします。weeklyやmonthlyへの変更、あるいはsize 250Mのようなファイルサイズを基準とした閾値指定も可能です。missingok:ログファイルがまだ生成されていない場合でも、エラーを出力せずに処理を継続します。rotate 14:古いログを削除する前に、ローテーションされたアーカイブを14世代分保持します。これにより、直近2週間分の監査ログ履歴をきれいに維持できます。compress&delaycompress:アーカイブをgzipで圧縮します。delaycompressは直近のアーカイブ(app.log.1)の圧縮を次のローテーションサイクルまで1回遅らせることで、実行中のプロセスによる保留中のバッファ書き込みを確実に完了させます。notifempty:ファイルサイズが0バイトの場合はローテーションをスキップし、空のアーカイブファイルでディレクトリが乱雑になるのを防ぎます。create 0640 myappuser myappgroup:ローテーション直後に、厳格なパーミッションビットと適切なアプリケーション所有権を持つ新しいログファイルを生成します。
ステップ2:パーミッション管理とサイズトリガーの設定
セキュリティが強化された環境では、ディレクトリへのアクセス権限が専用のサービスアカウントに限定されていることがよくあります。非特権ユーザーが所有するディレクトリに対してlogrotateがroot権限で実行されると、処理が拒絶される場合があります。suディレクティブを追加して、実行コンテキストを明示的に指定してください。
/var/log/my-app/*.log {
su myappuser myappgroup
daily
rotate 7
maxsize 500M
compress
create 0644 myappuser myappgroup
}
maxsize 500Mディレクティブは安全弁として機能します。日次のローテーションが実行される前にアプリケーションが突然無限ループに陥り2GBのエラーを吐き出したとしても、ファイルが500MBに達した時点でlogrotateが前倒しでローテーションを強制実行します。
ステップ3:copytruncateとシグナル送信によるサービス停止の回避
logrotateがアクティブなログファイルの名前を変更(リネーム)すると、実行中のデーモンはファイルパスの再オープン指示を受け取るまで、元の古いinodeに対してログを書き込み続けます。対象サービスがログ出力をどのように処理しているかに応じて、2つの選択肢があります。
選択肢A:アプリケーションがシグナルによるログ再オープンをサポートしている場合(推奨)
Nginx、Apache、HAProxyなどの本番ソフトウェアは、アクティブな接続を切断することなく、ログファイルハンドルを動的に再読み込みできます。postrotateブロック内でUSR1などのPOSIXシグナルを送信します。
/var/log/nginx/*.log {
daily
rotate 30
compress
delaycompress
sharedscripts
postrotate
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}
選択肢B:アプリケーションがシグナルをサポートしていない場合(Node.js、Python、Goバイナリなど)
独自のバックエンドサービスでは、ファイルローテーション用のシグナルハンドラが実装されていないケースが多く見られます。このような場合はcopytruncateを使用します。logrotateはアクティブなログ内容を別ファイルにコピーした上で、元のファイルをその場で切り詰める(truncate)ため、開いているファイルディスクリプタをそのまま維持できます。
/var/log/legacy-app/*.log {
daily
rotate 7
copytruncate
compress
missingok
}
ただし、copytruncateにはわずかな競合状態(レースコンディション)が存在します。コピーから切り詰めまでのほんの一瞬の間に書き込まれたログは失われる可能性があります。高トラフィックなトランザクションAPIの場合は、適切なシグナルハンドリングを実装するか、ログの出力先をjournaldへルーティングすることを検討してください。
ステップ4:ドライランによる動作テストと検証
深夜のCron実行まで設定ミスに気づかないといった事態は避けなければなりません。ドライランフラグを使用して、構文の検証とルールのテストを事前に行いましょう。
# ファイルを変更せずに実行予定のアクションを確認するドライラン
sudo logrotate -d /etc/logrotate.d/my-app
# スクリプトの実行とファイル権限をテストするため、強制的にローテーションを実行
sudo logrotate -vf /etc/logrotate.d/my-app
対象ディレクトリを確認し、生成されたファイルのパーミッション、所有者、圧縮状態が想定通りになっているかを検証します。
ls -la /var/log/my-app/
適切に設定されたlogrotateのルーチンがあれば、ディスク使用量を予測可能な範囲に保ち、重要なフォレンジック履歴を保護しつつ、深夜の突然のサーバーダウンから本番環境を守ることができます。

