docs: Update README with comprehensive project documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
374
README.md
374
README.md
@@ -1,21 +1,375 @@
|
||||
# haproxy-mcp
|
||||
# HAProxy Dynamic Load Balancer Management Suite
|
||||
|
||||
HAProxy Dynamic Load Balancer Management with MCP Interface
|
||||
Runtime management of HAProxy without restarts. Provides an MCP (Model Context Protocol) interface for dynamic domain/server management with zero-reload operations.
|
||||
|
||||
## Overview
|
||||
## Key Features
|
||||
|
||||
MCP server for managing HAProxy load balancer configurations dynamically.
|
||||
- **Zero-reload domain management** using HAProxy map files and pool backends
|
||||
- **Dynamic server management** with auto-persistence
|
||||
- **SSL/TLS certificate management** with zero-downtime deployment via Runtime API
|
||||
- **Pool sharing** for multiple domains pointing to the same backend
|
||||
- **HTTP/3 (QUIC)** support alongside HTTP/1.1 and HTTP/2
|
||||
- **29 MCP tools** across domain, server, health, monitoring, config, and certificate management
|
||||
- **IPv4 and IPv6** support
|
||||
|
||||
## Features
|
||||
## Architecture
|
||||
|
||||
- Dynamic backend management
|
||||
- SSL certificate handling
|
||||
- Health checks
|
||||
- Domain routing
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ HAProxy (host network) │
|
||||
│ TCP 80/443 (HTTP/1.1, HTTP/2) + UDP 443 (HTTP/3) │
|
||||
│ Runtime API: localhost:9999 │
|
||||
│ MCP Proxy: mcp.inouter.com → localhost:8000 │
|
||||
└──────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────▼─────┐
|
||||
│ MCP │
|
||||
│ Server │
|
||||
│ :8000 │
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
### Domain Routing Flow
|
||||
|
||||
```
|
||||
Request → HAProxy Frontend
|
||||
↓
|
||||
domains.map lookup (Host header)
|
||||
↓
|
||||
pool_N backend (N = 1-100)
|
||||
↓
|
||||
Server (from servers.json, auto-restored on startup)
|
||||
```
|
||||
|
||||
### 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 (`pool_N_1` to `pool_N_10`)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **HAProxy 3.x** (running as Podman Quadlet container)
|
||||
- **Python 3.11+**
|
||||
- **uv** (Python package manager)
|
||||
- **acme.sh** (for SSL certificate management)
|
||||
- **Cloudflare** DNS (for DNS-01 ACME challenges)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository to `/opt/haproxy/`
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
cd /opt/haproxy/haproxy_mcp
|
||||
uv sync
|
||||
```
|
||||
|
||||
3. Start HAProxy:
|
||||
```bash
|
||||
systemctl start haproxy
|
||||
```
|
||||
|
||||
4. Start the MCP server:
|
||||
```bash
|
||||
systemctl start haproxy-mcp
|
||||
```
|
||||
|
||||
## 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`
|
||||
|
||||
### 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)
|
||||
|
||||
### 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_SERVERS_FILE` | `/opt/haproxy/conf/servers.json` | Servers config path |
|
||||
| `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) |
|
||||
|
||||
## Usage
|
||||
|
||||
Configure as MCP server in Claude Code settings.
|
||||
### Zero-Reload Domain Management
|
||||
|
||||
#### Adding a Domain
|
||||
|
||||
```python
|
||||
haproxy_add_domain("example.com", ip="10.0.0.1")
|
||||
# Creates: pool_N_1 → 10.0.0.1:80
|
||||
|
||||
haproxy_add_domain("api.example.com", ip="10.0.0.1", http_port=8080)
|
||||
# Creates: pool_N_1 → 10.0.0.1:8080
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Find an available pool (e.g., `pool_5`)
|
||||
2. Add to `domains.map`: `example.com pool_5`
|
||||
3. Update HAProxy runtime map via Runtime API
|
||||
4. Configure server in pool (HTTP only)
|
||||
5. Save to `servers.json` for persistence
|
||||
|
||||
**No HAProxy reload required!**
|
||||
|
||||
#### Pool Sharing
|
||||
|
||||
Multiple domains can share the same pool/backend servers:
|
||||
|
||||
```python
|
||||
# First domain gets its own pool
|
||||
haproxy_add_domain("example.com", ip="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 share the same backend servers
|
||||
- Removing a shared domain keeps servers intact
|
||||
|
||||
#### Bulk Server Operations
|
||||
|
||||
```python
|
||||
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}
|
||||
]')
|
||||
```
|
||||
|
||||
### SSL/TLS Certificates
|
||||
|
||||
Certificates are managed via acme.sh with Cloudflare DNS validation and deployed to HAProxy with zero downtime using the Runtime API.
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| ACME Client | acme.sh (Google Trust Services CA) |
|
||||
| Cert Directory | `/opt/haproxy/certs/` |
|
||||
| DNS Validation | Cloudflare |
|
||||
|
||||
```python
|
||||
# Issue a new certificate (with wildcard)
|
||||
haproxy_issue_cert("example.com", wildcard=True)
|
||||
|
||||
# Renew a certificate
|
||||
haproxy_renew_cert("example.com")
|
||||
|
||||
# List all certificates
|
||||
haproxy_list_certs()
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```python
|
||||
# System health
|
||||
haproxy_health()
|
||||
# Returns: {"status": "healthy", "components": {...}}
|
||||
|
||||
# Domain-specific health
|
||||
haproxy_domain_health("api.example.com")
|
||||
# Returns: {"status": "healthy", "servers": [...], "healthy_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 |
|
||||
| `haproxy_restore_state` | Restore state from disk |
|
||||
|
||||
### Certificate Management (Zero-Downtime)
|
||||
|
||||
| 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) |
|
||||
|
||||
## MCP Connection
|
||||
|
||||
**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}'
|
||||
```
|
||||
|
||||
Access rules:
|
||||
| Source | Token Required |
|
||||
|--------|----------------|
|
||||
| Tailscale (100.64.0.0/10) | No |
|
||||
| External | Yes |
|
||||
|
||||
Token location: `/opt/haproxy/conf/mcp-token.env`
|
||||
|
||||
## Safety Features
|
||||
|
||||
- **Atomic file writes**: Temp file + rename prevents corruption
|
||||
- **File locking**: Prevents race conditions on concurrent operations
|
||||
- **Disk-first pattern**: Config saved 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
|
||||
- **Command batching**: Multiple HAProxy commands sent in single TCP connection
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
/opt/haproxy/
|
||||
├── haproxy_mcp/ # MCP server package (streamable-http)
|
||||
│ ├── server.py # Main entry point
|
||||
│ ├── config.py # Configuration and constants
|
||||
│ ├── exceptions.py # Exception classes
|
||||
│ ├── validation.py # Input validation
|
||||
│ ├── haproxy_client.py # HAProxy Runtime API client
|
||||
│ ├── file_ops.py # File I/O operations
|
||||
│ ├── 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)
|
||||
│ ├── domains.map # Domain → Pool mapping
|
||||
│ ├── servers.json # Server persistence (auto-managed)
|
||||
│ ├── certificates.json # Certificate domain list (auto-managed)
|
||||
│ └── 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
|
||||
```
|
||||
|
||||
## 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
|
||||
↓
|
||||
5. MCP server reads servers.json & certificates.json
|
||||
↓
|
||||
6. MCP server restores servers via Runtime API
|
||||
↓
|
||||
7. MCP server loads certificates via Runtime API (zero-downtime)
|
||||
↓
|
||||
8. Ready to serve traffic
|
||||
```
|
||||
|
||||
## HAProxy Runtime API
|
||||
|
||||
```bash
|
||||
# Show domain mappings
|
||||
echo "show map /usr/local/etc/haproxy/domains.map" | nc localhost 9999
|
||||
|
||||
# Add domain mapping
|
||||
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
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Reference in New Issue
Block a user