Update CLAUDE.md for SQLite architecture

Reflects the JSON → SQLite migration: architecture diagram now shows
K8s pod + SSH/SCP remote sync, data flow documentation, updated
file descriptions, startup sequence, and write flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-08 12:03:50 +09:00
parent 12fd3b5e8f
commit 1be615be99

124
CLAUDE.md
View File

@@ -11,20 +11,24 @@ HAProxy Dynamic Load Balancer Management Suite - a system for runtime management
## Architecture ## Architecture
``` ```
┌──────────────────────────────────────────────────┐ ┌─ K8s Pod (MCP Server) ──────────┐ SSH/SCP ┌─ HAProxy Host ──────────────────┐
HAProxy (host network) │ ───────────→ │
TCP 80/443 (HTTP/1.1, HTTP/2) + UDP 443 (HTTP/3) /app/data/haproxy_mcp.db │ Runtime API │ HAProxy (host network)
Runtime API: localhost:9999 (SQLite - local working copy) │ ───────────→ TCP 80/443 + UDP 443 (HTTP/3)
MCP Proxy: mcp.inouter.com → localhost:8000 │ :9999 │ Runtime API: localhost:9999
└──────────────┬───────────────────────────────────┘ │ MCP Server :8000 │ │ │
Transport: streamable-http │ SCP sync │ /opt/haproxy/conf/
────────── │ │ ←──────────→ │ haproxy_mcp.db (persistent) │
│ MCP │ │ map sync │ domains.map
│ Server │ │ ───────────→ │ wildcards.map
│ :8000 │ └──────────────────────────────────┘ └─────────────────────────────────┘
└───────────┘
``` ```
### Data Flow
- **SQLite DB** (`haproxy_mcp.db`): Single source of truth for domains, servers, certificates
- **Map files** (`domains.map`, `wildcards.map`): Generated from SQLite, HAProxy reads directly
- **Runtime API**: Real-time HAProxy configuration (map entries, server addresses)
### Domain Routing Flow ### Domain Routing Flow
``` ```
Request → HAProxy Frontend Request → HAProxy Frontend
@@ -33,7 +37,7 @@ domains.map lookup (Host header)
pool_N backend (N = 1-100) pool_N backend (N = 1-100)
Server (from servers.json, auto-restored on startup) Server (from SQLite DB, auto-restored on startup)
``` ```
## Services ## Services
@@ -58,7 +62,7 @@ journalctl -u haproxy-mcp -f # Logs
- Transport: `streamable-http` - Transport: `streamable-http`
- Internal: `http://localhost:8000/mcp` - Internal: `http://localhost:8000/mcp`
- External: `https://mcp.inouter.com/mcp` (via HAProxy) - External: `https://mcp.inouter.com/mcp` (via HAProxy)
- **Auto-restore:** Servers restored from `servers.json` on startup - **Auto-restore:** Servers restored from SQLite DB on startup
### Environment Variables ### Environment Variables
| Variable | Default | Description | | Variable | Default | Description |
@@ -69,7 +73,10 @@ journalctl -u haproxy-mcp -f # Logs
| `HAPROXY_PORT` | `9999` | HAProxy Runtime API port | | `HAPROXY_PORT` | `9999` | HAProxy Runtime API port |
| `HAPROXY_STATE_FILE` | `/opt/haproxy/data/servers.state` | State file path | | `HAPROXY_STATE_FILE` | `/opt/haproxy/data/servers.state` | State file path |
| `HAPROXY_MAP_FILE` | `/opt/haproxy/conf/domains.map` | Map file path | | `HAPROXY_MAP_FILE` | `/opt/haproxy/conf/domains.map` | Map file path |
| `HAPROXY_SERVERS_FILE` | `/opt/haproxy/conf/servers.json` | Servers config path | | `HAPROXY_DB_FILE` | `/opt/haproxy/conf/haproxy_mcp.db` | Local SQLite DB path |
| `HAPROXY_REMOTE_DB_FILE` | `/opt/haproxy/conf/haproxy_mcp.db` | Remote SQLite DB path (SCP target) |
| `HAPROXY_SERVERS_FILE` | `/opt/haproxy/conf/servers.json` | Legacy servers config (migration only) |
| `HAPROXY_CERTS_FILE` | `/opt/haproxy/conf/certificates.json` | Legacy certs config (migration only) |
| `HAPROXY_POOL_COUNT` | `100` | Number of pool backends | | `HAPROXY_POOL_COUNT` | `100` | Number of pool backends |
| `HAPROXY_MAX_SLOTS` | `10` | Max servers per pool | | `HAPROXY_MAX_SLOTS` | `10` | Max servers per pool |
| `HAPROXY_CONTAINER` | `haproxy` | Podman container name | | `HAPROXY_CONTAINER` | `haproxy` | Podman container name |
@@ -78,10 +85,10 @@ journalctl -u haproxy-mcp -f # Logs
## Zero-Reload Domain Management ## Zero-Reload Domain Management
### How It Works ### How It Works
1. **domains.map**: Maps domain → pool backend (e.g., `example.com pool_1`) 1. **SQLite DB** (`haproxy_mcp.db`): Single source of truth for all persistent data
2. **Pool backends**: 100 pre-configured backends (`pool_1` to `pool_100`) 2. **domains.map**: Generated from SQLite, maps domain → pool backend
3. **Runtime API**: Add/remove map entries and configure servers without reload 3. **Pool backends**: 100 pre-configured backends (`pool_1` to `pool_100`)
4. **servers.json**: Persistent server configuration, auto-restored on MCP startup 4. **Runtime API**: Add/remove map entries and configure servers without reload
### Adding a Domain ### Adding a Domain
@@ -94,11 +101,12 @@ haproxy_add_domain("api.example.com", "10.0.0.1", 8080)
``` ```
This will: This will:
1. Find available pool (e.g., `pool_5`) 1. Find available pool (e.g., `pool_5`) via SQLite query
2. Add to `domains.map`: `example.com pool_5` 2. Save to SQLite DB (domain, backend, server)
3. Update HAProxy runtime map: `add map ... example.com pool_5` 3. Sync `domains.map` from DB (for HAProxy file-based lookup)
4. Configure server in pool via Runtime API (HTTP only) 4. Update HAProxy runtime map: `add map ... example.com pool_5`
5. Save to `servers.json` for persistence 5. Configure server in pool via Runtime API (HTTP only)
6. Upload DB to remote host via SCP (persistence)
**No HAProxy reload required!** **No HAProxy reload required!**
@@ -120,7 +128,7 @@ haproxy_add_domain("api.example.com", share_with="example.com")
**Benefits:** **Benefits:**
- Saves pool slots (100 pools can serve unlimited domains) - Saves pool slots (100 pools can serve unlimited domains)
- Multiple domains → same backend servers - Multiple domains → same backend servers
- Shared domains stored as `_shares` reference in `servers.json` - Shared domains stored as `shares_with` in SQLite domains table
**Behavior:** **Behavior:**
- Shared domains cannot specify `ip` (use existing servers) - Shared domains cannot specify `ip` (use existing servers)
@@ -146,9 +154,12 @@ haproxy_add_servers("api.example.com", '[
### Files ### Files
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `domains.map` | Domain → Pool mapping | | `haproxy_mcp.db` | SQLite DB — single source of truth (domains, servers, certs) |
| `servers.json` | Server IP/port persistence | | `domains.map` | Domain → Pool mapping (generated from DB, HAProxy reads directly) |
| `wildcards.map` | Wildcard domain mapping (generated from DB) |
| `haproxy.cfg` | Pool backend definitions (static) | | `haproxy.cfg` | Pool backend definitions (static) |
| `servers.json` | Legacy — used only for initial migration to SQLite |
| `certificates.json` | Legacy — used only for initial migration to SQLite |
## SSL/TLS Certificates ## SSL/TLS Certificates
@@ -243,7 +254,7 @@ Returns overall system status:
"components": { "components": {
"mcp": {"status": "ok"}, "mcp": {"status": "ok"},
"haproxy": {"status": "ok", "version": "3.3.2", "uptime_sec": 3600}, "haproxy": {"status": "ok", "version": "3.3.2", "uptime_sec": 3600},
"config_files": {"status": "ok", "files": {"map_file": "ok", "servers_file": "ok"}}, "config_files": {"status": "ok", "files": {"map_file": "ok", "db_file": "ok"}},
"container": {"status": "ok", "state": "running"} "container": {"status": "ok", "state": "running"}
} }
} }
@@ -342,15 +353,17 @@ api.example.com → pool_6
└─ pool_6_1 (slot 1) └─ pool_6_1 (slot 1)
``` ```
### Persistence ### Persistence (SQLite)
- **domains.map**: Survives HAProxy restart - **SQLite DB** (`haproxy_mcp.db`): All domains, servers, certificates stored in single file
- **servers.json**: Auto-restored by MCP on startup - **Remote sync**: DB uploaded to HAProxy host via SCP after every write
- No manual save required - `haproxy_add_server` auto-saves - **Startup restore**: DB downloaded from remote on pod start, servers restored via Runtime API
- **Migration**: First startup auto-migrates from legacy JSON/map files (one-time, idempotent)
- **WAL mode**: Write-Ahead Logging for concurrent access, checkpointed before SCP upload
### Safety Features ### Safety Features
- **Atomic file writes**: Temp file + rename prevents corruption - **Atomic file writes**: Temp file + rename prevents corruption (map files)
- **File locking**: Prevents race conditions on concurrent operations - **SQLite transactions**: All DB writes are atomic
- **Disk-first pattern**: Config saved before HAProxy update, rollback on failure - **DB-first pattern**: Config saved to SQLite before HAProxy update, rollback on failure
- **Command validation**: HAProxy responses checked for errors - **Command validation**: HAProxy responses checked for errors
- **Input validation**: Domain format, IP (v4/v6), port range, slot limits - **Input validation**: Domain format, IP (v4/v6), port range, slot limits
- **Bulk limits**: Max 10 servers per bulk add, 10KB JSON size limit - **Bulk limits**: Max 10 servers per bulk add, 10KB JSON size limit
@@ -365,7 +378,7 @@ api.example.com → pool_6
- **Runtime API**: Certificates loaded/updated without HAProxy reload - **Runtime API**: Certificates loaded/updated without HAProxy reload
- `new ssl cert``set ssl cert``commit ssl cert` - `new ssl cert``set ssl cert``commit ssl cert`
- No connection drops during certificate changes - No connection drops during certificate changes
- **Persistence**: `certificates.json` stores domain list - **Persistence**: Certificate domains stored in SQLite `certificates` table
- **Auto-restore**: Certificates reloaded into HAProxy on MCP startup - **Auto-restore**: Certificates reloaded into HAProxy on MCP startup
## HAProxy Runtime API ## HAProxy Runtime API
@@ -397,10 +410,12 @@ echo "set server pool_1/pool_1_1 state ready" | nc localhost 9999
├── haproxy_mcp/ # MCP server package (streamable-http) ├── haproxy_mcp/ # MCP server package (streamable-http)
│ ├── server.py # Main entry point │ ├── server.py # Main entry point
│ ├── config.py # Configuration and constants │ ├── config.py # Configuration and constants
│ ├── db.py # SQLite database (single source of truth)
│ ├── ssh_ops.py # SSH/SCP remote execution
│ ├── exceptions.py # Exception classes │ ├── exceptions.py # Exception classes
│ ├── validation.py # Input validation │ ├── validation.py # Input validation
│ ├── haproxy_client.py # HAProxy Runtime API client │ ├── haproxy_client.py # HAProxy Runtime API client
│ ├── file_ops.py # File I/O operations │ ├── file_ops.py # File I/O (delegates to db.py)
│ ├── utils.py # Parsing utilities │ ├── utils.py # Parsing utilities
│ └── tools/ # MCP tools (29 total) │ └── tools/ # MCP tools (29 total)
│ ├── domains.py # Domain management (3 tools) │ ├── domains.py # Domain management (3 tools)
@@ -411,9 +426,11 @@ echo "set server pool_1/pool_1_1 state ready" | nc localhost 9999
│ └── certificates.py # Certificate management (7 tools) │ └── certificates.py # Certificate management (7 tools)
├── conf/ ├── conf/
│ ├── haproxy.cfg # Main HAProxy config (100 pool backends) │ ├── haproxy.cfg # Main HAProxy config (100 pool backends)
│ ├── domains.map # Domain → Pool mapping │ ├── haproxy_mcp.db # SQLite DB (single source of truth)
│ ├── servers.json # Server persistence (auto-managed) │ ├── domains.map # Domain → Pool mapping (generated from DB)
│ ├── certificates.json # Certificate domain list (auto-managed) │ ├── wildcards.map # Wildcard domain mapping (generated from DB)
│ ├── servers.json # Legacy (migration source only)
│ ├── certificates.json # Legacy (migration source only)
│ └── mcp-token.env # Bearer token for MCP auth │ └── mcp-token.env # Bearer token for MCP auth
├── certs/ # SSL/TLS certificates (HAProxy PEM format) ├── certs/ # SSL/TLS certificates (HAProxy PEM format)
├── data/ # Legacy state files ├── data/ # Legacy state files
@@ -433,8 +450,9 @@ echo "set server pool_1/pool_1_1 state ready" | nc localhost 9999
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `/opt/haproxy/conf/haproxy.cfg` | HAProxy main config | | `/opt/haproxy/conf/haproxy.cfg` | HAProxy main config |
| `/opt/haproxy/conf/domains.map` | Domain routing map | | `/opt/haproxy/conf/haproxy_mcp.db` | SQLite DB (single source of truth) |
| `/opt/haproxy/conf/servers.json` | Server persistence | | `/opt/haproxy/conf/domains.map` | Domain routing map (generated from DB) |
| `/opt/haproxy/conf/wildcards.map` | Wildcard domain map (generated from DB) |
| `/opt/haproxy/certs/*.pem` | SSL certificates (auto-loaded) | | `/opt/haproxy/certs/*.pem` | SSL certificates (auto-loaded) |
| `/etc/containers/systemd/haproxy.container` | Podman Quadlet | | `/etc/containers/systemd/haproxy.container` | Podman Quadlet |
| `/etc/systemd/system/haproxy-mcp.service` | MCP service | | `/etc/systemd/system/haproxy-mcp.service` | MCP service |
@@ -451,13 +469,27 @@ echo "set server pool_1/pool_1_1 state ready" | nc localhost 9999
3. HAProxy loads domains.map (domain → pool mapping) 3. HAProxy loads domains.map (domain → pool mapping)
4. systemd starts haproxy-mcp.service 4. systemd starts haproxy-mcp.service (or K8s pod starts)
5. MCP server reads servers.json & certificates.json 5. init_db(): Download SQLite DB from remote host via SCP
(first time: migrate from legacy JSON/map files → create DB → upload)
6. MCP server restores servers via Runtime API 6. MCP server restores servers from DB via Runtime API
7. MCP server loads certificates via Runtime API (zero-downtime) 7. MCP server loads certificates from DB via Runtime API (zero-downtime)
8. Ready to serve traffic 8. Ready to serve traffic
``` ```
### Write Flow (runtime)
```
MCP tool call (add domain/server/cert)
Write to local SQLite DB
Sync map files to remote (domains only)
Update HAProxy via Runtime API
Upload DB to remote host via SCP (persistence)
```