Integrate Espressif's QEMU fork for virtual ESP device management: - QemuManager component with 5 MCP tools (start/stop/list/status/flash) - Config auto-detects QEMU binaries from ~/.espressif/tools/ - Supports esp32, esp32s2, esp32s3, esp32c3 chip emulation - Virtual serial over TCP (socket://localhost:PORT) transparent to esptool - Scan integration: QEMU instances appear in esp_scan_ports results - Blank flash images initialized to 0xFF (erased NOR flash state) - 38 unit tests covering lifecycle, port allocation, flash writes
970 lines
30 KiB
Markdown
970 lines
30 KiB
Markdown
# 🚀 Production Deployment Guide
|
|
|
|
## Overview
|
|
|
|
This guide covers deploying the FastMCP ESPTool server in production environments, including containerization, scaling, monitoring, and enterprise integration patterns.
|
|
|
|
## 🏭 Production Architecture
|
|
|
|
### Deployment Topology
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Production Environment │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ Load │ │ Reverse │ │ SSL/TLS │ │
|
|
│ │ Balancer │───▶│ Proxy │───▶│ Termination │ │
|
|
│ │ (HAProxy) │ │ (Caddy) │ │ (Cert) │ │
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
│ │ │
|
|
│ ┌─────────────────────────────────────────────────────────┤
|
|
│ │ MCP ESPTool Server Cluster │
|
|
│ │ │
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ │ Server │ │ Server │ │ Server │ │
|
|
│ │ │ Instance 1 │ │ Instance 2 │ │ Instance 3 │ │
|
|
│ │ │ │ │ │ │ │ │
|
|
│ │ │ Port: 8080 │ │ Port: 8081 │ │ Port: 8082 │ │
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
│ └─────────────────────────────────────────────────────────┤
|
|
│ │ │
|
|
│ ┌─────────────────────────────────────────────────────────┤
|
|
│ │ Shared Services │
|
|
│ │ │
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ │ Redis │ │ PostgreSQL │ │ Monitoring │ │
|
|
│ │ │ Cache │ │ Database │ │ (Grafana) │ │
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
│ └─────────────────────────────────────────────────────────┤
|
|
│ │ │
|
|
│ ┌─────────────────────────────────────────────────────────┤
|
|
│ │ Hardware Interface │
|
|
│ │ │
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ │ ESP Device │ │ ESP Device │ │ ESP Device │ │
|
|
│ │ │ Station 1 │ │ Station 2 │ │ Station N │ │
|
|
│ │ │ │ │ │ │ │ │
|
|
│ │ │/dev/ttyUSB0 │ │/dev/ttyUSB1 │ │/dev/ttyUSBN │ │
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
│ └─────────────────────────────────────────────────────────┘
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## 🐳 Container Production Setup
|
|
|
|
### Production Dockerfile
|
|
|
|
```dockerfile
|
|
# Multi-stage production Dockerfile
|
|
FROM python:3.11-slim-bookworm AS base
|
|
|
|
# Install system dependencies
|
|
RUN apt-get update && apt-get install -y \
|
|
git \
|
|
curl \
|
|
build-essential \
|
|
cmake \
|
|
ninja-build \
|
|
libusb-1.0-0-dev \
|
|
udev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Install uv for fast package management
|
|
RUN pip install uv
|
|
|
|
# Create non-root user for security
|
|
RUN groupadd -r esptool && useradd -r -g esptool -d /app -s /bin/bash esptool
|
|
RUN mkdir -p /app && chown esptool:esptool /app
|
|
|
|
# Production stage
|
|
FROM base AS production
|
|
|
|
USER esptool
|
|
WORKDIR /app
|
|
|
|
# Copy project files
|
|
COPY --chown=esptool:esptool pyproject.toml ./
|
|
COPY --chown=esptool:esptool src/ ./src/
|
|
|
|
# Install production dependencies
|
|
RUN uv venv .venv && \
|
|
. .venv/bin/activate && \
|
|
uv pip install -e ".[production]" && \
|
|
uv pip install esptool esp-idf-tools
|
|
|
|
# Set up ESP-IDF (production minimal)
|
|
RUN git clone --depth 1 --branch v5.1 \
|
|
https://github.com/espressif/esp-idf.git /opt/esp-idf && \
|
|
cd /opt/esp-idf && \
|
|
./install.sh --targets esp32,esp32s3,esp32c3
|
|
|
|
# Environment setup
|
|
ENV ESP_IDF_PATH=/opt/esp-idf
|
|
ENV PATH="/opt/esp-idf/tools:/app/.venv/bin:$PATH"
|
|
ENV PYTHONPATH=/app/src
|
|
|
|
# Create directories for data persistence
|
|
RUN mkdir -p /app/data /app/logs /app/config
|
|
|
|
# Health check script
|
|
COPY --chown=esptool:esptool scripts/health-check.py ./health-check.py
|
|
RUN chmod +x health-check.py
|
|
|
|
# Security: Remove package management tools in production
|
|
USER root
|
|
RUN apt-get remove -y git curl build-essential cmake && \
|
|
apt-get autoremove -y && \
|
|
rm -rf /var/lib/apt/lists/*
|
|
|
|
USER esptool
|
|
|
|
# Health check
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
|
CMD python health-check.py || exit 1
|
|
|
|
# Expose port
|
|
EXPOSE 8080
|
|
|
|
# Production startup
|
|
CMD ["/app/.venv/bin/python", "-m", "mcp_esptool_server.server", "--production"]
|
|
```
|
|
|
|
### Production Docker Compose
|
|
|
|
```yaml
|
|
# docker-compose.prod.yml
|
|
version: '3.8'
|
|
|
|
services:
|
|
# Load balancer
|
|
haproxy:
|
|
image: haproxy:2.8-alpine
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "8404:8404" # Stats
|
|
volumes:
|
|
- ./config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
|
- ./certs:/etc/ssl/certs:ro
|
|
networks:
|
|
- frontend
|
|
- backend
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- esptool-server-1
|
|
- esptool-server-2
|
|
- esptool-server-3
|
|
|
|
# MCP ESPTool Server Instances
|
|
esptool-server-1: &esptool-server
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
target: production
|
|
image: mcp-esptool-server:${VERSION:-latest}
|
|
environment:
|
|
- SERVER_ID=1
|
|
- SERVER_PORT=8080
|
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
- DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
|
|
- REDIS_URL=redis://redis:6379/0
|
|
- ESP_DEVICE_BASE_PATH=/dev/serial
|
|
- PRODUCTION_MODE=true
|
|
- MAX_CONCURRENT_OPERATIONS=10
|
|
- ENABLE_METRICS=true
|
|
- METRICS_PORT=9090
|
|
volumes:
|
|
- ./data/server-1:/app/data
|
|
- ./logs:/app/logs
|
|
- ./config:/app/config:ro
|
|
- /dev/serial:/dev/serial:rw
|
|
networks:
|
|
- backend
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- postgres
|
|
- redis
|
|
labels:
|
|
- "com.docker.compose.service=esptool-server"
|
|
|
|
esptool-server-2:
|
|
<<: *esptool-server
|
|
environment:
|
|
- SERVER_ID=2
|
|
- SERVER_PORT=8080
|
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
- DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
|
|
- REDIS_URL=redis://redis:6379/0
|
|
- ESP_DEVICE_BASE_PATH=/dev/serial
|
|
- PRODUCTION_MODE=true
|
|
volumes:
|
|
- ./data/server-2:/app/data
|
|
- ./logs:/app/logs
|
|
- ./config:/app/config:ro
|
|
- /dev/serial:/dev/serial:rw
|
|
|
|
esptool-server-3:
|
|
<<: *esptool-server
|
|
environment:
|
|
- SERVER_ID=3
|
|
- SERVER_PORT=8080
|
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
- DATABASE_URL=postgresql://esptool:${DB_PASSWORD}@postgres:5432/esptool
|
|
- REDIS_URL=redis://redis:6379/0
|
|
- ESP_DEVICE_BASE_PATH=/dev/serial
|
|
- PRODUCTION_MODE=true
|
|
volumes:
|
|
- ./data/server-3:/app/data
|
|
- ./logs:/app/logs
|
|
- ./config:/app/config:ro
|
|
- /dev/serial:/dev/serial:rw
|
|
|
|
# Database
|
|
postgres:
|
|
image: postgres:15-alpine
|
|
environment:
|
|
POSTGRES_DB: esptool
|
|
POSTGRES_USER: esptool
|
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
|
networks:
|
|
- backend
|
|
restart: unless-stopped
|
|
|
|
# Cache and session store
|
|
redis:
|
|
image: redis:7-alpine
|
|
command: redis-server --appendonly yes --maxmemory 1gb --maxmemory-policy allkeys-lru
|
|
volumes:
|
|
- redis-data:/data
|
|
networks:
|
|
- backend
|
|
restart: unless-stopped
|
|
|
|
# Monitoring and metrics
|
|
prometheus:
|
|
image: prom/prometheus:latest
|
|
ports:
|
|
- "9090:9090"
|
|
volumes:
|
|
- ./config/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
- prometheus-data:/prometheus
|
|
networks:
|
|
- backend
|
|
- monitoring
|
|
restart: unless-stopped
|
|
|
|
grafana:
|
|
image: grafana/grafana:latest
|
|
ports:
|
|
- "3000:3000"
|
|
environment:
|
|
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
|
|
volumes:
|
|
- grafana-data:/var/lib/grafana
|
|
- ./config/grafana:/etc/grafana/provisioning:ro
|
|
networks:
|
|
- monitoring
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- prometheus
|
|
|
|
# Log aggregation
|
|
loki:
|
|
image: grafana/loki:latest
|
|
ports:
|
|
- "3100:3100"
|
|
volumes:
|
|
- loki-data:/tmp/loki
|
|
- ./config/loki.yml:/etc/loki/local-config.yaml:ro
|
|
networks:
|
|
- monitoring
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
postgres-data:
|
|
redis-data:
|
|
prometheus-data:
|
|
grafana-data:
|
|
loki-data:
|
|
|
|
networks:
|
|
frontend:
|
|
driver: bridge
|
|
backend:
|
|
driver: bridge
|
|
monitoring:
|
|
driver: bridge
|
|
```
|
|
|
|
## ⚙️ Configuration Management
|
|
|
|
### Production Configuration
|
|
|
|
```python
|
|
# src/mcp_esptool_server/config/production.py
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Dict, Optional
|
|
from pathlib import Path
|
|
|
|
@dataclass
|
|
class ProductionConfig:
|
|
"""Production environment configuration"""
|
|
|
|
# Server settings
|
|
server_id: str = field(default_factory=lambda: os.getenv('SERVER_ID', '1'))
|
|
server_port: int = int(os.getenv('SERVER_PORT', '8080'))
|
|
max_workers: int = int(os.getenv('MAX_WORKERS', '4'))
|
|
max_concurrent_operations: int = int(os.getenv('MAX_CONCURRENT_OPERATIONS', '10'))
|
|
|
|
# Database configuration
|
|
database_url: str = os.getenv('DATABASE_URL', 'postgresql://localhost:5432/esptool')
|
|
redis_url: str = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
|
|
|
|
# Security settings
|
|
enable_auth: bool = os.getenv('ENABLE_AUTH', 'true').lower() == 'true'
|
|
jwt_secret: str = os.getenv('JWT_SECRET', 'change-me-in-production')
|
|
api_key_required: bool = os.getenv('API_KEY_REQUIRED', 'true').lower() == 'true'
|
|
|
|
# ESP device settings
|
|
esp_device_base_path: str = os.getenv('ESP_DEVICE_BASE_PATH', '/dev/serial')
|
|
max_device_connections: int = int(os.getenv('MAX_DEVICE_CONNECTIONS', '20'))
|
|
device_timeout: int = int(os.getenv('DEVICE_TIMEOUT', '30'))
|
|
|
|
# Logging and monitoring
|
|
log_level: str = os.getenv('LOG_LEVEL', 'INFO')
|
|
enable_metrics: bool = os.getenv('ENABLE_METRICS', 'true').lower() == 'true'
|
|
metrics_port: int = int(os.getenv('METRICS_PORT', '9090'))
|
|
|
|
# Performance tuning
|
|
enable_caching: bool = os.getenv('ENABLE_CACHING', 'true').lower() == 'true'
|
|
cache_ttl: int = int(os.getenv('CACHE_TTL', '300'))
|
|
|
|
# Factory programming settings
|
|
enable_factory_mode: bool = os.getenv('ENABLE_FACTORY_MODE', 'false').lower() == 'true'
|
|
factory_batch_size: int = int(os.getenv('FACTORY_BATCH_SIZE', '100'))
|
|
|
|
# Data retention
|
|
log_retention_days: int = int(os.getenv('LOG_RETENTION_DAYS', '30'))
|
|
metrics_retention_days: int = int(os.getenv('METRICS_RETENTION_DAYS', '90'))
|
|
|
|
def __post_init__(self):
|
|
"""Validate configuration after initialization"""
|
|
if self.enable_auth and self.jwt_secret == 'change-me-in-production':
|
|
raise ValueError("JWT_SECRET must be set in production with authentication enabled")
|
|
|
|
if not Path(self.esp_device_base_path).exists():
|
|
raise ValueError(f"ESP device base path does not exist: {self.esp_device_base_path}")
|
|
```
|
|
|
|
### Environment Configuration
|
|
|
|
```bash
|
|
# .env.production
|
|
# Core server settings
|
|
SERVER_ID=1
|
|
SERVER_PORT=8080
|
|
MAX_WORKERS=4
|
|
MAX_CONCURRENT_OPERATIONS=10
|
|
|
|
# Database and cache
|
|
DATABASE_URL=postgresql://esptool:secure_password@postgres:5432/esptool
|
|
REDIS_URL=redis://redis:6379/0
|
|
|
|
# Security (CHANGE THESE IN PRODUCTION)
|
|
ENABLE_AUTH=true
|
|
JWT_SECRET=your-256-bit-secret-key-here
|
|
API_KEY_REQUIRED=true
|
|
|
|
# ESP device configuration
|
|
ESP_DEVICE_BASE_PATH=/dev/serial
|
|
MAX_DEVICE_CONNECTIONS=20
|
|
DEVICE_TIMEOUT=30
|
|
|
|
# Logging and monitoring
|
|
LOG_LEVEL=INFO
|
|
ENABLE_METRICS=true
|
|
METRICS_PORT=9090
|
|
|
|
# Performance
|
|
ENABLE_CACHING=true
|
|
CACHE_TTL=300
|
|
|
|
# Factory programming
|
|
ENABLE_FACTORY_MODE=true
|
|
FACTORY_BATCH_SIZE=50
|
|
|
|
# Data retention
|
|
LOG_RETENTION_DAYS=30
|
|
METRICS_RETENTION_DAYS=90
|
|
|
|
# External services
|
|
GRAFANA_PASSWORD=secure_grafana_password
|
|
DB_PASSWORD=secure_db_password
|
|
```
|
|
|
|
## 🔐 Security Configuration
|
|
|
|
### Authentication and Authorization
|
|
|
|
```python
|
|
# src/mcp_esptool_server/security/auth.py
|
|
import jwt
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, Dict, Any
|
|
from fastapi import HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
|
|
class ProductionAuth:
|
|
"""Production authentication and authorization"""
|
|
|
|
def __init__(self, config: ProductionConfig):
|
|
self.config = config
|
|
self.security = HTTPBearer()
|
|
self.valid_api_keys: set = self._load_api_keys()
|
|
|
|
def _load_api_keys(self) -> set:
|
|
"""Load valid API keys from configuration"""
|
|
# In production, load from secure key management system
|
|
api_keys_file = Path(self.config.config_dir) / "api_keys.txt"
|
|
if api_keys_file.exists():
|
|
return set(api_keys_file.read_text().strip().split('\n'))
|
|
return set()
|
|
|
|
def create_access_token(self, data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
|
"""Create JWT access token"""
|
|
to_encode = data.copy()
|
|
if expires_delta:
|
|
expire = datetime.utcnow() + expires_delta
|
|
else:
|
|
expire = datetime.utcnow() + timedelta(minutes=15)
|
|
|
|
to_encode.update({"exp": expire})
|
|
encoded_jwt = jwt.encode(to_encode, self.config.jwt_secret, algorithm="HS256")
|
|
return encoded_jwt
|
|
|
|
def verify_token(self, credentials: HTTPAuthorizationCredentials) -> Dict[str, Any]:
|
|
"""Verify JWT token"""
|
|
try:
|
|
payload = jwt.decode(
|
|
credentials.credentials,
|
|
self.config.jwt_secret,
|
|
algorithms=["HS256"]
|
|
)
|
|
return payload
|
|
except jwt.PyJWTError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Could not validate credentials"
|
|
)
|
|
|
|
def verify_api_key(self, api_key: str) -> bool:
|
|
"""Verify API key"""
|
|
return api_key in self.valid_api_keys
|
|
|
|
# Security middleware
|
|
class SecurityMiddleware:
|
|
"""Production security middleware"""
|
|
|
|
def __init__(self, app: FastMCP, auth: ProductionAuth):
|
|
self.app = app
|
|
self.auth = auth
|
|
self._setup_security_headers()
|
|
|
|
def _setup_security_headers(self):
|
|
"""Configure security headers"""
|
|
@self.app.middleware("http")
|
|
async def add_security_headers(request, call_next):
|
|
response = await call_next(request)
|
|
|
|
# Security headers
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
response.headers["Content-Security-Policy"] = "default-src 'self'"
|
|
|
|
return response
|
|
```
|
|
|
|
## 📊 Monitoring and Observability
|
|
|
|
### Metrics Collection
|
|
|
|
```python
|
|
# src/mcp_esptool_server/monitoring/metrics.py
|
|
from prometheus_client import Counter, Histogram, Gauge, start_http_server
|
|
from typing import Dict, Any
|
|
import time
|
|
|
|
class ProductionMetrics:
|
|
"""Production metrics collection"""
|
|
|
|
def __init__(self, config: ProductionConfig):
|
|
self.config = config
|
|
|
|
# Operation metrics
|
|
self.operations_total = Counter(
|
|
'esptool_operations_total',
|
|
'Total number of ESPTool operations',
|
|
['operation_type', 'status', 'server_id']
|
|
)
|
|
|
|
self.operation_duration = Histogram(
|
|
'esptool_operation_duration_seconds',
|
|
'Duration of ESPTool operations',
|
|
['operation_type', 'server_id']
|
|
)
|
|
|
|
# Device metrics
|
|
self.connected_devices = Gauge(
|
|
'esptool_connected_devices',
|
|
'Number of connected ESP devices',
|
|
['server_id']
|
|
)
|
|
|
|
self.device_operations = Counter(
|
|
'esptool_device_operations_total',
|
|
'Total device operations by port',
|
|
['port', 'operation', 'status', 'server_id']
|
|
)
|
|
|
|
# System metrics
|
|
self.active_connections = Gauge(
|
|
'esptool_active_connections',
|
|
'Number of active MCP connections',
|
|
['server_id']
|
|
)
|
|
|
|
self.memory_usage = Gauge(
|
|
'esptool_memory_usage_bytes',
|
|
'Memory usage in bytes',
|
|
['server_id']
|
|
)
|
|
|
|
# Error metrics
|
|
self.errors_total = Counter(
|
|
'esptool_errors_total',
|
|
'Total number of errors',
|
|
['error_type', 'component', 'server_id']
|
|
)
|
|
|
|
if config.enable_metrics:
|
|
start_http_server(config.metrics_port)
|
|
|
|
def record_operation(self, operation_type: str, duration: float, status: str):
|
|
"""Record operation metrics"""
|
|
self.operations_total.labels(
|
|
operation_type=operation_type,
|
|
status=status,
|
|
server_id=self.config.server_id
|
|
).inc()
|
|
|
|
self.operation_duration.labels(
|
|
operation_type=operation_type,
|
|
server_id=self.config.server_id
|
|
).observe(duration)
|
|
|
|
def record_device_operation(self, port: str, operation: str, status: str):
|
|
"""Record device-specific operation"""
|
|
self.device_operations.labels(
|
|
port=port,
|
|
operation=operation,
|
|
status=status,
|
|
server_id=self.config.server_id
|
|
).inc()
|
|
|
|
def update_connected_devices(self, count: int):
|
|
"""Update connected devices count"""
|
|
self.connected_devices.labels(server_id=self.config.server_id).set(count)
|
|
|
|
def record_error(self, error_type: str, component: str):
|
|
"""Record error occurrence"""
|
|
self.errors_total.labels(
|
|
error_type=error_type,
|
|
component=component,
|
|
server_id=self.config.server_id
|
|
).inc()
|
|
```
|
|
|
|
### Logging Configuration
|
|
|
|
```python
|
|
# src/mcp_esptool_server/logging/production.py
|
|
import logging
|
|
import logging.config
|
|
from pathlib import Path
|
|
import json
|
|
|
|
LOGGING_CONFIG = {
|
|
"version": 1,
|
|
"disable_existing_loggers": False,
|
|
"formatters": {
|
|
"json": {
|
|
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
|
"format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d"
|
|
},
|
|
"standard": {
|
|
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
}
|
|
},
|
|
"handlers": {
|
|
"console": {
|
|
"level": "INFO",
|
|
"class": "logging.StreamHandler",
|
|
"formatter": "standard",
|
|
"stream": "ext://sys.stdout"
|
|
},
|
|
"file": {
|
|
"level": "DEBUG",
|
|
"class": "logging.handlers.RotatingFileHandler",
|
|
"formatter": "json",
|
|
"filename": "/app/logs/esptool-server.log",
|
|
"maxBytes": 10485760, # 10MB
|
|
"backupCount": 5
|
|
},
|
|
"error_file": {
|
|
"level": "ERROR",
|
|
"class": "logging.handlers.RotatingFileHandler",
|
|
"formatter": "json",
|
|
"filename": "/app/logs/esptool-errors.log",
|
|
"maxBytes": 10485760,
|
|
"backupCount": 5
|
|
}
|
|
},
|
|
"loggers": {
|
|
"mcp_esptool_server": {
|
|
"level": "DEBUG",
|
|
"handlers": ["console", "file", "error_file"],
|
|
"propagate": False
|
|
},
|
|
"esptool": {
|
|
"level": "INFO",
|
|
"handlers": ["file"],
|
|
"propagate": False
|
|
}
|
|
},
|
|
"root": {
|
|
"level": "INFO",
|
|
"handlers": ["console"]
|
|
}
|
|
}
|
|
|
|
def setup_production_logging(config: ProductionConfig):
|
|
"""Set up production logging configuration"""
|
|
|
|
# Ensure log directory exists
|
|
log_dir = Path("/app/logs")
|
|
log_dir.mkdir(exist_ok=True)
|
|
|
|
# Update logging level from configuration
|
|
LOGGING_CONFIG["handlers"]["console"]["level"] = config.log_level
|
|
LOGGING_CONFIG["loggers"]["mcp_esptool_server"]["level"] = config.log_level
|
|
|
|
# Configure logging
|
|
logging.config.dictConfig(LOGGING_CONFIG)
|
|
|
|
# Set up structured logging context
|
|
logger = logging.getLogger("mcp_esptool_server")
|
|
logger.info("Production logging initialized", extra={
|
|
"server_id": config.server_id,
|
|
"log_level": config.log_level,
|
|
"environment": "production"
|
|
})
|
|
```
|
|
|
|
## 🚀 Deployment Automation
|
|
|
|
### Kubernetes Deployment
|
|
|
|
```yaml
|
|
# k8s/deployment.yaml
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: mcp-esptool-server
|
|
labels:
|
|
app: mcp-esptool-server
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
matchLabels:
|
|
app: mcp-esptool-server
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: mcp-esptool-server
|
|
spec:
|
|
containers:
|
|
- name: mcp-esptool-server
|
|
image: mcp-esptool-server:latest
|
|
ports:
|
|
- containerPort: 8080
|
|
- containerPort: 9090 # Metrics
|
|
env:
|
|
- name: SERVER_ID
|
|
valueFrom:
|
|
fieldRef:
|
|
fieldPath: metadata.name
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: esptool-secrets
|
|
key: database-url
|
|
- name: REDIS_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: esptool-secrets
|
|
key: redis-url
|
|
- name: JWT_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: esptool-secrets
|
|
key: jwt-secret
|
|
volumeMounts:
|
|
- name: device-access
|
|
mountPath: /dev/serial
|
|
- name: config
|
|
mountPath: /app/config
|
|
readOnly: true
|
|
- name: data
|
|
mountPath: /app/data
|
|
resources:
|
|
requests:
|
|
memory: "512Mi"
|
|
cpu: "250m"
|
|
limits:
|
|
memory: "1Gi"
|
|
cpu: "500m"
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: 8080
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /ready
|
|
port: 8080
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 5
|
|
volumes:
|
|
- name: device-access
|
|
hostPath:
|
|
path: /dev/serial
|
|
- name: config
|
|
configMap:
|
|
name: esptool-config
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: esptool-data
|
|
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: mcp-esptool-service
|
|
spec:
|
|
selector:
|
|
app: mcp-esptool-server
|
|
ports:
|
|
- name: http
|
|
port: 80
|
|
targetPort: 8080
|
|
- name: metrics
|
|
port: 9090
|
|
targetPort: 9090
|
|
type: ClusterIP
|
|
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: mcp-esptool-ingress
|
|
annotations:
|
|
kubernetes.io/ingress.class: nginx
|
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
spec:
|
|
tls:
|
|
- hosts:
|
|
- esptool.yourdomain.com
|
|
secretName: esptool-tls
|
|
rules:
|
|
- host: esptool.yourdomain.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: mcp-esptool-service
|
|
port:
|
|
number: 80
|
|
```
|
|
|
|
### CI/CD Pipeline
|
|
|
|
```yaml
|
|
# .github/workflows/production-deploy.yml
|
|
name: Production Deployment
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: ${{ github.repository }}
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v4
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
pip install uv
|
|
uv venv
|
|
uv pip install -e ".[dev,testing]"
|
|
|
|
- name: Run tests
|
|
run: |
|
|
uv run pytest tests/ --cov=src --cov-fail-under=85
|
|
|
|
- name: Security scan
|
|
run: |
|
|
uv run bandit -r src/
|
|
uv run safety check
|
|
|
|
build:
|
|
needs: test
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Log in to Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Extract metadata
|
|
id: meta
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
tags: |
|
|
type=ref,event=tag
|
|
type=raw,value=latest
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
target: production
|
|
push: true
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
|
|
deploy:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Configure kubectl
|
|
run: |
|
|
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
|
|
export KUBECONFIG=kubeconfig
|
|
|
|
- name: Deploy to production
|
|
run: |
|
|
kubectl set image deployment/mcp-esptool-server \
|
|
mcp-esptool-server=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
|
|
|
kubectl rollout status deployment/mcp-esptool-server --timeout=300s
|
|
|
|
- name: Verify deployment
|
|
run: |
|
|
kubectl get pods -l app=mcp-esptool-server
|
|
kubectl logs -l app=mcp-esptool-server --tail=50
|
|
```
|
|
|
|
## 📋 Production Checklist
|
|
|
|
### Pre-Deployment
|
|
|
|
- [ ] **Security Configuration**
|
|
- [ ] Change default passwords and secrets
|
|
- [ ] Configure JWT secrets
|
|
- [ ] Set up API key management
|
|
- [ ] Configure TLS certificates
|
|
- [ ] Review firewall rules
|
|
|
|
- [ ] **Infrastructure Setup**
|
|
- [ ] Database cluster configured
|
|
- [ ] Redis cache configured
|
|
- [ ] Load balancer configured
|
|
- [ ] Monitoring stack deployed
|
|
- [ ] Log aggregation configured
|
|
|
|
- [ ] **Hardware Access**
|
|
- [ ] ESP device permissions configured
|
|
- [ ] USB device access verified
|
|
- [ ] Device path mapping documented
|
|
- [ ] Hardware redundancy planned
|
|
|
|
### Post-Deployment
|
|
|
|
- [ ] **Monitoring Setup**
|
|
- [ ] Grafana dashboards configured
|
|
- [ ] Alerting rules configured
|
|
- [ ] Log analysis configured
|
|
- [ ] Performance baselines established
|
|
|
|
- [ ] **Operational Procedures**
|
|
- [ ] Backup procedures documented
|
|
- [ ] Disaster recovery tested
|
|
- [ ] Scaling procedures documented
|
|
- [ ] Maintenance windows scheduled
|
|
|
|
- [ ] **Team Training**
|
|
- [ ] Operations team trained
|
|
- [ ] Troubleshooting guides created
|
|
- [ ] Escalation procedures documented
|
|
- [ ] User documentation published
|
|
|
|
This production deployment guide ensures enterprise-grade reliability, security, and scalability for the FastMCP ESPTool server in production environments.
|
|
|
|
`★ Insight ─────────────────────────────────────`
|
|
**Enterprise-Grade Deployment**: This production guide demonstrates how to scale the FastMCP ESPTool server from development to enterprise production environments with proper security, monitoring, and operational procedures.
|
|
|
|
**Hardware Integration Challenges**: The unique aspect of this deployment is managing physical ESP device access in containerized environments, requiring careful consideration of USB device mapping and permissions across different deployment platforms.
|
|
|
|
**Observability Excellence**: The comprehensive monitoring, logging, and metrics collection ensures production visibility and enables proactive problem resolution, critical for embedded development workflows where hardware issues can be difficult to diagnose.
|
|
`─────────────────────────────────────────────────`
|
|
|
|
<function_calls>
|
|
<invoke name="TodoWrite">
|
|
<parameter name="todos">[{"content": "Create project initialization and setup documentation", "status": "completed", "activeForm": "Creating project initialization and setup documentation"}, {"content": "Design concrete implementation examples", "status": "completed", "activeForm": "Designing concrete implementation examples"}, {"content": "Create production deployment guide", "status": "completed", "activeForm": "Creating production deployment guide"}] |