個人開発でも本番・テスト・開発の3環境を1台のマシンで運用するケースは少なくありません。しかし「同じマシン上に3環境がある」ことで、企業のサーバー分離環境では起きない独特の問題が発生します。PIDファイルの共有、トークンキャッシュの汚染、__pycache__の残存、CLAUDE.mdのブランチ間同期 — 筆者が2週間で遭遇した4つの罠と、それぞれの対処法を記録します。
環境構成
| 環境 | ディレクトリ | データ | DB | ブランチ |
|---|---|---|---|---|
| 本番 | /path/to/project | 共有ストレージ(symlink) | project_db | main |
| テスト | /path/to/project-pre | 本番と共有(symlink) | project_pre_db | pre |
| 開発 | /path/to/project-dev | ローカル | - | develop/feature/* |
個人開発でDBやストレージを完全分離するのはコスト的に厳しいです。データディレクトリをsymlinkで共有する構成は現実的ですが、ここに罠があります。
罠1: PIDファイルの共有 — 4日間の起動不能
何が起きたか
本番ジョブ(09:00)とテストジョブ(09:05)が同じ /tmp/daily_pipeline.pid を参照していました。本番ジョブがゾンビ化してPIDファイルを保持 → テストジョブが多重起動防止で起動失敗。4日間、両方のジョブが起動不能になっていました。
原因の連鎖:
- 本番ジョブがゾンビ化(プロセスは残っているがハングしている)
- PIDファイルにゾンビプロセスのPIDが残る
- テストジョブ起動時に「既に実行中」と判定され起動拒否
- exit 0で終了するため、監視は「正常」と判断
対処法
環境ごとにPIDファイルのパスを分離します。
1_WORKSPACE_SUFFIX = '_pre' if 'project-pre' in str(PROJECT_ROOT) else ''2PID_FILE = Path(f'/tmp/daily_pipeline{_WORKSPACE_SUFFIX}.pid')さらに、stale PIDの検出ロジックを追加します。
1def _acquire_pid_lock() -> bool:2 if PID_FILE.exists():3 old_pid = int(PID_FILE.read_text().strip())4 try:5 os.kill(old_pid, 0) # プロセス存在チェック(シグナルは送らない)6 return False # プロセスが生きている → ロック取得失敗7 except ProcessNotFoundError:8 PID_FILE.unlink() # stale PIDファイルを除去9 PID_FILE.write_text(str(os.getpid()))10 return True教訓: /tmp/に置くファイルは、環境名のサフィックスを付けてください。これは設計時に決めるべきルールで、後から追加するとデバッグが困難になります。
罠2: トークンキャッシュの環境汚染
何が起きたか
API認証のトークンキャッシュ(token_cache.json)がプロジェクトの_tmp/ディレクトリに保存されていました。テスト実行時にダミートークンがこのパスに書き込まれ、本番のキャッシュを上書きする可能性がありました。
対処法
テスト環境ではPIDベースの一時ディレクトリにキャッシュを分離します。
1def _default_token_cache_path(environment: str = "production") -> Path:2 if environment == "test":3 return Path(tempfile.gettempdir()) / f"token_cache_test_{os.getpid()}.json"4 return PROJECT_ROOT / "_tmp" / "token_cache.json"テスト分離の検証スクリプト(check_test_isolation.sh)を作成し、CIで以下を検証するようにしました:
- テストが本番ファイル(
_tmp/配下)を直接読み書きしていないか - テストが本番DBに接続していないか
- テスト実行後に残留ファイルがないか
罠3: __pycache__の残存 — hotfixが反映されない
何が起きたか
本番環境に緊急hotfixを適用しましたが、__pycache__に古いバイトコードが残っていたため修正が反映されませんでした。防御コードが欠落した古いバージョンで動き続け、障害が継続しました。
対処法
3段階の対策を実施しました。
1. plistに PYTHONDONTWRITEBYTECODE=1 を追加
1<key>EnvironmentVariables</key>2<dict>3 <key>PYTHONDONTWRITEBYTECODE</key>4 <string>1</string>5</dict>これにより.pycファイルが生成されなくなります。起動時間が若干増えますが、キャッシュ不整合のリスクをゼロにできます。
2. デプロイ前チェックリストに追加
ブランチ切替後は必ず実行します:
1find . -name "__pycache__" -exec rm -rf {} +3. ブランチ状態の一括確認コマンド
1for dir in /path/to/project /path/to/project-pre /path/to/project-dev; do2 echo "$dir: $(cat $dir/.git/HEAD 2>/dev/null)"3done本番がdevelopブランチで動いていた事例もあったため、ブランチの整合性確認を習慣化しました。
罠4: CLAUDE.md/WBSのブランチ間同期
何が起きたか
CLAUDE.mdが各ブランチ(main/pre/develop)で独立に更新されるため、頻繁にコンフリクトが発生しました。教訓の追記が一方のブランチだけに残り、他のブランチに反映されないケースが多発しました。
2週間で7回のCLAUDE.md同期コミットが必要でした。
対処法
ルール化: mainへのhotfixをコミット/マージした場合、同日中に他のブランチに同期します。
1git checkout develop && git merge main && git push origin develop2git checkout pre && git merge main && git push origin pre「週1回の定期同期で十分」と考えていましたが、hotfixが16時間反映されなかった事例が発生し、同日同期をルール化しました。
共有_tmp/ディレクトリの競合
環境間でデータディレクトリをsymlinkで共有している場合、もう1つ注意が必要です。
問題: 本番ジョブが処理済みファイルをos.remove()で削除 → 5分後のテストジョブがファイルを読めません。
対処:
- 処理済みファイルは
os.remove()ではなく.bakリネームに変更 - Dry-Run環境では削除もリネームもしない
_atomic_write()の一時ファイル名にPIDを含める(書き込み競合の防止)
1def _atomic_write(path: Path, data: bytes) -> None:2 tmp_path = path.with_suffix(f'.tmp.{os.getpid()}')3 tmp_path.write_bytes(data)4 tmp_path.rename(path)チェックリスト — マルチ環境運用の安全確認
同じマシンで複数環境を運用するときに確認すべき項目をまとめました。
- PIDファイルのパスは環境ごとに分離されているか
- トークンキャッシュのパスは環境ごとに分離されているか
- テストが本番のファイル・DBに触れないことを検証するスクリプトがあるか
- plistに
PYTHONDONTWRITEBYTECODE=1が設定されているか - hotfix適用時の
__pycache__クリアが手順化されているか - CLAUDE.md/設定ファイルのブランチ間同期ルールがあるか
- 共有ディレクトリの書き込み競合対策(PIDベースの一時ファイル名)があるか
- 各環境のブランチ状態を一括確認するコマンドがあるか
まとめ
1台のマシンに3環境を同居させる個人開発では、「分離しているつもりで共有している」リソースがサイレント障害を生みます。PIDファイル、トークンキャッシュ、__pycache__、データディレクトリ — 一つひとつは小さな見落としですが、組み合わさると4日間の起動不能やhotfix未反映といった深刻な問題になります。
環境を作ったときに「この環境は何を共有していて、何を分離すべきか」を明示的にリストアップすることが、最も効果的な予防策です。