導入(問題提起)

開発はゴールではなくスタートです。Webシステム開発を終えて公開しても、保守運用が弱いと「気づけない障害」「戻せない変更」「守れないデータ」によって事業が止まります。特にExcel Web化で業務システムを内製・半内製した小さな会社では、担当者の離任や多忙が直撃リスクです。本記事は、最小のチームでも回る「紙1枚の運用設計書」に落とし込む実務手順を、Pythonベースの小規模Webアプリ(Bottle/FastAPI+SQLite/MySQL)を前提に解説します。

課題の詳細説明

現場でよくある“穴”を具体化します。

  • バックアップは取っているが、復元(リストア)訓練をしていないため、いざという時に戻せない。
  • 障害連絡が担当者の個人チャットに流れ、休日や退職で連絡不能になる。
  • 権限が増え続け「誰が何に触れるか」を説明できない。監査ログも散在。
  • 仕様変更や小修正が口頭で実施され、変更履歴・ロールバック手順がない。
  • 表示速度やエラー率の計測がなく、「遅い」「落ちる」の主観議論になる。
  • Excel運用からWeb化したのに、操作マニュアルやFAQが未整備でサポートが属人化。

これらは、役割と閾値、手順と記録、そして定例レビューという“運用の三点セット”が欠けていることが原因です。

解決方法(運用設計書の骨子)

運用設計書はA4一枚からで十分です。次の章立てを最小セットとして明文化します。

  1. 組織体制と連絡先
  2. - 役割: オーナー(最終責任)/運用担当/代行/開発(内製or外注) - 連絡: 平時の窓口(メール/Chat)/緊急連絡(電話)/ベンダー窓口 - 可用性: 対応時間帯/休日対応方針/エスカレーション基準

  3. 稼働監視と通知
  4. - 監視項目: 死活/応答時間/エラー率/バッチ結果/ディスク残容量 - 閾値: 警告/致命の数値基準と継続時間 - 通知: チャネル(Chat/メール)/誰に/夜間の扱い

  5. データ保護(バックアップ/リストア)
  6. - 対象: DB(本番/ステージング)/添付ファイル/設定 - 世代: 日次N世代+週次M世代 - リストア: 四半期に一度の訓練(手順書・所要時間の記録)

  7. 変更管理(リリース/ロールバック)
  8. - フロー: 申請→レビュー→リリース→検証→クローズ - ブルーグリーンやDBマイグレーションの手順/チェックリスト - 緊急変更の扱いと翌営業日の事後承認

  9. セキュリティ(権限/RBAC/監査/パスワード)
  10. - 最小権限/RBACロール設計/四半期の権限棚卸 - 監査ログの不可逆保存と検索手順 - 秘匿情報(APIキー/DBパス)の管理とローテーション

  11. サポート/ナレッジ
  12. - FAQ/操作手順書/障害対応テンプレート - 週次/隔週の運用レビューでの改善サイクル

具体例(ケース別の落とし穴と処方箋)

  • 小売の在庫システム:深夜バッチが稀に失敗し翌朝在庫が0表示→「ジョブ結果を監視し、失敗時はメール+チャット通知。手動再実行ボタンと手順を用意」。
  • 予約システム:カレンダー連携のWebhookがタイムアウト→「リトライとデッドレタキュー、手動再配送の管理画面を整備」。
  • 請求管理:月末にPDF出力が集中して応答低下→「事前レンダリングのスケジューラ、ピークシフト設定、CPU/IO監視の閾値調整」。
  • 社内ポータル:管理者が誤って全社員に閲覧権限を付与→「RBACのロール上限と二段承認、権限変更の監査通知」。

それぞれの根っこは「計測できていない」「戻せない」「誰が何をしたか不明」です。運用設計書に“測る/戻す/記録する”を埋め込むのが解決です。

技術的な解説(Python×小規模Webシステム実装例)

ここではBottle(またはFastAPI)+SQLite/MySQLで、監視・バックアップ・RBAC・監査の最小構成を例示します。

1) データモデル(抜粋)


-- 監査ログ:不可逆追記。誰が・いつ・何を・どのデータに
CREATE TABLE audit_logs (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  actor TEXT NOT NULL,
  action TEXT NOT NULL,
  entity TEXT NOT NULL,
  entity_id TEXT,
  detail TEXT
);

-- RBACロールと付与
CREATE TABLE roles (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT UNIQUE NOT NULL
);
CREATE TABLE user_roles (
  user_id INTEGER NOT NULL,
  role_id INTEGER NOT NULL,
  UNIQUE(user_id, role_id)
);

-- バッチ実行履歴(成功/失敗/所要時間)
CREATE TABLE job_runs (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  job_name TEXT NOT NULL,
  started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  finished_at TIMESTAMP,
  status TEXT CHECK(status IN ('success','failure')),
  message TEXT
);

2) 監査の記録と通知(Bottleの例)


from bottle import Bottle, request, response
import sqlite3, json, time

app = Bottle()

def db():
    return sqlite3.connect('app.db')

def audit(actor, action, entity, entity_id=None, detail=None):
    con = db(); cur = con.cursor()
    cur.execute('INSERT INTO audit_logs(actor,action,entity,entity_id,detail) VALUES (?,?,?,?,?)',
                (actor, action, entity, entity_id, json.dumps(detail or {})))
    con.commit(); con.close()

@app.post('/admin/users/<uid:int>/roles')
def add_role(uid):
    actor = request.headers.get('X-User','system')
    role = request.json.get('role')
    con = db(); cur = con.cursor()
    cur.execute('INSERT OR IGNORE INTO user_roles(user_id, role_id) SELECT ?, id FROM roles WHERE name=?', (uid, role))
    con.commit(); con.close()
    audit(actor, 'grant_role', 'user', str(uid), {'role': role})
    # 重要な権限変更は通知(メール/Chat)にフック
    return {'ok': True}

3) バックアップ/リストア運用(疑似コード)


import shutil, datetime as dt, subprocess

def backup_sqlite(db_path, dst_dir):
    ts = dt.datetime.now().strftime('%Y%m%d-%H%M')
    shutil.copy2(db_path, f'{dst_dir}/app-{ts}.sqlite3')

def backup_mysql(dsn, dst_dir):
    ts = dt.datetime.now().strftime('%Y%m%d-%H%M')
    subprocess.run(['mysqldump', dsn, '--single-transaction', f'> {dst_dir}/app-{ts}.sql'], shell=True, check=True)

def restore_sqlite(src, db_path):
    shutil.copy2(src, db_path)

4) 監視(アプリ内ヘルスチェック+ジョブ監視)


@app.get('/healthz')
def healthz():
    t0 = time.time()
    con = db(); con.execute('SELECT 1'); con.close()
    latency_ms = int((time.time()-t0)*1000)
    status = 'ok' if latency_ms < 300 else 'degraded'
    return {'status': status, 'db_latency_ms': latency_ms}

@app.post('/jobs/<name>/done')
def job_done(name):
    payload = request.json or {}
    con = db(); cur = con.cursor()
    cur.execute('INSERT INTO job_runs(job_name, finished_at, status, message) VALUES (?, CURRENT_TIMESTAMP, ?, ?)',
                (name, payload.get('status','success'), payload.get('message','')))
    con.commit(); con.close()
    return {'ok': True}

導入の流れ(最短4週間プラン)

  1. 週1の運用レビューを設定(出席者・議事録・改善タスク管理を確立)
  2. 監視の最小セットを実装(/healthz・エラー率・バッチ失敗通知)
  3. バックアップの世代管理と復元訓練を実施(手順書と所要時間を記録)
  4. RBACの棚卸と監査ログの可視化(検索/CSV出力)
  5. 変更管理の申請→レビュー→リリース→ロールバック手順を紙1枚で定義
  6. “数値で見る”運用ダッシュボードをトップリンクに設置(運用KPI:エラー率、MTTR、失敗バッチ、復元所要時間)

まとめ

  • 運用は「人」に寄せず「仕組み」に寄せる。役割・閾値・手順を紙1枚に落とすことが出発点。
  • Pythonで最小の業務システムを作るなら、監査とバックアップ、ヘルスチェックを最初から入れると後が楽。
  • Excel Web化後こそ、数字で回す保守運用へ。計測→改善→定着のサイクルを週次でまわす。

問い合わせ導線

保守運用の立ち上げ・改善の伴走支援はお問い合わせへ。Webシステム開発/業務システムの安定運用をご一緒に設計します。

Webシステム開発のご相談は monou まで