Files
haproxy-mcp/CLAUDE.md
kappa 1be615be99 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>
2026-02-08 12:03:50 +09:00

496 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
HAProxy Dynamic Load Balancer Management Suite - a system for runtime management of HAProxy without restarts. Provides MCP interface for dynamic domain/server management. Frontend supports HTTP/HTTPS/HTTP3, backends use HTTP only (SSL termination at HAProxy).
**Key Feature:** Zero-reload domain management using HAProxy map files and pool backends.
## Architecture
```
┌─ K8s Pod (MCP Server) ──────────┐ SSH/SCP ┌─ HAProxy Host ──────────────────┐
│ │ ───────────→ │ │
│ /app/data/haproxy_mcp.db │ Runtime API │ HAProxy (host network) │
│ (SQLite - local working copy) │ ───────────→ │ TCP 80/443 + UDP 443 (HTTP/3) │
│ │ :9999 │ Runtime API: localhost:9999 │
│ MCP Server :8000 │ │ │
│ Transport: streamable-http │ SCP sync │ /opt/haproxy/conf/ │
│ │ ←──────────→ │ haproxy_mcp.db (persistent) │
│ │ map sync │ domains.map │
│ │ ───────────→ │ wildcards.map │
└──────────────────────────────────┘ └─────────────────────────────────┘
```
### 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
```
Request → HAProxy Frontend
domains.map lookup (Host header)
pool_N backend (N = 1-100)
Server (from SQLite DB, auto-restored on startup)
```
## Services
### HAProxy (Podman Quadlet)
```bash
systemctl start haproxy # Start
systemctl stop haproxy # Stop
systemctl restart haproxy # Restart
systemctl status haproxy # Status
```
- Quadlet config: `/etc/containers/systemd/haproxy.container`
- Network mode: `host` (direct access to host services)
### MCP Server (systemd)
```bash
systemctl start haproxy-mcp # Start
systemctl stop haproxy-mcp # Stop
systemctl restart haproxy-mcp # Restart
journalctl -u haproxy-mcp -f # Logs
```
- Transport: `streamable-http`
- Internal: `http://localhost:8000/mcp`
- External: `https://mcp.inouter.com/mcp` (via HAProxy)
- **Auto-restore:** Servers restored from SQLite DB on startup
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_HOST` | `0.0.0.0` | MCP server bind host |
| `MCP_PORT` | `8000` | MCP server port |
| `HAPROXY_HOST` | `localhost` | HAProxy Runtime API host |
| `HAPROXY_PORT` | `9999` | HAProxy Runtime API port |
| `HAPROXY_STATE_FILE` | `/opt/haproxy/data/servers.state` | State file path |
| `HAPROXY_MAP_FILE` | `/opt/haproxy/conf/domains.map` | Map file 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_MAX_SLOTS` | `10` | Max servers per pool |
| `HAPROXY_CONTAINER` | `haproxy` | Podman container name |
| `LOG_LEVEL` | `INFO` | Logging level (DEBUG/INFO/WARNING/ERROR) |
## Zero-Reload Domain Management
### How It Works
1. **SQLite DB** (`haproxy_mcp.db`): Single source of truth for all persistent data
2. **domains.map**: Generated from SQLite, maps domain → pool backend
3. **Pool backends**: 100 pre-configured backends (`pool_1` to `pool_100`)
4. **Runtime API**: Add/remove map entries and configure servers without reload
### Adding a Domain
```bash
haproxy_add_domain("example.com", "10.0.0.1")
# Creates: pool_N_1 → 10.0.0.1:80
haproxy_add_domain("api.example.com", "10.0.0.1", 8080)
# Creates: pool_N_1 → 10.0.0.1:8080
```
This will:
1. Find available pool (e.g., `pool_5`) via SQLite query
2. Save to SQLite DB (domain, backend, server)
3. Sync `domains.map` from DB (for HAProxy file-based lookup)
4. Update HAProxy runtime map: `add map ... example.com pool_5`
5. Configure server in pool via Runtime API (HTTP only)
6. Upload DB to remote host via SCP (persistence)
**No HAProxy reload required!**
### Pool Sharing
Multiple domains can share the same pool/backend servers using `share_with`:
```bash
# First domain gets its own pool
haproxy_add_domain("example.com", "10.0.0.1")
# Creates: example.com → pool_5
# Additional domains share the same pool
haproxy_add_domain("www.example.com", share_with="example.com")
haproxy_add_domain("api.example.com", share_with="example.com")
# Both use: pool_5 (same backend servers)
```
**Benefits:**
- Saves pool slots (100 pools can serve unlimited domains)
- Multiple domains → same backend servers
- Shared domains stored as `shares_with` in SQLite domains table
**Behavior:**
- Shared domains cannot specify `ip` (use existing servers)
- Removing a shared domain keeps servers intact
- Removing the last domain using a pool clears the servers
### Backend Configuration
- Backends always use HTTP (port 80 or custom)
- SSL/TLS termination happens at HAProxy frontend
- Each pool has 10 server slots (pool_N_1 to pool_N_10)
- **IPv6 supported**: Both IPv4 and IPv6 addresses accepted
### Bulk Server Operations
```bash
# Add multiple servers at once
haproxy_add_servers("api.example.com", '[
{"slot": 1, "ip": "10.0.0.1", "http_port": 80},
{"slot": 2, "ip": "10.0.0.2", "http_port": 80},
{"slot": 3, "ip": "2001:db8::1", "http_port": 80}
]')
```
### Files
| File | Purpose |
|------|---------|
| `haproxy_mcp.db` | SQLite DB — single source of truth (domains, servers, certs) |
| `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) |
| `servers.json` | Legacy — used only for initial migration to SQLite |
| `certificates.json` | Legacy — used only for initial migration to SQLite |
## SSL/TLS Certificates
### Current Setup
| Item | Value |
|------|-------|
| ACME Client | acme.sh (Google Trust Services CA) |
| Cert Directory | `/opt/haproxy/certs/` (auto-loaded by HAProxy) |
| acme.sh Home | `~/.acme.sh/` |
### How It Works
1. HAProxy binds with `crt /etc/haproxy/certs/` (directory, not file)
2. All `.pem` files in directory are auto-loaded
3. SNI selects correct certificate based on domain
4. acme.sh deploy hook auto-creates combined PEM on issue/renewal
### Adding New Certificate
```bash
# Issue certificate (Google Trust Services)
~/.acme.sh/acme.sh --issue \
--dns dns_cf \
-d "newdomain.com" -d "*.newdomain.com" \
--reloadcmd "cat ~/.acme.sh/newdomain.com_ecc/fullchain.cer ~/.acme.sh/newdomain.com_ecc/newdomain.com.key > /opt/haproxy/certs/newdomain.com.pem && podman exec haproxy kill -USR2 1"
```
### Certificate Commands
```bash
# List certificates
~/.acme.sh/acme.sh --list
# Renew all
~/.acme.sh/acme.sh --cron
# Force renew specific domain
~/.acme.sh/acme.sh --renew -d "domain.com" --force
# Check loaded certs in HAProxy
ls -la /opt/haproxy/certs/
```
### Cloudflare Credentials
- File: `~/.secrets/cloudflare.ini`
- Format: `dns_cloudflare_api_token = TOKEN`
- Also export: `export CF_Token="your_token"`
## HTTP/3 (QUIC)
Frontend supports all protocols:
```
TCP 443: HTTP/1.1, HTTP/2
UDP 443: HTTP/3 (QUIC)
```
Clients receive `alt-svc: h3=":443"; ma=86400` header for HTTP/3 upgrade.
## MCP Security
HAProxy proxies MCP with Bearer Token authentication (configured in frontend ACL).
### Access Rules
| Source | Token Required |
|--------|----------------|
| Tailscale (100.64.0.0/10) | No |
| External (CF Worker, etc.) | Yes |
### Token Location
- File: `/opt/haproxy/conf/mcp-token.env`
### Connection Examples
**Tailscale (no auth):**
```bash
claude mcp add --transport http haproxy http://100.108.39.107:8000/mcp
```
**External (with auth):**
```bash
curl -X POST https://mcp.inouter.com/mcp \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":0}'
```
## Health Check
### System Health (`haproxy_health`)
Returns overall system status:
```json
{
"status": "healthy",
"components": {
"mcp": {"status": "ok"},
"haproxy": {"status": "ok", "version": "3.3.2", "uptime_sec": 3600},
"config_files": {"status": "ok", "files": {"map_file": "ok", "db_file": "ok"}},
"container": {"status": "ok", "state": "running"}
}
}
```
### Domain Health (`haproxy_domain_health`)
Returns backend server status for a specific domain:
```json
{
"domain": "api.example.com",
"backend": "pool_3",
"status": "healthy",
"servers": [
{"name": "pool_3_1", "addr": "10.0.0.1:80", "status": "UP", "check_status": "L4OK"}
],
"healthy_count": 1,
"total_count": 1
}
```
**Status values:** `healthy` (all UP), `degraded` (partial UP), `down` (all DOWN), `no_servers`
## MCP Tools (29 total)
### Domain Management
| Tool | Description |
|------|-------------|
| `haproxy_list_domains` | List domains (use `include_wildcards=true` for wildcards) |
| `haproxy_add_domain` | Add domain to pool (no reload), supports `share_with` for pool sharing |
| `haproxy_remove_domain` | Remove domain from pool (no reload) |
### Server Management
| Tool | Description |
|------|-------------|
| `haproxy_list_servers` | List servers for a domain |
| `haproxy_add_server` | Add server to slot (1-10 or 0=auto), auto-saved |
| `haproxy_add_servers` | Bulk add servers (JSON array), auto-saved |
| `haproxy_remove_server` | Remove server from slot, auto-saved |
| `haproxy_set_server_state` | Set state: ready/drain/maint |
| `haproxy_set_server_weight` | Set weight (0-256) |
| `haproxy_set_domain_state` | Set all servers of domain to ready/drain/maint |
| `haproxy_wait_drain` | Wait for connections to drain (timeout configurable) |
### Health Check
| Tool | Description |
|------|-------------|
| `haproxy_health` | System health (MCP, HAProxy, config files) |
| `haproxy_domain_health` | Domain-specific health with server status |
| `haproxy_get_server_health` | Get UP/DOWN/MAINT status for all servers |
### Monitoring
| Tool | Description |
|------|-------------|
| `haproxy_stats` | HAProxy statistics |
| `haproxy_backends` | List backends |
| `haproxy_list_frontends` | List frontends with status |
| `haproxy_get_connections` | Active connections per server |
### Configuration
| Tool | Description |
|------|-------------|
| `haproxy_reload` | Validate, reload config, auto-restore servers |
| `haproxy_check_config` | Validate config syntax |
| `haproxy_save_state` | Save server state to disk (legacy) |
| `haproxy_restore_state` | Restore state from disk (legacy) |
### Certificate Management (Zero-Downtime via Runtime API)
| Tool | Description |
|------|-------------|
| `haproxy_list_certs` | List all certificates with expiry info |
| `haproxy_cert_info` | Get detailed certificate info (expiry, issuer, SANs) |
| `haproxy_issue_cert` | Issue new certificate via acme.sh + Cloudflare DNS |
| `haproxy_renew_cert` | Renew specific certificate (force option available) |
| `haproxy_renew_all_certs` | Renew all certificates due for renewal |
| `haproxy_delete_cert` | Delete certificate from acme.sh and HAProxy |
| `haproxy_load_cert` | Load/reload certificate into HAProxy (manual trigger) |
## Key Conventions
### Pool-Based Routing
- Domains map to pools: `example.com``pool_N`
- 100 pools available: `pool_1` to `pool_100`
- Each pool has 10 server slots
### Server Naming
Pool backends use simple naming: `pool_N_1` to `pool_N_10`
**Examples:**
```
example.com → pool_5
└─ pool_5_1 (slot 1)
└─ pool_5_2 (slot 2)
└─ ... up to pool_5_10
api.example.com → pool_6
└─ pool_6_1 (slot 1)
```
### Persistence (SQLite)
- **SQLite DB** (`haproxy_mcp.db`): All domains, servers, certificates stored in single file
- **Remote sync**: DB uploaded to HAProxy host via SCP after every write
- **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
- **Atomic file writes**: Temp file + rename prevents corruption (map files)
- **SQLite transactions**: All DB writes are atomic
- **DB-first pattern**: Config saved to SQLite before HAProxy update, rollback on failure
- **Command validation**: HAProxy responses checked for errors
- **Input validation**: Domain format, IP (v4/v6), port range, slot limits
- **Bulk limits**: Max 10 servers per bulk add, 10KB JSON size limit
### Performance Optimization
- **Command batching**: Multiple HAProxy commands sent in single TCP connection
- Server config (addr + state): 1 connection instead of 2
- Startup restore: All servers restored in 1 connection (was 2×N for N servers)
- Example: 7 servers restored = 1 connection (was 14 connections)
### Zero-Downtime Certificate Management
- **Runtime API**: Certificates loaded/updated without HAProxy reload
- `new ssl cert``set ssl cert``commit ssl cert`
- No connection drops during certificate changes
- **Persistence**: Certificate domains stored in SQLite `certificates` table
- **Auto-restore**: Certificates reloaded into HAProxy on MCP startup
## HAProxy Runtime API
```bash
# Show domain mappings
echo "show map /usr/local/etc/haproxy/domains.map" | nc localhost 9999
# Add domain mapping (runtime only)
echo "add map /usr/local/etc/haproxy/domains.map example.com pool_5" | nc localhost 9999
# Show server state
echo "show servers state" | nc localhost 9999
# Show backends
echo "show backend" | nc localhost 9999
# Set server address
echo "set server pool_1/pool_1_1 addr 10.0.0.1 port 80" | nc localhost 9999
# Enable server
echo "set server pool_1/pool_1_1 state ready" | nc localhost 9999
```
## Directory Structure
```
/opt/haproxy/
├── haproxy_mcp/ # MCP server package (streamable-http)
│ ├── server.py # Main entry point
│ ├── config.py # Configuration and constants
│ ├── db.py # SQLite database (single source of truth)
│ ├── ssh_ops.py # SSH/SCP remote execution
│ ├── exceptions.py # Exception classes
│ ├── validation.py # Input validation
│ ├── haproxy_client.py # HAProxy Runtime API client
│ ├── file_ops.py # File I/O (delegates to db.py)
│ ├── utils.py # Parsing utilities
│ └── tools/ # MCP tools (29 total)
│ ├── domains.py # Domain management (3 tools)
│ ├── servers.py # Server management (7 tools)
│ ├── health.py # Health checks (3 tools)
│ ├── monitoring.py # Monitoring (4 tools)
│ ├── configuration.py # Config management (4 tools)
│ └── certificates.py # Certificate management (7 tools)
├── conf/
│ ├── haproxy.cfg # Main HAProxy config (100 pool backends)
│ ├── haproxy_mcp.db # SQLite DB (single source of truth)
│ ├── domains.map # Domain → Pool mapping (generated from DB)
│ ├── 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
├── certs/ # SSL/TLS certificates (HAProxy PEM format)
├── data/ # Legacy state files
├── scripts/ # Utility scripts
└── run/ # Runtime socket files
~/.acme.sh/
├── *_ecc/ # Certificate directories (one per domain)
└── acme.sh # ACME client script
/etc/containers/systemd/
└── haproxy.container # Podman Quadlet config
```
## Configuration Files
| File | Purpose |
|------|---------|
| `/opt/haproxy/conf/haproxy.cfg` | HAProxy main config |
| `/opt/haproxy/conf/haproxy_mcp.db` | SQLite DB (single source of truth) |
| `/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) |
| `/etc/containers/systemd/haproxy.container` | Podman Quadlet |
| `/etc/systemd/system/haproxy-mcp.service` | MCP service |
| `/opt/haproxy/conf/mcp-token.env` | MCP auth token |
| `~/.acme.sh/` | acme.sh certificates and config |
| `~/.secrets/cloudflare.ini` | Cloudflare API token |
## Startup Sequence
```
1. systemd starts haproxy.service
2. HAProxy loads haproxy.cfg (100 pool backends)
3. HAProxy loads domains.map (domain → pool mapping)
4. systemd starts haproxy-mcp.service (or K8s pod starts)
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 from DB via Runtime API
7. MCP server loads certificates from DB via Runtime API (zero-downtime)
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)
```