「本番環境で開発用のコマンドを実行してしまった」——この種の事故は、環境の取り違えから発生します。本番・Pre・開発の3ワークスペースを1人で運用していると、iTerm2のタブを間違えるだけで本番に影響を与えかねません。
きっかけは単純でした。「iTerm2の本番、Pre、開発のワークスペースそれぞれを開いている時に色を変えたい」——この要望から始まった小技が、7つの多層防御として定着しました。本記事では、環境の取り違えを防ぐ7つの小技と、それぞれの実装方法を記録します。
なぜ「色」なのか
3つのワークスペースを同時に開いていると、以下のリスクがあります。
1タブ1: 本番環境 ← git push origin main2タブ2: Pre環境 ← テスト実行中3タブ3: 開発環境 ← 機能実装中4
5間違えてタブ1で開発用のデストラクティブなコマンドを実行6→ 本番データが破損する可能性人間は文字列(/Users/htada/data-processing vs /Users/htada/data-processing-pre)を読み分けるより、色を認知する方がはるかに速いです。認知心理学で「プレアテンティブ処理(前注意的処理)」と呼ばれる現象で、色は意識的な注意を向ける前に脳が処理します。
小技1: iTerm2ワークスペースの色分け
iTerm2のプロファイル機能を使い、ディレクトリに応じて背景色を自動切り替えします。
iTerm2プロファイルの設定
- iTerm2の Preferences > Profiles で3つのプロファイルを作成
- それぞれの背景色を設定
| プロファイル名 | 背景色 | 用途 |
|---|---|---|
| Production | 暗い赤(#1a0000) | 本番環境 |
| PreProduction | 暗い黄(#1a1a00) | Pre環境 |
| Development | 暗い緑(#001a00) | 開発環境 |
自動切り替えスクリプト
1# ~/.zshrc に追加2function set_iterm_color() {3 case "$PROJECT_ROOT" in4 */data-processing)5 # 本番: 赤背景6 echo -e "\033]1337;SetProfile=Production\a"7 ;;8 */data-processing-pre)9 # Pre: 黄背景10 echo -e "\033]1337;SetProfile=PreProduction\a"11 ;;12 */work/data-processing)13 # 開発: 緑背景(デフォルト)14 echo -e "\033]1337;SetProfile=Development\a"15 ;;7 collapsed lines
16 esac17}18
19# ディレクトリ移動時に自動実行20chpwd() {21 set_iterm_color22}iTerm2のSetProfileエスケープシーケンスにより、cdでディレクトリを移動するたびに背景色が自動的に切り替わります。赤い背景が見えたら「ここは本番だ」と瞬時に認識できます。
小技2: シェルプロンプトの環境表示
背景色に加えて、プロンプト自体にも環境名を表示します。二重の視覚的フィードバックです。
1# ~/.zshrc に追加2function get_env_label() {3 case "$PWD" in4 */htada/data-processing-pre*) echo "[PRE]" ;;5 */htada/data-processing*) echo "[PROD]" ;;6 */work/data-processing*) echo "[DEV]" ;;7 esac8}9
10# プロンプトに環境名を表示11PS1='$(get_env_label) %~ $ '実際のターミナル表示:
1[PROD] ~/data-processing $ ← 赤背景 + [PROD]表示2[PRE] ~/data-processing-pre $ ← 黄背景 + [PRE]表示3[DEV] ~/work/data-processing $ ← 緑背景 + [DEV]表示注意: case文のパターン順序が重要です。*/htada/data-processing*を先に書くとdata-processing-preもマッチしてしまいます。data-processing-preを先に評価する必要があります。
小技3: 緊急停止ファイル
本番システムを即座に停止させる最もシンプルな方法です。特定のファイルが存在すれば、全プロセスが起動時にチェックして停止します。
1from pathlib import Path2import logging3
4logger = logging.getLogger(__name__)5
6EMERGENCY_STOP_FILE = Path("/tmp/data_processing_emergency_stop")7
8def check_emergency_stop() -> bool:9 """緊急停止ファイルの存在をチェック"""10 if EMERGENCY_STOP_FILE.exists():11 logger.warning(12 "Emergency stop file detected: %s. Halting all operations.",13 EMERGENCY_STOP_FILE,14 )15 return True7 collapsed lines
16 return False17
18# 各エントリポイントの冒頭で呼ぶ19def main() -> None:20 if check_emergency_stop():21 return22 # ... 通常処理操作は極めてシンプルです。
1# 緊急停止(全システム即時停止)2touch /tmp/data_processing_emergency_stop3
4# 復旧(全システム再開)5rm /tmp/data_processing_emergency_stopなぜデータベースのフラグやAPI呼び出しではなくファイルなのか。理由は2つあります。
- 依存なし: DB障害中でもファイル作成は可能
- 速度: ファイルの存在チェックはナノ秒オーダー
実際にこの仕組みに助けられたケースがあります。負荷の急上昇時にtouchコマンド一発で全データ処理エンジンを停止できました。DBに接続してフラグを変更するより、パニック時の操作としてはるかに確実です。
小技4: Git pre-pushフックによるブランチ保護
mainブランチへの直接pushを防止します。個人開発でもPR経由のマージを強制します。
1#!/bin/bash2branch=$(git rev-parse --abbrev-ref HEAD)3if [ "$branch" = "main" ]; then4 echo "Direct push to main is not allowed."5 echo "Use: git push origin feature/xxx && create PR"6 exit 17fi8
9# Pre環境のブランチ命名規則チェック10remote="$1"11if [[ "$remote" == *"pre"* ]] && [[ "$branch" != pre/* ]]; then12 echo "Pre remote requires pre/* branch naming."13 echo "Current branch: $branch"2 collapsed lines
14 exit 115fiこのフックにより、以下の操作ミスを防止します。
1# 防止される操作2$ git push origin main3Direct push to main is not allowed.4Use: git push origin feature/xxx && create PR5
6# 許可される操作7$ git push origin feature/new-358# → 成功。その後PRを作成小技5: launchdジョブの命名規則
90個のlaunchdジョブを運用する上で、命名規則によるジョブの識別は不可欠です。
1本番環境:2 com.dataprocessing.collect.daily_feed_a3 com.dataprocessing.domestic.phase_alpha.entry4 com.dataprocessing.overseas.scoring.daily5
6Pre環境("pre" を含む):7 com.dataprocessing.pre.collect.daily_feed_a8 com.dataprocessing.pre.domestic.phase_alpha.entry命名規則: com.dataprocessing.{pre.}{engine}.{type}.{target}
この規則により、環境別のジョブ一覧が即座に取得できます。
1# Pre環境のジョブのみ表示2launchctl list | grep "dataprocessing\.pre"3
4# データ収集本番のジョブのみ表示5launchctl list | grep "dataprocessing\.collect"6
7# 全ジョブの環境別カウント8echo "本番: $(launchctl list | grep -c 'dataprocessing\.' | grep -v 'pre')"9echo "Pre: $(launchctl list | grep -c 'dataprocessing\.pre')"「preが名前に含まれているかどうか」で本番/Pre環境を即座に判別できます。曖昧な命名(job1, test_jobなど)は絶対に避けます。
小技6: dry-runモードフラグ
本番環境でも安全にテスト実行できるdry-runモードを全エントリポイントに実装します。
1import sys2import logging3
4logger = logging.getLogger(__name__)5
6def main() -> None:7 dry_run = "--dry-run" in sys.argv8 if dry_run:9 logger.info("[DRY-RUN] No actual operations will be executed")10
11 # データ取得(dry-runでも実行)12 signals = generate_signals()13 logger.info("Generated %d signals", len(signals))14
15 for signal in signals:11 collapsed lines
16 task = create_task(signal)17
18 if not dry_run:19 result = execute_task(task)20 logger.info("Executed: %s -> %s", task, result)21 else:22 logger.info("[DRY-RUN] Would execute: %s", task)23
24 # 集計(dry-runでも実行)25 summary = calculate_summary(signals)26 logger.info("Summary: %s", summary)dry-runモードの設計原則は3つあります。
- データ取得と分析は実行する: シグナル生成・集計など読み取り系の処理は通常通り実行
- 書き込み系のみスキップ: タスク実行・ファイル書き込み・DB更新をスキップ
- ログには
[DRY-RUN]プレフィックス: 後からログを見て「これはdry-runだった」と判別可能
1# 本番で安全にテスト2python main.py --dry-run3
4# 本番実行5python main.pyPre環境のlaunchd plistには、デフォルトで--dry-runフラグを付与しています。
1<key>ProgramArguments</key>2<array>3 <string>/usr/bin/python3</string>4 <string>main.py</string>5 <string>--dry-run</string> <!-- Pre環境はデフォルトdry-run -->6</array>小技7: デイリーヘルスチェック通知(ハートビートパターン)
「通知が来ない = 異常」というハートビートパターンです。毎朝決まった時間にSlack通知を送信し、通知が来なければジョブ自体が停止していると判断します。
1#!/bin/bash2# launchdで毎朝06:30に実行3
4PROJECT_ROOT="${PROJECT_ROOT:-/Users/htada/data-processing}"5source "$PROJECT_ROOT/.env"6
7# 各ジョブのログ鮮度チェック8stale_jobs=""9for log_dir in /tmp/dataprocessing_*/; do10 job_name=$(basename "$log_dir")11 stdout_log="$log_dir/stdout.log"12
13 if [ -f "$stdout_log" ]; then14 last_modified=$(stat -f %m "$stdout_log")21 collapsed lines
15 now=$(date +%s)16 age_hours=$(( (now - last_modified) / 3600 ))17
18 if [ "$age_hours" -gt 24 ]; then19 stale_jobs="$stale_jobs\n- $job_name (${age_hours}h ago)"20 fi21 else22 stale_jobs="$stale_jobs\n- $job_name (log not found)"23 fi24done25
26# Slack通知27if [ -n "$stale_jobs" ]; then28 message="Daily Health Check: WARNING\nStale jobs:$stale_jobs"29else30 message="Daily Health Check: OK ($(date '+%Y-%m-%d %H:%M'))"31fi32
33curl -s -X POST "$SLACK_WEBHOOK_URL" \34 -H "Content-Type: application/json" \35 -d "{\"text\": \"$message\"}"このヘルスチェックが16日間のサイレント障害を防ぐ仕組みです。ログの最終更新が24時間以上前のジョブがあれば、「そのジョブは動いていない可能性がある」と警告します。
重要なのは「エラー通知」ではなく「正常通知」だという点です。
1エラー通知方式: エラーが出たら通知する2 → エラーが出ない障害(サイレント障害)を検出できない3
4ハートビート方式: 毎朝必ず通知する5 → 通知が来ない = ジョブ自体が停止 = 異常7つの小技の多層防御
個々の小技は単純です。しかし組み合わせると、異なるレイヤーでの防御が形成されます。
1Layer 1: 視覚的防御2 ├── 小技1: iTerm2背景色 ← 環境を色で即座に認識3 └── 小技2: プロンプト環境表示 ← 文字でも確認4
5Layer 2: 操作的防御6 ├── 小技4: pre-pushフック ← 本番への直接pushを遮断7 └── 小技6: dry-runモード ← 本番でも安全にテスト8
9Layer 3: 識別的防御10 └── 小技5: launchd命名規則 ← 本番/Preのジョブを即座に判別11
12Layer 4: 緊急対応13 └── 小技3: 緊急停止ファイル ← touchコマンドで全停止14
15Layer 5: 検出1 collapsed line
16 └── 小技7: ハートビート ← サイレント障害を検出1つのレイヤーが突破されても、次のレイヤーが防御します。例えば、背景色を見落としても(Layer 1突破)、pre-pushフックが本番pushを防止します(Layer 2で防御)。
実際の効果
これらの小技を導入してからの3ヶ月間で、以下の効果がありました。
| 指標 | 導入前(3ヶ月) | 導入後(3ヶ月) |
|---|---|---|
| 環境取り違えインシデント | 4件 | 0件 |
| 本番への直接push | 2件 | 0件(フックで遮断) |
| サイレント障害の発見 | 平均8.3日後 | 平均0.5日後 |
| 緊急停止の実行時間 | 3分(DBフラグ変更) | 2秒(touch) |
最も効果が大きかったのは小技3(緊急停止ファイル)です。負荷急上昇時に「とにかく全部止める」がtouchコマンド一発でできる安心感は、精神的な余裕にもつながります。
学んだこと
1. 環境の取り違えは「色」で防げる
人間は文字より色の方が早く認知します。赤い背景が目に入った瞬間、「ここは本番だ」と無意識レベルで認識できます。意識的に文字列を読んで環境を判断する方式は、疲労時に失敗します。
2. 小技は1つでは弱いが、7つ組み合わせると多層防御になる
iTerm2の背景色だけでは見落とします。pre-pushフックだけではpush以外の操作は防げません。7つの小技を5つのレイヤーに配置することで、1つが突破されても次が防御する多層防御が成立します。
3. 最も効果的なのは「緊急停止ファイル」
技術的には最も単純ですが、実用面での効果は最大です。touchコマンド一発で全システムを停止できる安心感が、本番運用の精神的負荷を大きく下げました。依存関係ゼロ(DB不要、API不要、ネットワーク不要)で動作する点も重要です。
まとめ
ヒューマンエラー防止で重要なのは以下の3点です。
- 視覚的フィードバックを最優先: iTerm2の背景色 + プロンプト表示で、環境を「読む」のではなく「見る」ことで認識します。認知負荷を最小化する設計です
- 多層防御の構築: 視覚的防御・操作的防御・識別的防御・緊急対応・検出の5レイヤーで、1つの対策が失敗しても次が防御します
- シンプルさが信頼性: 緊急停止ファイル(touch/rm)、pre-pushフック(シェルスクリプト)、ハートビート(curl)——複雑な仕組みは障害時に動きません。最もシンプルな手段が最も信頼できます
個々の小技は5分で実装できます。しかしその積み重ねが、本番運用の安全性を根本的に変えます。